디스페처 서블릿은 컨트롤러에서 발생한 예외를 처리하기 위한 ExceptionResolver를 제공한다.
여기서 처리되지 않고 WAS까지 예외가 전달되면 뷰 템플릿을 찾기 위해 내부적으로 다시 컨트롤러까지 호출되는 일이 발생한다. 이는 컨트롤러는 물론, 필터와 인터셉터까지 재호출이 되기 때문에 비효율적인 동작이 추가되어 버린다.
ExceptionResolver는 이 현상을 방지하고자 컨트롤러에서 발생한 예외를 해결하기 위해 ViewResolver나 HttpConverter이 호출되기 전에 ExceptionResolver를 호출하여 적절한 처리를 하게 한다.
인터셉터를 통해 컨트롤러에서 발생한 예외처리를 하려고 하면 preHandler나 afterCompletion에서 해결을 해야하는데, preHandler는 컨트롤러에서 예외가 발생하면 아예 호출이 되지 않고 afterCompletion은 뷰 리졸버가 호출된 이후에 호출이 되기 때문에 적절하지 않은 방식이다.
package org.springframework.web.servlet;
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler,
Exception ex
);
}
ExceptionResolver의 인터페이스이다.
자신만의 예외처리를 하고 싶다면 위의 인터페이스를 상속받아서 resolveException 메서드를 구현하면 된다.
response.sendError
, response.getWrite().write()
같은 메서드를 통해 클라이언트에게 값을 전달해야 한다.@Configuration
public class ExceptionResolverConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(
List<HandlerExceptionResolver> resolvers
) {
resolvers.add(new MyHandlerExceptionResolver());
}
}
WebMvcConfigurer 인터페이스를 상속받아 extendHandlerExceptionResolvers 메서드를 오버라이딩해 등록할 수 있다.
기본적으로 제공하는 ExceptionResolver는 다음 3가지 방식의 Resolver를 지원한다.
이 들은 위에 나타난 순서대로 처리된다.
ExceptionHandlerExceptionResolver를 제외한 다른 2개의 Resolver는 내부적으로 response.sendError를 통해 전달하기 때문에 WAS에서는 다시 오류페이지(/error
)를 요청하는 로직이 실행된다.
ExceptionResolver의 2순위 Resolver이다. 이 Resolver는 다음 2가지의 예외를 처리한다.
@ResponseStatus(
code = HttpStatus.BAD_REQUEST,
reason = "잘못된 요청 오류"
)
public class CustomException extends Exception {}
자신의 예외에 @ResponseStatus 애노테이션을 사용하면 해당 예외가 발생했을때 어떤 방식으로 처리할지 정할 수 있다.
또한, 컨트롤러의 메서드에도 애노테이션을 사용할 수 있는데, 이 경우에는 해당 요청의 기본 응답을 설정할 수 있는 기능을 제공한다.
HttpStatus.INTERNAL_SEVER_ERROR
""
package org.springframework.web.server;
public class ResponseStatusException extends ErrorResponseException {
public ResponseStatusException(
HttpStatusCode status
) { ... }
public ResponseStatusException(
HttpStatusCode status,
@Nullable String reason
) { ... }
public ResponseStatusException(
int rawStatusCode,
@Nullable String reason,
@Nullable Throwable cause
) { ... }
public ResponseStatusException(
HttpStatusCode status,
@Nullable String reason,
@Nullable Throwable cause
) { ... }
}
@GetMapping("/exception")
public String exception() {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND,
"잘못된 요청 오류",
new IllegalArgumentException()
);
}
ResponseStatusException을 발생시켜 어떤 방식으로 처리할지 정할 수 있다.
public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
// ...
protected ModelAndView handleTypeMismatch(
TypeMismatchException ex,
HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler
) throws IOException {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return new ModelAndView();
}
// ...
}
ExceptionResolver 중 3순위 Resolver이다. 가장 마지막에 처리되는 Resolver이기 때문에 여러 기본적인 예외에 대한 처리가 작성되어있다.
위에 작성한 코드의 경우, 파라미터 바인딩 실패(TypeMismatchException)에 대한 처리를 담당하는 부분이다.
이외에도 여러 예외에 대한 처리가 기술되어있으니 자세한 내용은 공식 Docs에서 확인해보면 된다.
ExceptionResolver 중 1순위로 처리되는 Resolver이다.
컨트롤러 내부 메소드에 @ExceptionHandler 애노테이션을 사용하여 해당 예외에 대한 처리를 진행할 수 있다.
컨트롤러 내부 메소드에 사용하게 되면 해당 컨트롤러에서 발생한 예외에 한해 적용되며, 후술할 @ControllerAdvice 애노테이션을 사용하여 전역 예외 리졸버로 사용하거나, 특정 컨트롤러에 한한 예외 리졸버로 작성하여 파일을 분리할 수 있다.
반환값에 따른 컨버터가 적용되기 때문에 ModelAndView를 반드시 반환할 필요가 없어진다.
package org.springframework.web.bind.annotation;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective(ExceptionHandlerReflectiveProcessor.class)
public @interface ExceptionHandler {
Class<? extends Throwable>[] value() default {};
}
@Slf4j
@RestController
@RequestMapping("/api")
public class ApiExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(
IllegalArgumentException e
) {
log.error("illegalExHandler call: {}", e.toString());
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(
UserException e
) {
log.error("userExHandler call: {}", e.toString());
ErrorResult result = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<ErrorResult>(result, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler
public ErrorResult exHandler(
Exception e
) {
log.error("exHandler call: {}", e.toString());
return new ErrorResult("EX", e.getMessage());
}
@GetMapping("/members/{id}")
public MemberDto getMember(
@PathVariable String id
) {
switch (id) {
case "ex" -> throw new RuntimeException("잘못된 사용자");
case "bad" -> throw new IllegalArgumentException("잘못된 입력 값");
case "user-ex" -> throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
}
여러 예외에 대한 세부적인 처리가 가능하며, 컨트롤러 클래스단에 작성하게 되면 해당 클래스의 메서드에서 발생한 예외만 처리하게 된다.
value = RuntimeException.class
부모 예외는 자식 예외를 모두 처리할 수 있다. 이에 대한 순서는 다음과 같다.
package org.springframework.web.bind.annotation;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
/**
* 특정 애노테이션이 있는 컨트롤러
* - @RestController
*/
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
/**
* 특정 패키지
* - org.example.controllers 패키지
*/
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
/**
* 특정 클래스
*/
@ControllerAdvice(
assignableTypes = {
ControllerInterface.class,
AbstractController.class
}
)
public class ExampleAdvice3 {}
컨트롤러 클래스 안에서 ExceptionHandler를 작성하면 실제로 컨트롤러 역할을 진행하는 메서드와 예외 처리를 진행하는 메서드가 섞이게 된다. 이는 바람직한 코드 작성법이 아니다. (단일 책임 원칙 위배)
그래서 Spring은 @ControllerAdvice
애노테이션을 통해 예외 처리 메서드를 분리할 수 있도록 기능을 제공한다.
특정 패키지, 클래스, 애노테이션에서 발생하는 예외를 처리할지 지정할 수 있는 기능을 제공하며, 아무것도 지정하지 않으면 모든 예외에 대한 처리를 담당하게 된다. (전역 Advice)
"org.example.controllers"
ControllerInterface.class
RestController.class