티스토리 뷰
[Spring]Spring MVC - @RestControllerAdvice를 이용한 예외처리
Vagabund.Gni 2022. 8. 24. 18:17Spring MVC - Controller + Service
[Spring]Spring MVC - Controller 클래스 구조 생성 및 설계
[Spring]Spring MVC - Controller 클래스에 핸들러 메서드(Handler Method) 구현
[Spring]Spring MVC - Controller 클래스에 ResponseEntity 적용
[Spring]Spring MVC - Controller 클래스에 DTO 적용
[Spring]Spring MVC - DTO 유효성 검증(Validation)
[Spring]Spring MVC - DI를 통한 API 계층 ↔ 서비스 계층 연동
[Spring]Spring MVC - 매퍼(Mapper)를 이용한 DTO 클래스 ↔ 엔티티(Entity) 클래스 매핑
[Spring]Spring MVC - @ExceptionHandler를 이용한 예외처리
[Spring]Spring MVC - @RestControllerAdvice를 이용한 예외처리
Spring Data JDBC
[Spring]JDBC(Java DataBase Connectivity)
[Spring]Spring Data JDBC, Spring Data JDBC 사용법
[Spring]Spring Data JDBC - 도메인 엔티티&테이블 설계
Spring Data JPA
[Spring]JPA(Java Persistence API)
[Spring]JPA - Entity ↔ DB Table Mapping
[Spring]JPA - Entity ↔ Entity Mapping
[Spring]Spring Data JPA - 데이터 액세스 계층 구현
지난 글에서 @ExceptionHandler를 이용한 예외처리를 알아보았다.
글 마지막에 @ExceptionHandler를 이용할 때의 단점에 대해 언급했는데, 요약하면
- 코드 중복
- 유연성 떨어짐
이 그것이었다.
이번 글에선 @RestControllerAdvice 애너테이션을 이용해 위 문제들을 해결하는 방법을 보겠다.
@RestControllerAdvice는 @ControllerAdvice와 @ResponseBody를 포함하는 애너테이션으로,
직접 뜯어보면 아래와 같이 생겼다.
즉,
- @ControllerAdvice - 발생한 예외를 한 곳에서 전역적으로 관리하고 처리할 수 있게 하는 모듈화 애너테이션. View 리턴
- @ResponseBody - 메서드의 리턴 값을 JSON으로 변환해 HTTP Response body에 담음
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody
라는 뜻이다.
구체적으로는 @RestControllerAdvice가 붙은 클래스의 @ExceptionHandler, @InitBinder, @ModelAttribute 메서드를
여러 Controller에서 공유할 수 있게 해 주며, 그 결과 예외 처리를 공통화할 수 있게 해 준다.
이 중 @InitBinder, @ModelAttribute는 SSR 방식에서 사용하는 방식이기 때문에 생략하고,
CSR 방식에서 주로 사용하는 @ExceptionHandler를 중심으로 알아본다.
MemberController 클래스에서 @ExceptionHandler 로직 제거
먼저 지난 글까지 작성했던 MemberController에서 @ExceptionHandler가 적용된 메서드를 지운다.
각 Controller 클래스에서 발생하는 예외를 GlobalExceptionAdvice 클래스를 작성해 공통으로 처리할 것이기 때문이다.
GlobalExceptionAdvice 클래스 정의
방금 말한 대로 예외를 공통으로 처리할 GlobalExceptionAdvice 클래스를 정의한다.
상위 패키지에 advice 폴더를 생성했다.
package com.gnidinger.advice;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionAdvice {
}
@RestControllerAdvice를 적용해 컨트롤러 클래스의 예외를 공통으로 처리하도록 한다.
GlobalExceptionAdvice 클래스 내에 Exception 핸들러 메서드 구현
예외를 처리할 핸들러 메서드를 GlobalExceptionAdvice 클래스에 작성한다.
package com.gnidinger.advice;
import com.gnidinger.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
public ResponseEntity handleMethodArgumentNotValidException(
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(
ConstraintViolationException e) {
// TODO should implement for validation
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
지난 글에서 MemberController에 추가했던 Exception 핸들러 메서드의 로직을 그대로 추가했다.
ConstraintViolationException 메서드의 바디는 잠시 후에 작성하기로 하고, 코드가 동작하는 것을 확인하자.
CoffeeController에서도 정확하게 동작하는 것을 확인할 수 있다.
이처럼 @RestControllerAdvice을 이용해 예외 처리를 공통화하면 중복 코드를 제거하고 코드를 단순화할 수 있다.
ErrorResponse 클래스 수정
계속해서 URI 변수 유효성 검증에 대한 에러(ConstraintViolationException) 처리를 구현해 보자.
먼저 ErrorResponse 클래스가 ConstraintViolationException에 대한 Error Response를 생성할 수 있도록 수정한다.
package com.gnidinger.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(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());
}
}
}
DTO 클래스의 유효성 검증과 URI의 변수 값 검증에 대한 Error Response를 포함할 수 있게 바뀌었다.
(1) - MethodArgumentNotValidException(DTO 클래스 검증 에러)에서 발생하는 에러 정보를 담는 변수
(2) - ConstraintViolationException(URI 검증 에러)에서 발생하는 에러 정보를 담는 멤버 변수
(3) - 생성자 앞에 private 제한자 지정, 강한 결합(new를 통한 객체 생성) 방지 및 ErrorResponse의 역할 특정
(4) - of()를 통해 MethodArgumentNotValidException에 대한 ErrorResponse 객체 생성
- BindingResult - MethodArgumentNotValidException에서 에러 정보를 얻기 위해 필요한 객체
- 따라서 of() 메서드를 호출하는 쪽에서 BindingResult 객체를 파라미터로 넘겨줌
- BindingResult의 에러 정보를 추출하고 가공하는 일은 같은 클래스의 static 멤버 클래스인 FieldError에게 위임
(5) - of()를 통해 ConstraintViolationException에 대한 ErrorResponse 객체 생성
- Set<ConstraintViolation<?>> - ConstraintViolationException에서 에러 정보를 얻기 위해 필요한 객체
- 따라서 of() 메서드를 호출하는 쪽에서 Set<ConstraintViolation<?>> 객체를 파라미터로 넘겨줌
- Set<ConstraintViolation<?>>의 에러 정보를 추출하고 가공하는 일은 같은 클래스의 ConstraintViolationError에게 위임
(4)와 (5)를 통해서 ErrorResponse 객체에 에러 정보를 담는 역할이 명확하게 분리된다.
(6) - DTO 클래스 변수의 유효성 검증에서 발생하는 에러 정보 가공
(7) - URI 변수 유효성 검증에서 발생하는 에러 정보 가공
*of() - 객체 생성 시 매개변수로 주어지는 값들의 객체를 생성한다는 의미를 가진 네이밍 컨벤션
클래스의 구현 복잡도가 늘어나긴 했지만, 에러 유형에 따른 정보 생성 역할을 분리해 사용하기엔 편해진 것을 확인할 수 있다.
GlobalExceptionAdvice 클래스 내에 Exception 핸들러 메서드 수정
위에서 작성한 ErrorResponse 클래스의 메서드를 사용하도록 메서드를 수정해 보자.
package com.gnidinger.advice;
import com.gnidinger.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;
}
}
에러 정보를 만드는 역할을 ErrorResponse 클래스가 대신해주기 때문에 코드가 간결해진 것을 확인할 수 있다.
또한 ErrorResponse 객체를 ResponseEntity로 래핑 했던 것이 ErrorResponse 객체를 바로 리턴하도록 변경되었는데,
이는 처음에 알아봤듯이 @RestControllerAdvice 안에 포함된 @ResponseBody 덕분이다.
추가로 @ResponseStatus 애너테이션을 이용해 HTTP Status를 HTTP Response에 포함시키는 것도 볼 수 있다.
'Java+Spring > Spring' 카테고리의 다른 글
[Spring]JDBC(Java DataBase Connectivity) (0) | 2022.08.26 |
---|---|
[Spring]Spring MVC - Custom Exception을 이용한 클라이언트 출력 (2) | 2022.08.26 |
[Spring]Spring MVC - 비즈니스로직 예외 던지기(throw) 및 처리 (0) | 2022.08.25 |
[Spring]Spring MVC - @ExceptionHandler를 이용한 예외처리 (1) | 2022.08.24 |
[Spring]Spring MVC - 매퍼(Mapper)를 이용한 DTO 클래스 ↔ 엔티티(Entity) 클래스 매핑 (4) | 2022.08.24 |
[Spring]Spring MVC - DI를 통한 API 계층 ↔ 서비스 계층 연동 (2) | 2022.08.23 |
- Total
- Today
- Yesterday
- 지지
- spring
- 야경
- 알고리즘
- 유럽여행
- 남미
- RX100M5
- a6000
- 칼이사
- 세계일주
- Algorithm
- 중남미
- 동적계획법
- BOJ
- 면접 준비
- 여행
- 리스트
- 자바
- 세모
- 백준
- 유럽
- 스프링
- Backjoon
- 스트림
- java
- 세계여행
- 파이썬
- Python
- 맛집
- 기술면접
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |