[스프링/테스트] 스프링의 테스트 컨텍스트 프레임워크와 트랜잭션
테스트 컨텍스트 프레임워크에서 트랜잭션은 테스트 클래스에 @TestExecutionListeners
를 명시적으로 선언하지 않더라도 기본적으로 구성된 TransactionalTestExecutionListener
에 의해 관리된다. 하지만 트랜잭션 지원을 활성화하려면 @ContextConfiguration
에 의해 로드된 ApplicationContext
에 PlatformTransactionManager
빈을 구성해야 하며, 테스트 클래스나 메서드 수준에서 @Transactional
을 선언해야 한다. @Transactional
가 적용된 테스트 클래스 내에서 수행되는 트랜잭션은 TransactionalTestExecutionListener
에 의해 자동으로 롤백되므로 트랜잭션 내에서 테스트 메서드를 실행한 후 직접 해당 트랜잭션을 롤백하는 코드는 필요하지 않다. 테스트 컨텍스트 프레임워크에 의해 기본적으로 수행되는 이러한 트랜잭션 관리 동작을 테스트 컨텍스트 프레임워크가 제공하는 @Commit
또는 @Rollback
을 사용하여 변경하거나 TestTransaction
을 사용하여 보다 세밀하게 사용자화할 수도 있다.
TransactionalTestExecutionListener
에 의해 자동으로 관리되거나 TestTransaction
을 통해 프로그래밍 방식으로 관리되는 트랜잭션을 테스트 관리(test-managed) 트랜잭션이라고 한다. 테스트 관리 트랜잭션은 테스트를 위해 로드된 ApplicationContext
내에서 스프링에 의해 직접 관리되는 트랜잭션인 스프링 관리(spring-managed)(또는 컨테이너 관리(container-managed)) 트랜잭션과, 테스트 시 호출되는 애플리케이션 코드 내에서 프로그래밍 방식으로 관리되는 트랜잭션인 애플리케이션 관리(application-managed) 트랜잭션과 다르다. 스프링 관리 및 애플리케이션 관리 트랜잭션은 일반적으로 테스트 관리 트랜잭션에 참여하지만 스프링 관리 또는 애플리케이션 관리 트랜잭션이 REQUIRED
또는 SUPPORTS
이외의 트랜잭션 전파 속성으로 구성된 경우 주의해야 한다.
TestTransaction
의 정적 메서드를 사용하면 프로그래밍 방식으로 테스트 관리 트랜잭션을 관리할 수 있다. 예를 들어, 테스트 메서드 실행 전, 실행 중, 실행 후에 TestTransaction
을 사용하여 현재 테스트 관리 트랜잭션을 수동으로 시작 또는 종료하거나 롤백 또는 커밋할 수 있다. TransactionalTestExecutionListener
가 활성화되어 있다면 TestTransaction
의 기능은 별다른 설정 없이 자동으로 사용 가능하다.
테스트 메서드(예: JUnit Jupiter의 @Test
를 설정한 메서드)에 @Transactional
을 설정하면 기본적으로 테스트가 완료된 후 자동으로 롤백되는 트랜잭션 내에서 테스트 메서드가 실행된다. 테스트 클래스에 @Transactional
을 설정하는 경우, 해당 클래스 계층 구조 내의 모든 테스트 메서드는 하나의 트랜잭션 내에서 실행된다. 클래스 또는 메서드 수준에서 @Transactional
을 설정하지 않은 테스트 메서드는 트랜잭션 내에서 실행되지 않으므로 해당 메서드에서 이루어지는 데이터베이스 변경 작업은 반영 또는 취소되지 않는다.
중요한 점은 @Transactional
은 모든 테스트 클래스 내 모든 메서드에 대해 지원되지 않는다는 점이다. 테스트 프레임워크가 제공하는 테스트 수명 주기(test lifecycle) 메서드에 대해서는 @Transactional
을 설정하더라도 트랜잭션(테스트 관리 트랜잭션)이 적용되지 않는다. 예를 들어, JUnit Jupiter의 @BeforeAll
, @BeforeEach
@AfterAll
또는 @AfterEach
등의 어노테이션이 설정된 테스트 메서드가 이에 해당한다. 또한 @Transactional
이 설정되었지만 트랜잭션 전파 속성이 NOT_SUPPORTED
또는 NEVER
로 설정된 테스트 메서드는 트랜잭션 내에서 실행되지 않는다. 따라서 위의 경우 TransactionalTestExecutionListener
에 의해 자동으로 롤백되지 않는다.
메서드 수준의 수명 주기 메서드(예: JUnit Jupiter의 @BeforeEach
또는 @AfterEach
가 설정된 메서드)는 테스트 관리 트랜잭션 내에서 실행되는 반면, 클래스 수준의 수명 주기 메서드(예: JUnit Jupiter의 @BeforeAll
또는 @AfterAll
이 설정된 메서드와 TestNG의 @BeforeSuite
, @AfterSuite
, @BeforeClass
또는 @AfterClass
가 설정된 메서드)는 테스트 관리 트랜잭션 내에서 실행되지 않는다. 따라서 메서드 수준의 수명 주기 메서드에서 TestTransaction
을 사용하여 프로그래밍 방식으로 테스트 관리 트랜잭션을 관리할 수 있지만 클래스 수준 수명 주기 메서드에서는 불가능하다.
트랜잭션 내에서 클래스 레벨 수명 주기 메서드를 실행해야 하는 경우, PlatformTransactionManager
를 테스트 클래스에 주입한 후 TransactionTemplate
을 사용하여 프로그래밍 방식의 트랜잭션(애플리케이션 관리 트랜잭션) 관리를 수행할 수 있다.
특정 테스트 메서드는 트랜잭션 내에서 실행되지 않는 것이 의도될 수도 있다. 예를 들어, 트랜잭션이 적용된 테스트 코드를 실행하기 전에 초기 데이터베이스 상태를 확인하거나, 트랜잭션을 커밋하는 테스트 코드 실행 후 예상된 트랜잭션 커밋 동작을 확인하는 경우 이를 위해 트랜잭션이 적용된 테스트 메서드 실행 전후에 트랜잭션 외부에서 특정 코드를 실행할 필요성이 있다. 이러한 상황에서는 TransactionalTestExecutionListener
가 제공하는 @BeforeTransaction
및 @AfterTransaction
을 사용하면 된다. 테스트 클래스의 void
메서드 또는 테스트 인터페이스의 void
기본 메서드에 두 어노테이션 중 하나를 사용할 수 있으며, 이 경우 TransactionalTestExecutionListener
는 트랜잭션 전 메서드 또는 트랜잭션 후 메서드가 적절한 시점에 실행되도록 보장한다.
모든 테스트 메서드 실행 전 메서드(예: JUnit Jupiter의 @BeforeEach
메서드)와 모든 실행 후 메서드(예: JUnit Jupiter의 @AfterEach
메서드)는 트랜잭션 내에서 실행된다. 모든 실행 전 메서드와 모든 실행 후 메서드에 대해 @BeforeTransaction
또는 @AfterTransaction
을 적용할 수 있다. 트랜잭션 내에서 실행되도록 구성되지 않은 테스트 메서드에 대해서는 @BeforeTransaction
또는 @AfterTransaction
가 적용되지 않는다.
@BeforeEach
메서드에서 데이터베이스의 데이터 초기화 작업(테스트에 필요한 데이터를 삽입 또는 갱신) 및 커밋 후 테스트 트랜잭션을 종료하지 않으면 이후 실행되는 테스트 메서드에서 커밋된 초기화 데이터를 확인할 수 없다(트랜잭션 격리 수준이 커밋되지 않은 읽기라면 가능하다). @BeforeEach
메서드는 메서드 수준의 수명 주기 메서드이므로 테스트 관리 트랜잭션 내에서 실행되지만 @Transactional
이 적용되지 않으므로 메서드 종료 시 테스트 트랜잭션이 롤백되지 않는다. 따라서 데이터베이스의 데이터를 초기화하는 테스트 메서드에서는 테스트 트랜잭션을 직접 커밋 후 종료하고, 이후 데이터를 확인하는 테스트 메서드에서는 새로운 테스트 트랜잭션을 시작하면 된다. 초기 데이터를 저장하는 작업은 @BeforeTransaction
을 사용하여 테스트 트랜잭션 외부에서 실행할 수 없다.
@BeforeEach
void init() {
데이터베이스 데이터 초기화 및 커밋 작업;
TestTransaction.flagForCommit();
TestTransaction.end();
}
@Test
@Transactional
void test() {
TestTransaction.start();
데이터베이스 데이터 확인 작업;
}
테스트 메서드에 @Transactional
적용하는 것의 문제점
데이터베이스 변경 작업을 단위 및 통합 테스트하기 위해 테스트 클래스 및 메서드에서 해당 작업을 수행할 수 있다. 이때, 실제 운영 데이터베이스에 대한 테스트를 수행하는 경우 운영 데이터 변경을 막기 위해 스프링의 테스트 컨텍스트 프레임워크가 지원하는 트랜잭션 자동 롤백 기능을 사용하면 된다. 테스트 메서드에 @Transactional
을 적용하는 경우 테스트 메서드 실행 후 해당 트랜잭션(테스트 관리 트랜잭션) 내 모든 데이터베이스 변경 작업은 롤백된다.
JPA를 사용하는 경우 영속성 컨텍스트가 닫힌 상태가 되면 이후 연관 객체에 대한 지연 로딩(lazy loading)이 수행될 때 LazyInitializationException
예외가 발생하게 된다. 해당 예외 발생을 막기 위해 연속성 컨텍스트가 닫힌 상태이더라도 연관 객체 조회 시 지연 로딩 대신 즉시 로딩을 사용하거나, 엔티티 그래프 기능을 사용할 수 있다. 주의할 점은 즉시 로딩 사용 시 JPA는 내부적으로 최적화를 위해 조인을 사용하지만 JPQL 사용 시 N+1 문제가 발생할 수 있다는 것이다. 성능 문제를 보완하기 위해 JPQL의 페치(fetch) 조인을 사용하거나 하이버네이트의 기능 지원을 통해 IN
절 또는 서브쿼리를 사용할 수도 있다.
LazyInitializationException
예외 발생을 막는 또다른 방법으로는 영속성 컨텍스트가 열린 상태에서만 연관 객체의 지연 로딩이 수행되도록 하는 것이다. 즉, 트랜잭션이 종료될 때까지 영속성 컨텍스트가 열린 상태를 유지하도록 한다. 트랜잭션에 결합되는 영속성 컨텍스트를 트랜잭션 범위 영속성 컨텍스트(transaction-scoped persistence context)라고 한다. 트랜잭션 범위 영속성 컨텍스트에서는 트랜잭션이 종료되는 즉시 영속성 컨텍스트에 있는 엔티티가 데이터베이스로 플러시(flush)된다. 테스트 메서드가 완료될 때까지 영속성 컨텍스트를 열린 상태로 만들고 테스트 메서드가 완료되어 트랜잭션이 종료되면 영속성 컨텍스트도 닫히도록 하면 되며, 이를 위해 테스트 메서드 레벨에 @Transactional
을 적용하면 된다.
@Transactional
을 적용한 테스트 메서드가 테스트 통과되어 데이터베이스 변경 작업이 성공적으로 완료됨을 성공적으로 테스트하였더라도 실제 서비스 동작 시에는 LazyInitializationException
예외가 발생할 수 있다. 따라서 영속성 컨텍스트가 열린 상태에서 연관 객체에 대한 지연 로딩이 이루어지도록 서비스 코드 레벨에도 @Transactional
을 적용하거나 메서드를 호출하는 상위 레벨에서 영속성 컨텍스트가 열린 상태로 적절하게 관리되도록 해야 한다.
참고
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/context/transaction/TransactionalTestExecutionListener.html
- https://docs.spring.io/spring-framework/reference/testing/testcontext-framework.html
- https://www.baeldung.com/spring-test-programmatic-transactions
- https://www.baeldung.com/java-jpa-lazy-collections
- https://www.baeldung.com/jpa-hibernate-persistence-context#transaction_persistence_context
Comments