코드스테이츠_국비교육/[Section3]

44_[Spring MVC] 예외 처리_(1) Validation Exception_22.10.25

생각없이 해도 생각보다 좋다. 2022. 10. 25. 23:05

>애플리케이션 구현 시 발생하는 대표 예외 상황
1. 클라이언트 요청 데이터에 대한 유효성 검증(Validation)에서 발생하는 예외
2. 서비스 계층의 비즈니스 로직에서 던져지는 의도된 예외
3. 웹 애플리케이션 실행 중에 발생하는 예외(RuntimeException)

>Spring에서의 예외 처리
: 애플리케이션에서 발생하는 문제가 발생할 경우, Spring은 이를 예외로 인식하고 문제를 알리고 처리한다.
: 애플리케이션에서 발생하는 문제뿐만 아니라 유효성 검증에 실패했을 때도 Spring은 예외로 인식한다.

@ExceptionHandler
>유효성 검증에서 발생하는 예외 처리
: 클라이언트 요청 데이터에 대한 유효성 검증(Validation)에서 발생하는 예외 메세지에서는 어떤 항목이 유효성 검증에 실패했는지 명확히 알 수 없음.
: @ExceptionHandler 애너테이션을 이용하면 어떤 항목에서 예외가 발생했는지 알려줄 수 있는 친절한 코드를 작성할 수 있음.
>Controller 레벨에서의 예외 처리(1)
-메서드 생성
: Controller 내부에 @ExceptionHandler를 사용하는 메서드(예, handlerException)를 만든다.
-매개변수 설정
: 매개변수로 유효성 검증에서 실패하면 발생하는 Spring의 예외를 받는다. (예, MethodArgumentNotValidException)
: 매개변수로 제공받은 참조변수에서 getBindingResult().getFieldErrors()를 호출하여 발생한 정보를 확인할 수 있다.(반환값 : List<FieldError>)
-(1)의 결과
: 예외 처리(1)의 과정을 진행하면 기존에 뜨던 HttpStatus에 대한 에러 코드가 아닌, 어디에서 에러가 발생했는지 찾아볼 수 있는 결과를 출력한다.
>Controller 레벨에서의 예외 처리(2)
: 예외 처리(1)에서 의미있는, 즉 에러를 명확히 알 수 있는 부분만 받기 위해 추가적인 과정이 필요하다.
-클래스 생성
: 예외 처리(1)의 에러 메세지들을 받을 수 있는 클래스를 생성한다. (예, ErrorResponse)
: @Getter, @AllArgsConstructor(Lombok의 애너테이션)을 추가한다.
-멤버 변수 생성
: 예외가 한 개 이상일 수 있으니, 예외 처리(1)에서 예외값들의 반환형인 List<FieldError>의 변수를 생성한다.
-멤버 클래스 생성
: 낱개의 에러 메세지를 받을 수 있는 FieldError클래스를 생성한다.
: 변수로는 field(String), rejectedValue(Object), reason(String)을 선언한다.
: 같은 역할(혹은 유기적인), 공통의 관심사를 갖는 클래스이기 떄문에 ErrorResponse 내부에 Fielderror를 생성하고, 이너 클래스가 아닌 static 멤버 클래스라고 표현한다. 
-Controller 내부의 @ExceptionHandler를 사용한 메서드 수정
: 추가 코드로, List<ErrorResponse.FieldError>의 참조형을 갖는 변수를 선언하고, 스트림을 통해 .getBindingResult().getFieldErrors() 메서드의 결과를 저장한 값을 List<ErrorResponse.FieldError>로 변환시킨다.
: 추가 코드로 만든 값을 인자로 제공한 새로운 ErrorResponse 객체를 반환한다. (사실 그냥 추가 코드로 만든 값을 반환해도 되지만, 객체를 생성하면 반환하면 배열명(멤버클래스명)까지 같이 반환할 수 있어서 좀 더 명확히 할 수 있다.)
>@ExceptionHandler과 ErrorResponse 클래스를 이용한 응답의 단점
-AOP에 어긋나는 프로그래밍을 하게 된다.
: 위 방법으로 에러 응답 메세지를 제공하게 되면, 프로그램 내에 존재하는 모든 Controller에 똑같은 코드를 작성해야 한다.
때문에 AOP에 어긋나는 프로그래밍이 될 수 있다.
-유효성 검증 실패에 대한 예외가 하나가 아니다.
: 위에서는 MethodArgumentNotValidException만 다뤘지만, URI에 유효하지 않은 변수값을 제공할 경우 발생하는 ConstraintViolationException와 같은 예외도 있다.
이러한 모든 예외에 대해서 일일이 Handler를 만들어줘야하는 단점이 있다.

>기타
-@Slf4J
: Simple Logging Facade For Java
: Java Logging API를 제공하는 애너테이션. 즉, 로깅에 대한 추상 레이어를 제공하는 인터페이스의 모음이다.
: Spring Boot에서 로그를 남기는 방법 중 가장 편하게 사용되는 어노테이션이다.
-Logging
: 프로그램에서 발생할 수 있는 오류에 대비하기 위해 프로그램 상태를 모니터링하는데, 여기에 필요한 정보를 로그(Log)라고 하고 이를 기록하는 것을 로깅(Logging)한다고 표현한다.
: 로깅은 프로그램 개발 중 혹은 개발이 완료된 후에도 디버깅과 사용자의 상호 작용 기록을 저장하기 위해 사용된다.

 

