3 분 소요

 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술편을 학습하고 정리한 내용 입니다.

API 예외 처리 - @ExceptionHandler

HTML 화면 오류 vs API 오류

  • HTML 화면 제공 시 오류 발생 → BasicErrorController
  • API → ???

API는 각 시스템마다 응답의 모양도 다르고, 스펙도 모두 다르다.

예외 상황에 단순히 오류 화면을 보여주는 것이 아니라, 예외에 따라서 각각 다른 데이터를 출력해야 할 수도 있다.

그리고 같은 예외라고 해도 어떤 컨트롤러에서 발생했는가 에 따라서 다른 예외 응답을 내려주어야 할 수도 있다.

API 예외 처리의 어려운 점

  • HandlerExceptionResolverModelAndView를 리턴 해야 했다. 이건 API는 사실 필요 없다.
  • API 응답을 위해서 HttpServletResponse에 직접 응답 데이터를 만들어 넣어 줘야 했다. 마치 스프링 안 쓰고 서블릿을 쓴 것 처럼
  • 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵다.
    • 회원 처리 컨트롤러에서 발생한 RuntimeException
      상품 관리 컨트롤러에서 발생한 RuntimeException
      서로 다르게 처리해야 하는데 어떻게 처리할까?

@ExceptionHandler

스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler라는 애노테이션을 제공한다.

매우 편리한 예외 처리 기능을 제공하는데, 이것이 바로 ExceptionHandlerExceptionResolver이다.

ExceptionHandlerExceptionResolver 를 기본으로 제공하고, ExceptionResolver중에 우선 순위도 가장 높다.

예제 준비비

먼저 에러 데이터를 바인딩할 ErrorResult.java를 생성

1
2
3
4
5
6
@Data  
@AllArgsConstructor  
public class ErrorResult {  
    private String code;  
    private String message;  
}

이제 컨트롤러를 하나 만들어 보자.

hello.exception.api.ApiExceptionV2Controller

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
@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;  
    }  
  
}

간단하게 예외를 발생 시키는 컨트롤러를 만들었다.

이제 @ExceptionHandler처리 방법을 알아보자.

@ExceptionHandler 예외 처리 사용

@ExceptionHandler애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다.

해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다.

참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다.

컨트롤러 위에 다음과 같이 추가하자.

1
2
3
4
5
@ExceptionHandler(IllegalArgumentException.class)  
public ErrorResult illegalExHandler(IllegalArgumentException e) {  
    log.error("[exceptionHandle] ex", e);  
    return new ErrorResult("BAD", e.getMessage());  
}

이 메서드는 IllegalArgumentException가 컨트롤러 클래스 내에서 발생하면 실행 된다.

ErrorResult를 반환한다.

내가 원하던 code, message가 왔다. 하지만 200 OK가 응답 됐다.

스프링 입장에서 정상 처리한 건 맞으니깐..

그럼 다음과 같이 @ResponseStatus를 추가하면 된다.

1
2
3
4
5
6
@ResponseStatus(HttpStatus.BAD_REQUEST)  
@ExceptionHandler(IllegalArgumentException.class)  
public ErrorResult illegalExHandler(IllegalArgumentException e) {  
    log.error("[exceptionHandle] ex", e);  
    return new ErrorResult("BAD", e.getMessage());  
}

400 Bad Request가 잘 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@ExceptionHandler  
public ResponseEntity<ErrorResult> userExHandler(UserException e) {  
    log.error("[exceptionHandle] ex", e);  
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());  
    return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);  
}  
  
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 500 에러  
@ExceptionHandler  
public ErrorResult exHandler(Exception e) {  
    log.error("[exceptionHandle] ex", e);  
    return new ErrorResult("exception", "내부 오류");  
}

@ExceptionHandler는 다양한 리턴과 파라미터를 사용할 수 있다.

참고
스프링 공식 문서에서 다양한 리턴, 파라미터를 확인해 보자.

다음과 같이 ResponseEntity<>를 리턴 하면 메시지와 코드를 한번에 보낼 수도 있다.

실행 흐름

  • 컨트롤러를 호출한 결과 IllegalArgumentException(예시) 예외가 컨트롤러 밖으로 던져진다.
  • 예외가 발생했으므로 ExceptionResolver가 작동한다.
    1. ExceptionHandlerExceptionResolver ← 우선순위 1등
    2. ResponseStatusExceptionResolver
    3. DefaultHandlerExceptionResolver
  • ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리할 수있는 @ExceptionHandler가 있는지 확인한다.
  • illegalExHandler()를 실행한다. @RestController이므로 illegalExHandler()@ResponseBody가 적용된다.
    • HTTP 컨버터가 사용되고, 응답이 JSON으로 반환된다.
  • @ResponseStatus(HttpStatus.BAD_REQUEST)로 지정했기 때문에 400으로 응답한다.

@ExceptionHandler - 우선 순위

스프링의 우선 순위는 항상 자세한 것이 우선 순위를 가진다.

부모, 자식 클래스가 있다 치면 다음과 같이 처리된다.

1
2
3
4
5
@ExceptionHandler(부모예외.class) 
public String 부모예외처리()(부모예외 e) {} 

@ExceptionHandler(자식예외.class) 
public String 자식예외처리()(자식예외 e) {}

이렇게 있다면

부모예외처리()자식예외.class를 처리할 수 있다. 하지만 자세한 자식예외처리()가 있기 때문에 자식예외처리()에서 처리된다.

부모예외.class자식예외처리()에서 처리할 순 없고, 부모예외처리()에서 처리할 수 있다.

다양한 예외

1
2
3
4
@ExceptionHandler({AException.class, BException.class}) 
public String ex(Exception e) { 
	log.info("exception e", e); 
}

다음과 같이 다양한 예외를 한 번에 처리할 수 있다.

예외 생략

@ExceptionHandler를 생략할 수 있다. 생략하면 메서드 파라미터의 예외가 지정된다.

1
2
@ExceptionHandler  
public ResponseEntity<ErrorResult> userExHandler(UserException e) {}

이러면 UserException이 지정된 것이다.

기타

1
2
3
4
@ExceptionHandler  
public String ex(RuntimeException e) {  
    return "error/500";  
}

이런 식으로 뷰 페이지를 띄워 줄 수도 있다.

단지 여기 서는 @RestController가 있기 때문에 그냥 문자열 그대로 나가니깐

@ResponseBody에 주의하자.

댓글남기기