관리 메뉴

제뉴어리의 모든것

[Section3] [Spring MVC] 예외처리 본문

코드스테이츠/정리 블로깅

[Section3] [Spring MVC] 예외처리

제뉴어리맨 2022. 8. 24. 23:07

@ExceptionHandler

Controller 계층에서 발생하는 에러를 잡아서 메소드로 처리해주는 애노테이션이다.

 

  • 사용법
@RestController
@RequestMapping("/v6/members")
@Validated
public class MemberController {
 	
    //Something Handler Method
	
    :	
	:
    
    @ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {

        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors(); //유효성 에러를 발생시키는 필드가 여러개 일 수 있으므로

        ErrorResponse.FieldError fieldError = new ErrorResponse.FieldError();
        List<ErrorResponse.FieldError> errorList = fieldErrors.stream().map(error -> new ErrorResponse.FieldError(
                error.getField(), error.getRejectedValue(), error.getDefaultMessage())).collect(Collectors.toList());

        ErrorResponse errorResponse = new ErrorResponse(errorList);
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler
    public ErrorResponse handleConstraintViolationException(
            ConstraintViolationException e) {
        final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());

        return response;
    }
    
    :
    :

}

- handleException(MethodArgumentNotValidException e) 메소드 역할

Controller 레벨에서 MethodArgumentNotValidException 예외 발생시 콜백되는 메소드. (DTO 내의 필드 유효성 에러)

 

- handleException(ConstraintViolationException e) 메소드 역할

Controller 레벨에서 ConstraintViolationException 예외 발생시 콜백되는 메소드. (URI 관련 에러)

 

 

 

  • 기타 소스 (ErrorResponse)
@Getter
@AllArgsConstructor
public class ErrorResponse {
		// (1)
    private List<FieldError> fieldErrors;

    @Getter
    @AllArgsConstructor
    public static class FieldError {
        private String field;
        private Object rejectedValue;
        private String reason;
    }
}

위에 Controller 코드를 보면

발생된 에러 하나하나를 ErrorResponse클래스의 내부 클래스인 FieldError라는 객체로 맵핑하여 List에 담은 뒤,
최종적으로 ErrorResponse라는 객체로 Client에게 전달한다.

 

즉, 발생한 에러에서 필요한 데이터만 뽑아서 에러 하나를 FieldError 라는 객체로 생성.

여러 FieldError 를 가진 ErrorResponse를 클라이언트에게 전달.

 

 

  • 현재 코드의 문제점
    1. 각 Controller 마다 동일한 Error에 대한 메소드 중복이 발생.
     
    EX )
    CoffeeController, MemberController 존재시
    각 컨트롤러 클래스마다
    MethodArgumentNotValidException 예외사항을 처리하는 메소드를 중복으로 넣어줘야함.

    2. MethodArgumentNotValidException 이외에 여러 예외처리가 발생 될 수 있는데  만 처리 됨.

 

문제점 해결 방법

위에서 제시한 문제점들을 해결하여 보자

 

  • 1번 문제의 대한 해결
    @RestControllerAdvice 사용
  • 2번 문제의 대한 해결
    다른 예외사항에 대하여 @ExceptionHandler를 사용하여 핸들링하는 메소드를 만든다

문제 해결 코드

우선 1번 문제를 해결 하기 위해

@RestControllerAdvice 애노테이션을 사용하여 클래스 하나를 생성해준다.

@RestControllerAdvice
public class GlobalExceptionAdvice {

}

위 코드처럼 클래스에 @RestControllerAdvice 애노테이션을 붙이면

모든 Controller 에서 발생하는 예외사항에 대하여 공통적으로 처리 할 수 있다.

@RestControllerAdvice 에 대해서는 밑에 Controller Advice 항목에서 조금 더 자세히 다루겠다.

 

  • 예외사항 공통처리 클래스 내에 각 예외사항에 대한 처리 메소드 추가

@RestControllerAdvice
public class GlobalExceptionAdvice {
	
    @ExceptionHandler
    public ResponseEntity handleMethodArgumentNotValidException( // Controller 단에서 MethodArgumentNotValidException 예외 발생시 콜백
            MethodArgumentNotValidException e) {
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();

        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 handleConstraintViolationException( //// Controller 단에서 ConstraintViolationException 예외 발생시 콜백
            ConstraintViolationException e) {
        // TODO should implement for validation

        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
}

위 코드처럼

@RestControllerAdvice 적용 클래스 내에

@ExceptionHandler 붙인 메소드를 정의하고, 해당 메소드의 매개변수로 

MethodArgumentNotValidException 와 같은 Exception 타입 변수를 선언하면,

Controller 레벨에서 매개변수로 지정한 Exception이 발생 됬을때 해당 메소드가 콜백되므로

예외사항에 대한 처리가 가능하다.

 

  • 현재 코드의 문제점
    1. ConstraintViolationException 에러를 처리하는 handleConstraintViolationException 메소드의 내용이 정의되어 있지 않다.


    2. 클라이언트에게 전달할 ErrorResponse 객체를 생성하는 로직이 handleMethodArgumentNotValidException 과 같은 ExceptionHandler 메소드내에 있으므로 코드가 간결하지 않고 ExceptionHandler 메소드가 너무 많은 역할을 하고 있다.


문제 해결 코드 2

  • ErrorResponse 코드 수정
@Getter
public class ErrorResponse {
    private List<FieldError> fieldErrors; // (1)
    private List<ConstraintViolationError> violationErrors;  // (2)

		// (3)
    private ErrorResponse(final List<FieldError> fieldErrors,
                          final 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());
        }
    }
}

 

