Error Message 통일

Kim jisu·2025년 2월 24일
0

 Debugging Note

목록 보기
15/37

디버깅 노트

1. 상황 설명

  • 목표:
    BE와 FE 간의 협업에서 서비스 지속성과 로직 보호를 위해 모든 API 응답을 일관된 JSON 포맷(예: ApiResponse<ErrorData>)으로 통일하고자 함.

  • 문제 발생 배경:
    컨트롤러에서 발생하는 예외는 @RestControllerAdvice의 GlobalExceptionHandler가 잡아 일관된 응답을 제공하지만, Spring Security 필터(JwtAuthorizationFilter) 단계에서 발생하는 예외는 GlobalExceptionHandler의 대상이 아니므로 기본 에러 처리 로직에 의해 처리됩니다.

2. 문제점

  • 필터 단계 예외:
    JwtAuthorizationFilter에서 발생하는 CustomException(예: "로그아웃된 토큰" 예외)은 필터 내부에서 발생하여 GlobalExceptionHandler로 전달되지 않습니다.
  • 불일치한 응답 포맷:
    이로 인해 FE 측에서는 컨트롤러 응답과 달리 기본 스프링 부트 에러 응답(예: /error 경로를 통한 HTML 혹은 기본 JSON 구조)을 받게 되어, 전체 응답 포맷이 통일되지 않습니다.

3. 원인 분석

  • 필터와 컨트롤러 처리 차이:

    • 컨트롤러 단계:
      예외가 컨트롤러에서 발생하면 DispatcherServlet이 이를 GlobalExceptionHandler로 전달해 ApiResponse<ErrorData> 형태의 일관된 응답을 생성합니다.

    • 필터 단계:
      JwtAuthorizationFilter와 같은 서블릿 필터에서 발생한 예외는 DispatcherServlet에 도달하지 않고, 기본 에러 핸들러(BasicErrorController 등)가 처리하게 됩니다.

  • 결과:
    FE는 필터에서 발생하는 예외에 대해 예상하지 않은 응답 구조를 수신하게 되어 클라이언트 로직에 혼선이 발생할 수 있습니다.

4. 해결 방안

  • 필터 내 예외 직접 처리:
    JwtAuthorizationFilter의 doFilterInternal 메서드 내에서 try-catch 구문을 사용해 발생하는 CustomException을 직접 잡고, ObjectMapper를 이용해 ApiResponse<ErrorData> 형태의 JSON 응답을 클라이언트에 전송합니다.

  • 통일된 응답 생성:
    이렇게 하면, 컨트롤러와 필터 모두에서 동일한 API 응답 포맷을 유지할 수 있으므로 FE에서 에러 처리가 일관되게 동작합니다.

5. 처리 방법 (예시 코드)

@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
    try {
        String tokenValue = jwtUtil.getTokenFromRequest(req);
        log.info("Extracted Token: {}", tokenValue);

        if (StringUtils.hasText(tokenValue)) {
            Optional<UserToken> tokenEntityOpt = tokenRepository.findByAccessToken(JwtUtil.BEARER_PREFIX + tokenValue);
            if (tokenEntityOpt.isPresent() && tokenEntityOpt.get().isBlacklisted()) {
                log.error("Token is blacklisted");
                throw new CustomException(ErrorCode.AUTH006, "로그아웃된 토큰");
            }

            if (!jwtUtil.validateToken(tokenValue)) {
                throw new CustomException(ErrorCode.AUTH005, "Token validation failed");
            }

            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
            log.info("Claims: {}", info);
            setAuthentication(info.getSubject());
        }
        filterChain.doFilter(req, res);
    } catch (CustomException e) {
        // 필터 단계에서 발생한 CustomException을 잡아서 일관된 JSON 응답 생성
        res.setContentType("application/json;charset=UTF-8");
        res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  // 필요에 따라 다른 상태 코드 사용

        ErrorData errorData = new ErrorData(
            e.getErrorCode().getCode(),
            e.getErrorCode().getMessage(),
            e.getDetails()
        );
        ApiResponse<ErrorData> errorResponse = new ApiResponse<>("fail", errorData);

        ObjectMapper mapper = new ObjectMapper();
        String jsonResponse = mapper.writeValueAsString(errorResponse);

        res.getWriter().write(jsonResponse);
        res.getWriter().flush();
        return;
    }
}

6. 결론

  • 서비스 안정성:
    필터 단계에서 발생하는 예외도 컨트롤러 예외와 동일한 ApiResponse<ErrorData> 구조로 반환함으로써 FE와의 협업에서 응답 형식의 일관성을 보장할 수 있습니다.

  • 로직 보호:
    에러 발생 시, 예상치 못한 기본 스프링 부트 에러 페이지 대신 사전에 정의한 포맷으로 에러를 전달하여, 클라이언트에서 로직 변경 없이 일관된 처리가 가능하게 됩니다.

  • 추가 검증:
    배포 전 다양한 에러 시나리오(예: 로그아웃된 토큰, 만료된 토큰, 형식 오류 등)를 충분히 테스트하여 모든 경우에 대해 일관된 응답이 생성되는지 확인이 필요합니다.

profile
Dreamer

0개의 댓글