기억해야 할 만한기억해야 할 만한 글귀들을 적는 공간입니다. 개인적으로 이해하기 쉽게 바꾼 말도 있으니, 맥락만 봐주시길 바랍니다. 문서의 변경이 있을 수 있습니다.

모든 소프트웨어 모듈에는 세 가지 목적이 있다.

  1. 실행 중에 제대로 동작하는 것. 이것은 모듈의 존재 이유다.
  2. 변경을 위해 존재한다. 대부분 모듈은 생명주기 동안 변경되기 때문에 간단한 작업만으로도 변경이 가능해야 한다.
  3. 코드를 읽는 사람과 의사소통하는 것이다. 특별한 훈련 없이도 개발자가 쉽게 읽고 이해할 수 있어야 한다.

프로그래밍 패러다임의 공존. - 절차형 패러다임과 객체지향 패러다임은 공존할 수 없는가?

예를 들어 절차형 패러다임객체지향 패러다임 이 공존할 수는 없는 걸까?
서로 다른 패러다임이 하나의 언어 안에서 공존함으로써 서로의 장단점을 보완한다.
프로그래밍 패러다임은 과거의 패러다임을 폐기시키는 혁명적인 과정을 거치지 않는다.
오히려 과거 패러다임의 단점을 보완하는 발전적인 혁명을 거친다.

이는 비록 객체지향 패러다임을 주로 사용한다고 해도, 다른 패러다임을 배우는 것이 도움이 될 것이라는 사실을 암시한다.
은총알은 없다 라는 말을 기억하라. 객체지향이 적합하지 않은 상황에서는 언제라도 다른 패러다임을 적용할 수 있는 시야를 기르고, 지식을 갈고닦아야 한다.

객체 사이의 의존성 - 설계의 목표

의존성은 변경에 대한 영향을 암시한다. 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼있다.
그렇다면 객체 사이의 의존성을 완전히 없애야 하는 것인가? 아니다. 객체지향 설계는 서로 의존하며 협력하는 객체들의 공동체를 구축하는 것이다
따라서 설계의 목표는 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이어야 한다.

설계는 균형의 예술이다.

  1. 어떤 기능을 설계하는 방법은 한 가지 이상일 수 있다.
  2. 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있기 때문에 결국 설계는 트레이드오프의 산물이다.

설계는 균형의 예술이다. 훌륭한 설계는 적절한 트레이드오프의 결과물이다.

소프트웨어는 곧 생물이다. - 의인화

현실에서는 수동적인 존재라 하더라도, 객체지향의 세계에서는 모든 것이 능동적이고 자율적인 존재다.
현실의 전화는 서로에게 전화를 걸지 않으며, 색은 스스로 칠하지 않는다. 반드시 인간이 필요하다.
하지만 객체지향의 세계에서 객체들은 _능동적이고 자율적인 존재_다.
소프트웨어는 태어나고, 삶을 영위하고, 그리고 죽는다.

객체지향은 말 그대로 객체를 지향하는 것.

객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때만 얻을 수 있다. 다음 두 가지에 집중하자.

  1. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
    • 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이다.
    • 따라서 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는 지를 먼저 결정해야한다.
    • 객체를 중심에 두는 접근 방법은 설계를 단순하고 깔끔하게 만든다.
  2. 객체는 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원이다.
    • 객체는 홀로 존재하지 않는다. 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재다.
    • 이 관점으로 바라보는 것은 설계를 유연하고 확장 가능하게 만든다.
    • 객체를 고립된 존재로 바라보지말고, 협력에 참여하는 협력자로 바라볼 것.
    • 객체들의 모양과 윤곽이 잡히면, 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고, 이 타입을 기반으로 클래스를 구현할 것.
    • 훌륭한 협력이 훌륭한 객체를 낳고 훌륭한 객체가 훌륭한 클래스를 낳는다.

'JAVA > OOP' 카테고리의 다른 글

7월 30일 객체지향 공부  (1) 2019.07.30

본 포스팅은 백기선님의 "더 자바, 애플리케이션을 테스트하는 다양한 방법" 을 보고 정리한 글 입니다.
관심 있으신 분들은 https://www.inflearn.com/course/the-java-application-test 를 살펴보세요

개요

테스트 자동화를 위한 Assertions 를 알아보자.

