[Spring] 스프링의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)
예외 처리는 애플리케이션을 만드는데 매우 중요한 부분을 차지한다. Spring 프레임워크는 매우 다양한 에러 처리 방법을 제공하는데, 어떠한 방법들이 있고 가장 좋은 방법(Best Practice)은 무엇인
mangkyu.tistory.com
@ExceptionHandler 어노테이션을 사용하면 매우 유연하게 예외를 처리할 수 있게 된다.
이 어노테이션은 컨트롤러의 메소드, (@ControllerAdvice | @RestControllerAdvice)가 있는 클래스의 메소드에 존재해야 한다.
@ExceptionHandler(Exception.class)
public ResponseEntity<String> commonException(Exception exception) {
return ResponseEntity.status(HttpStatus.Bad_Request).body(exception.getMessage());
}
사용법은 위와 같다. 어노테이션 속성에 .class 값을 주지 않는다면, 파라미터에 설정된 예외 클래스를 처리하게 된다.
자식에 대한 @ExceptionHandler를 안 해놨다면 예외는 부모 클래스로 오게 된다. 즉 위에서 따로 자식에 대한 예외처리를 안 했다면 모든 예외는 위의 메소드에서 처리하게 된다.
@ExceptionHandler는 ResponseEntity를 통해 반환하는 응답을 자유롭게, 직접적으로 다룰 수 있다. body 안에 보통 아래의 세가지 정도를 넣게 된다.
- code : 예외 코드
- message : 예외에 대한 설명
- errors : 에러 목록(@Valid를 이용했을 때 여러가지 건네는 목록과 같음)
컨트롤러 단에서만 @ExceptionHandler를 사용하게 된다면 중복되는 예외 처리 메소드가 존재하게 된다. 이를 위해 위에서 언급한 @ControllerAdvice | @RestControllerAdvice 중 하나를 붙여주면 된다. 그러면 이제부터 여러 컨트롤러에 대해 전역적으로 예외 처리가 이뤄진다.
@RestControllerAdvice의 속성 중에 basePackages가 존재하는데, 이는 특정 패키지만 적용시키는 역할이다. 클래스로 범위를 줄이고 싶으면 basePackageClasses를 사용하면 된다. 이때 package는 문자열로 줘야하고, class는 .class로 인자를 넘겨줘야 한다.
Spring Boot에는 잘못된 URI를 호출하여 발생하는 NoHandlerFoundException와 같이 기본적인 예외를 미리 처리해둔 ResponseEntityExceptionHandler 추상 클래스가 존재한다.
기본적인 예외는 DefaultHandlerExceptionResolver에서 처리하는데, 예외처리기가 달라져 클라이언트가 일관되지 못한 에러 응답을 받으므로 ResponseEntityExceptionHandler을 상속받는 것이 좋다.
ResponseEntityExceptionHandler 클래스가 에러 응답을 반환하는 방식을 그대로 사용하기 위해 handleExceptionInternal메소드를 오버라이딩 해야한다.
아래는 ControllerAdvice를 사용할 때 주의해야할 점이자 팁이다.
- 한 프로젝트당 하나의 ControllerAdvice만 관리하는 것이 좋다.
- 여러 ControllerAdvice가 필요하다면 basePackages, basePackageClsses 등을 지정해줘야 한다. 여러 ControllerAdvice가 존재하고 @Order가 없다면 임의의 순서로 처리하게 되므로 일관되지 않게 된다.
- 직접 구현한 Exception 클래스들은 한 공간에서 관리한다.
본인은 ResponseEntityExceptionHandler을 참고하여 비슷하게 만들어 보았다.
@RestControllerAdvice(basePackages = "com.rosoa0475") // 이 어노테이션 안에 @Component 선언되어 있어서 자동으로 Bean으로 설정 됨.
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({CustomError.class})
public ResponseEntity<Object> handleCustomException(CustomError ex) {
ErrorResponse body = new ErrorResponse(ex.getCode(), ex.getMessage());
return this.handleExceptionInternal(body, ex.getStatusCode());
}
private ResponseEntity<Object> handleExceptionInternal(ErrorResponse body, HttpStatusCode statusCode) {
return ResponseEntity.status(statusCode)
.body(body);
}
}
ResponseEntityExceptionHandler와는 다르게 개별로 @ExceptionHandler를 붙여주기로 했다. handle~Exception이름의 메소드는 body를 만들어 주는 역할의 메소드이고 handleExceptionInternal메소드는 ResponseEntity를 만들어서 리턴해주는 메소드이다.
@Getter
public class CustomException extends RuntimeException{
private int code;
private String message;
private HttpStatusCode statusCode;
public CustomException(int code, String message, HttpStatusCode statusCode) {
super(message);
this.code = code;
this.message = message;
this.statusCode = statusCode;
}
}
json형태로 보내줘야하므로 JSON화를 위해 @Getter가 꼭 있어야 한다.
code와 message, statusCode를 생성시에 무조건 받게 만들었다.
기본 생성자도 만들고 default 값을 넣어줘도 된다.
@Getter
public class ErrorResponse{
private int code;
private String message;
public ErrorResponse(int code, String message) {
this.code = code;
this.message = message;
}
}
code를 보내는 이유는 프론트에서 이 코드를 통해 일관된 로직을 처리하도록 하기 위해서이다.
@RestController
public class MainController {
@GetMapping("/")
public ResponseEntity<Object> handleError() {
throw new CustomException(1, "CustomError", HttpStatus.ACCEPTED);
}
}
'Spring Boot' 카테고리의 다른 글
| Presigned Url을 통한 파일 업로드 최적화 (1) | 2025.08.13 |
|---|---|
| Spring Boot - 이미지 파일 업, 다운로드 (feat. @ModelAttribute) (2) | 2025.08.13 |
| Spring Boot - RESTful API (0) | 2025.08.06 |
| 앱 서비스 시 백엔드 처리 전략 (0) | 2025.07.14 |
| 원자적 업데이트 매커니즘 row-level locking (0) | 2025.05.22 |