API 예외 처리는 어떻게 해야할까?
HTML 페이지의 경우4xx, 5xx와 같은 오류 페이지를 만들어 문제를 해결했다.API의 경우각 오류 상황에 맞는 오류 응답 스펙을 정하고 JSON으로 데이터를 내려주어야 한다.
Customized Error Page Setting
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
test code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable(name = "id") String id) {
if ("ex".equals(id)) {
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
API 요청 후, 정상처리될 경우 JSON 형식으로 데이터가 정상 반환된다.
그렇지 않을 경우 미리 만들어둔 오류 페이지로 넘어가게 되는데,
정상/오류 요청 모두 JSON이 반환되도록 수정하자.
ErrorPageController 코드 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response) {
log.info("API error page 500");
Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION); // jakarta.servlet.error.exception
String exceptionMessage = ex.getMessage();
Object status = request.getAttribute(ERROR_STATUS_CODE); // jakarta.servlet.error.status_code
HashMap<String, Object> result = new HashMap<>();
result.put("status", status);
result.put("message", exceptionMessage);
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 일 때 해당 메소드 호출
즉, 클라이언트가 받고 싶은 미디어 타입이 json 이면 컨트롤러의 메소드가 호출된다.
1
2
3
4
{
"message": "잘못된 사용자",
"status": 500
}
PostMan에서 HTTP Header에 Accept application/json 설정 후 http://localhost:8080/api/members/ex 호출 한 결과값.
스프링 부트 기본 오류 처리
BasicErrorController Code
1
2
3
4
5
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}
errorHtml():클라이언트 요청의 Accept 해더 값이 text/html 인 경우에는 errorHtml() 을 호출해서 view를 제공
error():그외 경우에 호출되고 ResponseEntity 로 HTTP Body에 JSON 데이터를 반환
HTML 페이지 오류 vs API 오류
- HTML 페이지 오류 처리: 스프링 부트에서 자동 등록된 BasicErrorController 사용
- API 오류 처리: @ExceptionHandler가 제공하는 기능을 사용하여 JSON 메시지 변경 가능
HandlerExceptionResolver
- 예외가 WAS 까지 전달되면 HTTP 상태코드는 500으로 처리된다. 발생하는 예외에 따라 400, 404 등 다른 상태코드로 처리하려면 어떻게 해야 할까?
상태코드 변환
- IllegalArgumentException 을 처리하지 못해 컨트롤러 밖으로 넘어왔을 경우, HTTP 상태코드를 400으로 처리해보자.
controller code added
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable(name = "id") String id) {
if ("ex".equals(id)) {
throw new RuntimeException("잘못된 사용자");
}
if ("bad".equals(id)) {
throw new IllegalArgumentException("잘못된 입력 값");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
스프링 부트에서 지원하는 에러 코드 형식
1
2
3
4
5
6
7
8
// http://localhost:8080/api/members/bad 호출 시
{
"timestamp": "2024-06-27T07:46:14.735+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.IllegalArgumentException",
"path": "/api/members/bad"
}
HandlerExceptionResolver- 컨트롤러 밖으로 던져진 예외를 해결하고 동작 방식을 변경한다.
- 줄여서
ExceptionResolver라고 한다.
- ExceptionResolver 적용 전
- ExceptionResolver 적용 후
![]()
- ExceptionResolver 로 예외를 해결해도 postHandle()은 호출되지 않는다.
HandlerExceptionResolver Interface
1
2
3
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
handler: 핸들러(컨트롤러) 정보
Exception ex: 핸들러(컨트롤러)에서 발생한 예외
HandlerExceptionResolver Interface 구현
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
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
/**
* @param request
* @param response
* @param handler: 컨트롤러
* @param ex: 컨트롤러에서 발생한 예외
* @return
*/
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
/**
* IllegalArgumentException 이 발생하면,
* response.sendError(400) 를 호출해서 HTTP 상태코드를 400으로 지정하고,
* 빈 ModelAndView 를 반환
*/
if (ex instanceof IllegalArgumentException) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
빈 ModelAndView 반환: 뷰를 렌더링하지 않고, 정상 흐름으로 서블릿이 리턴된다.
ModelAndView 지정: ModelAndView에 View, Model 등의 정보를 지정해서 반환하면 뷰를 렌더링한다.
null 반환: 다음 ExceptionResolver를 찾아서 실행한다. 처리할 수 있는 ExceptionResolver가 없다면 예외 처리가 안되고, 기본에 발생한 예외를 서블릿 밖으로 던진다.
커스텀한 ExceptionResolver 적용
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
...
@Override
public void extendHandlerExceptionResolve(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
}
WebMvcConfigurer 인터페이스 구현(extendHandlerExceptionResolve() 메소드 오버 라이딩)
HandlerExceptionResolver 활용
- 예외 발생 시 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아 다시 /error 를 호출하는 과정은 너무 복잡한다.
- ExceptionResolver 를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결할 수 있다.

