들어가면서
이번 글에서는 개발하면서 만난 이슈를 해결해 가는 과정을 공유하겠다.
Issue
서버 상태를 확인해주는 HealthCheck API를 만들었다. 이 때 ServletWebServerApplicationContext를 이용해 서버 포트 번호를 확인했다.
@RestController
@RequestMapping("/health")
class HealthCheckController(
private val webServerAppCtxt: ServletWebServerApplicationContext,
) {
@GetMapping("/info")
fun serverInfo(request: HttpServletRequest) = mapOf(
"IPAdress" to request.getHeader("X-FORWARDED-FOR"),
"Port" to webServerAppCtxt.webServer.port.toString()
)
}
개발을 끝내고 테스트를 작성했다.
@SpringBootTest
@AutoConfigureMockMvc
internal class HealthCheckControllerTest(
private val mockMvc: MockMvc,
) : FunSpec({
test("get /health/info should return server info") {
mockMvc.get("/health/info").andExpect {
status { isOk() }
jsonPath("\$.port") { value(8080) }
}.andReturn()
}
})
하지만 예상과 달리 테스트가 실패했다. 에러 로그는 아래와 같았다.
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
로그 내용은 ServletWebServerApplicationContext 타입을 가진 빈을 찾을 수 없다는 것이었다. 하지만!! 로컬에서 실행하면 정상적으로 실행됐다.
Why?
문제의 원인은 Spring Container의 구현체가 테스트 환경과 로컬이 다르기 때문이라고 예상됐다. 가설을 검증하기 위해 디버거를 이용해 각 환경에서 Spring Cotainer 구현체가 무엇인지 확인했다.
로컬 환경에서 Spring Context
ServletWebServerApplicationContextFactory에 의해 AnnotationConfigServletWebServerApplicationContext 구현체가 사용된다.
AnnotationConfigServletWebServerApplicationContext의 Hierarchy는 다음과 같다.
AnnotationConfigServletWebServerApplicationContext는 ServletWebServerApplicationContext를 상속했다.
테스트 환경에서 Spring Context
SpringBootContextLoader에 의해 GenericWebApplicationContext 구현체가 사용된다.
GenericWebApplicationContext의 Hierarchy는 다음과 같다.
나의 가설이 맞았다! 테스트 환경에서는 Spring Container의 구현체로 GenricWebApplicationContext가 사용돼 ServletWebServerApplicationContext를 찾을 수 없어 테스트가 실패했던 것이였다.
차이가 발생한 이유는 SpringBootContextLoader에 있었다.
테스트 환경에서 GenericWebApplicationContext를 구현체로 사용한 이유는, 서버가 EmbeddedWebEnvironment를 사용하지 않고, MockWebEnvironment를 사용했기 때문이다.
@SpringBootTest를 사용하면 기본값이 MockWebEnvironment이다.
The type of web environment to create when applicable. Defaults to SpringBootTest.WebEnvironment.MOCK.
해결
테스트 환경에서 EmbeddedWebEnvironment를 사용하도록 수정했다. (WebEnvironment.RANDOM_PORT와 WebEnvironment.DEFINED_PORT는 EmbeddedWebEnvironment를 사용한다.)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
internal class HealthCheckControllerTest(
private val mockMvc: MockMvc,
) : FunSpec({
...
})
그 결과 테스트에 성공했다.
정리
이번 글을 통해 내가 만났던 이슈와 해결해 가는 과정을 공유했다. 문제 해결 과정에서 WebEnvironment가 Spring Container 구현체 선택에 영향을 준다는 사실을 배울 수 있었다.