김영한의 스프링 MVC 2편 -

2025. 2. 28. 23:34·김영한의 스프링 MVC 2편

Question

  • 컨트롤러에서 예외가 발생한 경우 디스패처 서블릿은 예외 처리를 어디로 넘길 수 있는가?
  • @ExceptionHandler는 어떻게 예외를 처리하는가
  • @ExceptionHandler가 여러 컨트롤러에 적용되게 해주는 것은?

 

 

디스패처 서블릿에서의 에러 처리

  • 디스패처 서블릿에서 원래 예외가 발생하면 postHandler는 동작하지 않고 afterCompletion이 동작후 WAS로 예외를 전달하게 됨
  • 하지만 ExceptionResolver를 사용하면 컨트롤러에서 발생한 예외에 대한 처리 로직이 추가될 수 있음
  • 물론 이때도 postHandler는 동작하지 않음

 

ExceptionResolver 구현

@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;
    }
}
  • HandlerExceptionResolver를 구현하면 컨트롤러에서 발생한 에러가 넘어와 처리가 가능함
  • 위 예제는 IllegalArgumentException에 대해 에러 코드를 400으로 바꿔줌
  • ModelAndView를 반환하면 try - catch를 한 것 처럼 에러를 잡고 정상 흐름 처럼 변경함
  • 만약 null을 반환하면 다른 ExceptionResolver를 찾거나 예외를 서블릿 밖으로 던짐
  • 구현 이후 config에 등록을 해 줘야함

 

 

스프링이 제공하는 ExceptionResolver

  • 스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같음
  • HandlerExceptionResolverComposite 에 다음 순서로 등록
    • ExceptionHandlerExceptionResolver: @ExceptionHandler를 처리하며 대부분 이 기능으로 해결
    • ResponseStatusExceptionResolver: HTTP 상태 코드를 지정해줌
    • DefaultHandlerExceptionResolver: 스프링 내부 기본 예외를 처리하며 우선 순위가 가장 낮음

 

 

ResponseStatusExceptionResolver

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
  • BadRequsetException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver 가 해당 어노테이션을 확인해 오류코드를 400으로 변경하고 메시지도 남김
  • ResponseStatusExceptionResolver는 내부에서 response.sendError(statusCode, resolvedReason)을 호출함

 

DefaultHandlerExceptionResolver

  • 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생
  • 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 예외가 올라가고 결과적으로 500 에러 발생
  • 하지만, 파라미터 바인딩인 클라이언트가 요청을 잘못 보낸것이기 때문에 이런 경우엔 400 에러를 내려줘야 함
  • DefaultHandlerExceptionResolver는 이러한 오류에 대해 미리 정의를 해서 발견 후 400으로 변경해줌

 

 

ExceptionHandlerExceptionResolver 와 @ExceptionHandler

@Slf4j
@RestController
public class ApiExceptionV2Controller {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandle(IllegalArgumentException e) {
        log.error("[exceptionHandle] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandle(Exception e) {
        log.error("[exceptionHandle] ex", e);
        return new ErrorResult("EX", "내부 오류");
    }

    @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);
    }
}
  • 스프링은 ExceptionHandlerExceptionResolver를 기본으로 제공하고, 우선순위도 가장 높음
  • 실무에서는 대부분 이 기능을 사용해서 처리
  • @ExceptionHandler 어노테이션을 선언하고, 해당 컨트롤러 내부에서 처리하고 싶은 예외를 지정
  • 예를 들어, illegalExHandler는 IllegalArgumentException와 그 하위에 대해 ResponseStatus를 400으로 변경하고 메시지를 지정해줄 수 있음
  • exHandle 함수는 Excpetion e를 인자로 받는데, 이는 모든 Exception을 처리할 수 있다는 의미.
  • 하지만 IllegalArgumentExcpetion이 더 구체적인 예외이므로 IllegalArgumentExcpetion 예외가 발생할 경우에는 illegalExHandle이 호출 됨
  • 더 구체적인 예외 처리가 우선
  • 따라서 exHandle은 그 외 나머지에 대한 Default 처리를 위해 존재함
  • 이 방식은 해당 컨트롤러에서만 동작함

 

@ControllerAdvice

@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandle(IllegalArgumentException e) {
        log.error("[exceptionHandle] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandle(Exception e) {
        log.error("[exceptionHandle] ex", e);
        return new ErrorResult("EX", "내부 오류");
    }
}


//대상컨트롤러 지정 방법

// 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 {}
  • @ExceptionHandler가 하나의 컨트롤러에서만 동작하는데, 이 @ExceptionHandler를 모아 여러 컨트롤러에서 동작할 수 있게 해줌
  • @RestControllerAdvice는 @ControllerAdvice와 같고 @ResponseBody가 추가됨
  • @ControllerAdvcie에 대상을 지정하지 않으면 모든 컨트롤러에 적용
  • 패키지에 따라 도메인이 달라 예외처리를 다르게 해줄 경우 패키지를 지정해줄 수 있음

'김영한의 스프링 MVC 2편' 카테고리의 다른 글

김영한의 스프링 MVC 2편 - 필터, 인터셉터  (0) 2025.02.26
김영한의 스프링 MVC 2편 - 메세지, 국제화  (0) 2025.02.26
'김영한의 스프링 MVC 2편' 카테고리의 다른 글
  • 김영한의 스프링 MVC 2편 - 필터, 인터셉터
  • 김영한의 스프링 MVC 2편 - 메세지, 국제화
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      hibernate5module
      reentarantlock
      jpq
      프록시
      @args
      양방향 맵핑
      Target
      버퍼
      WAS
      단방향 맵핑
      자바
      고급
      @discriminatorcolumn
      typequery
      김영한
      빈 후처리기
      프록시 팩토리
      JPQL
      @within
      Thread
      log trace
      cglib
      gesingleresult
      페치 조인
      스레드
      requset scope
      조회 성능 최적화
      락
      @discriminatorvalue
      jdk 동적 프록시
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    김영한의 스프링 MVC 2편 -
    상단으로

    티스토리툴바