assertEquals

  • 이름에서 알 수 있다시피 기대하는 값과 실제 값이 동일한 지 검사하는 메서드다.

      static void assertEquals(Object expected, Object actual) {
          assertEquals(expected, actual, (String) null);
      }
    
      static void assertEquals(Object expected, Object actual, String message) {
          if (!objectsAreEqual(expected, actual)) {
              failNotEqual(expected, actual, message);
          }
      }
    
      static void assertEquals(Object expected, Object actual, Supplier<String> messageSupplier) {
          if (!objectsAreEqual(expected, actual)) {
              failNotEqual(expected, actual, messageSupplier);
          }
      }

사용법

    @Test
    void assert_test() {
        Study study = new Study(-10, 0);
        assertEquals(StudyStatus.DRAFT, study.getStatus(), "처음 스터디를 만들면 상태값은" + StudyStatus.DRAFT + "여야 한다.");
        assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "처음 스터디를 만들면 상태값은" + StudyStatus.DRAFT + "여야 한다.");
        assertEquals(StudyStatus.DRAFT, study.getStatus(), new Supplier<String>() {
            @Override
            public String get() {
                return "처음 스터디를 만들면 상태값은" + StudyStatus.DRAFT + "여야 한다.";
            }
        });
    }
  • 첫 번째 인수
    • expected. 기대하는 값을 넣어준다.
  • 두 번째 인수
    • actual. 실제 값을 넣어준다.
  • 세 번째 인수
    • message. 실패했을 때 콘솔에 기록되는 message를 넣어준다.

세 번째 인수에 String 이나 Supplier을 넣어줄 수 있다.
차이점은 String을 넣는다면 테스트가 실패하든 성공하든 매번 메시지를 생성하지만, Supplier는 실패할 때만 메시지를 생성한다.
조그마한 성능차이에도 불안하다면 Supplier를 사용하자.


assertThrows

  • 예상하는 익셉션이 발생하는 지 확인한다.

      public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable) {
          return AssertThrows.assertThrows(expectedType, executable);
      }

사용법

    @Test
    void assert_throws() {
        IllegalArgumentException illegalArgumentException =
                assertThrows(IllegalArgumentException.class, () -> new Study(10, -1));
        String message = illegalArgumentException.getMessage();
        assertEquals("최소 참석인원은 0 보다 커야 합니다.", message);
    }
  • 첫 번째 인수
    • Class expectedType. 발생하는 예외의 클래스를 넣어준다.
  • 두 번째 인수
    • Executable. 실제 행동을 넣어준다. 위 코드에서는 new Study(..) 를 했을 때 IllegalArgumentException이 발생하기를 기대한다.

assertAll

    public static void assertAll(Executable... executables) throws MultipleFailuresError {
        AssertAll.assertAll(executables);
    }
  • 원래 assertions 는 assertion 실패하면 그 밑의 코드는 더 이상 진행하지 않는다.
  • 만약 실패하더라도 다른 assertions 의 결과를 보고 싶다면 assertAll을 쓰자.

사용법

    @Test
    void assert_all() {
        Study study = new Study(-10, 0);
        assertAll(
                () -> assertNotNull(study),
                () -> assertEquals(StudyStatus.DRAFT, study.getStatus(),
                        () -> "스터디를 처음 만들면 " + StudyStatus.DRAFT + "상태 여야 합니다."),
                () -> assertTrue(study.getLimit() > 0, "스터디 최대 참석 가능 인원은 0보다 커야 합니다.")
        );
    }
  • assertAll 안에 있는 모든 assertion을 확인한다.

assertTimeout

/**
     * <em>Assert</em> that execution of the supplied {@code executable}
     * completes before the given {@code timeout} is exceeded.
     *
     * <p>Note: the {@code executable} will be executed in the same thread as that
     * of the calling code. Consequently, execution of the {@code executable} will
     * not be preemptively aborted if the timeout is exceeded.
     *
     * @see #assertTimeout(Duration, Executable, String)
     * @see #assertTimeout(Duration, Executable, Supplier)
     * @see #assertTimeout(Duration, ThrowingSupplier)
     * @see #assertTimeout(Duration, ThrowingSupplier, String)
     * @see #assertTimeout(Duration, ThrowingSupplier, Supplier)
     * @see #assertTimeoutPreemptively(Duration, Executable)
     */
    public static void assertTimeout(Duration timeout, Executable executable) {
        AssertTimeout.assertTimeout(timeout, executable);
    }
  • 지정한 시간 내에 assertion 실행이 완료되지 않는다면 테스트가 실패한다.

