서블릿 API 에러 처리
에러 페이지는 단순히 에러 페이지만 랜더링 하면 되지만,
API 는 각 오류 상황에 맞는 응답 스펙을 정하고, 데이터를 JSON 으로 주어야 한다.
오류 페이지 컨트롤러에 JSON 응답을 해주는 메서드를 추가하면 된다.
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(
HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> result = new HashMap<>();
Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
result.put("status", request.getAttribute(ERROR_STATUS_CODE));
result.put("message", ex.getMessage());
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
return new ResponseEntity(result, HttpStatus.valueOf(statusCode));
}
produces = MediaType.APPLICATION_JSON_VALUE
: 클라이언트가 요청하는 HTTP Header 의 Accept
값이 application/json
일 때 해당 메서드가 호출
스프링 부트 API 에러 처리
BasicErrorContoller
BasicErrorController
코드에는 errorHtml()
, error()
두 메서드가 있다.
- errorHteml()
: produces = MediaType.TEXT_HTML_VALUE 를 통해 클라이언트 요청의 Accept 헤더 값이 text/heml 인 경우에 호출되어 view 를 제공한다.
- error()
: 그 외 경우에 호출되며 ResponseEntity 로 HTTP Body 에 JSON 데이터를 반환한다.
따라서 스프링 부트는 BasicErrorController
가 제공하는 기본 정보를 활용하여 오류 API 를 자동으로 생성해 준다.
HandlerExceptionResolver 구현하여 만들기
예외가 발생하여 WAS 까지 예외가 전파되면 HTTP 상태코드가 500으로 처리된다.
이렇게 HandlerExceptionResolver 는 예외가 발생했을 때
상태코드, 에러 메시지, 형식 등 다양하게 처리할 수 있도록 도와준다.
동작 흐름
preHandle ↗ ① preHandle ↗ |
||||||
클라이언트 | HTTP 요청 → → → → |
Dispatcher Servlet |
② handle(handler) → → → → → → ← ← ← ← ← ← ④ 예외 전달 |
핸들러 어댑터 |
③ 예외 발생 → → → → |
핸들러 (컨트롤러) |
↓ ⑥ render(model) 호출 ↓ |
↘ ↘ ⑤ 예외 해결 시도 ↘ ↘ ExceptionResolver ↘ ↘ ⑦ afterCompletion afterCompletion |
|||||
HTML 응답 ← ← ← ← |
View |
예외가 발생하면 ExceptionResolver 가 호출되어 예외 해결을 시도한다.
이때 예외가 해결되어도 postHandle( ) 은 호출되지 않는다.
사용 예시 - response.sendError(xxx)
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
}
return null;
}
}
응답에 response.sendError(xxx) 를 통해 원하는 HTTP 상태코드와 메시지를 담아준다.
사용 예시 - response.setStatus(xxx)
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if ("application/json".equals(acceptHeader)) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
return new ModelAndView();
} else {
return new ModelAndView("error/400");
}
}
} catch (IOException e) {
}
return null;
}
}
응답에 response.setStatus 를 통해 원하는 HTTP 상태코드를 담아준다.
HTTP 요청 헤더의 ACCEPT 값이 application/json 이면 JSON 으로 오류를 전달하고,
그 외의 경우는 HTML 에러 페이지를 보여준다.
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver());
}
extendHandlerExceptionResolvers
를 통해 ExceptionResolver 를 등록한다.
> 참고 <
등록할 때, configureHandlerExceptionResolvers( ) 를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver 가 제거되므로 extendHandlerExceptionResolver 를 사용해야 한다.
반환값에 따른 동작 방식
빈 ModelAndView
: 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴
ModelAndView 지정
: 지정한 뷰를 렌더링
null
: 처리할 수 있는 다음 ExceptionResolver 를 찾고, 없으면 기존에 발생한 예외를 서블릿 밖으로 던짐
활용
- 예외 상태 코드 변환
: response.sendError(xxx) 사용
> 이후 WAS 는 내부적으로 BasicErrorController 를 통해 에러 페이지를 찾아 호출하여 추가 프로세스가 실행된다.
: response.setStatus(xxx) 사용
> 스프링 MVC 에서 예외 처리가 끝이 나고, WAS 에서는 정상 처리되어 추가 프로세스가 실행되지 않는다.
- 뷰 템플릿 처리
: ModelAndView 를 지정하여 예외에 따른 새로운 에러 페이지 뷰 렌더링 가능
- API 응답 처리
: response.getWriter( ).println("hello") 처럼 HTTP 응답 바디에 직접 JSON 등 데이터를 넣어줄 수도 있음
스프링 ExceptionResolver
스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음 순서로 등록되어 사용된다.
1. ExceptionHandlerExceptionResolver
2. ResponseStatusExceptionResolver
3. DefaultHandlerExceptionResolver
ExceptionHandlerExceptionResolver
@ExceptionHandler 를 처리한다.
@ExceptionHandler
API 오류 응답의 경우에 발생하는 아래의 문제점을 해결한다.
1. ModelAndView 로 반환
2. response 에 직접 응답 데이터를 넣기
3. 동일한 예외를 컨트롤러 별로 처리하기 어려움
사용 방법
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
예외가 발생했을 때 API 응답으로 사용하는 객체를 정의한다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(Exception e) {
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@RestController 로, @ResponseBody 가 적용되어 HTTP 컨버터가 사용되어 응답이 JSON 으로 자동 변환된다.
@ExceptionHandler
: 해당 컨트롤러에서 처리하고 싶은 예외를 지정
: 예외가 발생하면 이 메서드가 호출되며, 지정한 예외 + 자식 클래스까지 잡을 수 있다.
: @ExceptionHandler 에 예외를 생략하면, 메서드 파라미터의 예외가 지정된다.
@ResponseStatus
: HTTP 상태 코드를 지정한다.
: 애노테이션이므로 HTTP 상태 코드를 동적으로 변경할 수 없다.
ResponseEntity
: HTTP 상태 코드와 메시지를 지정한다.
: 프로그래밍해서 HTTP 상태 코드를 동적으로 변경할 수 있다.
ResponseStatusExceptionResolver
HTTP 상태 코드를 지정해 준다.
- @ResponseStatus
달려있는 예외
response.sendError(xxx) 로 HTTP 상태 코드를 변경하고, 메시지도 담는다.
이 때, response.sendError(xxx) 를 사용했기 때문에 WAS 에서 내부적으로 에러 페이지를 호출한다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
reason 은 MessageSource
에서 찾는 기능도 제공한다.
(ex. reason = "error.bad"
)
하지만 직접 변경할 수 없는 예외에는 적용할 수 없고, 조건에 따라 동적으로 변경하는 것이 어렵다.
- ResponseStatusException
예외
동적으로 HTTP 상태코드와, 에러 메시지, 실제 발생한 예외를 지정할 수 있다.
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
이때도 MessageSource
에서 찾는 기능이 제공된다.
DefaultHandlerExceptionResolver
스프링 내부에서 발생하는 기본 예외를 처리한다.
예를 들어, 파라미터 바인딩 시점에 타입이 맞지 않으면 발생하는 TypeMismatchException
은
WAS 까지 전달되어 500 오류를 발생한다.
하지만 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이므로
DefaultHandlerExceptionResolver 가 400 오류로 변경한다.
이때, response.sendError(xxx) 를 사용했기 때문에 WAS 에서 내부적으로 에러 페이지를 호출한다.
@ControllerAdvice
@ControllerAdvice, @RestControllerAdvice 를 사용하여 @ExceptionHandler 와 컨트롤러 메서드를 분리할 수 있다.
@ControllerAdvice
- 대상으로 지정한 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해 주는 역할
- 대상을 지정하지 않으면 모든 컨트롤러에 적용
- @RestControllerAdvice 는 @ResponseBody 가 추가된 것과 같다.
대상 컨트롤러 지정 방법
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
출처 | 스프링 MVC 2(김영한) - 인프런
'💠프로그래밍 언어 > Java' 카테고리의 다른 글
[Spring] 서블릿 파일 업로드와 스프링 파일 업로드 (0) | 2025.04.06 |
---|---|
[Spring] 컨버터와 포맷터 (컨버전 서비스, 스프링 기본 제공 포맷터) (0) | 2025.04.06 |
[Spring] 서블릿 예외 처리와 스프링 부트 예외 처리 (0) | 2025.04.04 |
[Spring] 컨트롤러 공통 작업 자동화 (ArgumentResolver 의 활용) (0) | 2025.04.04 |
[Spring] 인터셉터란? + 요청 로그 인터셉터, 인증 체크 인터셉터 예시 (0) | 2025.04.04 |