Mini
@ExceptionHandler로 예외처리문제 쉽게 풀기! 본문
웹 에러인경우 문제상황
없는경로로 접근시 웹사이트가 망한것 같은 에러페이지가 등장합니다.
이를 개선해고자 합니다.

해결방법
간단히 아래경로에 html 파일을 추가해 주시만 하면 됩니다.

에러발생시 전체 흐름은 아래와 같습니다.
1. WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
2. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
3. WAS 오류 페이지 확인
4. WAS(/error, dispatchType=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) -> 컨트롤러(/error/500) ->View
이때, WAS까지 오류가 전달되면 WAS는 다시 /error 경로로 서버에 요청을 한다는 점이 키포인트 입니다.
관련 스프링 코드를 보겠습니다.
BasicErrorController에서 /error 에 대한 요청들을 처리해주고 있습니다.
server.error.path, error.path 둘다 환경설정에 없다면, /error 경로를 기본값으로 처리합니다.

errorHtml 함수에서 에러코드에 맞는 modelAndView를 반환해줍니다.



주의할점
인터셉터를 사용중인경우 excludePathPatterns에 /error를 추가해줘야 합니다.
그렇지않으면, error 경로로 재요청시 불필요한 인터셉터 호출이 일어납니다.



API의 에러인경우 문제상황
웹상의 에러페이지는 스프링부트의 도움을 받아 깔끔히 처리할 수 있었습니다.
API 에러의 경우에는 어떤 문제가 있는지 보겠습니다.
아래와같이 error trace같은 원치않은 정보도 포함하여 응답해주는 문제가 있습니다.
API 예외는 에러상황에 따라 세밀한 응답이 필요한 경우가 많습니다.

과정분석
WAS까지 error가 도달하면, WAS가 내부서버로 /error 요청을 하는것은 동일합니다.
이를 처리하는 BasicErrorController를 살펴보겠습니다.
accept header가 text/html일때는 ModelAndView를 반환하고, 아닐때는 ResponseEntity를 반환합니다.



