JAVA/JAVA | Spring 학습기록

[Spring] @SpringBootTest의 webEnvironment와 @Transactional

kth990303 2022. 8. 23. 18:08
반응형

우리는 @SpringBootTest 어노테이션을 이용하여 손쉽게 통합테스트를 진행할 수 있다. 하지만 @SpringBootTest의 webEnvironment 설정에 따라 테스트 격리가 잘 되지 않는 현상을 발견할 수 있다. 왜 그런 것일까?


@SpringBootTest의 webEnvironment

@SpringBootTest를 파고 들어가면 아래와 같이 webEnvironment 세팅을 볼 수 있다.

@SpringBootTest 어노테이션의 webEnvironment Enum

기본값은 MOCK이다. WebApplicationContext를 로드하지만, 내장된 서블릿 컨테이너가 아닌 MOCK 서블릿을 제공한다. 실제 서블릿 환경에서 테스트를 진행해보고 싶다면 RANDOM_PORT나 DEFINED_PORT로 진행해주어야 한다. RANDOM_PORT랑 DEFINED_PORT는 위 사진에서도 볼 수 있듯이 embedded값이 true여서 EmbeddedWebApplicationContext를 로드하기 때문이다. 

 

NONE은 아무런 서블릿 환경을 구성하지 않고 일반적인 ApplicationContext를 로드한다. 아직까지 NONE을 쓰는 경우는 못봤다.

 

RANDOM_PORT와 DEFINED_PORT는 임의의 port를 listen하는지와 application.yml에서 지정한 port를 listen하는지의 차이가 존재하고, 그 외에는 동일하다. 둘 다 실제 서블릿 환경을 제공해준다. 


@Transactional과 함께 쓸 때의 테스트 격리

주의할 점은 RAMDOM_PORT, DEFINED_PORT로 @SpringBootTest를 세팅해주면 @Transactional을 붙여주어도 롤백이 되지 않아 테스트 격리가 되지 않을 수 있다. 이는 RANDOM_PORT, DEFINED_PORT일 경우 별도의 스레드에서 스프링 컨테이너를 실행시키기 때문이다. 그렇기 때문에 @SpringBootTest, @SpringBootTest(webEnvironment = SpringBootTest.webEnvironment.NONE)일 경우는 @Trnasactional을 붙여주지만, @SpringBootTest(webEnvironment = SpringBootTest.webEnvironment.RANDOM_PORT)와 같은 경우는 @Transactional을 붙여주어도 의미가 없게 된다.

 

그렇다면 @Transactional로 편리하게 테스트 격리를 하기 위해 RANDOM_PORT, DEFINED_PORT를 지양하고 MOCK을 쓰는 것만이 좋은걸까?

그렇지도 않다.

실제와 유사한 서블릿 환경에서 테스트하기 위해선 RANDOM_PORT, DEFINED_PORT를 사용하는 것이 좋다

 

또한, api를 직접 통신하는 인수테스트의 경우 애플리케이션을 실행함과 동시에 RANDOM_PORT나 DEFINE_PORT가 아닌 환경으로 진행하면 아래 에러가 발생할 수 있다.

 

Could not resolve placeholder 'local.server.port' in value "${local.server.port}"

 

그렇기 때문에 @SpringBootTest(webEnvironment = SpringBootTest.webEnvironment.RANDOM_PORT), @SpringBootTest(webEnvironment = SpringBootTest.webEnvironment.DEFINED_PORT) 으로 사용해야 할 경우가 존재할 수 있다.

 

위 세팅으로 테스트 격리를 진행하고 싶으면 아래와 같은 방법들이 존재한다.

 

1. @AfterEach로 SQL문으로 TRUNCATE 시켜주는 방법을 사용하기

이 방법이 가장 좋을 듯하다. deleteAll과 같은 쿼리로 데이터를 지워주는 방법은 id를 공유하기 때문에 완전한 테스트 격리가 진행됐다고 보기 어렵다. 따라서 TRUNCATE로 테이블을 아예 비워주어 id 공유까지 막는 것이 가장 현명할 듯하다. @DirtiesContext를 이용한 방법보다 좋은 성능을 자랑한다.

 

외래키가 세팅돼있을 경우 테이블이 truncate되지 않을 수 있으므로 아래 명령어로 제약 조건을 무효화시켜주도록 하자.

SET REFERENTIAL_INTEGRITY FALSE

 

2. @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)로 테스트 메서드가 끝나면 매번 스프링 컨테이너를 재생성해주기

이 방법은 스프링 컨테이너를 아예 재생성하여 ApplicationContext 공유를 확실하게 막으므로 테스트 격리를 철저하게 지킬 수 있다. 하지만 @DirtiesContext는 엄청나게 느리다는 단점이 있다. 테스트 코드의 성능에 있어서 치명적이기 때문에 선호되지 않는 방법이긴 하다.


 

참고

 

반응형