Spring Framework/Spring Boot

[Spring Boot] @RestControllerAdvice 를 사용한 예외 처리

장쫄깃 2022. 9. 25. 21:18
728x90


들어가며


지난 게시글에서 @ControllerAdvice, @RestControllerAdvice에 대해 알아보았다. 이번 게시글에서는 해당 어노테이션을 사용하여 예외 처리하는 방법에 대해서 알아보려고 한다.

 

Rest API 구현 중 오류 메시지는 개발자가 의도한 오류(Custom Exception)와 예상치 못한 오류(System Exception)로 구분된다. 이번 게시글에선 이러한 예외 상황에 대한 공통 예외 처리(Exception Handler)를 적용하는 방법에 대해 알아보겠다.

 

네이버 오픈 API 오류 메시지 형식

네이버 오픈 API 오류 메시지 형식 가이드처럼 API 오류 메시지에 대해 일관된 형식으로 응답하도록 설계해야 한다. 신규 API를 구현할 때마다 작성하도록 설계하는 것은 작업자마다 일관된 응답 구조를 보장하기 어렵기 때문에, 작업자의 별도 작업 없이 일관된 오류 메시지 형식을 응답할 수 있게 해야 한다.

 

@ControllerAdvice, @RestControllerAdvice에 대한 설명은 해당 게시글을 참고하면 된다.

링크 : https://jangjjolkit.tistory.com/51

 

[Spring Boot] @RestControllerAdvice 란

들어가며 자사 서드파티 API를 개발하는 업무를 담당했을 때, 처음에는 모든 예외처리를 try-catch로 처리하였다. 그렇다 보니 불필요한 중복 코드들이 많아지고 가독성도 떨어졌다. 또, 코드가 점

jangjjolkit.tistory.com

 

 

1. Exception 응답 모델 생성


일관된 오류 메시지 형식으로 응답하기 위한 공통 Entity를 생성한다.

@Getter
@ToString
public class ApiExceptionEntity {
    private String errorCode;
    private String errorMsg;

    @Builder
    public ApiExceptionEntity(String errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
}

 

 

2. Exception Enum Class 선언


Enum Class를 활용하여 Custom 오류 메시지를 선언한다.

@Getter
@ToString
public enum ExceptionEnum {
    RUNTIME_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "E0001")
    , ACCESS_DENIED(HttpStatus.UNAUTHORIZED, "E0002", "인증되지 않은 사용자입니다.")
    , BAD_REQUEST(HttpStatus.BAD_REQUEST, "E0003", "필수 요청 변수가 누락되었습니다.")
    , METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "E0004", "요청 메소드를 확인해주세요.")
    , INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E0005", "기타 오류")

    , FORBIDDEN(HttpStatus.FORBIDDEN, "S0001", "권한이 없습니다.");

    private final HttpStatus status;
    private final String code;
    private String message;

    ExceptionEnum(HttpStatus status, String code) {
        this.status = status;
        this.code = code;
    }

    ExceptionEnum(HttpStatus status, String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }
}

 

 

3. Custom Exception 구현


개발자가 의도한 예외 상황에서 사용될 공통 Exception을 구현한다.

@Getter
public class ApiException extends RuntimeException{
    private ExceptionEnum error;

    public ApiException(ExceptionEnum e) {
        super(e.getMessage());
        this.error = e;
    }
}

 

그러면 아래와 같이 예외 처리를 할 수 있다.

throw new ApiException(ExceptionEnum.RUNTIME_EXCEPTION);

 

더보기
RuntimeException을 상속받은 이유는 Unchecked Exception 처리를 위함이다.
Unchecked Exception은 컴파일 단계가 아닌 런타임 단계에서 발생하기 때문에 에러 발생을 예측하기 쉽지 않다.
때문에 전부 예상할 수 없는 예외처리에 대한 일괄된 결과 코드, 메시지 등 공통 처리를 위하여 Runtime Exception을 상속받았다.

 

만약 Checked Exception에 대한 예외처리를 처리하고 싶다면 예외 전환을 하는 방법이 있다.

try {
	// ...
} catch(SQLException e) {
	// 커스텀 Exception을 사용하여 직관적인 확인이 가능
	// 해당 커스텀 Exception은 RuntimeException을 상속
	throw new CustomException(e.getMessage);
}

 

추가적으로 이렇게 예외 전환을 하게 되면 Checked Exception에 대한 Transaction Rollback 사용도 가능해진다.

 

자세한 내용은 해당 글을 참고하면 된다.

링크 : https://jangjjolkit.tistory.com/2
 

[JAVA] 자바 예외(Exception) 구분 - Checked Exception, Unchecked Exception

예외(Exception) 란? 예외(Exception)란 입력 값에 대한 처리가 불가능하거나, 프로그램 실행 중에 참조된 값이 잘못된 경우 등 정상적인 프로그램의 흐름을 어긋나는 것을 말한다. 그리고 자바에서 예

jangjjolkit.tistory.com

링크 : https://jangjjolkit.tistory.com/3

 

[Spring Boot] Checked Exception에 Rollback 적용하기

들어가며 프로젝트 진행 중 MySQLTransactionRollbackException, SQLException이 발생하는 경우가 있었다. 해당 프로젝트는 AOP를 이용하여 트랜잭션을 적용한 상태였다. 그런데 해당 예외가 발생했을 때 정상

