이번에는 스프링의 예외처리에 대해서 정리하려고 한다.
실무에서는 정상 동작은 당연히 되어야하는 것이고, 서비스를 제공할 때, 예외도 함께 고려되어야한다.
예상치 못한 에러가 발생 되어도 서비스 운영에는 문제가 없어야하기 때문이다.
아직도 예외처리에 대해서 완벽히 알지는 못하지만 기본적인 흐름에 대해서는 정리가 됐다 🤔
ErrorResult 클래스
예외 발생 시 API 응답으로 사용할 객체
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
ApiExceptionV2Controller
컨트롤러 역할을 하기 위한 객체
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalArgumentException(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 result = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity(result, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e) {
log.error("[exceptionHandler] 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 IllegalStateException("잘못된 입력 값");
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello" + id);
}
@Data
@AllArgsConstructor
private class MemberDto {
private String memberId;
private String name;
}
}
@ExceptionHandler 애노테이션을 선언하면 해당 컨트롤러에서 처리할 예외들을 지정해서 처리가 가능하다.
throw new IllegalStateException("잘못된 입력 값") => 에러 발생 시 @ExceptionHandler(IllegalArgumentException.class) 로 선언한 illegalArgumentException() 메서드에서 해당 에러를 처리 할 수 있다.
@ResponseStatus 애노테이션을 사용할 경우 해당 에러의 Http 상태 값을 원하는 상태로 변경할 수 있다.
위의 컨트롤러를 확인해보면 해당 @ExceptionResolver가 해당 컨트롤러에서만 사용할 수 있게 되어있다.
매번 컨트롤러마다 동일한 에러 처리를 하는건 상당히 귀찮다,,,(개발자는 귀찮아 해야함,,,)
@ControllerAdvice를 이용하면 @ExceptionResolver를 한번만 생성하고, 여러 컨트롤러에서 사용할 수 있다!!
ExControllerAdvice 클래스
@ExceptionHandler를 모아놓은 클래스
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult IllegalArgumentException(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 result = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity(result, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", "내부오류");
}
}
ApiExceptionV2Controller
컨트롤러 클래스
@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 IllegalStateException("잘못된 입력 값");
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello" + id);
}
@Data
@AllArgsConstructor
private class MemberDto {
private String memberId;
private String name;
}
}
@ControllerAdvice는 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 사용할 수 있게 함.
대상을 지정하지 않으면 모든 컨트롤러에 적용 됨.
대상 컨트롤러 지정 예시
// 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 {}