들어가며
@Valid
어노테이션으로 유효성 검사를 해주고 예외 처리를 해주지 않으면 사진과 같이 에러 로그가 그대로 노출되어버린다.
이렇게 되면 클라이언트 입장에서 유용한 정보를 주기도 어렵고, trace에서 운영환경에서의 구현이 노출되기 때문에 해커의 위협에서 벗어나기 어렵다.
따라서 적절하게 예외 처리를 함으로써 에러 응답을 변경해 줄 필요가 있고, 이 예외 처리 방법 중 가장 좋은 방식이 @RestControllerAdivce
(혹은 @ControllerAdvice
)어노테이션과 @ExceptionHandler
를 함께 사용하는 방식이라고 한다.
다양한 예외처리 방법과 @RestControllerAdivce
어노테이션을 사용하는게 가장 좋은 이유는 다음 글을 참고하자.
[Spring] Spring의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)
유효성 검사
유효성 검사는 앞서 말했듯이 @Valid
어노테이션을 활용해서 적용해주었다.
@Valid 어노테이션의 사용 방법은 아래 포스팅을 참고하자.
[TIL] 06/08 항해99 27일차 - JPA 예외처리, 삽질로그
@RestControllerAdvice 를 이용해서 예외 처리를 해보자!
@RestControllerAdvice
를 적용하기 위해 두 가지의 시도를 해보았고, 내가 보기에 좋아보이는 방식을 적용해주었다.
최종적으로 완성된 패키지 구조는 아래와 같다. 사진에서 볼 수 있는 UserErrorCode는 아직 사용되는 부분이 없지만 미리 구현해둔 클래스이기 때문에 따로 언급하지 않겠다.
1. 에러코드 정의하기
클라이언트에게 보내줄 에러 코드를 정의해주기 위해서 클래스를 추가해준다.
에러 이름, HTTP 상태, 에러 메세지를 가지고 있는 클래스를 만들어주는 단계이다.
에러 코드를 클래스별로 나누기 위해 ErrorCode를 인터페이스로 구현 해준 다음, CommonErrorCode에 추상화해주었다.
ErrorCode.java
public interface ErrorCode {
String name();
HttpStatus getHttpStatus();
String getMessage();
}
CommonErrorCode.java
@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode {
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter included"),
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not exists"),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"),
;
private final HttpStatus httpStatus;
private final String message;
}
CommonErrorCode에는 전역적으로 나타나는 에러 코드들과 메세지가 정의되어 있다.
그리고 발생한 예외를 처리해줄 예외 클래스(Exception Class)를 추가해준다.
RestApiException.java
@Getter
@RequiredArgsConstructor
public class RestApiException extends RuntimeException{
private final ErrorCode errorCode;
}
2. 에러 응답 클래스 정의하기
아래와 같은 형태의 응답을 보내주기 위해 에러 응답 클래스를 추가해준다.
{
"code": "INACTIVATE_USER",
"message": "User is inactive"
}
ErrorResponse.java
@Getter
@Builder
@RequiredArgsConstructor
public class ErrorResponse {
private final String code;
private final String message;
// Errors가 없다면 응답이 내려가지 않게 처리
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private final List<ValidationError> errors;
@Getter
@Builder
@RequiredArgsConstructor
public static class ValidationError {
// @Valid 로 에러가 들어왔을 때, 어느 필드에서 에러가 발생했는 지에 대한 응답 처리
private final String field;
private final String message;
public static ValidationError of(final FieldError fieldError) {
return ValidationError.builder()
.field(fieldError.getField())
.message(fieldError.getDefaultMessage())
.build();
}
}
}
3. @RestControllerAdvice 구현하기
이제 마지막이다. 전역적으로 에러를 처리해주는 @RestControllerAdvice
를 구현해주면 된다.
Spring은 예외를 미리 처리해둔 ResponseEntityExceptionHandler
를 추상클래스로 제공하고 있기 때문에 이를 상속받아주면 되는데, 에러 메세지는 반환하지 않기 때문에 handleExceptionInternal
을 오버라이드 해줘야한다.
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// RuntimeException 처리
@ExceptionHandler(RestApiException.class)
public ResponseEntity<Object> handleCustomException(RestApiException e) {
return handleExceptionInternal(e.getErrorCode());
}
// IllegalArgumentException 에러 처리
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Object> handleIllegalArgument(IllegalArgumentException e) {
log.warn("handleIllegalArgument", e);
return handleExceptionInternal(CommonErrorCode.INVALID_PARAMETER, e.getMessage());
}
// @Valid 어노테이션으로 넘어오는 에러 처리
@Override
public ResponseEntity<Object> handleBindException(
BindException e,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
log.warn("handleIllegalArgument", e);
return handleExceptionInternal(e, CommonErrorCode.INVALID_PARAMETER);
}
// 대부분의 에러 처리
@ExceptionHandler({Exception.class})
public ResponseEntity<Object> handleAllException(Exception ex) {
log.warn("handleAllException", ex);
return handleExceptionInternal(CommonErrorCode.INTERNAL_SERVER_ERROR);
}
// RuntimeException과 대부분의 에러 처리 메세지를 보내기 위한 메소드
private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode));
}
// 코드 가독성을 위해 에러 처리 메세지를 만드는 메소드 분리
private ErrorResponse makeErrorResponse(ErrorCode errorCode) {
return ErrorResponse.builder()
.code(errorCode.name())
.message(errorCode.getMessage())
.build();
}
private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode, String message) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode, message));
}
// 코드 가독성을 위해 에러 처리 메세지를 만드는 메소드 분리
private ErrorResponse makeErrorResponse(ErrorCode errorCode, String message) {
return ErrorResponse.builder()
.code(errorCode.name())
.message(message)
.build();
}
// @Valid 어노테이션으로 넘어오는 에러 처리 메세지를 보내기 위한 메소드
private ResponseEntity<Object> handleExceptionInternal(BindException e, ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(e, errorCode));
}
// 코드 가독성을 위해 에러 처리 메세지를 만드는 메소드 분리
private ErrorResponse makeErrorResponse(BindException e, ErrorCode errorCode) {
List<ErrorResponse.ValidationError> validationErrorList = e.getBindingResult()
.getFieldErrors()
.stream()
.map(ErrorResponse.ValidationError::of)
.collect(Collectors.toList());
return ErrorResponse.builder()
.code(errorCode.name())
.message(errorCode.getMessage())
.errors(validationErrorList)
.build();
}
}
유효성 검사를 통과하지 못할 때 에러처리 응답이 어떻게 넘어오는지 확인해보면 이쁘게 잘 넘어오는 걸 볼 수 있다.
마치며
코드의 대부분은 링크된 글을 참고했다!
출처 및 참고
- 본문 예외처리 코드
[Spring] @RestControllerAdvice를 이용한 Spring 예외 처리 방법 - (2/2)
- @Valid만 예외처리된 코드
'Study > Spring boot' 카테고리의 다른 글
[JPA] 영속성 관리 (0) | 2022.06.21 |
---|---|
[JPA] JPA, Hibernate, Spring Data JPA의 차이점 (0) | 2022.06.20 |
[Spring boot] 스프링 프레임워크(Spring Framework) (0) | 2022.06.09 |
[JPA] ORM(Object-Relational Mapper) (0) | 2022.06.05 |
[Spring boot] Ajax로 보낸 JSON 서버에서 받기 (0) | 2022.04.12 |