| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 페이징
- DI
- CORS
- kafka
- 쿠버네티스
- Spring
- 지연 로딩
- JWT
- Dead Letter Queue
- Web
- mybatis
- Routing Key
- AWS
- JdbcTemplate
- @ComponentScan
- docker compose
- DLQ
- redis
- Spring Container
- dockerhub
- JPQL
- 컨테이너
- securitycontextholderfilter
- JPA
- 스프링 부트
- @Transactional
- 서블릿 컨테이너
- docker
- MSA
- Spring Data JPA
- Today
- Total
look-forest
API 예외 처리 본문
오류 페이지는 단순히 고객에게 오류 화면을 보여주고 끝이지만,
API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 한다.
지금부터 API의 경우 어떻게 예외 처리를 하면 좋은지 알아보자.
1. 서블릿 오류 페이지 방식 구현
에러페이지를 처리하는 컨트롤러에 요청 Accept 헤더가 application/json일 경우 응답하는 API를 만들어야 한다.

2. 스프링 부트 기본 오류 처리 구현
BasicErrorController는 API 응답 방식도 구현이 되어 있다.
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}
- 클라이언트 요청의 Accept 해더 값이 text/html 인 경우에는 errorHtml() 을 호출해서 view를 제공한다.
- 그외 경우에 error() 가 호출되고 ResponseEntity 로 HTTP Body에 JSON 데이터를 반환한다.

스프링 부트가 제공하는 BasicErrorController 는 HTML 페이지를 제공하는 경우에는 매우 편리하다.
그런데 API 오류 처리는 형식이 매번 다를 수 있으므로, BasicErrorController 를 확장해서 사용해야만 한다.
이보다는 API 오류 처리는 뒤에서 설명할 @ExceptionHandler 를 사용하자.
HandlerExceptionResolver
목표 - 상태코드 변환
예외가 발생해서 서블릿을 넘어 WAS까지 예외가 전달되면 HTTP 상태코드가 500으로 처리된다.
발생하는 예외에 따라서 400, 404 등등 다른 상태코드로 처리하고 싶으면, HandlerExceptionResolver를 활용할 수 있다.
HandlerExceptionResolver
스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있도록 HandlerExceptionResolver를 제공한다.

1. HandlerExceptionResolver 인터페이스를 구현한다.

2. 구현한 exceptionResolver를 등록한다.

반환 값에 따른 동작 방식
HandlerExceptionResolver 의 반환 값에 따른 DispatcherServlet 의 동작 방식은 다음과 같다.
- 빈 ModelAndView
new ModelAndView()처럼 빈 ModelAndView를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다. - ModelAndView 지정
ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다. - null
null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다.
만약 처리할 수 있는 ExceptionResolver가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.
ExceptionResolver 활용
- 예외 상태 코드 변환 → response.sendError(400)
- 뷰 템플릿 처리 → BasicErrorController 없이 바로 응답
- API 응답 처리 → BasicErrorController 없이 바로 응답
예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아 다시 /error 를 호출하는 과정은 복잡하다. ExceptionResolver를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결할 수 있다.

정리
ExceptionResolver 를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver 에서 예외를 처리해버린다.
따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리는 끝이 난다.
결과적으로 WAS 입장에서는 정상 처리가 된 것이다. 이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심이다.
그런데 직접 ExceptionResolver를 구현하려니 상당히 복잡하다. 스프링이 제공해주는 ExceptionResolver들을 알아보자.
API 예외 처리 - 스프링이 제공하는 ExceptionResolver
스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같다.
HandlerExceptionResolverComposite 에 다음 순서로 등록
1. ExceptionHandlerExceptionResolver → @ExceptionHandler
2. ResponseStatusExceptionResolver → HTTP 응답 코드 변경
3. DefaultHandlerExceptionResolver → 스프링 내부 예외 처리
ResponseStatusExceptionResolver
HTTP 상태 코드를 지정해준다.
다음 두 가지 경우 에러일 경우에 ResponseStatusExceptionResolver가 동작한다.
- @ResponseStatus 가 달려있는 예외

- ResponseStatusException 예외 (개발자가 직접 변경할 수 없는 예외에 적용)

