전체 보기
♻️

레거시 Exception Custom 로직 리팩토링(1)_기본 예외 동작 안함, 응답 형식 서로 다름

작성일자
2024/01/08
태그
SPRING
프로젝트
FIS
책 종류
1 more property

1. 401로 잡혀버리는 커스텀 안된 예외들 → 잘못된 레거시 로직 수정

(1) 문제점

url이 잘못 됐을 때 스프링 기본 에러로 잡혔어야 할 404가 401 Unauthorized로 잡혀버린다.
또, 예외를 던지기만 하고 handler에서 예외를 잡아주지 않아
스프링 기본 에러로 잡혔어야 할 500도 마찬가지로 401 Unauthorized로 잡혀버린다.
에러 메시지론 “유효하지 않은 토큰입니다.”가 떴다.
즉, 현재 구조에선 예외 처리가 제대로 안 된 경우 스프링 기본 에러로 잡히지 않고 엉뚱한 곳에서 예외로 잡혀버린다.

(2) 해결책 : 스프링 기본 예외 처리(BasicErrorController) 동작하게 고치기

현재 예외 처리가 제대로 안 된 경우, 발생한 예외의 경로를 추적해보면 아래와 같다.
Controller
→ JwtAuthorizationFilter:71 (chain.doFilter)
→ ExceptionHandlerFilter:24 (chain.doFilter)
exceptionHandlerFilter 는 LogoutFilter 전에 있는 필터다.
SecurityConfig:45 (addFilterBefore(exceptionHandlerFilter(), LogoutFilter.class)
여기까진 좋은데… 그럼 왜 AuthenticationEntryPointCustom 에서 이 예외를 낚아채갈까?
@Slf4j @Component public class AuthenticationEntryPointCustom implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { log.warn("[AuthenticationEntryPointCustom] Unauthorized error : {}", authException.getMessage()); ErrorResponse errorResponse; if (authException instanceof BadCredentialsException) { errorResponse = new ErrorResponse(HttpStatus.UNAUTHORIZED, "아이디 혹은 비밀번호가 잘못되었습니다."); } else { errorResponse = new ErrorResponse(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."); } ObjectMapper objectMapper = new ObjectMapper(); response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } }
Java
복사
바로 레거시 코드에서 else문을 썼기 때문이다…..
여기서 else를 지워주면 마법같이 스프링이 기본적으로 제공하는 예외(BasicErrorController)가 뜬다.
잘못된 url을 입력하면 404가 뜨고, 예외를 제대로 잡지 못한 부분은 500이 뜬다.
@Slf4j @Component public class AuthenticationEntryPointCustom implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { if (authException instanceof BadCredentialsException) { log.warn("[AuthenticationEntryPointCustom] Unauthorized error : {}", authException.getMessage()); ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNAUTHORIZED, "아이디 혹은 비밀번호가 잘못되었습니다."); ObjectMapper objectMapper = new ObjectMapper(); response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } } }
Java
복사
패스

2. 각기 다른 에러 메시지 응답 형식 → 공통 응답 형식 사용

에러 메시지 응답 형식이 어떤 건 {code, status} 로 나가고, 어떤 건 {code, httpStatus}로 나간다. 통일해주자.

3. 각기 다른 예외 커스텀 → 일괄된 방식으로 커스텀

IllegalArgumentException 같은 기본 예외를 던지고, 후에 일괄적으로 이를 400 같은 커스텀 예외로 변환하는 코드들이 존재한다.
Agent findAgent = agentRepository.findById(request.getAgent_id()) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 현장요원입니다."));
Java
복사
@ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ErrorResult IllegalArgumentException(IllegalArgumentException e) { log.error("[IllegalArgumentExHandler] ex", e); return new ErrorResult("400", e.getMessage()); }
Java
복사
이는 예외 처리의 정확성을 떨어트리고, 서버 내부 오류인지 클라이언트 단의 오류인지 헷갈리게 하는 등 혼란을 줄 수 있다.
이 방식은 없애자. 이 코드들도 커스텀 예외를 던지게 해서, 서버 내부 오류와 커스텀 예외를 구분해주자.
Agent findAgent = agentRepository.findById(request.getAgent_id()) .orElseThrow(() -> new AgentException(AgentErrorResult.NOT_FOUND_AGENT));
Java
복사
public enum AgentErrorResult implements ErrorResult { NOT_FOUND_AGENT(HttpStatus.BAD_REQUEST, "존재하지 않는 현장요원입니다.")
Java
복사

4. 일반적이지 않은 상태 코드들 → 상황에 맞는 상태 코드 사용

I_AM_A_TEAPOT 이란 상태 코드를 들어 보았는가..?
이름은 정말 귀엽지만, 우리 레거시 코드에선 이 상태 코드를 NOT_FOUND를 써야 할 상황에서 쓰고 있다.
왜…. 라는 의문이 드는 일반적이지 않은 상태 코드들은 일반적이게 바꿔주자.
HTTP 418 I'm a teapot 클라이언트 오류 응답 코드는 서버가 찻주전자이기 때문에 커피 내리기를 거절했다는 것을 의미합니다. 일시적으로 커피가 없는 커피/차 주전자는 대신 503을 반환해야 합니다. 이 오류는 1998년과 2014년 만우절 농담이었던 하이퍼 텍스트 커피 주전자 제어 규약(Hyper Text Coffee Pot Control Protocol)에 대한 참조입니다.
일부 웹사이트는 자동화된 쿼리와 같이 처리하고 싶지 않은 요청에 대해 이 응답을 사용합니다.
팀 내 규칙을 정하고 따르자. (클라이언트 분들도 함께 정하면 좋다)
우리 팀은 아래와 같이 설정했다.
Status Code
OK: 200 → 성공적으로 요청을 처리한 경우
CREATED: 201 → 새로운 리소스가 생성된 경우
NO_CONTENT: 204 → body에 담아보낼 게 없는 경우 #삭제API와 관련
BAD_REQUEST: 400 → 매개변수 누락과 같이 오류의 원인이 클라이언트의 요청과 관련된 경우 (쿼리 스트링 관련)
UNAUTHORIZED: 401 → 세션 정보를 제공하지 않았거나 올바르지 않은 세션 정보인 경우 (로그인 제외 모든 API와 관련)
FOR_BIDDEN: 403 → 접근 권한이 없는 경우 (관리자 API와 관련)
NOT_FOUND: 404 요청한 URL을 찾을 수 없거나 존재하지 않는 자원인 경우 (쿼리파라미터와 관련)
CONFLICTED: 409 → 중복되는 자원인 경우 (중복검사와 관련)
INTERNAL_SERVER_ERROR: 500