HTML 페이지 - 4xx, 5xx같은 오류 페이지만 보여주면 예외 처리 O
API - 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려줘야 함
1. 서블릿 API 예외 처리
1) WebServerCustomizer 동작시키기
- WAS에 예외 전달 / response.sendError() 호출 -> 여기서 등록한 예외 페이지 경로 호출
@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");
// Exception 발생
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
2) API 예외 컨트롤러
@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;
}
}
- API 니까 @RestController
- getMember() : 회원 조회하는 메소드 ( URL에 전달된 id 값이 ex이면 예외 발생 )
3) Postman으로 테스트
* HTTP Headers -> Accept -> application/json 필수! (accept : 클라이언트가 나는 json을 받아서 읽을 수 있다는 뜻)
(1) 정상 호출
- API로 JSON 형식의 데이터 정상 반환
(2) 예외 발생 호출
- API 통신
-> JSON으로 데이터를 주고 받아야 함
-> accept : application/json (클라이언트는 json을 받아서 읽을 수 있음)
-> HTML이 응답됨 (ex -> /error-page/500 -> 500.html)
-> 클라이언트가 HTML을 읽을 수가 없음 (웹 브라우저 말고는 HTML을 직접 받아서 할 수 있는 것은 별로 없음)
-> 오류 페이지 컨트롤러도 JSON 응답을 할 수 있도록 수정해야 함
4) API 응답 추가 - ErrorPageController
* 오류 페이지 컨트롤러가 JSON 응답할 수 있도록 수정
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, 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()); // Exception에서 던진 오류 메시지
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이면 ) 해당 메소드 호출
-> accept : 클라이언트 - 난 이 타입을 받을 거야
produces : 그럼 그 타입으로 만들어줄게
- 클라이언트가 요청하는 HTTP Header의 Accept 값이 application/json 일 때
- Map result = new HashMap<>();
- Jackson 라이브러리는 Map을 JSON 구조로 변환할 수 있음
- Jackson 라이브러리는 Map을 JSON 구조로 변환할 수 있음
- ResponseEntity<객체, 상태코드>
- HTTP 응답 - HTTP API -> 메시지 바디에 직접 입력해서 응답 - JSON 데이터 응답
- HTTP 메시지 컨버터를 통해 JSON 형식으로 변환되어서 반환
* HTTP accept, produce 복습
5) 수정 버전 포스트맨으로 다시 실행
- JSON 형식으로 응답 ✔
- 동작 흐름
- ex 호출
-> RuntimeException 발생 (오류 메시지: "잘못된 사용자")
-> ErrorPage에 RuntimeException 객체로 담겨있음
-> /error-page/500 으로 다시 호출하는데
-> accept를 확인 -> json이면
-> produces 가 json인 컨트롤러로 호출 - HTTP Header의 Accept가 application/json이 아니면,
-> ErrorPageController에서 produces타입이 json이 아닌, 그냥 기존 에러페이지 호출하는 컨트롤러 실행
-> 기존 오류 응답인 HTML 응답 출력
- ex 호출
2. 스프링 부트 API 예외 처리
2-1. 스프링 부트 기본 오류 처리
1) WebServerCustomizer 중지
- 주석처리
- 스프링부트가 기본으로 제공하는 BasicErrorController 사용
2) 스프링부트가 제공하는 BasicErrorController
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
- 스프링부트의 기본 설정 : 오류 발생 시 /error를 오류 페이지로 요청
- BasicErrorController는 이 경로를 기본으로 받음
( 기본 경로 : /error, server.error.path로 수정 가능 ) - errorHtml(), error() 두 메소드 모두 동일하게 /error 경로 처리 -> 둘 중 적절한 메소드 실행
- errorHtml()
- produces = MediaType.TEXT_HTML_VALUE : 클라이언트 요청의 Accept 헤더 값이 text/html인 경우 이 메소드 호출
- ModelAndView 반환 : 오류 페이지 반환
- error()
- 클라이언트 요청의 Accept 헤더 값이 text/html가 아닌 나머지 경우에 이 메소드 호출
- ResponseEntity<> 반환 : ResponseEntity로 HTTP Body에 JSON 데이터를 직접 반환
3) 포스트맨 실행
- BasicErrorController가 제공하는 기본 정보들을 활용해서 오류 API 생성
- 옵션 설정 시 더 자세한 오류 정보 추가 가능 ( BasicErrorController 확장 )
- always로 설정
- 하지만 보안상 위험
-> 간결한 메시지만 노출, 로그를 통해서 확인 권장
* application.properties
server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=always
server.error.include-binding-errors=always
4) Html 페이지 vs API 오류
Html 오류 처리 : BasicErrorController 사용 (오류 페이지만 개발)
API 오류 처리 : @ExceptionHandler 사용
2-2. HandlerExceptionResolver
예외 발생 -> 서블릿 넘어 WAS까지 예외 전달 -> 모두 HTTP 상태코드 : 500 (서버 내부)
근데 상태코드를 변경하고 싶을 때
(예: IllegalArgumentException - 파라미터 잘못 입력 -> 400 에러 (클라이언트가 파라미터 잘못 넘김) 기대 )
1) 현재 동작 확인 ( IllegalArgumentException 발생 시 500 에러 발생 )
* 컨트롤러
- bad 호출 -> IllegalArgumentException 발생
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException(("잘못된 입력 값"));
}
return new MemberDto(id, "hello " + id);
}
* 실행 (500에러)
2) HandlerExceptionResolver ( = ExceptionResolver )
컨트롤러 밖으로 예외가 던져진 경우, 예외를 해결하고 동작을 새로 정의할 수 있는 방법 제공
- WAS -> 서블릿 -> 인터셉터(preHandler) -> 핸들러 어댑터 -> 핸들러(컨트롤러) : 예외발생
- WAS (예외 전달) -> 서블릿 <- afterCompletion 호출 <- postHandler 호출 X <- 핸들러(컨트롤러)
- 서블릿 밖인 WAS까지 예외가 전달돼 500 에러 반환
- WAS -> 서블릿 -> 인터셉터(preHandle) -> 핸들러 어댑터 -> 핸들러(컨트롤러) : 예외발생
- WAS <- ExceptionResolver 호출 <- 서블릿에 예외 전달 <- postHandler 호출 X <- 핸들러(컨트롤러)
- 예외를 잡아서 정상적으로 나갈 수 있게 해줌
📌 ExceptionResolver 에서 처리할 수 있는 기회
1. 처리해서 완전 정상적인 흐름으로 바꾸기
2. sendError() 호출해서 상태코드만 바꾸고 WAS에서 다시 오류페이지 내부요청
* 인터페이스 - HandlerExceptionResolver
- handler : 핸들러(컨트롤러) 정보
- Exception ex : 핸들러(컨트롤러)에서 발생한 예외
public interface HandlerExceptionResolver {
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
3) HandlerExceptionResolver 구현 - MyHandlerExceptionResolver
@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(); // 예외를 먹어서 400을 다시 sendError, 정상호출
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
- 예외를 먹고 Exception 예외를 sendError로 바꿔서 상태코드도 변경해서 보냄
(Exception : 서블릿 -> 서블릿 밖 WAS 까지 던짐)
(sendError : 서블릿에서 오류 인지 -> 정상적으로 반환 -> WAS에서 response 확인 -> 오류 페이지 호출) - IllegalArgumentException 발생 -> response.sendError(400) 호출 -> 상태 코드를 400으로 지정 -> 빈 ModelAndView 반환
- ExceptionResolver가 ModelAndView를 반환하는 이유 ???
- try, catch 하듯이, Exception을 잡아서 처리해서 정상 흐름처럼 변경
- try, catch 하듯이, Exception을 잡아서 처리해서 정상 흐름처럼 변경
- 반환 값에 따른 DispatcherServlet 동작 방식
- 빈 ModelAndView : 뷰를 렌더링하지 않고, 정상 흐름으로 서블릿이 WAS에 리턴
(근데 이제 sendError()를 호출했으니까 그거는 따로 처리) - ModelAndView 지정 : 뷰 렌더링
- null : 다음 ExceptionResolver를 찾아서 실행, 만약 없으면 예외처리 안되고, 기존에 발생한 예외를 서블릿 밖으로 던짐 (Exception이 WAS까지 날아감 -> 500에러)
- 빈 ModelAndView : 뷰를 렌더링하지 않고, 정상 흐름으로 서블릿이 WAS에 리턴
4) 리졸버 등록 - WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
- configureHandlerExceptionResolvers(...) 사용하면
-> 스프링이 기본으로 등록하는 ExceptionResolver가 제거됨
-> extendHandlerExceptionResolvers 사용
5) 실행
2-3. HandlerExceptionResolver 활용해서 API 예외 처리
WAS -> 서블릿 -> 인터셉트 -> 컨트롤러 (예외 발생)
-> 예외 발생 -> 서블릿 -> WAS까지 예외 전달 -> WAS에서 오류 페이지 정보 찾아서 다시 /error 호출
-> WAS에서 /error 호출 -> 컨트롤러 -> 뷰 (오류 페이지) 반환
--> 왔다갔다 복잡함 -> HandlerExceptionResolver 활용
- ExceptionResolver 활용
- 예외 상태 코드 변환
- 예외를 sendError() 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임
- 이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출 (예: /error)
- 뷰 템플릿 처리
- ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링해서 제공할 수 있음
- ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링해서 제공할 수 있음
- API 응답 처리
- response.getWriter().println("hello") 처럼 HTTP 응답 바디에 직접 데이터 넣기 가능
JSON 을 넣으면 API 응답 처리 가능
- response.getWriter().println("hello") 처럼 HTTP 응답 바디에 직접 데이터 넣기 가능
- 예외 상태 코드 변환
1) 사용자 정의 예외 생성 - UserException
package hello.exception.exception;
public class UserException extends RuntimeException {
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
protected UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
2) 예외 추가 - ApiExceptionController
- user-ex 호출 시 UserException 예외 발생
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException(("잘못된 입력 값"));
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
3) 예외 처리 - UserHandlerExceptionResolver
package hello.exception.resolver;
import com.fasterxml.jackson.databind.ObjectMapper;
import hello.exception.exception.UserException;
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;
import java.util.HashMap;
import java.util.Map;
@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");
// accept header 꺼내기
String acceptHeader = request.getHeader("accept");
// response 상태코드 변경
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
// accept header가 json일 때
if ("application/json".equals(acceptHeader)) {
Map<String, Object> errorResult = new HashMap<>();
// 어떤 Exception이 터졌는지, 오류 메시지가 뭔지 저장
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
// json 형태인 errorResult를 string으로 변환
String result = objectMapper.writeValueAsString(errorResult);
// 이 데이터를 response 바디에 넣어줌 (ModelAndView를 반환해야 돼서 -> json은 바디에 직접 반환)
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
// 빈 ModelAndView 반환 -> 예외는 먹어버리지만 정상 흐름으로 서블릿까지 response를 전달
return new ModelAndView();
} else {
// accept header가 text/html 등 json이 아닐 때
return new ModelAndView("error/500");
}
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
- HTTP 요청 헤더의 accept가 application/json일 때 : JSON으로 오류 출력
- HTTP 요청 헤더의 accept가 application/json이 아닐 때 : error/500에 있는 HTML 오류 페이지 출력
4) 리졸버 등록 - WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver());
}
- ExceptionResolver에서 ModelAndView를 반환하지 않으면 다음 예외 리졸버 호출
5) 실행
(1) 예외처리 안할 때
(2) accept : application/json일 때
(3) accept : text/html 일 때
6) 정리
- 원래 이 과정
- WAS -> 서블릿 -> 인터셉트 -> 컨트롤러 (예외 발생)
-> 예외 발생 -> 서블릿 -> WAS까지 예외 전달 -> WAS에서 오류 페이지 정보 찾아서 다시 /error 호출
-> WAS에서 /error 호출 -> 컨트롤러 -> 뷰 (오류 페이지) 반환
- WAS -> 서블릿 -> 인터셉트 -> 컨트롤러 (예외 발생)
- ExceptionResolver 사용
- 컨트롤러에서 예외가 발생
-> 서블릿까지 전달 X, ExceptionResolver에서 예외 처리
-> 스프링 mvc에서 예외를 먹고, 예외처리를 끝내버리는 것
-> 빈 ModelAndView 반환 -> 서블릿은 정상 처리되었다고 판단
-> WAS 입장에서는 정상 처리
- 컨트롤러에서 예외가 발생
-> 근데 ExceptionResolver를 직접 구현하기 복잡 -> 스프링이 제공하는 ExceptionResolver
3. 스프링이 제공하는 ExceptionResolver (예외처리)
3-1. 스프링 부트가 기본으로 제공하는 ExceptionResolver
HandlerExceptionResolverComposite에 순서대로 등록 (1순위에서 null 반환되면 2,3순위)
- ExceptionHandlerExceptionResolver
- @ExceptionHandler 처리 - API 예외 처리는 대부분 이 기능으로 해결
- 너무 중요해서 뒤에서 설명
- ResponseStatusExceptionResolver
- HTTP 상태 코드 지정
- @ResponseStatus(value = HttaStatus.NOT_FOUND)
- DefaultHandlerExceptionResolver
- 스프링 내부 기본 예외 처리
3-2. ResponseStatusExceptionResolver (HTTP 상태 코드 지정)
원래 Exception 예외는 500 반환하는데, 예외에 따라서 HTTP 상태 코드 지정
- @ResponseStatus 어노테이션이 붙어있는 예외
- ResponseStatusException 예외
1) @ResponseStatus 어노테이션 적용 -> HTTP 상태 코드 변경
(1) 사용자 지정 예외 만들기 - BadRequestException
package hello.exception.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException{
}
- 이 예외가 컨트롤러 밖으로 넘어가면, ResponseStatusExceptionResolver가 해당 어노테이션 확인
-> 오류코드(code)를 HttpStatus.BAD_REQUEST(400)로 변경, 메시지(reason)도 담음
+) 메시지 기능 (reason을 MessageSource에서 찾는 기능)
* BadRequestException
//@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException{
}
* messages.properties
error.bad=잘못된 요청 오류입니다. 메시지 사용
(2) 예외 추가 - ApiExceptionController
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
(3) 실행
이게 어떻게 되냐?
ResponseStatusExceptionResolver 코드 확인
exception을 잡아서 sendError(statusCode, resolvedReason) 호출 -> 상태코드 변경 -> 빈 모델 반환 -> 정상 동작
-> sendError(400) 을 호출했기 때문에 WAS에서 다시 오류페이지(/error) 내부 요청
2) ResponseStatusException 예외 사용 -> HTTP 상태 코드 변경
@ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 X
어노테이션이기 때문에 조건에 따라 동적으로 변경할 수 X
-> ResponseStatusException 예외를 던져버림
(1) 예외 추가 - ApiExceptionController
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
(2) 실행
3-3. DefaultHandlerExceptionResolver - 스프링 내부 예외의 상태코드 변경
ex) 파라미터 바인딩 시점에 타입이 맞지 않을 때 -> TypeMismatchException 예외 발생
-> 예외니까 서블릿 -> WAS까지 예외 전달 -> 500 오류 발생 (원래)
근데 파라미터 타입 오류는 클라이언트 잘못 -> 400 오류여야 함
-> DefaultHandlerExceptionResolver 가 해결
1) 예외 추가 - ApiExceptionController
- 요청 파라미터 타입 - Integer
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
2) 실행
(1) 올바른 파라미터 타입
(2) 틀린 타입 ( -> 알아서 400 오류 발생 )
3) DefaultHandlerExceptionResolver 코드 확인
- sendError() 로 코드 변경 -> WAS에서 다시 오류 페이지(/error) 내부 요청
3-4. ExceptionHandlerExceptionResolver - 스프링 API 예외 처리 (우선순위 1등)
- HTML 화면 오류 vs API 오류
- HTML 화면 제공할 때 오류 발생 -> BasicErrorController 사용 -> 5xx, 4xx 관련 오류 화면 출력
- API 오류 발생 -> @ExceptionHandler 사용
- API 예외 처리의 어려운 점
- API 응답에는 ModelAndView 반환이 필요 X (@RestController, ResponseEntity 등 - json으로 바로 반환)
- API 응답을 ModelAndView로 반환하기 위해 HttpServletResponse에 직접 응답 데이터 넣는 것은 불편함
- 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어려움
(상품 컨트롤러 / 주문 컨트롤러에서 동일한 예외를 다른 방식으로 처리하고 싶을 때)
1) @ExceptionHandler
(1) 예외 발생 시 API 응답으로 사용하는 객체 - ErrorResult
package hello.exception.exhandler;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
(2) 예외 추가
package hello.exception.api;
import hello.exception.exception.UserException;
import hello.exception.exhandler.ErrorResult;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER_EX", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", e.getMessage());
}
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException(("잘못된 입력 값"));
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
- @ExceptionHandler 예외 처리 방법
- @ExceptionHandler 어노테이션 선언
- 해당 컨트롤러에서 처리하고 싶은 예외를 지정 ( 지정한 예외 + 자식 클래스까지 처리)
- 해당 컨트롤러에서 예외가 발생하면 이 메소드 호출 ( 해당 컨트롤러에서만 사용 가능 ! )
(3) 실행
예외 안잡았을 때 - 아래 응답은 서블릿 WAS까지 갔다가 재호출 돼서 내려온 응답임
① IllegalArgumentException 처리
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("bad")) {
throw new IllegalArgumentException(("잘못된 입력 값"));
}
return new MemberDto(id, "hello " + id);
}
- /bad 호출 -> IllegalArgumentException 예외가 컨트롤러 밖으러 던져짐 (Exception 이니까)
- 예외 발생 -> ExceptionResolver 작동 -> 가장 우선순위 높은 ExceptionHandlerExceptionResolver 실행
- ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리할 수 있는 @ExceptionHandler가 있는지 확인
- illegalExHandle() 실행 -> @RestController -> illegalExHandle()에도 @ResponseBody 적용
-> HTTP 컨버터 실행 -> ErrorResult 객체가 JSON으로 반환 - @ResponseStatus(HttpStatus.BAD_REQUEST) -> HTTP 상태 코드 400으로 응답
② UserException 처리
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER_EX", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
}
- @ExceptionHandler에 예외클래스 지정 X (생략) -> 해당 메소드의 파라미터를 예외로 사용
- ResponseEntity -> HTTP 메시지 바디에 직접 응답 -> HTTP 응답 코드를 동적으로 변경 가능
③ Exception 처리
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", e.getMessage());
}
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
}
- RuntimeException 발생 -> RuntimeException은 Exception의 자식 클래스 -> exHandle 호출
- Exception은 모든 예외의 조상 -> 모든 예외를 잡을 수 있음
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR로 상태 코드 500으로 응답
2) @ControllerAdivce, @RestControllerAdvice
정상 코드와 예외 처리 코드 분리
(1) 예외 처리 코드 컨트롤러 - ExControllerAdvice
- 예외 처리 코드 모음
- @RestControllerAdvice : @ControllerAdvice + @ResponseBody
- 대상 지정 X -> 모든 컨트롤러에 적용
package hello.exception.exhandler.advice;
import hello.exception.exception.UserException;
import hello.exception.exhandler.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER_EX", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", e.getMessage());
}
}
(2) 예외 발생 컨트롤러 - ApiExceptionV2Controller
- @ExceptionHandler 모두 제거
package hello.exception.api;
import hello.exception.exception.UserException;
import hello.exception.exhandler.ErrorResult;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException(("잘못된 입력 값"));
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
(3) @ControllerAdvice
- 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @Initbinder 기능 부여
(원래 @ExceptionHandler가 있는 해당 컨트롤러에서만 작동함 ) - 대상을 지정하지 않으면 모든 컨트롤러에 적용 (글로벌 적용)
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody
* 대상 컨트롤러 지정 방법
// @RestController가 붙은 컨트롤러
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// 특정 패키지에 있는 컨트롤러
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// 특정 클래스 지정
@ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class})
public class ExampleAdvice3 {}
(4) 적용 예시
* 예외 처리 컨트롤러 - 특정 패키지에 속한 컨트롤러에 모두 적용
@Slf4j
@RestControllerAdvice(basePackages = "hello.exception.api")
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER_EX", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", e.getMessage());
}
}
여기에 속한 컨트롤러에 모두 적용
api2 , api3 똑같이 적용
📌 예외 처리 방법
1. @ExceptionHandler 우선 고려
2. try ~ catch
'Spring' 카테고리의 다른 글
[인프런/스프링 MVC 2편] 11. 파일 업로드, 다운로드 (0) | 2023.06.02 |
---|---|
[인프런/스프링 MVC 2편] 10. 스프링 타입 컨버터 (0) | 2023.05.31 |
[인프런/스프링 MVC 2편] 8. 예외 처리와 오류 페이지 (0) | 2023.05.27 |
[Spring] 스프링 빈 등록 어노테이션 @Bean, @Configuration, @Component (0) | 2023.05.27 |
[인프런/스프링 MVC 2편] 7. 로그인 처리2 - 필터, 인터셉터 (0) | 2023.05.26 |