본문 바로가기
캠프 개발일지

개발일지 - 16일차: 버그 해결 및 원인 분석(1)

by JHBang 2024. 3. 15.

어제 작성했던 코드에서 버그가 발생해서 오늘은 그 버그를 해결하는 과정을 기록하겠다.

발생한 버그는 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

 

어떻게 해결했나?

 

처음에는 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

 

그리고 위 코드에서 발생한 예외처리는 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

 

여기서 핵심은 HttpMessageNotWritableException 클래스가 좀 더 아래 계층에 위치한다는 것이다.

 

사용자가 GlobalExceptionHandler로 exception을 handling하지 않으면, 해당 예외처리가 발생했을 시 가장 가까운 handling된 Exception을 발생시킨다. 내 코드에선 HttpMessageNotWritableException 이 GlobalExceptionHandler에 정의되어 있지 않았으므로 AuthenticationException이 발생한 것이다.

 

아직 예외처리를 제대로 작성하지 않고 코드를 작성하다 보니 생긴 문제였다.