사용법

    @Test
    void assert_timeout() {
        assertTimeout(Duration.ofMillis(100), () -> {
            new Study(10, 10);
            Thread.sleep(300);
        });
    }
  • 100 밀리세컨드 이내에 Executable 코드가 실행완료 되어야 한다.
  • 주의할 점은 100 밀리세컨드가 지나자마자 테스트가 실패하는 게 아니라, Executable 코드가 모두 완료된 후에야 테스트 성공/실패를 결정짓는다.

깃헙 링크

링크

본 포스팅은 백기선님의 "더 자바, 애플리케이션을 테스트하는 다양한 방법" 을 보고 정리한 글 입니다.
관심 있으신 분들은 https://www.inflearn.com/course/the-java-application-test 를 살펴보세요

개요

Junit5의 기본 애너테이션이라고 할 수 있는 @Test, @BeforeAll, @BeforeEach, @AfterAll, @AfterEach, @Disabled 를 알아보자.

@Test

본 어노테이션을 붙이면 Test 메서드로 인식하고 테스트 한다.
JUnit5 기준으로 접근제한자가 Default 여도 된다. (JUnit4 까지는 public이어야 했었다.)

    @Test
    void create1() {
        Study study = new Study();
        assertNotNull(study);
        System.out.println("create1()");
    }

    @Test
    void create2() {
        System.out.println("create2()");
    }

@BeforeAll

본 어노테이션을 붙인 메서드는 해당 테스트 클래스를 초기화할 때 딱 한번 수행되는 메서드다.
메서드 시그니쳐는 static 으로 선언해야한다.

    @BeforeAll
    static void beforeAll() {
        System.out.println("@BeforeAll");
    }

@BeforeEach

본 어노테이션을 붙인 메서드는 테스트 메서드 실행 이전에 수행된다.

    @BeforeEach
    void beforeEach() {
        System.out.println("@BeforeEach");
    }

@AfterAll

본 어노테이션을 붙인 메서드는 해당 테스트 클래스 내 테스트 메서드를 모두 실행시킨 후 딱 한번 수행되는 메서드다.
메서드 시그니쳐는 static 으로 선언해야한다.

    @AfterAll
    static void afterAll() {
        System.out.println("@AfterAll");
    }

@AfterEach

본 어노테이션을 붙인 메서드는 테스트 메서드 실행 이후에 수행된다.

    @AfterEach
    void afterEach() {
        System.out.println("@AfterEach");
    }

@Disabled

본 어노테이션을 붙인 테스트 메서드는 무시된다.

    @Disabled
    @Test
    void create3() {
        System.out.println("create3()");
    }

코드

/**
 * @author : Eunmo Hong
 * @since : 2020/08/13
 */

class StudyTest {

    @Test
    void create1() {
        Study study = new Study();
        assertNotNull(study);
        System.out.println("create1()");
    }

    @Test
    void create2() {
        System.out.println("create2()");
    }

    @Disabled
    @Test
    void create3() {
        System.out.println("create3()");
    }

    @BeforeAll
    static void beforeAll() {
        System.out.println("@BeforeAll");
    }


    @AfterAll
    static void afterAll() {
        System.out.println("@AfterAll");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("@BeforeEach");
    }

    @AfterEach
    void afterEach() {
        System.out.println("@AfterEach");
    }
}

실행 콘솔

  • @BeforeAll 이 실행된다.
    --------반복------
    1. @BeforeEach 가 실행된다
    2. @Test 를 붙인 메서드가 실행된다.
    3. @AfterEach 가 실행된다
    --------반복------
  • @AfterAll 이 실행된다

깃헙 주소

링크

QueryDsl이란?

  • 오픈소스 프로젝트다.
  • 일반적으로 복잡한 Creteria를 대체하는 JPQL 빌더다.
  • JPA의 표준스펙이 아니므로 약간의 설정이 더 필요하다.
  • 복잡한 쿼리와 동적쿼리를 깔쌈하게 해결해준다.
  • 쿼리를 자바 코드로 작성할 수 있다. 따라서 문법오류를 컴파일단계에서 잡아줄 수 있다.

JPQL vs QueryDSL

JPQL

String username = "gracelove"
String query = "select m from Member m"+
 "where m.username = :username";

