Exception 발생을 위한 class
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
서블릿의 에러 처리
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(
HttpServletRequestrequest, HttpServletResponse response) {
log.info("API errorPage 500");
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 일 때 해당 메서드가 호출된다는 것이다. 결국 클라어인트가 받고 싶은 미디어 타입이 json이면 이 컨트롤러의 메서드가 호출된다.
스프링 부트의 예외 처리
- 스프링 부트는 BasicErrorController 가 제공하는 기본 정보들을 활용해서 오류 API를 생성해준다.
- 다음 옵션들을 설정하면 더 자세한 오류 정보를 추가할 수 있다.
- server.error.include-binding-errors=always
- server.error.include-exception=true
- server.error.include-message=always
- server.error.include-stacktrace=alway
- 다음 옵션들을 설정하면 더 자세한 오류 정보를 추가할 수 있다.
BasicErrorController의 기본 오류 처리 방식
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}
errorHtml() : produces = MediaType.TEXT_HTML_VALUE : 클라이언트 요청의 Accept 해더 값이 text/html 인 경우에는 errorHtml() 을 호출해서 view를 제공한다.
error() : 그외 경우에 호출되고 ResponseEntity 로 HTTP Body에 JSON 데이터를 반환한다.
Html 페이지 vs API 오류
BasicErrorController 를 확장하면 JSON 메시지도 변경할 수 있다.
그런데 API 오류는 @ExceptionHandler 가 제공하는 기능을 사용하는 것이 더 나은 방법이므로 지금은 BasicErrorController를 확장해서 JSON 오류 메시지를 변경할 수 있다 정도로만 이해해두자.
스프링 부트가 제공하는 BasicErrorController 는 HTML 페이지를 제공하는 경우에는 매우 편리하다. 4xx, 5xx 등등 모두 잘 처리해준다. 그런데 API 오류 처리는 다른 차원의 이야기이다. API 마다, 각각의 컨트롤러나 예외마다 서로 다른 응답 결과를 출력해야 할 수도 있다.
예를 들어서 회원과 관련된 API에서 예외가 발생할 때 응답과, 상품과 관련된 API에서 발생하는 예외에 따라 그 결과가 달라질 수 있다. 결과적으로 매우 세밀하고 복잡하다. 따라서 이 방법은 HTML 화면을 처리할 때 사용하고, API 오류 처리는 @ExceptionHandler 를 사용하자.
HandlerExceptionResolver
스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver 를 사용하면 된다. 줄여서 ExceptionResolver 라 한다. |
인터페이스
public interface HandlerExceptionResolver { ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); } |
package hello.exception.resolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
ExceptionResolver 가 ModelAndView 를 반환하는 이유는 마치 try, catch를 하듯이, Exception 을 처리해서 정상 흐름 처럼 변경하는 것이 목적이다. 이름 그대로 Exception 을 Resolver(해결)하는 것이 목적이다.
- 반환 값에 따른 동작 방식 (HandlerExceptionResolver의 반환 값에 따른 DispatcherServlet의 동작 방식)
- 빈 ModelAndView
- 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
- ModelAndView 지정
- View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.
- null
- 다음 ExceptionResolver 를 찾아서 실행한다.
만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고,
기존에 발생한 예외를 서블릿 밖으로 던진다.
- 다음 ExceptionResolver 를 찾아서 실행한다.
- 빈 ModelAndView
- ExceptionResolver 활용
- 예외 상태 코드 변환
- 예외를 response.sendError(xxx) 호출로 변경해서
서블릿에서 상태 코드에 따른 오류를 처리하도록 위임 - 이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출,
예를 들어서 스프링 부트가 기본으로 설정한 /error 가 호출됨
- 예외를 response.sendError(xxx) 호출로 변경해서
- 뷰 템플릿 처리
- ModelAndView 에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 해서 고객에게 제공
- API 응답 처리
- response.getWriter().println("hello"); 처럼 HTTP 응답 바디에 직접 데이터를 넣어주는것도 가능하다.
여기에 JSON 으로 응답하면 API 응답 처리를 할 수 있다.
- response.getWriter().println("hello"); 처럼 HTTP 응답 바디에 직접 데이터를 넣어주는것도 가능하다.
- 예외 상태 코드 변환
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
Configuration 파일에 등록 하는 방법이 2가지이다.
- configureHandlerExceptionResolvers(..)
- 이를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver가 제거됨
- extendHandlerExceptionResolvers(...)
- 이를 사용하자 (기본 +@)
HandlerExceptionResolver 활용
예외를 마무리 하기
예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아서 다시 /error 를 호출하는 과정은 생각해보면 너무 복잡하다.
ExceptionResolver 를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결할 수 있다.
- 사용자 지정 예외 만들기 - (extends RuntimeException)
- 이 예외를 처리하는 UserHandlerExceptionResolver 구현하기
@Slf4j
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 UserException) {
log.info("UserException resolver to 400");
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());
//Map에 담았던 값을 String으로
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result); //String 화면 출력
return new ModelAndView();
}
else {
//TEXT/HTML
return new ModelAndView("error/400");
}
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
HTTP 요청 해더의 ACCEPT 값이 application/json 이면 JSON으로 오류를 내려주고, 그 외 경우에는 error/
500에 있는 HTML 오류 페이지를 보여준다.
3. WebConfig에 UserHandlerExceptionResolver 추가
ExceptionResolver 를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver 에서 예외를 처리해버린다.
따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리는 끝이 난다.
결과적으로 WAS 입장에서는 정상 처리가 된 것이다. 이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심이다.
서블릿 컨테이너까지 예외가 올라가면 복잡하고 지저분하게 추가 프로세스가 실행된다.
반면에 ExceptionResolver 를 사용하면 예외처리가 상당히 깔끔해진다.
그런데 직접 ExceptionResolver 를 구현하려고 하니 상당히 복잡하다.
스프링이 제공하는 ExceptionResolver
스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같다.
- HandlerExceptionResolverComposite 에 다음 순서로 등록
- ExceptionHandlerExceptionResolver
- @ExceptionHandler 을 처리한다. API 예외 처리는 대부분 이 기능으로 해결한다.
- ResponseStatusExceptionResolver
- HTTP 상태 코드를 지정해준다.
- DefaultHandlerExceptionResolver => 우선 순위가 가장 낮다.
- 스프링 내부 기본 예외를 처리한다.
- ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다.
- 다음 두 가지 경우를 처리한다.
- @ResponseStatus 가 달려있는 예외
- @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
- reason 메시지 적용가능
- 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.
- ResponseStatusException 예외
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
- new ResponseStatusException(HTTP상태, "메시지", "실제 Exception");
- @ResponseStatus 가 달려있는 예외
DefaultHandlerExceptionResolver
- 스프링 내부 기본 예외를 처리한다.
- @RequestParam Integer data 에 문자가 들어오면? 이 핸들러로 인해 400으로 처리된다.
지금까지 HTTP 상태 코드를 변경하고, 스프링 내부 예외의 상태코드를 변경하는 기능도 알아보았다.
그런데 HandlerExceptionResolver 를 직접 사용하기는 복잡하다.
API 오류 응답의 경우 response 에 직접 데이터를넣어야 해서 매우 불편하고 번거롭다.
ModelAndView 를 반환해야 하는 것도 API에는 잘 맞지 않는다.
스프링은 이 문제를 해결하기 위해 @ExceptionHandler 라는 매우 혁신적인 예외 처리 기능을 제공한다.
'Spring' 카테고리의 다른 글
[Spring] Exception (예외) - ExceptionHandler (0) | 2024.06.27 |
---|---|
[Spring] 스프링 시큐리티(Spring Security) (0) | 2024.06.06 |
[Spring] Exception (예외) - 기본 http 요청 에러처리 (0) | 2024.05.28 |
[Spring] ArgumentResolver (0) | 2024.05.25 |
[Spring] 필터와 인터셉터 (0) | 2024.05.25 |