어제 작성했던 코드에서 버그가 발생해서 오늘은 그 버그를 해결하는 과정을 기록하겠다.
발생한 버그는 2가지이다.
1. Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Unsupported field: HourOfDay]
어떤 상황에서 발생한 버그인가?
구현을 완료하고 테스트를 하던 도중 목표 페이지의 조회가 갑자기 조회되지 않는 버그가 발생했다.
버그 코드는 403 에러, 즉 인증 관련 예외처리가 발생했다.
아래는 오류가 발생한 API 코드이다.
@Operation(summary = "목표 전체 조회(페이징)")
@GetMapping
fun getResolutionListPaginated(
@RequestParam(defaultValue = "0") page: Int,
sortOrder: SortOrder?
): ResponseEntity<Page<ResolutionResponse>>{
val resolutionList = resolutionService.getResolutionListPaginated(page, sortOrder)
return ResponseEntity.ok(resolutionList)
}
어떻게 해결했나?
처음에는 403 에러가 발생해서 인증 관련해서 발생한 오류라고 생각했다. 그래서 SecurityConfig도 바꿔보고 토큰 인증 방식도 다시한번 검토했었다.
하지만 다른 api는 작동하는 것도 있었고, 작동하지 않는 api도 있었다. resolution 쪽 api가 유독 작동하지 않았다.
이상했다. 어떤 api는 작동하고 어떤 api는 작동하지 않고.. 인증 인가쪽 문제라면 모든 api가 작동하면 안되는 것 아닌가?
SecurityConfig쪽 설정을 봐도 api별 인가 처리는 잘 되어 있었다. hasRole 문제 또한 아니였다.
디버그를 해 봐도 제대로 service까지 들어가는 걸 확인했다. 인가쪽은 문제가 없었던 것이다! 그렇다면 다른 문제가 있는 게 분명했다.
그런데 이상하게도 IDE의 실행 창 콘솔에는 ERROR가 뜨지 않았다. 보통 api가 작동을 안하면 ERROR로 메세지를 날리지 않는가?
콘솔창은 이렇게 나와있었다.
2024-03-15T01:16:38.131+09:00 TRACE 25620 --- [io-8080-exec-10] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [1]
2024-03-15T01:16:38.165+09:00 WARN 25620 --- [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Unsupported field: HourOfDay]
2024-03-15T01:16:38.172+09:00 DEBUG 25620 --- [io-8080-exec-10] o.s.security.web.FilterChainProxy : Securing GET /error?page=0
2024-03-15T01:16:38.174+09:00 DEBUG 25620 --- [io-8080-exec-10] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
2024-03-15T01:16:38.235+09:00 DEBUG 25620 --- [io-8080-exec-10] o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8080/error?page=0&continue to session
2024-03-15T01:16:38.235+09:00 DEBUG 25620 --- [io-8080-exec-10] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
ERROR는 아니지만 WARN이 하나 보인다.
자세히 살펴보자.
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Unsupported field: HourOfDay]
HttpMessageNotWritableException이 발생했다고 한다. HourOfDay를 처리하는 과정에서 문제가 있는것 같은데...
HourOfDay는 보통 LocalDateTime과 연관이 있다고 한다. 내가 작성한 코드에서 LocalDateTime을 사용하는 부분, 그리고 resolution의 조회 API와 관련된 코드. 이 두 조건에 맞는 코드를 검토했다.
그중 한 코드를 보여주겠다. 아래 코드는 resolution의 ResponseDTO를 정의한 데이터 클래스이다.
data class ResolutionResponse(
val id: Long?,
val title: String,
val description: String,
val completeStatus: Boolean,
val dailyStatus: Boolean,
val category: String,
val progress: Long,
val likeCount: Long,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
val deadline: LocalDate,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
val createdAt: LocalDateTime,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
val updatedAt: LocalDateTime
)
뭔가 이상한 부분이 보이지 않는가?
deadline의 자료형이 LocalDate인데 @JasonFormat 어노테이션에서 패턴을 "yyyy-MM-dd HH:mm:ss" 으로 설정했다!
LocalDate는 날짜만 담고있는 정보지만, @JasonFormat 을 통해 시/분/초 까지 패턴을 설정해 버려서 발생한 문제였다.
@JsonFormat(pattern = "yyyy-MM-dd")
val deadline: LocalDate,
이런 식으로 고쳐주니 문제없이 잘 작동한다.
코드가 테스트에서 문제를 일으키는 경우 무조건 ERROR가 발생하는 줄 알았다. 따라서 WARN 같은 경고 문구는 잘 살펴보지 않았다. 하지만 문제 해결의 답은 WARN에서 알려주고 있었다!
그렇다면 왜 403 에러가 발생했을까?
이는 java의 Exception계층 구조에 의해 발생한 현상인데, java는 exception을 클래스 계층 구조로 이루어져 있다.
일단 403이 발생한 예외처리는 securityConfig 클래스에서 정의된 예외처리이며, 이름은 AuthenticationException이다.
이 예외처리 클래스의 계층 구조를 따라 올라가 보면 아래와 같다.
java.lang.Object
- java.lang.Throwable
- java.lang.Exception
- java.lang.RuntimeException
- org.springframework.security.core.AuthenticationException
- java.lang.RuntimeException
- java.lang.Exception
그리고 위 코드에서 발생한 예외처리는 HttpMessageNotWritableException이다. 이 exception도 계층 구조를 따라 올라가 보자.
java.lang.Object
- java.lang.Throwable
- java.lang.Exception
- java.lang.RuntimeException
- org.springframework.core.NestedRuntimeException
- org.springframework.http.converter.HttpMessageConversionException
- org.springframework.http.converter.HttpMessageNotWritableException
- org.springframework.http.converter.HttpMessageConversionException
- org.springframework.core.NestedRuntimeException
- java.lang.RuntimeException
- java.lang.Exception
여기서 핵심은 HttpMessageNotWritableException 클래스가 좀 더 아래 계층에 위치한다는 것이다.
사용자가 GlobalExceptionHandler로 exception을 handling하지 않으면, 해당 예외처리가 발생했을 시 가장 가까운 handling된 Exception을 발생시킨다. 내 코드에선 HttpMessageNotWritableException 이 GlobalExceptionHandler에 정의되어 있지 않았으므로 AuthenticationException이 발생한 것이다.
아직 예외처리를 제대로 작성하지 않고 코드를 작성하다 보니 생긴 문제였다.
'캠프 개발일지' 카테고리의 다른 글
개발일지 - 16일차: 버그 해결 및 원인 분석(2) (1) | 2024.03.15 |
---|---|
개발일지 - 15일차: 목표 랭킹 알고리즘 수정 및 오류 직면 (2) | 2024.03.14 |
개발일지 - 14일차: 랭킹 시스템 구현 (1) | 2024.03.13 |
개발일지 - 9일차: 좋아요 기능 추가 (0) | 2024.03.06 |
개발일지 - 8일차: daily Check 추가 (4) | 2024.03.05 |