
웹애플리케이션을 개발할 때 발생하는 예외는 HTML 화면에 표시되는 오류페이지로 처리될 수 있고 API를 사용시 JSON으로 전송되는 API 메시지로 예외처리될 수 있다.
@ExceptionHandler는 둘 중에 API를 사용할 때 발생하는 예외를 처리하는 Spring 애노테이션이다.
예외가 발생했을 때 HTML로 오류페이지를 보내주는 방법은 어렵지 않다. 그저 상태코드에 따라서 정해진 오류페이지와 메시지를 내려주면 되기 때문이다. 그러나 API 예외처리는 다르다. API는 각 시스템 마다 응답의 모양도 다르고, 스펙도 다르기 때문에 예외에 따라서 각각 다른 데이터를 출력해야 할 수도 있다.
예를 들어 상품 API와 주문 API는 응답 모양이 완전히 다를 수 있다. 따라서 예외가 발생하면 같은 예외일지라도 각각 다른 모양의 응답을 만들어야 하는 경우가 발생할 수 있다.
그런데 @ExceptionHandler를 사용하면 컨트롤러마다 예외를 별도로 처리할 수 있기 때문에 API 마다 다른 응답 모양을 지정해줄 수 있다.
package hello.exception.exhandler;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
package hello.exception.exhandler;
import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
public class ApiExceptionController {
@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("잘못된 입력 값");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
// 컨트롤러에서 IllegalArgumentException.class 발생시 아래의 메소드가 호출됨
@ExceptionHandler(IllegalArgumentException.class) // @ExceptionHandler와 예외지정
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
위의 @ExceptionHandler 사용 예시를 보면 마치 스프링 MVC의 컨트롤러에서 API를 쓰는 것과 비슷하게 동작한다.
URL로 매핑된 컨트롤러에서 예외가 발생되었을 때 예외에 따라@ExceptionHandler가 매핑된 메소드가 호출되는 것을 볼 수 있다. 이는 아래와 같은 실행 흐름으로 진행된다.
자식 예외처리() 가 호출된다.부모 예외처리() 가 호출된다.@ExceptionHandler(자식예외.class)가 없다면 자식 예외 발생시 @ExceptionHandler(부모예외.class)로 간주하여 부모 예외처리가 호출된다.@ExceptionHandler(부모예외.class)
public String 부모예외처리()(부모예외 e) {}
@ExceptionHandler(자식예외.class)
public String 자식예외처리()(자식예외 e) {}
// AException.class, BException.class (두가지 예외 등록)
@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
log.info("exception e", e);
}
// @ExceptionHandler("생략") => 메소드의 파라미터인 UserException 예외로 대체됨.
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
// ...
}
@ExceptionHandler의 한계
ExControllerAdvice - @ControllerAdvice 예시
package hello.exception.exhandler.advice;
import hello.exception.exception.UserException;
import hello.exception.exhandler.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@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());
}
@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);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
}
@ControllerAdvice 사용 방법 :
ApiExceptionController)의 코드에서 @ExceptionHandler가 적힌 메소드(예외처리 코드)를 모두 새로운 클래스(ExControllerAdvice)로 잘라내어 옮긴다.@ControllerAdvice에 대상 컨트롤러 지정하는 방법
// 어노테이션 단위 (ex) @RestController가 달린 모든 컨트롤러)
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// 특정 패키지 단위 (ex) "org.example.controllers" 패키지 내의 모든 컨트롤러)
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// 특정 클래스 단위 (ex) ControllerInterface.class, AbstractController.class를 선언한 모든 컨트롤러)
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
@ExceptionHandler와 @ControllerAdvice의 충돌
@ControllerAdvice의 이점