@RestControllerAdvice
>@ExceptionHandler의 중복 코드 문제 해결
: @RestControllerAdvice을 추가하면 여러개의 Controller 클래스에서 @ExceptionHandler가 추가된 메서드를 공유해서 사용할 수 있음.
: 추가로 @InitBinder와 @ModelAttribute가 추가된 매소드도 공유해서 사용할 수 있음. (얘네들은 SSR 방식에서 주로 사용됨. 나중에 더 공부하자)
: 쉽게 말하면, @RestControllerAdvice를 추가한 클래스에서 예외 처리를 한번에 다루고, 이를 필요한 곳에서 가져다쓰게 하면 된다.

>ErrorResponse & GrobalExceptionAdvice
: 에러 메세지를 친절하고 출력하기 위해 에러에 대한 정보를 받아주는 ErrorResponse 클래스
: 프로그램의 모든 예외를 받는 GrobalExceptionAdvice 클래스
: 두 클래스를 이용하여 예외를 받고, 예외에 대한 필요한 정보를 담은 에러 메시지를 출력할 수 있음.

>ErrorResponse
1. MethodArgumentNotValidException 예외 에러 메세지
2. ConstraintViolationException 예외 에러 메세지(응답)

 

>정리

Spring MVC 기반의 Application에서 유효성 검증(Validation)에 대한 예외 발생 시, @RestControllerAdvice을 추가한 GrobalExceptionAdvice 클래스로 예외에 대한 정보를 받고 정보를 추출 및 가공하여 사용자에게 알기 쉬운 에러 메세지를 전달한다.

@RestControllerAdvice
@RestControllerAdvice는 @ControllerAdvice 의 기능과 @ResponseBody 의 기능을 포함하는 애너테이션이다.
따라서, 예외 처리를 공통으로 처리할 수 있는 기능과 Response body(응답 메세지)를 전달하기 위해 ResponseEntity로 Wrapping을 안해도 되는 기능이 있다.

GrobalExceptionAdvice 클래스가 이러한 기능을 하기 위해서 내부에 Validation에 대해 발생할 수 있는 예외 상황에 대한 메서드를 구현한다. 예시로 2개의 예외 상황을 다룬다. Request Body를 받는 필드(DTO 클래스의 멤버 변수)의 유효성 검증에서 발생하는 예외 정보를 다루는 클래스인 (1) MethodArgumentNotValidException과 URI 변수로 넘어오는 값의 유효성 검증에 대한 예외 정보를 다루는 클래스인 (2) ConstraintViolationException를 다룬다.

GrobalExceptionAdvice 클래스 내부에 (1), (2) 에 대한 메서드를 생성하고, 각각 @ExceptionHandler 를 붙인다. 각 메서드는 예외 상황 발생 시, 예외의 대한 정보를 받고 에러 메세지로 응답하는 기능만 갖는다. 예외의 대한 정보를 추출하고 가공하는 로직은 다른 클래스로 구현한다.

ErrorResponse 라는 클래스를 만들어 예외의 대한 정보를 추출하고 가공하는 로직을 구현한다. ErrorResponse 클래스는 GrobalExceptionAdvice 클래스의 메서드들과 연동되어, 예외 정보를 전달받고 이를 처리하여 보기 좋은 에러 메세지로 반환하는 클래스이다.

따라서, ErrorResponse 클래스는 2가지 예외 정보를 할당할 각각의 변수와 각각의 생성자, 그리고 각각의 정보 처리할 Static 멤버 클래스가 필요하다.

여기서 Static 멤버 클래스를 생성하는 이유는 2가지가 있다. 첫번째는 변수와 메서드가 둘 다 필요하기 때문이다. 예외 상황을 처리하기 위해서는 예외가 발생한 지점, 에러를 발생시킨 데이터, 에러가 발생한 이유 등의 변수와 이를 추출할 로직(메서드)가 필요하기 때문에 메서드가 아닌 내부 클래스로 생성한다. 두번째는 공통 관심사를 갖는 클래스이기 때문에 따로 만들지 않고 GrobalExceptionAdvice 클래스 내부에 선언하며, 이너 클래스가 아닌 Static 멤버 클래스로 표현된다.

2가지 예외 정보를 추출하고 응답 메세지를 만드는 메서드를 구현할 때 팁은 각각의 예외 상황 클래스가 예외 정보를 저장하는 공간을 활용하는 것이 좋다. MethodArgumentNotValidException 에는 BindingResult 가 있고, ConstraintViolationException 에는 Set<ConstraintViolation<?>> 가 있다. 이를 활용해서 해당 예외의 정보를 받고 각각의 static 멤버 클래스가 받아 처리할 수 있다.

생성자 또한 좀 더 명확하게 표현할 수 있다. GrobalExceptionAdvice 클래스는 모든 상황을 매개변수로 하는 생성자를 갖되, private 접근 제어자를 사용하여 외부에서 생성할 수 없도록 한다. 그리고 각각의 static 멤버 클래스에서 GrobalExceptionAdvice 클래스 생성자를 이용해서 각 상황에 대한 객체를 만들 수 있는 of() 메서드를 따로 구현하면 사용자가 더 편하고 명확하게 사용할 수 있다.