[Sectoion3] Spring MVC - 예외 처리
🧑🏻💻 TIL(Today I Learned)
✔️ @ExceptionHandler, @RestControllerAdvice
➡️ 어떤 요청 데이터 중에서 어떤 항목이 에러가 났는지 좀 더 구체적으로 알 수 있도록 바꾸는 작업이라고 할 수 있겠음
1. @ExceptionHandler
➡️ Spring에서의 예외는 애플리케이션에 문제가 발생할 경우 문제를 알려서 처리하는 것뿐만이 아니라 유효성 검증에 실패했을 때와 같이 이 실패를 하나의 예외로 간주하여 예외를 던져 예외처리를 유도함
➡️ Controller 계층에서 발생하는 에러를 잡아서 메서드로 처리해주는 기능 (Service, Repository에서 발생하는 에러는 제외)
- MemberController에 위 코드를 작성하고 postMember() 핸들러 메서드에 올바르지 않은 형식으로 요청을 보내면 아래와 같이 포스트맨 화면이 나타남
🔻 자세한 응답 메세지
[
{
"codes": [
"Email.memberPostDto.email",
"Email.email",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"memberPostDto.email",
"email"
],
"arguments": null,
"defaultMessage": "email",
"code": "email"
},
[],
{
"arguments": null,
"defaultMessage": ".*",
"codes": [
".*"
]
}
],
"defaultMessage": "올바른 형식의 이메일 주소여야 합니다",
"objectName": "memberPostDto",
"field": "email",
"rejectedValue": "hgd@",
"bindingFailure": false,
"code": "Email"
}
]
- 예외 처리 과정
- 클라이언트 쪽에서 회원 등록을 위해 MemberController의 postMember() 핸들러 메서드에 요청을 전송
- RequestBody에 유효하지 않은 요청 데이터가 포함되어 있어 유효성 검증에 실패하고
MethodArgumentNotValidException 발생 - MemberController에는 @ExceptionHandler 애너테이션이 추가된 예외 처리 메서드인 hadleException()이 있기 때문에 유효성 검증 과정에서 내부적으로 던져진 MethodArgumentNotValidException을 handleException() 메서드가 전달받음
- getBindingResult().getFieldsErrors()를 통해 발생한 에러 정보 확인할 수 있음
- 4번을 통해 얻은 에러 정보를 ResposeEntity를 통해 Response Body로 전달함
- hadleException() 메서드에서 유효성 검사 실패에 대한 에러 메세지를 구체적으로 전송해 주기 때문에 클라이언트 입장에서는 이제 어느 곳에 문제가 있는지를 구체적으로 알 수 있게됨
➡️ 하지만 응답 정보를 전부 다 받을 필요는 없고 문제가 된 프로퍼티가 문엇인지와 에러 메세지만 받고 싶다면 어떻게 해야할까?
🔎 Error Response 클래스
➡️ DTO 클래스의 유효성 검증 실패 시 실패한 필드에 대한 Error 정보만 담아서 응답으로 전송하기 위한 클래스 만들기
➡️ 응답 메세지를 보면 JSON 응답 객체가 배열
: DTO클래스에서 검증해야 하는 멤버 변수에서 유효성 검증에 실패하는 멤버 변수들이 하나 이상이 될 수 있기 때문에 유효성 검증 실패 에러 또한 하나 이상이 될 수 있다는 의미
- 수정한 handleException() 메서드에는 필요한 정보만 선택적으로 골라서 전달함
- 유효성 검증에 실패한 필드가 두 개이기 때문에 에러 정보 역시 두 개로 나오는 것을 알 수 있음
✍🏻 @ExceptionHandler의 단점
- 각각의 Controller 클래스에서 @ExceptionHandler 애너테이션을 사용하여 Request Body에 대한 유효성 검증 실패에 대한 에러 처리를 해야하기 때문에 각 Controller 클래스마다 코드 중복 발생함
- Controller에서 처리해야 되는 예외가 유효성 검증 실패에 대한 예외만 있는 것이 아니기 때문에 하나의 Controller 클래스 내에서 @ExceptionHandler를 추가한 에러 처리 핸들러 메서드가 늘어남
2. @RestControllerAdvice를 사용한 예외 처리
➡️ 특정 클래스에 @RestControllerAdvice 애너테이션을 추가하면 여러 개의 Controller 클래스에서 @ExceptionHandler, @InitBinder, @ModerAttribute가 추가된 메서드를 공유해서 사용할 수 있음
➡️ 즉, 예외 처리를 공통화할 수 있다는 것!
@InitBinder와 @ModerAttribute
: JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR) 방식에서 주로 사용되는 방식
- 기존 Controller에 작성했던 로직들은 지우고 GlobalExceptionAdvice 클래스에 원래 Controller에 작성했던 핸들러 메서드 로직 작성하고 포스트맨 돌려보면 정상적으로 에러 메세지가 출력됨
-> 아직 URI 변수로 넘어오는 값의 유효성 검증에 대한 에러 처리는 구현되지 않음!
package com.codestates.response
import lombok.Getter;
import org.springframework.validation.BindingResult;
import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors; // (1)
private List<ConstraintViolationError> violationErrors; // (2)
// (3)
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());
}
}
// (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());
}
}
}
- ConstraintViolationException에 대한 Error Response를 생성할 수 있도록 수정
-> 기능이 늘어남에 따라 구현 복잡도가 늘어나긴 했지만 에러 유형에 따른 에러 정보 생성 역할을 분리함으로써 ErrorResponse를 사용하는 입장에서는 한층 더 사용하기 편리해졌다는 사실! - of() 메서드
: 네이밍 컨벤션(Naming Convention), 객체 생성 시 어떤 값들의 객체를 생성한다는 의미 - 그리고 마지막으로 Exception 핸들러 메서드까지 수정해주면 완료
@RestControllerAdvice vs @ControllerAdvice
: @RestControllerAdvice = @ControllerAdvice + @ResponseBody
: 그래서 ResponseEntity로 데이터를 래핑할 필요가 없음
@ResponseStatus
: HTTP Status를 대신 표현할 수 있음
'SEB_BE_45 > 공부 정리' 카테고리의 다른 글
[Section3] Spring MVC - JDBC 기반 데이터 액세스 실습 (0) | 2023.06.21 |
---|---|
[Section3] Spring MVC - 비즈니스 로직에 대한 예외처리 (0) | 2023.06.15 |
[Section3] Spring MVC - 서비스 계층 (0) | 2023.06.13 |
[Section3] Spring MVC - API 계층 2 (0) | 2023.06.12 |
[Section3] Spring MVC - API 계층 1 (0) | 2023.06.12 |