수정해야 하는 부분 -> TransactionEntityManager 사용은 '트랜잭션 분리' 는 아니고, 영속성 컨테스트 상태를 분리한 것 정도로 생각할 수 있다. 

동일한 Test manged transaction 내에서 그저 영속성 컨텍스트를 비우기 위해 em.flush() 및 em.clear() 를 하는 것이기 때문

 

Intro

JPA 강의를 들은지 굉장히 오래되었고.. 부끄럽지만 확실하지 않은 상태에서 코드를 작성했는데, 이로 인해 쿼리를 제대로 예상하지 못한 일이 발생했다. 생각했던데로 쿼리가 나가고 있긴 했지만 이를 확인하기 전에는 다른 분의 질문에 (매우매우 매우매우 기본적인 개념이었음에도..)“아마..맞을걸요?” 라는 대답을 하고 있었다 ㅠ-ㅠ.

거기에 더불어 쿼리를 확인하겠다고 작성한 테스트 코드임에도 그 코드 그대로 돌리면 원하는 쿼리를 볼 수 없었다. 실제 쿼리를 확인하려면 다른 설정을 해야 했고 이에 대해 나만 알고 있는 상황이었다.

 

( 사실 이런식으로 테스트 코드 만으로 성공, 실패를 알 수 없는 것은 좋지 못한 테스트로 알고는 있다(추가적으로 콘솔에 출력되는 쿼리를 확인해야 함으로). 그러나 아직까지 나는 Jpa 를 사용하면서 원하는 쿼리가 날아가는가에 대한 확인 또한 필요하다고 생각하여 테스트에 대해서도 show_sql 을 켜 두고, 해당 시나리오에 대해 내가 생각했던 쿼리만 날아가는지도 확인하는 편이다 )

따라서 사소하지만 기존에 원하던 대로 JpaRepository 에 대한 slice test 를 하면서 커스텀 메서드의 쿼리를 확인해보고자 한 과정을 정리해보고자 한다.


내가 작성하던 부분은 DB 에서 관리하는 ‘카테고리’와 ‘상위카테고리’ 사이의 연관관계를 표현하는 것이 필요한 부분이었다.

이 때 카테고리 들에 존재하는 계층은 2단계만이 존재했고 앞으로 추가될 가능성이 없는 부분이었다.

하지만 ‘상위카테고리’ 자체는 앞으로 비즈니스로직과 관련해서는 크게 관련이 있지 않고, 하위 카테고리들에 대해서는 추가될 기능이 존재하여 이 둘을 별도의 존재로 관리하기로 하였다.

카테고리를 조회할 때면 거의 항상 상위 카테고리에 대한 정보도 필요한 상황이었다.

fetchType 이 Eager && Nullable 한 컬럼인경우 → left outer join 쿼리가 날아간다

JPA 에 대한 기본 구현체인 Hibernate 의 경우 ManyToOne 관계에서 fetchType 을 Eager 로 할 경우 , ‘다’ 인 엔티티를 조회하는 경우 join 쿼리가 날아가 ‘일’인 엔티티 역시 함께 얻어오는 것을 기대하였다.

이것이 필요하다고 생각되었던 것은 일종의 카테고리 역할을 하는 엔티티 였다.

하위 카테고리를 나타낼 때 , 거의 항상 상위 카테고리에 대한 정보도 표시하도록 해야 했기 때문이다.

따라서 이 경우에는 Eager 를 통해, 항상 join 을 통해 상위 카테고리도 가져옴을 기대하였다.

Sector chineseSector = sectorRepository.findById(sectorId);

Assertions.assertThat(chineseSector.getSuperSector().getName()).isEqualTo("FOOD");

대충 위와 같은 것을 확인하고자 했다.

하위 업종을 조회 해 올 경우, 이 하위업종에 대한 상위업종에 대한 접근을 할 때 추가 쿼리가 날아가지 않음을 확인하고자 했다.

이 때 @DataJpaTest 를 사용하고 있었다.

이로 인해 @BeforeEach 에서 repository.save 메서드를 통해 저장한 엔티티들은 사실상 @Test 메서드 와 동일한 영속성 컨텍스트에 존재하고 있기에 위의 find 메서드 호출로는 어떠한 select 문도 날아가고 있지 않았다.

따라서 팀원분은 테스트 코드를 통해 쿼리를 확인 할 수 없고 , EAGER 여도 혹시 상위 카테고리에 대한 조회 쿼리는 따로 나가는 거 아닌가요? 라는 질문을 받게 되었다.


현재 상황에서 EAGER 를 설정할 경우 join 쿼리가 날아간다

JPA 의 구현체에 따라 다른 결과가 나올 수 있다고 한다.