jangjjolkit.tistory.com

 

 

4. Exception Handler 구현


@RestControllerAdvice를 이용하여 공통된 오류 메시지를 반환하는 기능을 구현한다.

@RestControllerAdvice
public class ApiExceptionAdvice {
    // custom exception
    @ExceptionHandler({ApiException.class})
    public ResponseEntity<ApiExceptionEntity> exceptionHandler(HttpServletRequest req, ApiException e) {
        // e.printStackTrace();
        return ResponseEntity
                .status(e.getError().getStatus())
                .body(ApiExceptionEntity.builder()
                        .errorCode(e.getError().getCode())
                        .errorMsg(e.getError().getMessage())
                        .build());
    }

    // runtime(checked) exception
    @ExceptionHandler({RuntimeException.class})
    public ResponseEntity<ApiExceptionEntity> exceptionHandler(HttpServletRequest req, final RuntimeException e) {
        e.printStackTrace();
        return ResponseEntity
                .status(ExceptionEnum.RUNTIME_EXCEPTION.getStatus())
                .body(ApiExceptionEntity.builder()
                        .errorCode(ExceptionEnum.RUNTIME_EXCEPTION.getCode())
                        .errorMsg(e.getMessage())
                        .build());
    }

    // access denied exception
    @ExceptionHandler({AccessDeniedException.class})
    public ResponseEntity<ApiExceptionEntity> exceptionHandler(HttpServletRequest req, final AccessDeniedException e) {
        e.printStackTrace();
        return ResponseEntity
                .status(ExceptionEnum.ACCESS_DENIED.getStatus())
                .body(ApiExceptionEntity.builder()
                        .errorCode(ExceptionEnum.ACCESS_DENIED.getCode())
                        .errorMsg(ExceptionEnum.ACCESS_DENIED.getMessage())
                        .build());
    }

    // bad request exception
    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    public ResponseEntity<ApiExceptionEntity> exceptionHandler(HttpServletRequest req, final HttpRequestMethodNotSupportedException e) {
        e.printStackTrace();
        return ResponseEntity
                .status(ExceptionEnum.METHOD_NOT_ALLOWED.getStatus())
                .body(ApiExceptionEntity.builder()
                        .errorCode(ExceptionEnum.METHOD_NOT_ALLOWED.getCode())
                        .errorMsg(ExceptionEnum.METHOD_NOT_ALLOWED.getMessage())
                        .build());
    }

    // unchecked exception
    @ExceptionHandler({Exception.class})
    public ResponseEntity<ApiExceptionEntity> exceptionHandler(HttpServletRequest req, final Exception e) {
        e.printStackTrace();
        return ResponseEntity
                .status(ExceptionEnum.INTERNAL_SERVER_ERROR.getStatus())
                .body(ApiExceptionEntity.builder()
                        .errorCode(ExceptionEnum.INTERNAL_SERVER_ERROR.getCode())
                        .errorMsg(e.getMessage())
                        .build());
    }
}

 

@ExceptionHandler에 지정된 예외 클래스가 ApiException 클래스인 경우는 개발자가 의도한 예외 처리이다. 그 외 예외처리 역시 추가했다. 응답 코드와 메시지는 상황에 따라 지정된 메시지를 반환하거나, 에러 메시지를 반환해줘도 된다. 해당 게시글에선 테스트를 위해 각각의 방법을 모두 사용했다.

 

또한, 의도한 예외 발생 시에는 에러 로그를 남기지 않지만, 의도하지 않은 예외 발생 시에는 에러 로그를 남기도록 했다.

 

 

5. 결과


Enum Class에 선언된 HTTP 상태 코드(HttpStatus)ApiExceptionEntity 형식에 맞게 응답이 된다.

 

해당 기능을 활용하면 http 상태 코드 값을 200이 아닌 다른 값으로도 설정할 수 있다.

 

 

정리하며


이번 게시글에서는 @RestControllerAdvice를 이용하여 공통 예외 처리 및 에러코드 반환 기능을 만들어보았다. 해당 기능을 사용하면 각각의 작업자가 에러 코드나 메시지를 일일이 확인하지 않고, 정해진 문서에 따라 Exception만 발생시키기만 해도 된다. 해당 예외 처리에 대한 응답은 자동으로 반환된다. 에러 코드는 다른 작업자나 사용자가 확인할 수 있게 문서로 관리하면 된다. 이렇게 하면 코드도 깔끔해지고 작업자의 생산성도 향상될 수 있을 것이다.

 

관련 소스 코드는 깃허브를 참고하면 된다.

링크 : https://github.com/JangDaeHyeok/spring_boot_common_exception_handle

 

GitHub - JangDaeHyeok/spring_boot_common_exception_handle: @RestControllerAdvice 와 @ExceptionHandler를 이용한 스프링

@RestControllerAdvice 와 @ExceptionHandler를 이용한 스프링 부트 공통 예외처리 - GitHub - JangDaeHyeok/spring_boot_common_exception_handle: @RestControllerAdvice 와 @ExceptionHandler를 이용한 스프링 부트 공통 예외처리

github.com

 

 


참고

https://javachoi.tistory.com/253

https://leeys.tistory.com/30

https://developers.naver.com/docs/common/openapiguide/errorcode.md

728x90