[Sectoion3] Spring MVC - 예외 처리

2023. 6. 14. 16:58

🧑🏻‍💻 TIL(Today I Learned)


✔️ @ExceptionHandler, @RestControllerAdvice

➡️ 어떤 요청 데이터 중에서 어떤 항목이 에러가 났는지 좀 더 구체적으로 알 수 있도록 바꾸는 작업이라고 할 수 있겠음 

 

1. @ExceptionHandler

➡️ Spring에서의 예외는 애플리케이션에 문제가 발생할 경우 문제를 알려서 처리하는 것뿐만이 아니라 유효성 검증에 실패했을 때와 같이 이 실패를 하나의 예외로 간주하여 예외를 던져 예외처리를 유도함

➡️ Controller 계층에서 발생하는 에러를 잡아서 메서드로 처리해주는 기능 (Service, Repository에서 발생하는 에러는 제외)

 

@ExceptionHandler를 사용한 handleException()

  • 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"
    }
]

 

  • 예외 처리 과정 
    1. 클라이언트 쪽에서 회원 등록을 위해 MemberController의 postMember() 핸들러 메서드에 요청을 전송
    2. RequestBody에 유효하지 않은 요청 데이터가 포함되어 있어 유효성 검증에 실패하고 
      MethodArgumentNotValidException 발생
    3. MemberController에는 @ExceptionHandler 애너테이션이 추가된 예외 처리 메서드인  hadleException()이 있기 때문에 유효성 검증 과정에서 내부적으로 던져진 MethodArgumentNotValidException을 handleException() 메서드가 전달받음
    4. getBindingResult().getFieldsErrors()를 통해 발생한 에러 정보 확인할 수 있음
    5. 4번을 통해 얻은 에러 정보를 ResposeEntity를 통해 Response Body로 전달함
  • hadleException() 메서드에서 유효성 검사 실패에 대한 에러 메세지를 구체적으로 전송해 주기 때문에 클라이언트 입장에서는 이제 어느 곳에 문제가 있는지를 구체적으로 알 수 있게됨

➡️ 하지만 응답 정보를 전부 다 받을 필요는 없고  문제가 된 프로퍼티가 문엇인지와 에러 메세지만 받고 싶다면 어떻게 해야할까?

 

🔎 Error Response 클래스 

➡️ DTO 클래스의 유효성 검증 실패 시 실패한 필드에 대한 Error 정보만 담아서 응답으로 전송하기 위한 클래스 만들기 

➡️ 응답 메세지를 보면 JSON 응답 객체가 배열

    : DTO클래스에서 검증해야 하는 멤버 변수에서 유효성 검증에 실패하는 멤버 변수들이 하나 이상이 될 수 있기 때문에 유효성 검증 실패 에러 또한 하나 이상이 될 수 있다는 의미 

Error Response 클래스
handleException() 메서드 수정

  • 수정한 handleException() 메서드에는 필요한 정보만 선택적으로 골라서 전달함

  • 유효성 검증에 실패한 필드가 두 개이기 때문에 에러 정보 역시 두 개로 나오는 것을 알 수 있음 

 

✍🏻 @ExceptionHandler의 단점

  1. 각각의 Controller 클래스에서 @ExceptionHandler 애너테이션을 사용하여 Request Body에 대한 유효성 검증 실패에 대한 에러 처리를 해야하기 때문에 각 Controller 클래스마다 코드 중복 발생함 
  2. Controller에서 처리해야 되는 예외가 유효성 검증 실패에 대한 예외만 있는 것이 아니기 때문에 하나의 Controller 클래스 내에서  @ExceptionHandler를 추가한 에러 처리 핸들러 메서드가 늘어남

 

 

2. @RestControllerAdvice를 사용한 예외 처리 

➡️ 특정 클래스에 @RestControllerAdvice 애너테이션을 추가하면 여러 개의 Controller 클래스에서 @ExceptionHandler, @InitBinder, @ModerAttribute가 추가된 메서드를 공유해서 사용할 수 있음 

➡️ 즉, 예외 처리를 공통화할 수 있다는 것!

@InitBinder와 @ModerAttribute
: JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR) 방식에서 주로 사용되는 방식 

Exception 핸들러 메서드

  • 기존 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를 대신 표현할 수 있음 

BELATED ARTICLES

more