한창 mockmvc로 컨트롤러에 API 요청을 날려 시나리오를 테스트하는 통합 테스트를 작성하는 중이었다.
늘 하던대로 예외를 잡는 코드를 assertJ로 작성했다.
//when & then
assertThatThrownBy { makeReservation(request, userB_AccessToken, userB_WaitingToken) }
.hasCauseInstanceOf(DuplicateResourceException::class.java)
그런데 예외가 잡히기는 하는데, 엉뚱한 SertvleException이 잡히는 것이 문제였다.
Expecting actual throwable to be an instance of:
kr.hhplus.be.server.exception.DuplicateResourceException
but was:
jakarta.servlet.ServletException: Request processing failed: kr.hhplus.be.server.exception.DuplicateResourceException: 이미 예약되어있는 좌석입니다.
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1022)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)
원인이 무엇인지 찾아보니.. mockmvc를 사용할 때 발생하는 전형적인 스택 트레이스라는 것이다.
저 makeReservation은 유저의 액세스 토큰과 대기열 토큰을 받아 API를 호출하는 메소드이다.
private fun makeReservation(request: ReservationMakeRequest, access: String, waiting: String): ReservationResponse {
val reservation = mockMvc.perform(
post("/api/reservation") // 예약 생성
.header("Authorization", "Bearer $access")
.header("X-Waiting-Token", "Bearer $waiting")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
).andExpect(status().isOk)
.andExpect(jsonPath("$.date").value(request.date.toString()))
.andExpect(jsonPath("$.seatNumber").value(request.seatNumber.toString()))
.andReturn()
}
나는 당연히 서비스 내부에서 발생한 예외도 그대로 바깥으로 나올 줄 알았지만, 그건 아니었다.
스프링의 흐름을 보면 요청이 서블릿 필터를 지나고 디스패처 서블릿에서 적절한 핸들러 (컨트롤러/서비스)를 찾아 요청을 전달해주고, 응답을 받을 시에는 역순으로 받는다.
이 과정에서 핸들러에서 발생한 예외는 디스패처 서블릿에서 ServletException으로 래핑하여 밖으로 던진다.
위 함수에서 봤듯이, 서비스의 메소드를 직접 호출한게 아니라 외부에서 요청했기 때문에 발생한 예외가 래핑되어서 나타났고, 기대한던 예외와 다르다는 로그가 발생한 것이다.
그래서 보통은 @ControllerAdvice를 사용해서 디스패처 서블릿으로 전달되는 예외를 가로채어 적절한 HTTP 상태 코드를 입힌 ResponseEntity를 반환한다.
즉 그말은 무엇이냐? 이 프로젝트에서 내가 @ControllerAdvice에 DuplicateResourceException을 핸들링하는 메소드를 작성하지 않았다는 것이다...0_0
일단 추가해주자...
@ExceptionHandler
fun exceptionhandler(e: DuplicateResourceException): ResponseEntity<*> {
log.error { "예외 발생 ${e.message}" }
return errorResponse(HttpStatus.CONFLICT, e.message.toString())
}
그러면 테스트를 어떻게 해야할까? 저 예외가 발생하면 HTTP 상태 코드로 CONFLICT 코드가 전달되도록 하였으니
//when & then
mockMvc.perform(
post("/api/reservation")
.header("Authorization", "Bearer $userB_AccessToken")
.header("X-Waiting-Token", "Bearer $userB_WaitingToken")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
).andExpect(status().isConflict)
이렇게 기대되는 상태 코드를 CONFLICT로 넣으면....

오케이 통과
'Spring > Kotlin' 카테고리의 다른 글
| Spring AI + Bedrock 사용해보기 (1) | 2026.01.16 |
|---|---|
| 마이그레이션 계획 (0) | 2025.12.03 |
| 서비스, API 작성 (2) | 2025.10.01 |
| 리포지토리 만들기 (0) | 2025.09.13 |
| 도메인 엔티티 작성 (0) | 2025.09.01 |