티스토리 뷰
(참고: Spring Boot를 기준으로 작성된 글입니다)
HTML 페이지 vs API 오류
단순히 HTML 웹페이지를 반환하였던 오류의 경우에는 스프링 부트가 제공하는 BasicErrorController로 충분하였다, 404 혹은 4xx의 오류코드만 확인하여 해당 HTML을 클라이언트(사용자)에게 반환해주었으면 그만이니 말이다
하지만 API의 경우 이것보다 훨씬 세밀하고 복잡한 상황을 마주한다, API 같은 경우는 안드로이드와 아이폰 혹은 기업과 기업 간의 다양한 객체들이 소통하며 각기 다른 조건과 요구사항을 만족하여 오류처리를 하여야 하기 때문이다
예를 들어 회원관리에서만 발생하는 오류와, 상품관리에서만 발생하는 오류를 각각 묶어서 처리하고 싶다면 기존의 단순히 HTML을 반환하는 오류처리와는 다르게 각각의 컨트롤러 혹은 예외마다 서로 다른 응답 결과를 출력해야 하는 상황이 발생하는 것이다
HandlerExceptionResolver의 기본 흐름과 목적(Exception Resolver)
이렇게 다양한 결과물을 출력하기 위하여 스프링은 HandlerExceptionResolver라는 것을 제공한다
그렇다면 Exception Resolver가 어떻게 작동하는지 흐름도를 살펴보자 (인터셉터가 적용되어 있는것 주의)

1. preHandle을 호출한다
2. 핸들러(컨트롤러)를 호출한다
3. 예외가 발생한다
4. postHandle은 호출되지 않고 afterCompletion이 되어진다

