본문 바로가기
카테고리 없음

스프링 API 예외 처리2

by o3oppp 2024. 7. 23.
API 예외처리의 어려운 점
  • HandlerExceptionResolver의 경우 ModelAndView를 반환하는데, 이는 API 응답에서는 불필요
  • 또한, API 응답을 위해서 HttpServlerResponse에 직접 응답 데이터를 넣었지만, 과정이 불편
  • 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어려움

ExceptionHandlerExceptionResolver
@ExceptionHandler({예외처리1.class, 예외처리2.class..})
Method 선언
  • 스프링에서 기본 제공
  • ExceptionResolver 중 우선순위가 가장 높음
  • @ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정
  • 지정한 예외 또는 그 예외의 자식 클래스는 모두 예외 처리 가능
  • 다양한 예외를 한번에 처리 가능
  • 예외 생략 시 Method 파라미터의 예외가 지정
  • ModelAndView를 사용해서 오류 화면(HTML)을 응답하는데 사용 가능

사용 예시

  • 예외가 발생했을 때 API 응답으로 사용하는 객체 정의
@Data
@AllArgsConstructor
public class ErrorResult {

    private String code;
    private String message;
}
  • 컨트롤러
@Slf4j
@RestController
public class ApiExceptionV2Controller {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e){
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD",e.getMessage());
    }
	
    // 예외 생략 시 Method의 파라미터의 예외가 지정. 여기서는 UserException.class
    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExHandler(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 exHandler(Exception e){
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("EX","내부 오류");
    }
    
    // ModelAndView를 사용해서 오류 화면을 응답하는데 사용 가능
    @ExceptionHandler(ViewException.class)
 	public ModelAndView ex(ViewException e) {
     	log.info("exception e", e);
     	return new ModelAndView("error");
	}
    
    @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(부모예외.class)
public String 부모예외처리()(부모예외 e) {}

@ExceptionHandler(자식예외.class)
public String 자식예외처리()(자식예외 e) {}
  • 항상 자세한 것이 우선권을 가짐
  • 자식예외가 발생하면 부모예외처리(), 자식예외처리() 둘 다 호출 대상
  • 그러나 둘 중 더 자세한 자식예외처리()가 호출
  • 물론 부모예외가 호출되면 부모예외처리()만 호출 대상이 되므로 부모예외처리()가 호출

실행 흐름

  • IllegalArgumentException 처리
 @ResponseStatus(HttpStatus.BAD_REQUEST)
 @ExceptionHandler(IllegalArgumentException.class)
 public ErrorResult illegalExHandle(IllegalArgumentException e) {
     log.error("[exceptionHandle] ex", e);
     return new ErrorResult("BAD", e.getMessage());
 }
  1. 컨트롤러를 호출한 결과 예외가 발생하여 컨트롤러 밖으로 던져짐
  2. 예외 발생으로 ExceptionResolver 작동(우선순위가 높은 ExceptionHandlerExceptionResolver 실행)
  3. ExceptionHandlerExceptionResolver가 @ExceptionHandler가 있는지 확인
  4. @ExceptionHandler가 있는 illegalExHandle() 실행
  5. @RestController 이므로 @ResponseBody가 적용되어 HTTP 컨버터가 사용되고, 응답이 JSON으로 변환
  6. @ResponseStatus으로 인해 HTTP 상태 코드 400으로 응답
  • UserException 처리
@ExceptionHandler
 public ResponseEntity<ErrorResult> userExHandle(UserException e) {
     log.error("[exceptionHandle] ex", e);
     ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
     return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
  1. @ExceptionHandler에 예외를 지정해주지 않았으므로 Method의 파라미터 예외를 사용
  2. ResponseEntity를 사용해서 HTTP 메시지 바디에 직접 응답

@ControllerAdvice, @RestControllerAdvice
  • 모든 @Controller에 대해 예외를 잡아 처리
  • Class 내의 해당 애노테이션 선언 후 @ExceptionHandler를 통해 핸들링하고 싶은 예외를 잡아서 처리 가능
  • @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여
  • 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있는 것을 방지(코드 중복 제거 및 관심사 분리 가능)
  • 에러 페이지로 리다이렉트를 원한다면 @ControllerAdvice
  • 객체만 return 시키길 원한다면 @RestContollerAdvice

사용 예시

@Slf4j
@RestControllerAdvice(basePackages = "hello.exception.api")
public class ExControllerAdvice {

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

    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExHandler(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 exHandler(Exception e){
        log.error("[exceptionHandler] 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 {}
  • 대상을 지정하지 않으면 모든 컨트롤러에 적용(글로벌 적용)