현재 사용하는 Hibernate 의 경우 Nullable 한 column 의 fk 로 연결된 연관관계에 대해서는 fetchType 을 EAGER 로 설정하는 경우 left outer join 쿼리가 날아간다.

		select
        sector0_.*
    from
        sector sector0_ 
    left outer join
        super_sector supersecto1_ 
            on sector0_.super_sector_id=supersecto1_.id 
    where
        sector0_.id=?
참고로 N + 1 문제는 발생하니 이에 대해서는 적절한 방법을 선택하자. 이 글의 주제는 테스트 데이터 트랜잭션 분리 이기 때문에 이에 대해서는 인터넷상의 수많은 자료들을 참고하도록 하자

생각해보니 나의 레포지토리 테스트가 무의미하다

JPA 에 대한 기본지식을 잊은것도 큰 문제 였지만, 그 보다도 내가 테스트 코드를 작성하던 목표가 달성되고 있지 않음은 더 큰 문제라는 생각이 들었다.

커스텀 메서드 또는 JPA 관련 설정으로 기대한 쿼리가 예상대로 날아가는지를 테스트하기 위 함이었는데, 이에 대한 쿼리가 제대로 날아가지 않는 상황이니 매우 무의미하다고 느껴졌다.


@BeforeEach 에 대한 트랜잭션만 분리할 수는 없나?

참조 : https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#testcontext-tx-enabling-transactions

이미 위에서도 조회 쿼리가 따로 날아가지 않음에 대해 얘기했는데, 이런 일이 발생한 이유와 이로인한 문제는 다음과 같았다.

 

  • @DataJpaTest 에는 @Transactional 이 클래스단에 붙어있다 → Test managed transaction 이 적용된다.
  • Test method 에 달리는 @Transactional 은 롤백된다
  • @BeforeEach, @AfterEach 실행 전에 생성된 트랜잭션에서 @BeforeEach - @Test method - @AfterEach 가 동작한다 → 이들은 모두 동일한 트랜잭션에 속해 있다.
  • 이미 @BeforeEach 에서 repository.save 를 하였기 때문에, find 해오려는 엔티티가 영속성컨텍스트에 존재한다. → 실제 쿼리를 날리는 것을 볼 수 없다.
    • 실제로는 잘못된 쿼리가 날아가 원하는 결과가 나오지 않을 수 있다.
    • 하지만 테스트 상에서는 항상 대부분의 엔티티들이 이미 영속성 컨텍스트에 존재하기 때문에 성공한다.

Test data 를 세팅하는 트랜잭션 분리 or 동일한 트랜잭션을 사용하면서 영속성 컨텍스트르 비워주는 방법을 찾아야 한다

이를 위한 방법으로

  • data.sql 과 같은 스크립트를 작성하는 방법과
  • 아래에서 소개할 방법들이 존재했다.

개인적으로, 테스트 코드만을 보는 것만으로도 테스트 데이터들에 대해 알고, Assertions.assertThat(sectors).hasSize(2) 에서 왜 이 위치에 ‘2’가 와야 하는지에 대해 알 수 있는 코드를 선호한다.

Assertions.assertThat(payments).hasSize(2);

에서 결제관련데이터를 우리가 2개 세팅하였기에, 여기에서는 ~~~ 한 시나리오 결과 2개를 갖고 있음을 검증해야함을 우리가 예상할 수 있어야 한다고 생각한다.

따라서 테스트 코드 자체에서 테스트 데이터를 세팅하는 방법을 원했기에 스크립트 사용방법은 일단 후순위로 미뤄두었다.

하지만 엔티티간의 관계가 매우 복잡해질 경우 아래와 같은 테스트 코드만으로는 한계가 존재할 것이고 이 경우 스크립트를 사용하거나, 추가적인 라이브러리를 사용하는 것을 고려해 봐야 할 것 같다. (참고)


@BeforeAll ?

  • BeforeAll 의 경우, 테스트 메서드 단위로 실행되는 메서드가 아니다. 각 Junit test method 메서드가 시작되기전에 시작되는 트랜잭션과는 별개로 실행된다.

하지만 @BeforeAll 를 붙이기 위해서는 static 메서드를 선언해야 한다.

정적 메서드가 실행되는 시점에는 @Autowired 로 주입받아와야 하는 필드들이 Null 상태이므로 이를 사용할 수 없었다.


@BeforeEach, @AfterEach 에서 TransactionTemplate 사용하기

따라서 가장 처음 든 생각은 TransactionTemplate 을 사용해 programmtically 하게 트랜잭션을 사용하는 방식이었다.

@BeforeEach 및 @AfterEach 에서는 기존에 존재하는 Test Managed Transaction 과 별도의 트랜잭션을 생성하여 사용하도록 해야 하기 때문에 다음과 같이 영속성 전파를 PROPAGATION_REQUIRES_NEW 로 설정해주었다.

    @Autowired
    private PlatformTransactionManager transactionManager;

    protected TransactionTemplate transactionTemplate;

    @BeforeEach
    void setTransactionTemplate() {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }

