@ExceptionHandler
스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler 라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공합니다.
사용방법은, 잡을 예외를 파라미터로 갖는 메서드를 만들고 메서드 위에 @ExceptionHandler 애노테이션을 선언하여 사용합니다.
해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 해당 컨트롤러에서 예외가 발생할 경우 이 메서드가 호출됩니다.
(발생한 예외를 잡으면 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡습니다.)
에러 코드와 메세지를 갖는 객체
오류가 발생하면 단순하게 오류 코드와 메시지를 JSON 데이터로 보내는 단순한 구조를 갖는 예제로 설명하겠습니다.
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
Member 객체
@Data
@AllArgsConstructor
static class Member {
private String memberId;
private String name;
}
API 예외 컨트롤러
@ExceptionHandler 에 예외를 지정할 수도 있고, 생략할 수 있습니다. 생략하면 메서드 파라미터의 예외가 지정됩니다.
@ResponseStatus 어노테이션을 선언하여 그 옵션에 표시하고 싶은 HTTP 상태코드를 지정할 수 있습니다.
@RestController
public class ApiExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
return new ErrorResult("BAD_INPUT", e.getMessage());
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
// Exception은 최상위 예외로 처리하지 않은 예외가 발생할 시 여기서 예외를 처리해 ErrorResult를 반환한다.
public ErrorResult exHandle(Exception e) {
return new ErrorResult("SERVER_EXCEPTION", "내부 오류 발생");
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ErrorResult userExHandler(NullPointerException e) {
return new ErrorResult("NULL_EXCEPTION", e.getMessage());
}
@GetMapping("/api2/members/{id}")
public Member getMember(@PathVariable("id") String id) {
if (id.equals("qeqeqe")) {
throw new RuntimeException("알수 없는 문제가 발생했습니다.");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값을 입력했습니다.");
}
if (id.equals("null")) {
throw new NullPointerException("공백을 입력할 수 없습니다.");
}
return new Member(id, "반갑습니다." + id + "님");
}
}
요청 파라미터 경로인 id 값을 위에서 정의한대로 보냈을 경우 각각의 응답결과는 다음과 같습니다.
- http://localhost:8080/api2/members/qeqeqe
RuntimeException을 잡는 @ExceptionHandler 가 없기 때문에 이 예외의 부모인 Exception을 잡은 @ExceptionHandler가 호출됩니다.
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 으로 HTTP 상태코드가 500번인 것을 볼 수 있습니다.
- http://localhost:8080/api2/members/bad
IllegalArgumentException을 잡는 @ExceptionHandler가 호출됩니다.
@ResponseStatus(HttpStatus.BAD_REQUEST) 으로 HTTP 상태코드가 400번인 것을 볼 수 있습니다.
* 우선 순위 *
스프링의 우선순위는 항상 자세한 것이 우선권을 가집니다.
여기서 예외의 최상위 부모인 Exception을 잡는 @ExceptionHandler가 있지만
그 자식이 더 자세한 것이므로 여기서는 IllegalArgumentException를 잡는 @ExceptionHandler가 호출됩니다.
- http://localhost:8080/api2/members/null
위와 같은 경우로 Exception이 아닌 NullPointerException을 잡는 @ExceptionHandler가 호출됩니다.
@ResponseStatus(HttpStatus.BAD_REQUEST) 으로 HTTP 상태코드가 400번인 것을 볼 수 있습니다.
@ControllerAdvice
@ExceptionHandler를 이용해 예외를 깔끔하게 처리할 수 있었습니다.
그런데, 문제점이있습니다.
바로 정상 처리 코드와 예외 처리 코드가 하나의 컨트롤러에 같이 있다는 것입니다.
이 문제점을 @ControllerAdvice 또는 @RestControllerAdvice 를 사용하여 둘을 분리해 문제점을 해결 할 수 있습니다.
Controller
@RestController
public class ApiExceptionController {
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("qeqeqe")) {
throw new RuntimeException("알수 없는 문제가 발생했습니다.");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값을 입력했습니다.");
}
if (id.equals("null")) {
throw new NullPointerException("공백을 입력할 수 없습니다.");
}
return new MemberDto(id, "반갑습니다." + id + "님");
}
}
API 예외 처리 ControllerAdvice
@Slf4j
@RestControllerAdvice(annotations = RestController.class) // @RestController에만 적용
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
return new ErrorResult("BAD_INPUT", e.getMessage());
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
// Exception은 최상위 예외로 처리하지 않은 예외가 발생할 시 여기서 예외를 처리해 ErrorResult를 반환한다.
public ErrorResult exHandle(Exception e) {
return new ErrorResult("SERVER_EXCEPTION", "내부 오류 발생");
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ErrorResult userExHandler(NullPointerException e) {
return new ErrorResult("NULL_EXCEPTION", e.getMessage());
}
}
@RestControllerAdvice를 사용하여 컨트롤러에 더 이상 예외를 처리하는 코드가 없이 깔끔하게 분리가 된 것을 볼 수 있습니다.
(@RestControllerAdvice 는 @ControllerAdvice 와 같고, @ResponseBody 가 추가된 것)
위 에서 @RestControllerAdvice의 대상으로 RestController을 지정하였는데, @RestController 어노테이션이 있는 대상에만 적용한다는 뜻입니다.
이렇게 특정 애노테이션이 있는 컨트롤러를 지정할 수 있고, 특정 패키지, 특정 클래스도 직접 지정할 수도 있습니다.
패키지 지정의 경우 해당 패키지와 그 하위에 있는 컨트롤러가 대상이 된다.
만약, @ControllerAdvice또는 @RestControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용이 됩니다. (글로벌 적용)
'◼ Spring' 카테고리의 다른 글
[Spring] DAO, DTO, VO란? 각각의 개념에 대해 알아보자. (2) | 2023.04.27 |
---|---|
[Spring] 스프링 인터셉터(Interceptor) (0) | 2023.04.27 |
[Spring] 스프링 부트 - 요청 타입변환(Converter), 포맷터(Formatter) (0) | 2023.04.26 |
[Spring] 스프링에서 HTML 오류 페이지 처리하기 (0) | 2023.04.26 |
[Spring] @Transactional 옵션 알아보기 + 트랜잭션 전파 (0) | 2023.04.26 |