Spring의 예외 관리
1️⃣ BasicErrorController
Spring Boot는 기본적으로 BasicErrorController에서 모든 에러를 처리하는 /error 매핑을 제공하며, 서블릿 컨테이너 전역의 오류페이지로 등록합니다.
Controller 이하에서 발생된 예외를 잡아 Http 상태 예외 메시지를 생성하는데, Accept Header의 text/html 포함여부에 따라 다음과 같은 두 가지 방법으로 반환합니다.
✔️ 머신 클라이언트 : error() 메소드 사용
{
"timestamp": "2021-04-28T00:00:00.000+00:00",
"status": 404,
"error": "Not Found",
"exception": "java.lang.NotFoundException",
"trace": "java.lang.RuntimeException: 사용자를 찾을 수 없습니다.\\n\\tat
hello.exception.web.api.ApiExceptionController.getMember(ApiExceptionController
.java:19...,
"message": "잘못된 사용자",
"path": "/api/members/ex"
}
- Accept Header에 text/html이 포함되지 않은 경우로, ResponseEntity로 JSON 데이터를 반환합니다.
✔️ 브라우저 클라이언트 : errorHtml() 메소드 사용
- Accept Header에 text/html이 포함된 경우로, 모델뷰를 반환합니다.
- 즉, html 포맷으로 렌더링해 whitelabel 에러 뷰를 보여줍니다.
✅ 단점
BasicErrorController를 사용하면 전역적으로 모든 API의 에러에 대해 공통적으로 처리하게 됩니다.
하지만 일반적으로 API 특성에 따라 다르게 처리하기 때문에, 직접 개발해야 합니다.
2️⃣ HandlerExceptionResolver
HandlerExceptionResolver는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작 방식을 변경하기 위하여 사용되는 인터페이스입니다.
이를 구현하면 컨트롤러에서 예외가 발생해도 서블릿까지 전달되지 않고, 스프링MVC에서 예외처리가 마무리되기 때문에 WAS 입장에서는 정상 처리 요청이 됩니다.
📈 흐름
- 적용 전
- 에러 발생 전 : WAS → 서블릿 → preHandle → 핸들러 어댑터 → 핸들러 (예외발생)
- 에러 발생 후 : 서블릿(예외 전달) → postHandle 호출하지 않음 (예외전달) → afterCompletion → WAS에 예외전달
- 적용 후
- 에러 발생 전 : WAS → 서블릿 → preHandle → 핸들러 어댑터 → 핸들러 (예외발생)
- 에러 발생 후 : 서블릿(예외전달) → ExceptionResolver가 예외해결시도, ModelAndView 전달 → postHandler 호출하지 않음 → afterCompletion → render(model)후 View → WAS 정상응답
✔️ 구현
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(..., Exception ex){
try{
if(ex instanceof **CustomException**){
response.sendError(HttpServletResponse.SC_BAD_REQUEST,
ex.getMessage());
return new ModelAndView();
}
} catch {...}
return null;
}
}
- **try-catch**를 사용하여, Exception을 하나하나 잡아 정상흐름처럼 변경합니다.
- 설정한 CustomException 종류의 예외가 발생하면, SC_BAD_REQUEST(400)상태코드와 메시지와 함께 빈 ModelAndView를 반환합니다.
✔️ 반환값에 따른 동작 방식
HandlerExceptionResolver는 ModelAndView를 반환해야하는데, 설정한 값에 따라 다르게 동작합니다.
- 빈 ModelAndView 반환
- 뷰를 렌더링하지 않고, 정상 흐름으로 서블릿을 리턴합니다.
- response.sendError()
- 서블릿에서 상태코드에 따른 에러 처리를 위임합니다.
- WAS는 서블릿의 에러 페이지를 찾아내 다시 호출합니다.
- ModelAndView 정보 지정
- 뷰를 렌더링합니다.
- 에러에 따른 새로운 오류 화면을 렌더링해 클라이언트에게 제공합니다.
⚠️ Response Body에 데이터 넣어주기
JSON으로 응답이 필요하다면 response.getWriter().println()로 직접 데이터를 넣어줄 수 있습니다.
⚠️ ExceptionResolver가 없을 때
null 다음 ExceptionResolver를 찾아 실행처리할 수 있는 ExceptionResolver가 없으면, 서블릿 밖으로 에러를 던집니다.
✔️ WebConfig에 HandlerExceptionResolver 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
publi void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new ExceptionResolverName());
}
}
해당 메소드를 구현하면 스프링이 기본으로 등록하는 ExceptionResolver는 제거되기 때문에 주의해야합니다.
✅ 단점
HandlerExceptionResolver는 렌더링할 페이지를 설정하는 ModelAndView를 반환해야합니다.
이는 JSON 데이터를 반환하는 API 응답에는 필요하지 않습니다.
또한 HttpServletResponse에 직접 응답데이터를 넣어주는 것은 매우 원시적(순수서블릿 사용)이고 불편한 방법입니다.
3️⃣ ExceptionResolver
위의 단점을 보완하기 위해서, Spring Boot는 Controller계층에서 발생한 Exception을 어노테이션으로 처리 할 수 있는 ExceptionResolver라는 예외처리 인터페이스를 지원합니다.
Srping Boot에서 ExceptionResolver로 구현된 클래스는 다음과 같이 총 3가지 종류가 있습니다.
💡 ExceptionResolver 구현체 및 등록 순서
1. ExceptionHandlerExceptionResolver의 @ExceptionHandler
2. ResponseStautsExceptionResolver의 @ResponseStatus
3. DefaultHandlerExceptionResolver
1. ExceptionHandlerExceptionResolver
@ExceptionHandler를 사용합니다.
예외 한번에 처리
@Controller
public class CustomController {
..Controller로직
**@ExceptionHandler({FileSystemException.class, RemoteException.class})**
public ResponseEntity<String> handle(IOException ex) {
...
}
}
예외 생략
@Controller
public class CustomController {
..Controller로직
**@ExceptionHandler**
public ResponseEntity<String> handle(IOException ex) {
...
}
}
어노테이션에서 예외 클래스를 생략할 시 메서드 파라미터의 예외가 지정됩니다. (여기서는 IOException)
2. ResponseStatusExceptionResolver
@ResponseStatus를 사용해, HTTP 상태코드를 지정하여 응답할 수 있습니다.
@Controller
public class CustomController {
..Controller로직
**@ResponseStatus(code = HttpStatus.NOT_BAD_REQUEST, reason = "잘못된 요청 오류")**
public class BadRequestException extends RuntimeException {
...
}
}
단점
- 개발자가 직접 변경할수 없는 예외에는 적용할 수 없습니다.
- 메소드 외부에서 어노테이션을 사용해, 조건에 따라 동적으로 변경할 수 없기 때문에 @ExceptionHandler의 사용이 권장됩니다.
3. DefaultHandlerExceptionResolver
1️⃣의 스프링내부 기본예외 처리방법 로직을 따릅니다.
✅ 단점
- ExceptionResolver를 사용 할 때 Controller내에서 각각 Exception에 대한 로직을 작성하기 때문에, 공통처리가 되지 않아 일관성이 떨어집니다. 또한 정상 처리가 되는 Controller의 코드와 예외 처리를 위한 Exception 코드가 분리되지 않습니다.
- 결국 response.sendError(statusCode, resolvedReason)를 호출하기 때문에, WAS에서 다시 오류페이지를 내부요청하게 됩니다.
✔️ 구현 코드
CustomException 클래스
public class CustomException extends RuntimeException {
public CustomException(){super();}
public CustomException(String message){super(message);}
public CustomException(String message, Throwable cause){super(message, cause);}
public CustomException(Throwable cause){super(cause);}
protected CustomException(String message, Throwable cause, boolean enabelSuppression, boolean writableStackTrace){super(message, cause, enableSuppression, writableStackTrace);}
}
API 예외 응답 객체 (Error DTO)
@Data
@AllArgsConstructor
public class ErrorDto {
private String code;
private String message;
}
RestController 안의 예외 처리 메소드
1️⃣
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorDto exHandle(IllegalArgumentException e) {
return new ErrorDto("code", e.getMessage());
}
- 위와같이 2가지를 선언한 경우, ExceptionResolver는 우선순위가 가장 높은 ExceptionHandlerExceptionResolver를 실행해 @ExceptionHandler가 적용된 메소드를 실행합니다.
- 응답 시에는 @ResponseStatus에 설정한 대로 HttpStatus.BAD_REQUEST인 400으로 응답합니다.
- @ResponseStatus는 Http응답코드를 동적으로 변경할 수 없기 때문에 권장되지 않습니다.
2️⃣
@ExceptionHandler
public ResponseEntity<ErrorDto> customExHandle(CustomException e){
ErrorDto errorDto = new ErrorDto("code", e.getMessage());
return new ResponseEntity<>(errorDto, HttpStatus.BAD_REQUEST);
}
- 파라미터인 CustomException 예외를 사용합니다.
- @RestController라 @ResponseBody가 적용됩니다. ResponseEntity로 Http 바디에 직접 응답 (Http 컨버터 사용)해 @ResponseStatus와 다르게 Http 응답 코드를 동적으로 변경할 수 있습니다.
예외 공통처리하기
✔️ @ControllerAdvice 사용
앞선 ExceptionResolver는 Controller에 로직을 작성하여 클라이언트의 정상 요청코드와 예외처리코드를 분리할 수 없으며, 예외에 대한 공통 처리가 불가능합니다.
이 때 @ControllerAdvice/@RestControllerAdvice와 함께 @ExceptionHandler를 사용하면 예외코드를 분리하고 공통적으로 처리할 수 있습니다.
💡 @RestController
@RestControllerAdvice는 @ResponseBody가 추가된 어노테이션입니다. RestAPI에서 주로 사용됩니다.
사용법
@RestControllerAdvice 또는 @ControllerAdvice
public class CustomControllerAdvice {
**@ExceptionHandler**
public ResponseEntity<String> handle(CustomException ex) {
...
}
}
✔️ 컨트롤러 지정
ControllerAdvice 기능이 필요한 컨트롤러의 타입을 지정할 수 있습니다.
대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여합니다.
대상을 지정하지 않으면 글로벌하게(모든 컨트롤러에) 적용됩니다.
@Restcontroller에 지정
@ControllerAdvice(annotations = RestController.class)
public class CustomAdvice {}
모든 컨트롤러에 지정 (생략 가능)
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class CustomAdvice {}
패키지 지정
@ControllerAdvice("com.package")
public class CustomAdvice {}