1. preHandle을 호출한다
2. 핸들러(컨트롤러)를 호출한다
3. 예외가 발생한다
4. postHandle을 여전히 호출되지 않고 ExceptionResolver에 예외를 처리할 수 있는지 확인한다
5. afterCompletion을 호출한다
6. 만약 ExceptionResolver에서 처리할 수 있다면 ModelAndView를 반환한다
얼핏 보면 큰 차이가 없을 것 같지만, 막상 활용을 하게 된다면 큰 차이가 있다. 특히 API의 오류인 경우 굉장히 다양한 형식으로 오류 메시지를 반환하거나 코드를 반환하여야 하는데, 기존의(ExceptionResolver 적용 전) 경우 결국 예외가 터지게 되면 WAS로 흘러들어 가 에러 페이지를 호출하는 과정을 겪는 반면에, ExceptionResolver를 적용하게 되면 이 예외를 잡아 해결하고 정상적인 흐름처럼 클라이언트에게 반환할 수 있다
즉 ExceptionResolver를 적용하는 핵심은 try ~ catch를 하듯 Exception(예외)이 터지게 되면 이를 해결하여(Resolver) 정상적인 흐름처럼 변경하는 것이 주된 목적이다
ExceptionResolver가 반환하는 값에 따른 동작은 아래와 같다
1. 빈(Empty) ModelAndView : 빈 모델 뷰를 반환하면 뷰를 렌더링 하지 않고 정상 흐름처럼 서블릿에게 리턴된다
2. ModelAndView 지정 : 모델 뷰에 model, view 등의 정보를 지정해서 반환하게 되면 뷰를 렌더링 한다
3. null : null을 반환하게 되면, 다음 ExceptionResolver를 찾아서 실행한다, 만약 없다면 예외 처리가 안되고 기존처럼 예외를 서블릿 밖으로 즉 WAS로 던지게 된다
ExceptionResolver의 종류와 우선순위
ExceptionResolver를 찾지 못하면 다음 Resolver를 검색하는데 이 과정에서 우선순위가 존재하며 아래와 같다
1. ExceptionHandlerExceptionResolver (중요)
@ExceptionResolver를 처리한다 API 예외 처리의 경우 대부분 이 기능으로 해결한다, 위에서 설명한 ExceptionResolver의 경우 모델 뷰를 반환하는데 이는 API 응답에 필요가 없을뿐더러, API의 JSON형식 응답을 위해서 매우 번거로운 작업이 필요하다. 이런 모든 문제를 해결하는 것이 바로 @ExceptionResolver이다
//예외 발생 시 API응답에 사용할 객체
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
위의 예시처럼 아주 간단하게 애노테이션만으로 예외가 발생하였을 때 해당 예외를 캐치하여 API 응답에 사용한 객체를 반환한다
{
"code": "BAD",
"message": (생략)
}
만약 @ExceptionHandler의 예외를 지정하지 않으면 해당 메서드 파라미터 예외를 사용하게 되는데 이럴 경우 ResponseEntity를 사용하여 응답 코드를 동적으로 변경할 수 있다 (아래에서 살펴볼 ResponseStatus는 동적 변경 불가능)
//사용자가 직접 만든 익셉션
@Slf4j
public class ExampleExeption extends RuntimeException{
public ExampleExeption() {
super();
}
public ExampleExeption(String message) {
super(message);
}
public ExampleExeption(String message, Throwable cause) {
super(message, cause);
}
public ExampleExeption(Throwable cause) {
super(cause);
}
protected ExampleExeption(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
@ExceptionHandler
public ResponseEntity<ErrorResult> exampleExHandle(ExampleException e) {}
이 외에도 다양한 반환 값을 사용할 수 있다
• @ReponseBody
• HttpEntity<B>, ResponseEntity<B>
• String
• View
• Map, Model
• @ModelAttribute
(더욱 자세한 내용은 공식문서 참고 :
• @ExceptionHnalder의 우선순위
@ExceptionHandler 또한 예외처리에서 우선순위가 존재한다
@ExceptionHandler(부모예외.class)
public String 부모예외처리()(부모예외 e) {}
@ExceptionHandler(자식예외.class)
public String 자식예외처리()(자식예외 e) {}
부모 클래스와 자식 클래스가 같이 있다고 한다면 부모 클래스의 예외 처리 시 자식 클래스 또한 같이 예외 처리가 된다, 이때 스프링에서 항상 디테일한 것이 우선순위를 가지듯이 자식 클래스와 같은 더 디테일한 예외조건이 있다면 해당 조건이 적용된다, 이를 활용한다면 범용적인 예외에서 디테일한 예외로 범위를 지정할 수 있는 즉 다양한 예외를 한 번에 처리할 수 있다
@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
log.info("exception e", e);
}
2. ReponseStatusExceptionResolver
HTTP 상태 코드를 지정할 수 있다 (ex. @ResponseStatus(value = HttpStatus.NOT_FOUND)
이를 사용하는 이유는 라이브러리를 활용하는 경우 개발자가 직접 예외 코드를 변경할 수 없기 때문에 이를 적용할 때 사용하게 된다, 예시와 함께 살펴보자
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
//실제 ResponseStatusExceptionResolver 코드
protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
throws IOException {
if (!StringUtils.hasLength(reason)) {
response.sendError(statusCode);
} else {
String resolvedReason = (this.messageSource != null ?
this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :reason);
response.sendError(statusCode, resolvedReason);
}
return new ModelAndView();
}
ResponseStatusExceptionResolver가 작동하는 코드를 확인해보면 결국 서블릿의 response.sendError(statusCode, resolvedReson)을 사용하기 때문에 sendError(400)을 호출하게 되면 WAS에서 오류 페이지를 요청하게 된다
@GetMapping("/api/responsereoslver")
public String responseStatus() {
throw new BadRequestException();
}
이를 위와 같이 API 오류에 반환하게 된다면 아래와 같이 출력된다
{
"status": 400,
"error": "Bad Request",
"exception": "hello.exception.exception.BadRequestException",
"message": "잘못된 요청 오류",\
"path": "/api/responsereoslver"
}
여기서 반환되는 reason의 경우 굉장히 재밌는 기능을 제공하는데, 바로 스프링 메시지를 활용할 수 있다는 것이다, 예를 들어 반환되는 reason에 error.badRequest와 같이 작성한 후 스프링 메시지 소스(messages.properties)에 등록하면 해당 내용이 출력된다
마지막으로 @ExceptionHandler와 같이 사용할 수도 있다
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
3. DefaultHandlerExceptionResolver
마지막 우선순위로 호출되는 DefaultHandlerExceptionResolver는 스프링 내부의 기본 예외 처리를 한다, 대표적으로 파라미터 바인딩 시점에 타입이 맞지 않은 경우 TypeMismatchException이 발생하는데, 이런 경우 예외가 발생하였기 때문에 그냥 두게 되면 서블릿 컨테이너까지 올라가게 되고 결과적으로는 서버 내부에서 발생한 오류 즉 500 에러코드가 발생한다, 하지만 타입 미스매치 같은 오류는 클라이언트가 잘못 보낸 것이지 서버상의 오류는 아니다
이때 DefaultHandlerExceptionResolver가 각 상황에 맞는 응답 코드를 리턴해준다
요청에 맞는 컨트롤러(핸들러)를 못 찾는 경우 -> 404 Not Found
컨트롤러 실행 중 예외가 발생하는 경우 -> 500 Internal Server Error
컨트롤러의 파라미터 타입 미스매치 경우 -> 400 BadRequest
@GetMapping("/api/defaulthandler")
public String defaultException(@RequestParam Integer data) {
return "ok"; (정상적인 경우 ok가 반환)
}
만약 위와 같은 컨트롤러에 문자열인 데이터를 보냈다고 한다면 기존처럼 500 에러코드를 반환하는 것이 아닌 아래와 같이 400 에러코드를 반환하게 된다
{
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException",
"message": (생략)
"path": "/api/defaulthandler"
}
ExceptionResolver 활용 후의 흐름 정리
위에서 살펴본 다양한 ExceptionResolver가 적용되고 나서의 흐름을 한번 더 정리해보고자 한다
//예외 예시
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}

3번까지는 기존과 동일한 흐름 (예외 발생은 IllegalArgumentException)
4.1. 예외가 발생하였기 때문에 ExceptionResolver가 작동한다, 이때 가장 우선순위가 높은 @ExceptionHandler에서 가 작동하고 해당 컨트롤러에 발생한 예외를 처리할 수 있는 @ExceptionHanlder가 존재하는지 확인한다
4.2. ResponseStatus에 지정된 응답 코드를 반환한다
5. afterCompletion 호출
6. 예외처리가 되었으므로 API 응답
@ControllerAdvice
API의 경우 각 컨트롤러마다 다른 예외를 응답해야 하는 상황이 필요하다 이때 @ControllerAdvice를 활용하여 각 컨트롤러에 적용시킬 예외처리를 나누어 관리할 수 있다 (만약 @ControllerAdvice를 지정하지 않으면 모든 컨트롤러에 적용된다 (글로벌 적용))
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class,AbstractController.class})
public class ExampleAdvice3 {}
위의 예시처럼 특정 애노테이션이 있는 컨트롤러를 지정할 수도 있고, 특정 패키지를 지정할 수도 있다. 패키지 지정 같은 경우 해당 패키지와 그에 속한 하위 패키지가 대상이 된다 마지막으로 클래스를 지정할 수도 있다
핵심정리
API 오류 처리 같은 경우, 단순한 웹페이지를 반환하는 것과는 달리 다양한 형식을 사용하거나 각각의 컨트롤러에 각기 다른 예외를 적용시켜야 하는 상황이 빈번하다, 또한 단순히 데이터를 반환하는데 WAS에서 다시 요청을 할 필요도 없다
이러한 문제점을 해결하기 위해 ExceptionResolver(HandlerExceptionResolver)를 사용하여, 예외가 발생하였을 때 Resolver에서 예외를 처리할 수 있는지 확인하고 예외 처리가 된다면 정상적인 흐름처럼 클라이언트에게 응답하게 된다
ExceptionResolver는 총 3가지가 있지만 대부분 API 응답에 사용하는 Resolver는 ExceptionHandlerExceptionResolver로 애노테이션화 되어 있어 매우 간편하게 사용할 수 있다, @ExceptionHandler를 사용하여 적용시킬 예외를 지정하고, 지정하지 않는다면 파라미터와 반환 값을 동적으로 만들어 반환할 수도 있다
마지막으로 @ControllerAdvice를 사용하게 되면 예외를 적용시키고자 하는 애노테이션, 패키지, 클래스까지 지정하여 각각 다른 예외 조건들을 설정할 수 있다
개인 학습을 위해 작성되는 글입니다.
제가 잘못 알고 있는 점에 대한 지적 / 더 나은 방향에 대한 댓글을 환영합니다.
참조 링크:
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'Spring > Spring MVC' 카테고리의 다른 글
[Spring MVC] MultipartFile (File Upload) (0) | 2021.09.23 |
---|---|
[Spring MVC] TypeConverter & Formatter (0) | 2021.09.21 |
[Spring MVC] Error Pages (오류 페이지) (0) | 2021.09.14 |
[Spring MVC] Filter & Interceptor (0) | 2021.09.10 |
[Spring MVC] Login with Cookie & Session (0) | 2021.09.09 |
- Total
- Today
- Yesterday
- maenco
- 맨코
- Spring TypeConverter
- Cache
- @ExceptionHandlere
- jQuery 직접 선택자
- 쿠키
- http
- OOP
- spring
- 캐시
- 제이쿼리 위치탐색선택자
- 제이쿼리란
- 제이쿼리 기본 선택자
- DefaultHandlerExceptionResolver
- Spring API Error
- uri
- Session
- @ResponseStatus
- 세션
- ExceptionHandlerExceptionResolver
- cookie
- application/x-www-form-urlencoded
- Spring Container
- 제이쿼리 탐색선택자
- 제이쿼리 직접 선택자
- ResponseStatusExeceptionResolver
- 제이쿼리 인접 관계 선택자
- DTO와 VO의 차이
- Spring MVC
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |