포스트

[Test] 테스트에서 @Transactional을 써도 될까

0. 서론

최근 우리 Moment의 기능을 추가하던중 기묘한 현상을 마주했다.

임시 질문을 사용하면 상태가 true로 업데이트되어야 하는데 프로덕션 환경에서는 이 처리가 되지 않았다. 애플리케이션 로그에서는 함수가 정상적으로 실행됐지만, DB로 쿼리가 날아가지 않은 것이다.

더 황당한 점은, 작성해둔 테스트 코드에서는 이 변경 사항을 정상적으로 감지하고 통과했다는 사실이다.

원인은 테스트 클래스에 달린 @Transactional어노테이션에 있었다.

1. 상황

문제의 1차적 원인은 내 코드의 실수였다. 상태 변수를 업데이트하는 메서드에 @Transactional 을 달아주지 않았다. 또한 이 메서드를 호출한 상위 메서드는 외부 API를 불러오는 역할을 하는 Facade 메서드라 @Transactional 을 달지 않았다.

결과적으로 상태를 변경하는 객체는 영속성 컨텍스트의 관리를 받는 ‘영속 상태’가 아니었고, 더티 체킹이 발생하지 않았다.

근데 왜? 테스트는 통과한걸까?

2. 테스트에서 @Transactional 이 하는 일

테스트에서 @Transactional 은 테스트 간의 데이터 격리를 위한 롤백의 용도로 흔히 사용된다. 테스트 메서드에 @Transactional 이 달리면 테스트가 실행되는 스레드는 하나의 트랜잭션 스코프로 묶이게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@Transactional
void A가_일어나면_B를_반환한다(){
	//given
	xxRepository.save(something);
	
	//when
	Object ob = xxService.doSomething(); // 이 메서드에 @Transactional 누락
	
	//then
	assertThat(ob.isXX()).isTrue();
}

이 시나리오에서 doSomething()@Transactional 이 없더라도, 이미 부모(테스트 메서드)가 트랜잭션을 시작했기 때문에 자연스럽게 그 트랜잭션에 참여하게 된다.

즉, 테스트 환경에서는 영속성 컨텍스트가 살아서 유지된다. 그 결과, 실제 DB에 update 쿼리가 커밋되지 않고 롤백되더라도, 메모리 상의 1차 캐시에서는 더티 체킹이 동작해 객체의 상태가 변경된다. assertThat은 이 메모리 상의 변경된 객체를 검증하기 때문에 테스트가 교묘하게 통과하게 됐다.

3. 해결방법

이러한 문제를 방지하려면 “테스트 환경은 최대한 프로덕션 환경과 동일해야 한다”는 원칙을 지켜야 한다. 우리 팀은 테스트 클래스에서 @Transactional을 제거했다. 대신, 테스트 간 데이터 전파를 막기 위해 DB 테이블을 초기화(Truncate)하는 유틸 클래스를 만들어 테스트 전후에 실행하도록 변경했다.

1
2
3
4
5
6
7
8
9
10
11
@BeforeEach
void setUp(){
	db.clean();
}

//... 테스트 코드 ...

@AfterEach
void tearDown(){
	db.clean();
}

4. 그럼 테스트에서 @Transactional 은 언제 사용할까?

테스트에서 @Transactional 은 절대악일까? 위 상황에서 얻은 교훈, “테스트 시나리오는 최대한 현실과 가까워야 한다.” 를 생각하면 비지니스 로직엔 가급적 사용하지 않는 것이 좋아보인다.

한 가지 예외, Repository 테스트에선 사용해도 좋을 것 같다. 스프링 팀에서도 테스트@Transactional을 사용하고 있다.

먼저, 비지니스 로직 테스트와 레포지토리 테스트는 “트랜잭션 경계 유무”라는 큰 차이점이 존재한다. 비지니스 로직에선 도메인 로직에 따라 트랜잭션 경계가 중요하지만, 레포지토레 레벨에선 중요하지 않다. 그렇기에 “빠른 데이터 롤백”의 이유로 적극적으로 사용해도 좋을 듯 싶다. 심지어 @DataJpaTest 어노테이션에 @Transactional 이 달려있다.

결론적으로, 테스트의 목적과 계층에 따라 트랜잭션 제어 방식을 명확하게 분리하는 것이 안전하고 신뢰성 있는 테스트 코드를 작성하는 길이다.



이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.