스프링에서 제공하는 BasicErrorController 만으로는 API 예외를 다루기에 한계가 있습니다.
@ExceptionHandler
이를 해결해줄 ExceptionHandler를 살펴 보겠습니다.
예외 예시는 아이템 등록시, 이름이 빈 경우 발생되는 MethodArgumentNotValidException인 경우로 보겠습니다.
코드는 다음과 같습니다.
@ExceptionHandler
public ResponseEntity<ErrorResult> methodArgumentNotValidExHandle (MethodArgumentNotValidException e) {
log.error("error : {}", e.getAllErrors());
ErrorResult errorResult = makeErrorResponseFrom(e.getBindingResult());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
작동 과정은 다음과 같습니다.

- 예외 발생: 컨트롤러나 서비스 등에서 예외가 발생합니다 (그림에서 3번 과정에서 예외 발생)
- 예외 전파: 예외가 DispatcherServlet까지 전파됩니다 (그림에서 4번 처럼 핸들러에서 예외가 반환됨)
- ExceptionResolver 호출: DispatcherServlet은 등록된 HandlerExceptionResolver들을 순서대로 호출합니다 (그림의 5번 과정)
- @ExceptionHandler 메서드 검색: ExceptionHandlerExceptionResolver는 발생한 예외 타입을 처리할 수 있는 @ExceptionHandler 메서드를 컨트롤러 또는 @ControllerAdvice 클래스에서 찾습니다
- 예외 처리 및 응답 생성: 적합한 @ExceptionHandler 메서드가 실행되어 응답을 생성합니다
- View 렌더링 또는 응답 반환: @RestController` 이므로 `exHandle()` 에도 `@ResponseBody` 가 적용됩니다. 따라서 HTTP 컨버터가 사용되고, 응답이 JSON으로 반환됩니다.
적용결과
주의사항
Contoller에 매개변수에서 BindingResult를 제거해야 작동합니다.
BindingResult가 있으면, MethodArgumentNotValidException 에러가 발생하지 않고 검증 오류 정보가 bindingResult에 담기게되어 ExceptionHandler가 실행되지 않습니다.
AS-IS vs TO-BE
기존에는 에러가 발생한경우, WAS에서 내부서버로 불필요한 /error 요청이 있었습니다.
ExceptionHandler를 적용한결과, 불필요한 /error 요청없이 적절한 json 응답을 보내줄수 있습니다. 또한 세밀한 응답이 필요한경우, 수정이 쉽습니다.
API 예외처리 - @ControllerAdvice
@ExceptionHandler 를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의
컨트롤러에 섞여 있는 문제가 있습니다. 또한, 컨트롤러가 100개면, 100개의 중복되는 예외처리 코드가 필요한 문제가 있습니다.
@ControllerAdvice` 또는 @RestControllerAdvice 를 사용하면 둘을 분리할 수 있습니다.
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
@ExceptionHandler
public ResponseEntity<ErrorResult> methodArgumentNotValidExHandle (MethodArgumentNotValidException e) {
log.error("error : {}", e.getAllErrors());
ErrorResult errorResult = makeErrorResponseFrom(e.getBindingResult());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
private ErrorResult makeErrorResponseFrom(BindingResult bindingResult){
String code = "";
String description = "";
//에러가 있다면
if(bindingResult.hasErrors()){
//DTO에 설정한 meaasge값을 가져온다
description = bindingResult.getFieldError().getDefaultMessage();
//DTO에 유효성체크를 걸어놓은 어노테이션명을 가져온다.
String bindResultCode = bindingResult.getFieldError().getCode();
if(bindResultCode == null){
return new ErrorResult("ERROR_CODE_0000", "유효성 체크 실패");
}
switch (bindResultCode){
case "NotNull":
case "NotEmpty":
case "NotBlank":
code = "ERROR_CODE_0001";
description = "필수값이 누락되었습니다";
break;
case "Min":
code = "ERROR_CODE_0002";
description = "최소값 커야 합니다.";
break;
case "Range":
code = "ERROR_CODE_0003";
description = "범위가 맞지 않습니다.";
break;
}
}
return new ErrorResult(code, description);
}
}
이제, 컨트롤러에서 에러처리 코드를 지워주면 됩니다.
작동과정
@ControllerAdvice의 실제 동작 방식을 보겠습니다.
- 애플리케이션 시작 시 컴포넌트 스캔:
- Spring은 @ControllerAdvice 애노테이션이 붙은 클래스를 탐지하고 빈으로 등록합니다.
- RequestMappingHandlerAdapter 설정:
- Spring MVC는 시작 시 RequestMappingHandlerAdapter에 @ControllerAdvice 클래스들에 정의된 @ExceptionHandler 메서드들을 등록합니다.
- ExceptionHandlerExceptionResolver 활용:
- Spring MVC는 ExceptionHandlerExceptionResolver라는 예외 해결자(resolver)를 사용해 컨트롤러에서 발생한 예외를 처리합니다.
- 이 resolver는 예외가 발생하면 적합한 @ExceptionHandler 메서드를 찾아 실행합니다.
- DispatcherServlet의 예외 처리 흐름:
- 컨트롤러에서 예외가 발생하면 DispatcherServlet은 등록된 예외 해결자들에게 예외 처리를 위임합니다.
- ExceptionHandlerExceptionResolver는 해당 예외 타입에 맞는 @ExceptionHandler 메서드를 찾아 실행합니다.
Advice -> 어드바이저? -> 프록시 방식으로 작동하는건가? -> 프록시 방식으로 작동하는게 아니었습니다..
ENUM을 통한 리팩토링
enum을 통해 에러 code, 에러 message 를 예상가능한 값들로 제한하고, 유지보수를 용이하게 할 수 있습니다.
@Getter
@AllArgsConstructor
public enum ErrorCode {
FAIL("ERROR_CODE_0000", "유효성 체크 실패"),
NOT_NULL("ERROR_CODE_0001", "필수값이 누락되었습니다"),
MIN_VALUE("ERROR_CODE_0002", "최소값 커야 합니다."),
RANGE("ERROR_CODE_0003", "범위가 맞지 않습니다.");
private final String code;
private final String message;
}
private ErrorResult makeErrorResponseFrom(BindingResult bindingResult){
String code = "";
String description = "";
//에러가 있다면
if(bindingResult.hasErrors()){
//DTO에 설정한 meaasge값을 가져온다
description = bindingResult.getFieldError().getDefaultMessage();
//DTO에 유효성체크를 걸어놓은 어노테이션명을 가져온다.
String bindResultCode = bindingResult.getFieldError().getCode();
if(bindResultCode == null){
return new ErrorResult(ErrorCode.FAIL.getCode(), ErrorCode.FAIL.getMessage());
}
switch (bindResultCode){
case "NotNull":
case "NotEmpty":
case "NotBlank":
code = ErrorCode.NOT_NULL.getCode();
description = ErrorCode.NOT_NULL.getMessage();
break;
case "Min":
code = ErrorCode.MIN_VALUE.getCode();
description = ErrorCode.MIN_VALUE.getMessage();
break;
case "Range":
code = ErrorCode.RANGE.getCode();
description = ErrorCode.RANGE.getMessage();
break;
}
}
return new ErrorResult(code, description);
}
정리
웹 에러의경우 스프링부트의 BasicErrorController에 의해 에러 html 들을 resources/error 경로에 위치시켜서 해결할수 있습니다.
api 에러의 경우 @ExceptionHandler로 보다 정밀한 에러 메시지를 줄 수 있고, 불필요한 서버내부 재호출을 막을 수 있습니다.
@ControllerAdvice로 에러처리로직을 컨트롤러 외부로 분리 할 수 있습니다.
레퍼런스
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/exceptionhandlers.html
Exceptions :: Spring Framework
If an exception remains unresolved by any HandlerExceptionResolver and is, therefore, left to propagate or if the response status is set to an error status (that is, 4xx, 5xx), Servlet containers can render a default error page in HTML. To customize the de
docs.spring.io
@Valid 와 @ControllerAdvice로 DTO 예외처리하기
@Valid 세팅 및 사용하기 해당편에 이어서 @Valid와 @ControllerAdvice를 이용한 Exception처리를 하려고한다.@Vailid 사용법에 관한 설명은 생략한다. 위의 링크를 참고하면된다 @Valid는 @ControllerAdvice와 같이
cchoimin.tistory.com
'기술블로그' 카테고리의 다른 글
객체지향으로 할인요구사항 구현하기 (1) | 2025.07.05 |
---|---|
No Offset 으로 페이징 성능 개선하기 (0) | 2025.05.01 |
ThreadLocal로 싱글톤에서 발생하는 동시성문제 쉽게 풀기! (0) | 2025.04.13 |
비관적 락으로 재고감소 로직에서 발생하는 '동시성 문제' 쉽게 풀기! (0) | 2025.04.03 |