첫 팀 프로젝트에서 검증과 예외처리를 도전해봤는데, 엄청 중요한 내용이라고 생각해서 까먹기 전에 포스팅으로 남겨놓기 위해 글을
작성해보려고 한다 ☺️
검증이 필요하다고 생각한 객체
클라이언트에서 카드 수정 요청이 왔다고 생각해보자.
요구 사항으로는 Title 은 30자 이내, Content 는 500자 이내의 데이터만을 받을 수 있어야 한다.
이런 요구사항도 물론이고, Title 이나 Content 에 데이터가 없는 채로 넘어오는 경우에서 검증이 필요하다고 생각했다.
package com.todolist.domain.dto;
import lombok.Getter;
@Getter
public class CardPatchDto {
private final String cardTitle;
private final String cardContent;
public CardPatchDto(String cardTitle, String cardContent) {
this.cardTitle = cardTitle;
this.cardContent = cardContent;
}
}
위의 CardPatchDto 객체에서 스프링이 제공하는 Bean Validation 을 통해 값을 검증할 수 있다.
1. @NotBlank
데이터가 NULL 이거나 " ", "" 를 허용하지 않는, @NotNull, @NotEmpty 보다 검증 강도가 가장 높은 Validation 이다.
2. @Size
데이터의 크기를 지정한다.
cardTitle 은 최소 1자 ~ 최대 30자로 크기를 지정했고, 이 범위의 크기가 아니라면 허용하지 않는다.
cardContent 은 최소 1자 ~ 최대 500자로 크기를 지정했고, 이 범위의 크기가 아니라면 허용하지 않는다.
package com.todolist.domain.dto;
import lombok.Getter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Getter
public class CardPatchDto {
@NotBlank(message = "카드 제목을 입력해주세요.")
@Size(min = 1, max = 30)
private final String cardTitle;
@NotBlank(message = "카드 내용을 입력해주세요.")
@Size(min = 1, max = 500)
private final String cardContent;
public CardPatchDto(String cardTitle, String cardContent) {
this.cardTitle = cardTitle;
this.cardContent = cardContent;
}
}
Postman 으로 테스트했을 때 어떻게 될까 ?
Patch 를 호출했을 때, Title 에는 제대로 된 값을 넣어주고, Content 는 비워서 보내면 ?
아래와 같이 400 Bad Reqeust 응답을 보내주게 된다 !
여기서 따로 테스트하진 않겠지만 @Size 가 맞지 않는 경우에도 400 Bad Request 가 발생한다.
검증을 통과하지 못한 요청 예외 처리하기 [1]
스프링에서 제공하는 @RestControllerAdvice 와 @ExceptionHandler 어노테이션을 사용해서 예외를 캐치해보자.
@ControllerAdvice 는 @ExceptionHandler, @ModelAttribute, @InitBinder 가 적용된 메서드들에 AOP를 적용해 Controller 단
에 적용하기 위해 고안된 어노테이션이다. @RestControllerAdvice 를 사용한 이유는 단순 예외 처리 뿐 아니라 응답으로 객체 리턴을
해야하기 때문에 사용했다 ! (@ResponseBody 와 @ControllerAdvice 어노테이션이 합쳐진 것)
클래스에 선언하면 되며, 모든 @Controller에 대한 전역적으로 발생할 수 있는 예외를 잡아서 처리할 수 있다.
Postman 을 확인해봤을 때 위에서 발생한 예외는 MethodArgumentNotValidException 인데, 이 예외를 캐치하기 위해@ExceptionHandler 어노테이션의 인자로 해당 예외의 클래스를 넣어준다.그리고 여러 예외를 담아서 응답하기 위해 ArrayList 를 하나 선언해주고 반복문을 돌면서 발생하는 에러를 List 에 담는다.마지막으로 에러가 담긴 List 와 400 Bad Request 를 ResponseEntity<> 로 생성해 클라이언트로 응답을 보내준다.
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Response<List<RestResponse>>
MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException exception) {
List<RestResponse> responses = new ArrayList<>();
for (FieldError error : exception.getFieldErrors()) {
log.error("errorFieldName : {}, error message : {}", error.getField(), error.getDefaultMessage());
RestResponse restResponse = RestResponse.of(error.getField(), error.getDefaultMessage());
responses.add(restResponse);
}
return new ResponseEntity<>(responses, HttpStatus.BAD_REQUEST);
}
}
@Getter
public class RestResponse {
private String errorFiledName;
private String errorMessage;
private RestResponse(String errorFiledName, String errorMessage) {
this.errorFiledName = errorFiledName;
this.errorMessage = errorMessage;
}
public static RestResponse of(String errorFiledName, String errorMessage) {
return new RestResponse(errorFiledName, errorMessage);
}
}
다시 Postman 으로 테스트했을 때 어떤 응답이 보내질까 ?
똑같이 Title 은 정상적인 데이터, Content 는 비워서 보내보자.
이렇게 List 에 한번 감싸져있는 JSON 데이터가 잘 응답되는 것을 확인할 수 있다.
1. 클라이언트에서 요청한 JSON 데이터
{
"cardTitle":"test",
"cardContent":"";
}
2. 예외 응답 JSON 데이터
[
{
"errorFiledName":"cardContent",
"errorMessage":"크기가 1에서 500 사이어야 합니다."
},
{
"errorFileName":"cardContent",
"errorMessage":"카드 내용을 입력해주세요."
}
]
검증을 통과하지 못한 요청 예외 처리하기 [2]
이번에는 Bean Validation 으로 검증하지 못하는 MethodArgumentTypeMismatchException 를 처리해보자.
현재 Patch 를 진행하는 컨트롤러를 확인하면, @PathVariable 어노테이션으로 URL 의 마지막 cardId 를 받아서 로직이 진행되는 것을
확인할 수 있다. cardId 는 Integer 타입이 와야 하는데 만약 문자가 온다면 ?
이 경우에는 컨트롤러 내 로직을 타기도 전에 MethodArgumentTypeMismatchException 예외가 발생하게 된다.
@PatchMapping("/{cardId}")
public ResponseEntity<CardResponseDto> patch(
@PathVariable Integer cardId,
@Validated @RequestBody CardPatchDto cardPatchDto) {
cardService.patch(cardId, cardPatchDto);
return ResponseEntity.ok(new CardResponseDto(cardId));
}
위에서 언급했듯, @ControllerAdvice 어노테이션을 통해 해당 예외도 캐치해 처리할 수 있다.
Enum 타입 클래스를 생성해서 에러 메세지를 관리하도록 하고 여기서 errorMessage 를 정의해서 RestResponse 에 저장하도록 하자.
(MethodArgumentTypeMismatchException 클래스에는 따로 errorMessage 를 받아오는 메서드가 없어서 직접 ...)
@Getter
@AllArgsConstructor
public enum ExceptionType {
CARD_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "cardId 타입이 맞지 않습니다. Card 번호를 확인해주세요.");
private final HttpStatus httpStatus;
private final String message;
}
MethodArgumentNotValidException 처리 했던 Handler 와 동일하게 작성하면 된다.
주석에서도 언급했듯 MethodArgumentTypeMismatchException 클래스에는 에러 메세지를 출력하는 메서드가 없어서 Enum 타입에
미리 만들어놓은 메세지를 가져와서 저장했다.
물론 이런 이유 외에 따로 에러 메세지를 관리하는 Enum 클래스가 있으면 좋겠다고 생각해서 따로 클래스를 만든 이유도 있다..😀
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<List<RestResponse>>
MethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException exception) {
List<RestResponse> responses = new ArrayList<>();
String errorFieldName = exception.getName();
// MethodArgumentTypeMismatchException 클래스에 따로 errorMessage 받아오는 메서드가 없어서
// 직접 Enum 에서 받아서 넣음
String errorMessage = ExceptionType.CARD_TYPE_MISMATCH.getMessage();
log.debug("errorFieldName : {}, errorMessage : {}", errorFieldName, errorMessage);
RestResponse restResponse = RestResponse.of(errorFieldName, errorMessage);
responses.add(restResponse);
return new ResponseEntity<>(responses, HttpStatus.BAD_REQUEST);
}
Postman 으로 확인해보자 !
에러 메세지에 Enum 에서 가져온 메세지가 잘 찍혀있는 것을 확인할 수 있다.
또한 cardContent 에 값이 비어있는데도 Bean Validation 이 발생하지 않은 이유는, 해당 예외의 경우 값 자체 타입이 다르기 때문에
컨트롤러 로직을 타지도 않고 설정해놓은 @Validated 를 통한 객체 데이터 검증이 발생하지 않기 때문이다.
로직에 의한 예외를 ExceptionHandler 로 처리하기 [1]
커스텀 예외 클래스를 생성해서 예외를 처리해보도록 하자.
이번에는 DB 내부에 1번, 2번 카드가 있고 cardId 가 Integer 타입으로 잘 들어왔다고 가정하자.
즉, 1,2 외의 다른 cardId 가 오면 해당 카드는 없는 카드이기 때문에 찾을 수 없고, 예외를 처리해줘야 한다.
만약 cardId 를 '3' 으로 클라이언트에서 요청했다면 ? 타입이 맞기 때문에 문제될 것 없이 컨트롤러 로직을 타게 된다.
따라서 나는 cardService 에서 예외를 처리해보기로 했다.
@PatchMapping("/{cardId}")
public ResponseEntity<CardResponseDto> patch(
@PathVariable Integer cardId,
@Validated @RequestBody CardPatchDto cardPatchDto) {
cardService.patch(cardId, cardPatchDto);
return ResponseEntity.ok(new CardResponseDto(cardId));
}
받아온 cardId 를 통해 먼저 카드를 찾아내 가져오고 patch 를 진행하는 로직이다.
EmptyResultDataAccessException 이 발생하기 때문에 해당 예외를 try - catch 문으로 캐치한다.
(JdbcTemplate 의 queryForObject 메서드를 통해 DB 를 조회하고 반환 결과가 아무것도 없을 때의 예외)
여기서 직접 만든 커스텀 예외인 NotFoundCardexception 으로 예외를 처리할 수 있도록 던져준다 !
public void patch(Integer cardId, CardPatchDto cardPatchDto) {
try {
CardInformationDto findCard = cardRepository.findCard(cardId);
} catch(EmptyResultDataAccessException emptyResultDataAccessException) {
throw new NotFoundCardException(ExceptionType.NO_FOUND_CARD, "cardId");
}
cardRepository.patch(cardId, cardPatchDto);
}
NotFoundCardException 은 이렇게 되어있다.
Enum 타입으로 예외 메세지를 받고, 어떤 값에서 예외가 발생했는지 알고 싶어서 errorFiledName 을 받을 수 있게 설정했다.
package com.todolist.exception;
public class NotFoundCardException extends RuntimeException {
private final ExceptionType exceptionType;
private final String errorFiledName;
public NotFoundCardException(ExceptionType exceptionType, String errorFiledName) {
this.exceptionType = exceptionType;
this.errorFiledName = errorFiledName;
}
public String getExceptionType() {
return exceptionType.getMessage();
}
public String getErrorFiledName() {
return errorFiledName;
}
}
마찬가지로 Handler 를 만들어주고 NotFoundCardException 클래스에서 예외 메세지, 예외 발생 필드를 가져온 뒤 응답하면 끝 !
@ExceptionHandler(NotFoundCardException.class)
public ResponseEntity<List<RestResponse>>
NotFoundCardException(NotFoundCardException exception) {
List<RestResponse> responses = new ArrayList<>();
String errorFiledName = exception.getErrorFiledName();
String errorMessage = exception.getExceptionType();
log.error("errorFiledName = {}, error message = {}", errorFiledName, errorMessage);
RestResponse restResponse = RestResponse.of(errorFiledName, errorMessage);
responses.add(restResponse);
return new ResponseEntity<>(responses, HttpStatus.BAD_REQUEST);
}
Postman 으로 확인해보자.
현재 내 DB에 1번 카드는 저장돼있지 않은 상태라, cardId 가 1이 왔을 때 해당 카드를 찾을 수 없다는 예외가 응답된다.
cardId 가 1인 요청이 왔을 때는 왜 해당 카드를 찾을 수 없다는 예외 말고 Bean Validation 에 의한 예외가 응답될까 ?
컨트롤러 로직을 다시보면, cardService.patch() 메서드가 호출되기 전에 @Validated 어노테이션에 의한 검증이 발생하기 때문이다 !
@PatchMapping("/{cardId}")
public ResponseEntity<CardResponseDto> patch(
@PathVariable Integer cardId,
@Validated @RequestBody CardPatchDto cardPatchDto) {
cardService.patch(cardId, cardPatchDto);
return ResponseEntity.ok(new CardResponseDto(cardId));
}
틀린 부분이 있을 수도 있어서 공부를 좀 더 하고 수정할 부분이 있어면 수정하도록 하자.. ㅎ_ㅎ
'SPRING' 카테고리의 다른 글
Custom Exception 활용법 (0) | 2023.06.10 |
---|---|
[Spring Security] Authentication 인증 처리 과정 알아보기 (0) | 2022.12.17 |
DTO 반환에 대해 (4) | 2022.10.13 |
[Spring] GitHub OAuth2.0 구현하기 (웹 버전) (2) | 2022.05.23 |