티스토리 뷰

SpringBoot 로 RestAPI 를 개발하다 보면, 클라이언트에 일관된 에러 응답을 제공하기 위해 예외처리의 표준화가 필요합니다. 이를 효율적으로 관리하기 위해 RestErrorAdvice 를 설정하면 각종 예외를 깔끔하게 처리할 수 있습니다. 이번 포스팅에선 RestErrorAdvice 를 설정하는 방법에 대해 소개하겠습니다.

 

1. Dependency 추가

dependencies {
	...
	implementation 'org.springframework.boot:spring-boot-starter-web'
    ...
}
  • gradle 또는 maven 에 Dependency 를 추가합니다.

 

2. 공통 에러 응답 포맷 정의

@Getter
@Setter
@NoArgsConstructor
public class BaseResponse {
  private int status = 200;
  private String message = "SUCCESS";
  private String code = "OK";
}
  • 이번 예제에서는 위처럼 간단한 상태와 메세지정도만 출력하도록 포맷을 정의하였습니다.

 

3. ExceptType 작성

@Getter
@RequiredArgsConstructor
@JsonFormat(shape = JsonFormat.Shape.OBJECT) // Enum 클래스 한글사용
public enum ExceptType {

  // 시스템 에러
  SYSTEM_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 API 입니다."),

  USER_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "사용자가 존재하지 않습니다."),
  USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증에 실패하였습니다."),
  USER_LOGIN_NOT_MATCH(HttpStatus.UNAUTHORIZED, "ID 또는 비밀번호가 일치하지 않습니다."),
  USER_PERMISSION_DENIED(HttpStatus.UNAUTHORIZED, "권한이 부족합니다."),

  TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."),
  TOKEN_INCORRECT(HttpStatus.UNAUTHORIZED, "올바르지 않은 토큰입니다."),
  TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),
  TOKEN_SIGNATURE(HttpStatus.UNAUTHORIZED, "서명이 올바르지 않습니다."),
  INTERNAL_SERVER(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다.");

  private final HttpStatus status;
  private final String message;
}
  • 기본 에러메시지 대신 응답할 메세지 코드와 내용을 정의합니다.
  • Enum Class 를 통하여 깔끔하게 관리할 수 있습니다.

 

4. Custom Exception 작성

@Getter
public class MyApiException extends RuntimeException {
  private ExceptType error;

  public MyApiException(ExceptType e) {
    super(e.getMessage());
    this.error = e;
  }
}
  • 시스템 기본 예외 이외의 Custom 예외를 처리할 소스를 작성합니다.

 

5. EntryPoint 작성

@Slf4j
@Component
public class MyEntryPoint implements AuthenticationEntryPoint {

  private final HandlerExceptionResolver resolver;

  public MyEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
    this.resolver = resolver;
  }

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
    log.warn("Authentication failed for request to {}: {}", request.getRequestURI(), authException.getMessage());
    resolver.resolveException(request, response, null, authException);
  }
}
  • SpringSecurity 에서 인증 실패 시 처리하는 엔트리 포인트를 지정합니다.
  • 기본적으로 인증 실패 시 Unauthroized 응답을 반환하지만, 이 과정을 커스터마이징하여 로그를 남기거나 사용자 정의 에러 메시지를 반환할 수 있습니다.

 

6. Security 설정 파일 수정

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
  ...
  .exceptionHandling(handler -> handler.authenticationEntryPoint(myEntryPoint))
  ...
  .build();
}
  • Security 설정파일에 EntryPoint 를 ExceptionHandling 에 추가합니다.

 

7. RestErrorAdvice 작성

/**
 * Rest Error 공통 처리 클래스
 */
@Slf4j
@RestControllerAdvice
public class MyRestErrorAdvice {

  /**
   * Custom Exception 처리
   */
  @ExceptionHandler(MyApiException.class)
  public ResponseEntity<BaseResponse> handleMyApiException(MyApiException ex) {
    return makeResponse(ex.getError());
  }

