❌ 문제 상황
테스트 시에 각각 개별 테스트는 통과했지만, 모든 테스트를 한 번에 돌리니 오류가 발생했다.
대충 요약하면, 이미 DB에 테이블이 초기화가 되어 있는데
DDL을 다시 재실행해서 테이블을 생성하려고 하니 오류가 발생한 것이다.
왜 이런 상황이 발생하는 것일까?
엄청 많이 파봤지만, 문제가 발생하는 정확한 원인을 찾지 못해서 깊게 파보면서 대충 예상해본 원인을 말해보겠다!
(그래서 틀릴 수도 있다 ㅠㅠ)
🎯 @SpringBootTest의 ApplicationContext 생성
스프링에서는 @SpringBootTest을 실행하면 ApplicationContext를 생성해서 모든 스프링 빈들을 등록한다.
이때, 모든 테스트를 돌릴 때 @SpringBootTest를 실행할 때마다 ApplicationContext를 생성할까?
답은, WebEnvrionment 속성이 같다면 새로 생성하지 않고 처음 생성한 ApplicationContext를 사용한다.
WebEnvironment 속성이 달라질 때 새로 ApplicationContext를 생성하게 된다.
이러한 특성때문에 문제 상황이 발생한 것이다.
※ 말랑신의 댓글 피드백 - webEnvironment가 같다고 항상 캐시되는 것은 아니다!
(맨 아래에 내용을 추가해놓았다!)
🎯 문제 원인 파헤치기
더 자세한 원인을 파악하기 위해 중요한 점은, 다음과 같다.
1. webEnvironment별로 ApplicationContext가 처음 생성될 때 'ContextCache'에 저장된다.
2. 따라서, webEnvironment가 같을 때 캐싱된 ApplicationContext가 없다면 생성하고,
있다면 캐싱된 ApplicationContext를 사용한다.
3. ApplicationContext가 처음 생성될 때마다 DB Connection을 연결한다.
4. 여러 테스트를 한 번에 실행할 때 각 테스트들이 실행될 때마다 DB Connection이 끊어지는 것이 아니라,
모든 테스트가 종료했을 때 DB Connection이 끊어진다.
현재 애플리케이션의 @SpringBootTest의 환경은 다음과 같은 2가지가 존재했다.
1. Mock 환경 Test1 - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
2. Mock 환경 Test2 - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
3. RANDOM_PORT 환경 Test3 - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
각각 테스트가 실행될 때 동작을 살펴보자.
✅ 1. Mock 환경 Test1
1-1. Mock 환경 Test가 처음 실행되므로, ApplicationContext가 새로 생성되어 ContextCache에 저장된다.
1-2. ApplicationContext가 새로 생성될 때, SpringBoot가 application-properties에 설정된 DB 정보로
DataSource를 생성하고 DB Connection을 연결하고, DDL을 실행하여 DB를 초기화한다.
✅ 2. Mock 환경 Test2
2-1. 이미 실행된 Mock 환경 Test(Test1)가 있으므로, ContextCache에 저장된 ApplicationContext를 사용한다.
이때, 캐싱된 ApplicationContext를 사용하기 때문에 DB Connection도 등록된 Connection을 사용하여
DDL이 실행되지 않는다.
✅ 3. Random_Port 환경 Test3
3-1. Random_Port 환경 Test가 처음 실행되므로, ApplicationContext가 새로 생성되어 ContextCache에 저장될 때, 에러가 발생한다.
ApplicationContext 생성 시에 SpringBoot가 application-properties에 설정된 DB 정보로
DataSource를 생성하고 DB Connection을 연결하고, DDL을 실행하려고 할 때 에러가 발생한다.
왜냐하면 이미 1, 2번에서 생성되어 사용한 DB를 참조하여 DDL을 실행하기 때문이다.
이미 테이블이 만들어진 상태인데, 또 schema.sql의 DDL을 실행하기 때문에 에러가 발생하는 것이다.
이러한 상황 및 문제 원인 요약을 그림으로 나타내보았다.
이제, 문제 원인을 알았으니 해결 방법을 살펴보자.
✅ 해결 방안 1 : DDL(schema.sql)에 IF NOT EXISTS 사용
※ schema.sql
CREATE TABLE IF NOT EXISTS RACE_RESULT
(
id INT NOT NULL AUTO_INCREMENT,
trial_count INT NOT NULL,
winners VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS CAR
(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
position INT NOT NULL,
play_result_id INT NOT NULL,
PRIMARY KEY (id)
);
이렇게 설정해주면, ApplicationContext가 새로 생성될 때 DDL을 실행하기는 하지만,
테이블이 이미 존재하므로 테이블이 생성되지 않아서 오류가 발생하지 않는다.
✅ 해결 방안 2 : @AutoConfigureTestDatabase
해결방안 1은 @SpringBootTest의 WebEnviroment 환경이 달라졌을 때
같은 인메모리 DB를 사용하는 ApplicationContext를 새로 생성하긴 하지만, DDL 자체를 바꿔서 오류를 해결한 방법이다.
@AutoConfigureTestDatabase는 어노테이션이 붙은 클래스의 테스트를 실행하면
application-properties에 정의된 DataSource가 아닌,
auto-configure된 DataSource로 생성된 인메모리 DB를 사용하는 방식이다.
따라서, 해당 테스트의 인메모리 DB 자체가 변경되는 것이다.
실제 로그를 남겨보면 다음과 같다.
※ application-properties의 DataSource를 사용한 인메모리 DB URI
※ @AutoConfigureTestDatabase를 사용한 인메모리 DB URI
✅ 5/4 내용 추가
@AutoConfigureTestDatabase를 사용하면 ApplicationContext별로
그때마다 DB를 새로 생성하는 것이 아니라, 미리 auto-configure 해둔 DataSource를 사용한다!
예시를 들면 다음과 같다.
ApplicationContext가 같은 여러 테스트를 한 번에 돌리면 모두 auto-configure 해둔 DataSource를 사용한다!
하지만, webEnvironment가 달라지는 등 ApplicationContext가 달라지면
auto-configure 해둔 DataSource도 달라져서 다른 DB를 사용하는 것을 알 수 있었다.
❓ DDL 수정 VS @AutoConfigureTestDatabase 중 무엇을 사용할까?
DDL을 수정하는 것은 테스트 코드를 위해서 실제 DB 로직을 수정하는 것이기 때문에 좋지 않다고 생각했다.
따라서, @AutoConfigureTestDatabase를 사용해서 문제를 해결하는 것이 좋다고 생각한다.
@AutoConfigureTestDatabase를 사용할 때도 환경이 다른 곳마다 어노테이션을 붙여줘야 하는 단점이 있다.
하지만 실제 DB 로직을 수정하는 것보다는 나을 것 같다고 개인적으로 생각한다!
※ 추가 내용 : 말랑신의 댓글 피드백 - webEnvironment가 같다고 항상 캐시되는 것은 아니다!
스프링 공식 문서에 따르면,
ApplicationContext를 구성하는 요소 중 다음과 같은 요소들이 달라지면 캐싱으로 재사용 될 수 없고,
ApplicationContext를 새로 생성한다.
* locations (from @ContextConfiguration)
* classes (from @ContextConfiguration)
* contextInitializerClasses (from @ContextConfiguration)
* contextCustomizers (from ContextCustomizerFactory)
– this includes @DynamicPropertySource methods as well as various features
from Spring Boot’s testing support such as @MockBean and @SpyBean.
* contextLoader (from @ContextConfiguration)
* parent (from @ContextHierarchy)
* activeProfiles (from @ActiveProfiles)
* propertySourceLocations (from @TestPropertySource)
* propertySourceProperties (from @TestPropertySource)
* resourceBasePath (from @WebAppConfiguration)
위의 공식문서에 따르면, webEnvironment가 같아도 ApplicationContext에 등록된 빈이나,
설정 정보 등이 달라지면 ApplicationContext가 새로 생성된다!
그래서 다음과 같이 @MockBean 등을 사용하여 등록된 빈의 정보가 다르게 되면,
webEnvironment가 같더라도 ApplicationContext를 새로 생성한다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class WebRaceServiceTest {
...
}
---
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class RaceResultDaoTest {
@MockBean
private WebRacingCarController webRacingCarController;
}
그래서 결론은,
'webEnvironment'가 ApplicationContext가 재사용될지를 정하는 기준이 아니라는 점이다!
정확히 말하면, 'webEnvironment'가 변하면 ApplicationContext가 재사용될지를 정하는 요소가 변경되어
ApplicationContext가 새로 생성되는 것이다!
이와 관련해서, 어떤 요소가 변경되어 ApplicationContext가 새로 생성되는지 궁금해서 ChatGPT에게 물어봤다.
MOCK -> RANDOM_PORT 변경 시
✅ 위의 요소 중 resourceBasePath가 변경되어 ApplicationContext가 새로 생성
MockMvc는 Servlet API를 사용하는 것이 아니라, mocking하기만 한다.
MOCK -> RANDOM_PORT가 되면,
MockMvc 대신 TestRestTemplate과 같은 Servlet API를 직접 이용하는 RestTemplate을 사용해야한다.
RestTemplate은 서블릿 컨테이너를 통해 HTTP 요청을 보내야 하므로,
resourcePath에 src/main/webapp 디렉토리와 같은 웹 리소스가 존재해야 한다.
따라서, resourceBasePath가 변경되어 ApplicationContext가 새로 생성되는 것이다.
이렇듯, 환경이 변경될 때 각 환경마다 ApplicationContext를 재사용하는 요소 중 하나가 변경되어
ApplicationContext가 새로 생성되는 것을 알 수 있었다!!
이러한 원리에 대해서 자세하게 알지 못했는데 공식 문서 링크까지 남겨서
공부하게 해준 말랑에게 감사하다!!! 🙇🏻♂️🙇🏻♂️🙇🏻♂️🙇🏻♂️🙇🏻♂️
'Spring > 기타' 카테고리의 다른 글
[Spring] 환경별 구성 다르게 하기 : 환경별 프로필 설정하기 & 프로필 활성화 하기(@Profile, @ActiveProfiles, application-properties 설정) (2) | 2023.05.20 |
---|---|
[IntelliJ] 인텔리제이에서 .http로 HTTP 요청 보내기 (0) | 2023.05.14 |
[Spring] @ResponseBody VS ResponseEntity<T> : 무엇을 사용할까? (1) | 2023.04.16 |
[Spring] JdbcTemplate 스프링 빈은 어떻게 자동으로 등록될까?(feat.DataSource) (4) | 2023.04.16 |
[Spring] @Valid 여러 검증 어노테이션 검증 순서 설정 방법 (1) | 2023.01.24 |