그리고는 @BeforeEach 및 @AfterEach 에서는 transactionTemplate 만을 사용하여 test data 들을 save 하였다.

결과적으로 여기선 트랜잭션이 분리되었기에 Test-managed transaction 의 영속성 컨텍스트에는 find 해오고자 한 엔티티가 존재하지 않기에 쿼리를 직접 날리는 것을 볼 수 있었다.

  • 하지만 이로인해 코드가 상당히 더러워짐을 볼 수 있었다. 이는 콜백을 사용하고 있기 때문이다.
    • 하나의 엔티티를 저장할 때 마다 아래와 같은 코드를 작성해야 한다.
				transactionTemplate.execute(status -> {
            foodSector = superSectorRepository.save(SuperSector.builder().name("음식").build());
            return null;
        });

TestEntityManager 사용하기

@DataJpaTest 사용시 TestEntityManager 를 주입받아와 사용할 수 있다.

이 경우 코드는 TransactionTemplate 사용할 때에 비해 매우 깔끔해진다.

foodSector = em.persist(SuperSector.builder().name("음식").build());
  • 반드시 @BeforeEach 에서는 영속화 시킨 데이터들을 flush 및 clear 를 해 주어야 한다.
  • 이는 앞선 TransactionTemplate 을 사용할 때와 달리, 동일한 Test managed Transaction 을 사용하지만, 영속성 컨텍스트를 비워주기 때문에 원하는 쿼리가 날아가는 지 확인할 수 있는 것이다. 

추가로 나는 RepositoryTest 에 사용할 공통 클래스를 선언해 두고 사용하도록 하는 부분을 만들어가고 있었다.

따라서 다른 팀원분들이 RepositoryTest 를 작성할 경우에는 @BeforeEach 에서 em.flush() 및 em.clear() 호출을 깜빡하는 실수가 생길 수 있는 부분이라 생각하였다.

이 부분은 템플릿 메소드 패턴을 통해 해결할 수 있었다.


결론

아직 나만의 생각이라 이런 방법이 맞는지 모르겠다.

Jpa 를 사용하며 커스텀 메서드를 만들 때면 테스트만으로 내가 생각하는 쿼리가 나오고 이에 대한 결과를 확인하고 싶다.

실제로 DB 로의 연결을 하고 쿼리가 날아가는 것까지 테스트하기 위한 방법으로 @SpringBootTest, @DataJpaTest 의 두 가지 방법이 존재하는 것 같았다. 이 중 SpringBootTest 의 경우 JpaRepository 테스트를 위한 의존성 외의 불필요한 많은 spring bean 들을 로드하기에 시간이 오래 걸린다. 따라서 Spring 에서 제공하는 slice 테스트를 위한 @DataJpaTest 를 사용해 테스트 시간을 줄이고자 했다.

@DataJpaTest 를 사용할 경우, 테스트의 독립성을 위해 Test 트랜잭션이 사용된다. 테스트 트랜잭션 은 자동으로 롤백 되고 @BeforeEach 는 Test Transaction 내에서 실행되는 것으로 인해 내가 테스트 하는 목적이 달성되지 않고 있었다.

이로 인해 세 가지 방법에 대해 알아보았다.

개인적 선호에 의해 Test data script 를 사용하는 것은 구미가 당기지 않았고 결과적으로

  • PlatformTransactionManager 를 사용하여 @BeforeEach 와 @AfterEach 에 대해 별개의 트랜잭션을 사용하도록 하는 방법 (TransactionTemplate 을 사용하여 Programmatically use transaction 하는 방법)
  • TestEntityManager 를 사용하는 방법

중에 선택하고자 하였다.

 

사실상 서비스 되지 않는.. 프로토타입 과 같은 것을 개발하고 있기 때문에 위와 같은 방식을 시도한 것은 작은 테스트에 불과할지도 모른다.

다른 좋은 방법이나 위와 같은 것이 필요하지 않다면 이에 대해 의견을 남겨주시는 누군가가 있다면 좋을 것 같다

 

참조

https://www.arhohuttunen.com/spring-boot-datajpatest/ 

 

Testing the Persistence Layer With Spring Boot @DataJpaTest | Code With Arho

Learn how to test Spring Boot repositories using @DataJpaTest. Learn what should be tested on the persistence layer and how.

www.arhohuttunen.com

https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#testcontext-tx-attribute-support

 

Testing

The classes in this example show the use of named hierarchy levels in order to merge the configuration for specific levels in a context hierarchy. BaseTests defines two levels in the hierarchy, parent and child. ExtendedTests extends BaseTests and instruct

docs.spring.io

 

'JPA' 카테고리의 다른 글

[JPA]EntityManager merge와 persist  (0) 2022.05.10
복사했습니다!