  /**
   * 처리되지 않은 모든 예외(Exception) 처리
   */
  @ExceptionHandler(Exception.class)
  public ResponseEntity<BaseResponse> handleAllExceptions(Exception ex) {
    return makeResponse(ExceptType.INTERNAL_SERVER, ex.getMessage());
  }

  /**
   * 404 Not Found 처리
   */
  @ExceptionHandler(NoHandlerFoundException.class)
  public ResponseEntity<BaseResponse> handleNotFound(NoHandlerFoundException ex) {
    return makeResponse(ExceptType.SYSTEM_NOT_FOUND_EXCEPTION);
  }

  /**
   * 처리되지 않은 런타임 예외 처리
   */
  @ExceptionHandler(RuntimeException.class)
  public ResponseEntity<BaseResponse> handleRuntimeException(RuntimeException ex) {
    return makeResponse(ExceptType.INTERNAL_SERVER, ex.getMessage());
  }

  /**
   * JWT 서명(Signature) 관련 예외 처리
   */
  @ExceptionHandler(SignatureException.class)
  public ResponseEntity<BaseResponse> handleSignatureException(SignatureException ex) {
    return makeResponse(ExceptType.TOKEN_SIGNATURE);
  }

  /**
   * 잘못된 형식의 JWT가 전달될 때 발생하는 예외 처리
   */
  @ExceptionHandler(MalformedJwtException.class)
  public ResponseEntity<BaseResponse> handleMalformedJwtException(MalformedJwtException ex) {
    return makeResponse(ExceptType.TOKEN_INCORRECT);
  }

  /**
   * 만료된 JWT 토큰에 대한 예외 처리
   */
  @ExceptionHandler(ExpiredJwtException.class)
  public ResponseEntity<BaseResponse> handleExpiredJwtException(ExpiredJwtException ex) {
    return makeResponse(ExceptType.TOKEN_EXPIRED);
  }

  /**
   * 인증 실패(AuthenticationException)에 대한 예외 처리
   */
  @ExceptionHandler(AuthenticationException.class)
  public ResponseEntity<BaseResponse> handleAuthException(AuthenticationException ex) {
    return makeResponse(ExceptType.USER_LOGIN_NOT_MATCH);
  }

  /**
   * 권한 부족(AccessDeniedException)에 대한 예외 처리
   */
  @ExceptionHandler(AccessDeniedException.class)
  public ResponseEntity<BaseResponse> handleAccessDeniedException(AccessDeniedException ex) {
    return makeResponse(ExceptType.USER_PERMISSION_DENIED);
  }

  /**
   * 응답 공통 처리 메서드 (기본 메시지 사용)
   */
  private ResponseEntity<BaseResponse> makeResponse(ExceptType exceptType) {
    return makeResponse(exceptType, exceptType.getMessage());
  }

  /**
   * 응답 공통 처리 메서드 (사용자 지정 메시지 지원)
   */
  private ResponseEntity<BaseResponse> makeResponse(ExceptType exceptType, String message) {
    BaseResponse response = new BaseResponse();
    response.setStatus(exceptType.getStatus().value()); // HTTP 상태 코드 설정
    response.setCode(exceptType.name()); // 예외 유형 코드 설정
    response.setMessage(message); // 예외 메시지 설정
    return ResponseEntity.status(exceptType.getStatus()).body(response);
  }

}
  • 필요한 응답을 작성합니다.

 

8. 사용

public User updateUser(UserRequest userRequest) {
    User user = userRepository.findById(userRequest.getUserId())
            .orElseThrow(() -> new MyApiException(ExceptType.USER_LOGIN_NOT_MATCH));

    return userRepository.save(user);
}
{
    "status": 401,
    "message": "ID 또는 비밀번호가 일치하지 않습니다.",
    "error": "USER_UNAUTHORIZED"
}
  • 직접 정의 또는, Runtime Exception 으로 발생한 예외들을 원하는 포맷에 맞게 처리할 수 있습니다.

 

감사합니다.

최근에 올라온 글
Total
Today
Yesterday