website: 테스트 16개 @DirtiesContext에서 @Transactional로 바꿔서 CI 시간 줄이기
CI에서 백엔드 유닛 테스트 샤드 하나가 유독 오래 걸리는 게 계속 눈에 걸렸다. 로컬에서 시간을 재보니 대상 16개 클래스가 합쳐서 271.7초, 4분이 넘게 걸리고 있었다.
원인을 찾아보니 컨트롤러/서비스 테스트 클래스마다 @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)가 붙어 있었다. 이 어노테이션은 테스트 메서드가 끝날 때마다 스프링 컨텍스트를 통째로 버리고 다음 테스트에서 새로 띄우게 만든다. @SpringBootTest + @AutoConfigureMockMvc 조합이라 컨텍스트 하나 띄우는 비용 자체가 작지 않은데, 그걸 메서드마다 반복하고 있었으니 시간이 쌓일 수밖에 없었다.
애초에 이 어노테이션이 붙은 이유는 DB 상태를 테스트 간에 격리하기 위해서였다. 각 테스트가 @WithMockAdminUser(id = 1L, ...) 같은 걸로 고정된 ID를 기대하는 경우가 많아서, 이전 테스트가 INSERT한 데이터가 남아있으면 auto-increment id가 어긋나 깨지는 구조였다. 그래서 컨텍스트를 매번 새로 만들어 DB까지 초기화하는 무거운 방식을 택했던 것 같다.
여기서 컨텍스트를 매번 새로 만드는 것과 DB 상태를 초기화하는 건 별개의 문제라는 걸 다시 확인했다. DB만 정리하면 되는 거면 트랜잭션 롤백으로 충분하다. @Transactional을 테스트 클래스에 붙이면 각 테스트 메서드가 트랜잭션 안에서 실행되고, 끝나면 자동으로 롤백되면서 다음 테스트는 항상 깨끗한 상태에서 시작한다. 컨텍스트는 캐싱되어 재사용되니 그 비용이 사라진다.
다만 이게 안전한 조건이 있다. MockMvc를 인프로세스로 호출하는 테스트여야 한다는 것 — 즉 실제 HTTP 요청을 별도 스레드나 서버로 보내는 게 아니라 테스트 스레드 안에서 컨트롤러를 직접 호출하는 구조여야, 트랜잭션 경계와 테스트 스레드가 일치해서 롤백이 제대로 먹힌다. 대상 16개 클래스를 하나씩 확인해서 전부 MockMvc 인프로세스 방식이라는 걸 확인한 다음에 전환했다. import를 org.springframework.test.annotation.DirtiesContext에서 org.springframework.transaction.annotation.Transactional로 바꾸고, 클래스 어노테이션과 관련 주석("@DirtiesContext로 DB가 매 테스트마다 초기화되므로..." → "@Transactional로 테스트마다 롤백되므로...")만 고치면 되는 단순한 변경이었다. 파일당 diff는 몇 줄 안 되지만 16개 클래스에 걸쳐 있어서 추가/삭제 각각 33줄 정도였다.
전환 후 측정해보니 같은 16개 클래스가 22.9초로 줄었다. 컨텍스트 재기동 비용이 얼마나 컸는지 체감되는 수치였다.
한 곳은 의도적으로 제외했다. RecruitmentManagementControllerTest는 RecruitmentOpenEmailEventListener가 @Async + @TransactionalEventListener(phase = AFTER_COMMIT)로 동작하는데, 이 리스너를 mock 처리하지 않고 실제로 쓰고 있었다. @Transactional로 테스트를 감싸면 테스트 자체의 트랜잭션은 테스트가 끝난 뒤에야 롤백되기 때문에, AFTER_COMMIT 시점에 발생해야 할 커밋 신호가 테스트 진행 중에는 오지 않는다. 그러면 verify(..., timeout(...))로 비동기 이벤트 처리를 검증하는 부분이 타임아웃으로 깨진다. 트랜잭션 경계에 의존하는 이벤트 리스너와 테스트 트랜잭션 롤백 방식은 근본적으로 상충하는 조합이라, 이 클래스만 기존 @DirtiesContext 방식을 그대로 남겨뒀다.
전체를 일괄 전환하기보다 각 클래스가 실제로 어떤 방식으로 동작하는지 — 특히 비동기/이벤트 기반 부수 효과가 있는지 — 확인하고 나눠서 처리한 게 맞았다고 본다. 나중에 비슷한 최적화를 할 때도 "테스트가 트랜잭션 경계 밖의 부수 효과에 의존하는지"를 먼저 체크하는 게 필요할 것 같다.