ResponseStatusExceptionResolver 코드를 확인해보면 결국 response.sendError(statusCode, resolvedReason)를 호출
DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver 는 스프링 내부에서 발생하는 스프링 예외를 해결한다.
대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException 이 발생하는데,
이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로 500 오류가 발생한다.
그런데 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이다.
HTTP 에서는 이런 경우 HTTP 상태 코드 400을 사용하도록 되어 있다.
DefaultHandlerExceptionResolver 는 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경한다.
DefaultHandlerExceptionResolver.handleTypeMismatch 를 보면 다음과 같은 코드를 확인할 수 있다.
response.sendError(HttpServletResponse.SC_BAD_REQUEST)
그런데 HandlerExceptionResolver 를 직접 사용하기는 복잡하다. 특히 API 오류 응답의 경우 response 에 직접 데이터를 넣어야 해서 매우 번거롭고, ModelAndView 를 반환해야 하는 것도 API에는 잘 맞지 않는다.
스프링은 이 문제를 해결하기 위해 @ExceptionHandler 라는 매우 혁신적인 예외 처리 기능을 제공한다.
API 예외 처리 - @ExceptionHandler
웹 브라우저에 HTML 화면을 제공할 때는 오류가 발생하면 BasicErrorController 를 사용하는게 편하다. 이때는 단순히 5xx, 4xx 관련된 오류 화면을 보여주면 된다.
그런데 API는 각 시스템 마다 응답의 모양도 다르고, 스펙도 모두 다르다. BasicErrorController 를 사용하거나 HandlerExceptionResolver 를 일일히 직접 구현하는 방식으로 API 예외를 다루기는 쉽지 않다.
- HandlerExceptionResolver 를 떠올려 보면 ModelAndView 를 반환해야 했다. 이것은 API 응답에는 필요하지 않다.
- API 응답을 위해서 HttpServletResponse 에 직접 응답 데이터를 넣어주었다. 매우 번거롭다..
- 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵다.
예를 들어서 회원을 처리하는 컨트롤러에서 발생하는 RuntimeException 예외와 상품을 관리하는 컨트롤러에서 발생하는 동일한 RuntimeException 예 외를 서로 다른 방식으로 처리하고 싶다면?
스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler 라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공하는데, 이것이 바로 ExceptionHandlerExceptionResolver 이다.
스프링은 ExceptionHandlerExceptionResolver 를 기본으로 제공하고, 기본으로 제공하는 ExceptionResolver 중에 우선순위도 가장 높다. 실무에서 API 예외 처리는 대부분 이 기능을 사용한다
@ExceptionHandler 예외 처리 방법
@ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다.
해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다.
지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있으며, 스프링의 우선순위는 항상 자세한 것이 우선권을 가진다.

실행 흐름
- 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.
- 예외가 발생했으로 ExceptionResolver가 작동, 우선순위가 높은 ExceptionHandlerExceptionResolver가 실행된다.
- ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리 할 수 있는 @ExceptionHandler가 있는지 확인한다.
- illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다.
따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다. - @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다. (기본 200)
@ControllerAdvice
@ExceptionHandler 를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있다. @ControllerAdvice 또는 @RestControllerAdvice 를 사용하면 둘을 분리할 수 있다.

@ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해주는 역할을 한다
@ControllerAdvice 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)
ProblemDetail: REST API의 에러 응답 표준화 도구
스프링 6(springboot 3 포함) 이후 REST API 예외 처리 시 ProblemDetail을 반환하는 것이 표준이 되었다.
ProblemDetail은 RFC 7807 (Problem Details for HTTP APIs) 표준을 구현한 객체로, HTTP API에서 에러 응답을 구조화된 JSON 또는 XML 형태로 반환할 때 사용된다.
장점
표준화된 에러 응답 포맷과, 기본 필드를 제공하여, 확장도 가능하다.
예전처럼 {status: -1, error: xxx, data: ...} 과 같이 직접 예외 DTO를 만들 필요 없이 표준화된 형식을 쉽게 활용할 수 있다.
@ControllerAdvice
public class ApiControllerAdvice extends ResponseEntityExceptionHandler {
// 공통
@ExceptionHandler(Exception.class)
public ProblemDetail handleException(Exception exception) {
return getProblemDetail(HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
// 필요 시 예외 클래스를 상속으로 묶는다.
@ExceptionHandler({DuplicateEmailException.class, DuplicateProfileException.class})
public ProblemDetail emailExceptionHandler(DuplicateEmailException exception) {
return getProblemDetail(HttpStatus.CONFLICT, exception);
}
private static ProblemDetail getProblemDetail(HttpStatus status, Exception exception) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, exception.getMessage());
problemDetail.setProperty("timestamp", LocalDateTime.now());
problemDetail.setProperty("exception", exception.getClass().getSimpleName());
return problemDetail;
}
}
응답 결과는 아래와 같이 출력된다.
MockHttpServletResponse:
Status = 409
Error message = null
Headers = [Content-Type:"application/problem+json"]
Content type = application/problem+json
Body = {"type":"about:blank","title":"Conflict","status":409,"detail":"이미 사용중인 이메일입니다: cjl2076@naver.com","instance":"/api/members","timestamp":"2025-09-15T23:09:39.5061465","exception":"DuplicateEmailException"}
참고 자료 & 이미지 출처
스프링 MVC 2편 (김영한 님)
토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처
'Spring > Spring MVC - 웹 개발 활용 기술' 카테고리의 다른 글
| 요약 - 로그인 처리 (0) | 2024.08.25 |
|---|---|
| 요약 - 검증과 예외 처리 (0) | 2024.08.24 |
| 예외 처리와 오류 페이지 (0) | 2024.08.10 |
| 로그인 처리2 - 필터, 인터셉터 (0) | 2024.08.10 |
| 로그인 처리 - 쿠키, 세션 (0) | 2024.08.06 |