List<Member> result = em.createQuery(query, Member.class)
                            .getResult.List();
  • 이 코드에 문제가 있다. 쉽게 발견할 수 있는가?
    • String query에 가독성을 위해 개행을 줬다. 이걸 한 줄로 풀어보자.
    • select m from Member mwhere m.username = :username
    • 한 줄로 풀어보니, "...m where..."가 아니라 "...mwhere..."다.
    • 컴파일러가 잡아주지도 않는다. 런타임때 이 쿼리를 실행시키면 그제서야 에러가 난다.

QueryDSL

String username = "gracelove
List<Member> result = queryFactory
                            .select(member)
                            .from(member)
                            .where(member.username.eq(username))
                            .fetch();
  • 자바 코드기 때문에 ide의 도움을 받을 수 있다. 또 컴파일 단계에서 오류를 발견할 수 있다. 게다가 자바 코드다 보니, 메서드추출도 할 수 있어서 재사용성 또한 높아진다.

maven에서의 설정방법

pom.xml 의존성 추가

    <dependencies>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>
    </dependencies>
  • 의존성을 위와 같이 추가한다.

pom.xml 빌드 플러그인 추가

<build>
  <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
  • 이렇게 설정한뒤 mvn compile 을 실행시키자. 그럼 @Entity를 붙인 클래스들이 Q타입으로 새롭게 생성된다. ex) Account -> QAccount
    • target 디렉토리 밑에 Q타입의 class가 생성된 걸 볼 수 있다.

사용하기.

  • 먼저 Entity 클래스들을 만들어보자.
  • Member 클래스와 Team 클래스를 만들 것이다.
  • 연관관계는 다 대 일이다. 양방향으로 건다. 연관관계의 주인 즉, fk를 갖고있는 건 '다' 쪽인 Member 클래스다.

Member.class

/**
 * Created by GraceLove
 * Github  : https://github.com/gracelove91
 * Blog    : https://gracelove91.tistory.com
 * Email   : govlmo91@gmail.com
 *
 * @author : Eunmo Hong
 * @since : 2020/06/07
 */

@ToString(of = {"id", "username", "age"})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;


    public Member(String username) {
        this(username, 0, null);
    }


    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

Team.class

/**
 * Created by GraceLove
 * Github  : https://github.com/gracelove91
 * Blog    : https://gracelove91.tistory.com
 * Email   : govlmo91@gmail.com
 *
 * @author : Eunmo Hong
 * @since : 2020/06/07
 */

@ToString(of = {"id", "name"})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

사용 코드

  • 먼저 EntityManager와 JPAQueryFactory를 필드로 두고, EntityManager는 빈을 주입받자.

  • 그 다음 JPAQueryFactory의 인스턴스를 생성하는데, 생성자 인자로 EntityManager를 주자.

    • JPAQueryFactory는 스레드 세이프 하기 때문에 빈으로 등록시켜서 싱글톤으로 이용해도 괜찮다.
  • 테스트를 해볼 데이터를 @BeforeEach 로 각 테스트 이전에 넣어준다.

    @SpringBootTest
    @Transactional
    class MemberTest {
    
      @PersistenceContext //@Autowired
      EntityManager em;
      JPAQueryFactory queryFactory;
    
      @BeforeEach
      public void testEntity() {
          queryFactory = new JPAQueryFactory(em);
    
          Team teamA = new Team("teamA");
          Team teamB = new Team("teamB");
          em.persist(teamA);
          em.persist(teamB);
    
          Member member1 = new Member("member1", 10, teamA);
          Member member2 = new Member("member2", 20, teamA);
    
          Member member3 = new Member("member3", 30, teamB);
          Member member4 = new Member("member4", 40, teamB);
          em.persist(member1);
          em.persist(member2);
          em.persist(member3);
          em.persist(member4);
      }
      ...
    }
  • 이렇게 한다면 디비에는 다음과 같이 값이 들어갈 것이다.

전체 소스 : https://github.com/gracelove91/querydsl-practice/blob/master/src/test/java/kr/gracelove/querydsl/entity/MemberTest.java

'JAVA > JPA' 카테고리의 다른 글

[JPA]open-session-in-view 를 알아보자  (1) 2020.07.01
@EntityGraph  (0) 2020.02.11
순수 JPA레포지토리의 페이징과 스프링 데이터 JPA레포지토리의 페이징.  (0) 2020.02.10
연관관계의 주인  (0) 2020.01.27
준영속 상태  (0) 2020.01.16

+ Recent posts