문제 상황
SpringBootTest를 사용하던 상황에서 db 롤백이 이루어지지 않아서 테스트 간 격리가 이루어지지 않는 상황이 발생하였는데요
이를 바탕으로 어떻게 test 마다 어떻게 Transactional 이 적용되는지, 그리고 어떤 경우에 롤백이 되지 않는지, 이를 해결하기 위한 방법은 어떤 것이 있는지에 대해서 정리해 보겠습니다
Test에서 Transactional 은 롤백된다
test에서 transactional 이 사용되면 자동적으로 롤백이 됩니다.
여기 적혀 있는 것처럼 db 호출은 자동적으로 rollback이 되어 매 테스트마다 깨끗한 db 상태를 유지할 수 있습니다
Junit과 함께 사용될 경우에 메서드마다 호출되는 @BeforeEach와, @AfterEach는 트랜잭션으로 묶여 있습니다
하지만 @BeforeAll, @AfterAll 같은 경우에는 transaction으로 묶이지 않습니다
SpringBootTest에서 rollback 이 되지 않는 경우가 있다
@SpringBootTest의 경우에는 기본적으로 @Transactional 이 없어서 롤백이 되지 않지만, @Transactional 어노테이션을 붙이면 롤백이 되는데요
이를 붙이더라도 2가지 종류의 SpringBootTest에서는 rollback 이 이루어지지 않습니다
1. @SpringBootTest(WebEnvironment.RANDOM_PORT)
2. @SpringBootTest(WebEnvironment.DEFINED_PORT)
이렇게 2가지는 스프링부트를 다른 스레드에서 실행시킵니다
2개의 스레드를 활용해 작동하는데
1. 우리가 Transactional 어노테이션을 붙인 스레드
2. 스프링부트가 돌아가는 스레드
1번 스레드에서 아무리 @Transactional을 붙이더라도, 2번 스레드에서는 있는지 여부를 확인할 수 없기에, 롤백을 시킬 수 없다는 문제가 처음 말씀드린 문제 상황의 원인인데요
롤백을 시키기 어렵다는 문제 때문에, 테이블 데이터를 없애는 방식으로 많이 사용하는데요
테이블 데이터를 합리적으로 제거하는 방법
여기서 말씀드리는 부분은 절대 정답이 아닙니다.
각각의 방식에는 분명히 장단점이 있습니다.
중요하게 생각하는 부분은 리뷰를 통해 받은 부분입니다.
그냥 truncate를 사용했을 때, 실수로 profile 이 바뀌면 진짜 db 가 날아갈 수 있어서 안전망이 필요하다
1. @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
이 어노테이션은 모든 테스트 메서드마다 애플리케이션 컨텍스트를 새로 만들게 됩니다.
장점 : embedded db(h2)를 사용했을 경우에, 자동으로 모든 데이터가 초기화가 됩니다
단점 : 성능상으로 컨텍스트를 만드는 비용이 많이 들어갑니다. 테스트 전체 시간이 오래 걸릴 수 있습니다
2. h2로 연결되어 있는지 확인한 이후에 truncate로 제거
방식이 2가지가 있는데요
1. @Sql로 실행하는 쿼리에 한 줄의 쿼리 추가
SELECT h2 version() from dual; 을 truncate 쿼리를 실행하기 전에 넣어둠으로써 다른 db에서는 실행이 되지 않도록 방어해 둘 수 있습니다.
장점 : 코드상에서는 Sql을 실행한다는 것만 추가하면 됩니다..
단점 : 매 truncate 쿼리마다 한 줄씩 다 들어가야 된다는 것이 중복으로 느껴질 수 있습니다
2. datasource url을 가져와서 h2 인지 확인하는 과정을 추가한다
datsSource url을 통해서 현재 연결된 db 가 h2 인지 확인하고, h2 가 아니라면 예외를 발생시킬 수 있습니다.
단점 : beforeEach로 테스트마다 들어가야 하기에, 상속 같은 것으로 중복을 제거할 수 있지만, 그럼에도 코드가 지저분해질 수 있습니다.
3. AutoConfigureTestDatabase
장점 : db 가 h2 임은 확실히 보장할 수 있다.
단점 : 테스트 메서드마다 rollback을 할 수 없기에, beforeEach 같은 작업은 여전히 들어가야 한다
4. TestExecutionLister를 사용한다
https://mangkyu.tistory.com/264
위 블로그에 있는 것처럼 단순히 annotation 만을 붙여주는 방식으로 truncate table을 작동시킨다
5. repository에서 직접 deleteAll 메서드를 만든다
단점 : 실제 db에서 실제로 호출되게 되면 모든 데이터가 한 번에 증발할 수 있다
6. dataSourceUrl 에 {uuid}를 추가한다
단점 : 매번 다른 db 를 사용하지만, class 단위로 작동하기에, 이 역시 클래스간의 격리는 힘들어보입니다
위와 같은 방식들이 있지만, 확실히 어떤 것이 가장 좋다고 말씀을 드리기는 쉽지 않네요
참고 링크
https://tecoble.techcourse.co.kr/post/2020-09-15-test-isolation/
https://mangkyu.tistory.com/264
https://stackoverflow.com/questions/12626502/rollback-transaction-after-test
'Spring' 카테고리의 다른 글
Application Context vs BeanFactory (0) | 2023.05.02 |
---|---|
DispatcherServlet 알아보기 - HttpServletBean편 (0) | 2023.04.26 |
DispatcherServlet 알아보기 - Servlet 편 (0) | 2023.04.22 |
DispatcherServlet 알아보기 - ServletConfig 편 (0) | 2023.04.19 |
DispatcherServlet 알아보기 - Aware 편 (6) | 2023.04.18 |