[예외 처리] @RestControllerAdvice 사용한 예외 처리
by mignon25@RestControllerAdvice를 사용한 예외 처리 공통화
- 클래스 레벨에 @RestControllerAdvice를 추가하고 해당 클래스에 예외 처리 관련 메서드 작성
- 이 애너테이션이 적용되어 있으면 모든 Controller 클래스에서 @ExceptionHandler, @InitBinder, @ModelAttribute 가 추가된 메서드를 공유해서 사용할 수 있다.
👉🏻 예외 처리 메서드의 중복을 제거하고 예외 처리를 공통화할 수 있다.
@InitBinder, @ModelAttribute
- JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR) 방식에서 주로 사용되는 방식
- @InitBinder 공식문서
- @ModelAttrubute 공식문서
1. MemberController에서 @ExceptionHandler 로직 제거
2. ExceptionAdvice 클래스 정의
@RestControllerAdvice
public class GlobalExceptionAdvice {
}
@RestControllerAdvice 를 적용했으므로, Controller 클래스에서 발생하는 예외를 GlobalExceptionAdvice 가 도맡아서 처리하게 된다.
3. Exception 핸들러 메서드 구현
- 이전에 Controller에 직접 적용했던 @ExceptionHander 메서드들을 옮겨놓고,
- 클래스 레벨에 @RestControllerAdvice를 추가한 것
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
final List<ErrorResponse.FieldError> errors =
fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler
public ResponseEntity handleConstraintValidationException(ConstraintViolationException e) {
// TODO should implement for validation
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}

=> RequestBody의 유효성 검증에 대한 에러는 @RestControllerAdvice 를 통해 모든 어드바이스에 공통 적용
4. ErrorResponse 수정
URI 변수로 넘어오는 값의 유효성 검증에 대한 에러(ConstraintViolationException) 처리 구현 목적
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors; // 1
private List<ConstraintViolationError> violationErrors; // 2
// 3. private 생성자
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
// 4. BindingResult 에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult), null);
}
// 5. Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
// 6. Field Error 가공
@Getter
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
private FieldError(String field, Object rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<FieldError> of(BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
} // Static Inner Class FieldError
// 7. ConstraintViolation Error 가공
@Getter
public static class ConstraintViolationError {
private String propertyPath;
private Object rejectedValue;
private String reason;
private ConstraintViolationError(String propertyPath, Object rejectedValue, String reason) {
this.propertyPath = propertyPath;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<ConstraintViolationError> of(Set<ConstraintViolation<?>> constraintViolations) {
return constraintViolations.stream()
.map(constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()
))
.collect(Collectors.toList());
}
} // Static Inner Class ConstraintViolationError
} // ErrorResponse
< 수정 사항 >
ConstraintViolationException에 대한 Error Response까지 생성 가능하도록 수정
- MethodArgumentNotValidException 으로부터 발생하는 에러 정보를 담는 멤버 변수
=> DTO 멤버 변수 필드의 유효성 검증 실패로 발생한 에러 정보를 담는 멤버 변수
- ConstraintViolationException 으로부터 발생하는 에러 정보를 담는 멤버 변수
=> URI 변수 값의 유효성 검증 실패로 발생한 에러 정보를 담는 멤버 변수
- ErrorResponse 클래스의 생성자에 private 접근 제한자(Access Modifier) 지정
=> 외부에서 new 로 ErrorResponse 객체 생성 불가
=> 대신 4, 5 처럼 of() 메서드를 이용해 ErrorResponse 객체 생성
=> ErrorResponse 객체 생성과 동시에 ErrorResponse의 역할을 명확하게 해준다.
- MethodArgumentNotValidException 에 대한 ErrorResponse 객체 생성
=> MethodArgumentNotValidException 에서 에러 정보를 얻기 위해 필요한 객체 : BindingResult
=> of() 메서드를 호출하는 쪽에서 BindingResult 객체를 파라미터로 넘겨준다.
=> BindingResult에서 필요한 에러 정보만 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 FieldError 클래스에게 위임
- ConstraintViolationException 에 대한 ErrorResponse 객체 생성
=> ConstraintViolationException 에서 에러 정보를 얻기 위해 필요한 객체 : Set<ConstraintViolation<?>>
=> of() 메서드를 호출하는 쪽에서 Set<ConstraintViolation<?>> 객체를 파라미터로 넘겨준다.
=> 4와 마찬가지로, Set<ConstraintViolation<?>> 에서 필요한 에러 정보만 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 ConstraintViolationError 클래스에게 위임
- 필드(DTO 클래스의 멤버 변수)의 유효성 검증에서 발생하는 에러 정보 생성
=> 필요한 정보만 추출, 가공
- URI 변수 값에 대한 에러 정보 생성
=> 필요한 정보만 추출, 가공
에러 유형에 따른 에러 정보 생성 역할을 분리함으로써, ErrorResponse를 사용하는 입장에서는 한층 더 사용하기 편리해졌다.
- of 메서드
더보기
of() 메서드
of() 메서드는 Java 8의 API에서도 흔히 볼 수 있는 네이밍 컨벤션(Naming Convention)입니다.
주로 객체 생성시 어떤 값들의(of~) 객체를 생성한다는 의미에서 of() 메서드를 사용한다는 점을 기억하면 좋을 것 같습니다.
5. Exception 핸들러 메서드 수정
수정된 ErrorResponse 클래스의 메서드를 사용하도록 GlobalExceptionAdvice 클래스 수정
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintValidationException(ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
}
< 변경 사항 >
- Error Response 정보를 만드는 역할을 ErrorResponse 클래스가 대신해주고 있어 코드 자체가 무척 간결해졌다.
- 기존에는 ErrorResponse 객체를 ResponseEntity로 래핑해서 리턴
- 현재는 ErrorResponse 객체 바로 리턴
- @ResponseStatus 애너테이션을 이용해 HTTP Status를 HTTP Response에 포함시킴
@RestControllerAdvice VS @ControllerAdvice
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
- Spring MVC 4.3 버전 이후부터 @RestControllerAdvice 애너테이션 지원
- @ResponseBody 기능도 포함하고 있기 때문에, JSON 형식 데이터를 Response Body로 전송하기 위해 ResponseEntity로 데이터를 래핑할 필요가 없다.
Additional Keywords
- Controller Advice 관련
- https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-controller-advice
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestControllerAdvice.html
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html
'Spring' 카테고리의 다른 글
| JDBC 란? (0) | 2023.04.21 |
|---|---|
| [Spring MVC] 서비스 계층 적용하기 (Prac Project - 3. 서비스 계층 <-> API 계층 연동) (0) | 2023.04.21 |
| [예외 처리] 비즈니스 로직에 대한 예외 처리 (0) | 2023.04.20 |
| [예외 처리] @ExceptionHandler를 이용한 예외 처리 (0) | 2023.04.20 |
| DTO 유효성 검증 (Validation) - (1) (0) | 2023.04.18 |
블로그의 정보
Mignon'S Dev Log
mignon25