(1) : 필드의 유효성 관련 에러 발생시, 해당 에러들의 내용들을 담을 리스트

(2) :  URI 관련 에러 발생시, 해당 에러들의 내용들을 담을 리스트

(3) : ErrorResponse 클래스 생성자
접근제어자가 private 이므로 외부에서는 생성할 수 없게 하였다.

대신에 of 메소드를 사용하여 외부에서 ErrorResponse 를 생성할 수 있다.

ErrorResponse 객체는 new로 생성하여 해당 객체의 메소드나 변수를 접근하여 주체적으로 사용되는 객체가 아니라,

단지 필요한 데이터를 담아서 ErrorResponse 객체로써 만들어 내는 용도이므로 생성자가 아닌 of 메소드로 객체를 생성한다. 즉, 역할을 명확히 보여주기 위함.

 

(4) : 필요한 데이터(필드유효성 관련 에러 정보)를 담아 ErrorResponse 객체를 생성하기 위한 메소드 

(5) : 필요한 데이터(URI 관련 에러 정보)를 담아 ErrorResponse 객체를 생성하기 위한 메소드 

(6) : 필드 유효성 관련 에러 정보를 담고 (멤버 필드들), 에러 정보를 취합하여 해당 클래스의 객체로 가공하여 리스트 형태로  반환하여 주는 메소드(of 메소드)

(7) : URI 관련 에러 정보를 담고 (멤버 필드들), 에러 정보를 취합하여 해당 클래스의 객체로 가공하여 리스트 형태로  반환하여 주는 메소드(of 메소드)

 

  • GlobalExceptionAdvice 클래스 수정
package com.codestates.advice;

import com.codestates.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;


@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 handleConstraintViolationException(
            ConstraintViolationException e) {
        final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());

        return response;
    }
}

2번 문제점이 해결된 코드이다.

즉, 1, 2번 문제점들을 해결하였다.

 

 

+ Controller Advice

컨트롤러 충고 라고 해석이 되는데.

뭐가 에러 발생시 충고를 해준다는 그런 의미인지,, 아무튼 에러 발생시 처리헤 대한 기능을 말한다.

우선 @Controller가 붙은 각각의 클래스마다 추가하여 에러를 처리하는 애노테이션은 다음과 같다.

@ExceptionHandler, @InitBinder, @ModelAttribute 

 

여기서 @InitBinder, @ModelAttribute 은

JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR, Server Side Rendering) 방식에서 주로 사용된다.

 

@ExceptionHandler은 CSR 방식에 사용된다.

 

 

그리고 @Controller가 붙은 모든 컨트롤러 클래스에 @ExceptionHandler 애노테이션을 붙여서 처리하기 싫고,

컨트롤러 레벨에서 발생되는 에러는 한 클래스내에서 정의하여 사용하고 싶다면

아래와 같은 애노테이션을 사용한다.

@ControllerAdvice , @RestControllerAdvice

 

@ControllerAdvice가 붙은 클래스는 ComponentScan으로 인해 빈으로 등록된다.

그래서 Controller 레벨에서 에러 발생시 해당 애노테이션이 붙은 클래스의 빈을 찾은 다음,

해당 클래스내에 

@ExceptionHandler 가 붙은 메소드를 탐색하여 발생한 Exception 에러와 동일한 파라미터를 받는 메소드를 찾아서 해당 메소드에 콜백을 시켜주는 원리이다.

 

그렇다면, @RestControllerAdvice 은 무엇인가?

쉽게 얘기하면 @ControllerAdvice기능과 @ResponseBody기능을 합친 애노테이션이다.

해당 애노테이션 내부를 보면 아래와 같다.

즉, @RestControllerAdvice 가 붙은 클래스에 정의된 메소드는 return 되는 데이터를 자동으로 JSON 형식으로 변환하여 전달해준다는것이다.

 

결론,

각각의 Controller 클래스에서 발생하는 에러를 각각의 내부에서 처리하고 싶다면,

@ExceptionHandler, @InitBinder, @ModelAttribute

을 사용.

 

모든 Controller 클래스에서 발생되는 에러를 공통의 클래스 하나에서 처리하고 싶다면

@ControllerAdvice , @RestControllerAdvice 를 사용하여 한 클래스만 정의하고

내부에 @ExceptionHandler, @InitBinder, @ModelAttribute를 사용할것.

 

참조 : https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-controller-advice

 

Web on Servlet Stack

Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, “Spring Web MVC,” comes from the name of its source module (spring-webmvc), but it is more com

docs.spring.io

 

 

추가 참조 사항 : https://velog.io/@kiiiyeon/%EC%8A%A4%ED%94%84%EB%A7%81-ExceptionHandler%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC