[Springboot] 스프링 요청 데이터(RequestDto)에 대해 @Valid로 유효성 검사하고 예외처리하기

winluck·2023년 11월 14일
0

Springboot

목록 보기
13/18

RequestDto의 유효성 검사

  • 때때로, 클라이언트 쪽에서 유효성 검사가 누락되어 서버로 잘못된 형식 혹은 규칙에 어긋난 데이터가 넘어오는 경우가 있다.
    • 예) gender 필드는 남자는 0, 여자는 1로 약속했는데 2가 오는 경우
    • height 필드는 200cm까지가 최대인데 1500cm가 오는 경우
    • null이면 안 되는 필드가 null로 오는 경우
  • 이런 경우는 서버 쪽에서도 한번 이러한 부적절한 데이터 필드를 막아주는 것이 필요하다.
    • 클라이언트가 누우면 버그지만, 서버가 누우면 사고다.
  • 클라이언트가 넘겨준 Dto의 각 필드에 대해 유효성을 검사하고, 실패 시 실제 비즈니스 로직에 진입하지 않고 클라이언트에게 구체적으로 상황을 알려주는 더 나은 구조를 설계해보자.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'

@Valid

  • @Validation이라는 또다른 어노테이션도 존재하고 둘의 성질도 약간 다르지만, 오늘은 간단하게 @Valid의 사용법에 대해서만 다루어보자.

  • 위와 같이 검증할 Dto 앞에 @Valid를 붙이면 관련 어노테이션들을 통한 유효성 검사가 시작된다.

RequestSimpleHospitalDto

@Getter
@NoArgsConstructor
public class RequestSimpleHospitalDto {

    @NotBlank(message = "null 또는 공백이 입력되었습니다.")
    private String address1;

    @NotBlank(message = "null 또는 공백이 입력되었습니다.")
    private String address2;

    @DecimalMin(value = "1", message ="0보다 큰 값을 입력해주세요.")
    private int pageNum;
}
  • 특정 지역의 주소와 상세주소, 페이지 번호를 필드에 담아 조회 기능을 요청하는 Dto이다.
  • @Valid의 명령을 받아 유효성 검사를 실행하는 어노테이션을 추가한다.
    • @NotNull: null인 경우를 허용하지 않음
    • @NotEmpty:null 과 "" 둘 다 허용하지 않음
    • @NotBlank: null 과 "" 과 " " 모두 허용하지 않음
    • @DecimalMin(value = ??): value 미만인 경우를 허용하지 않음
    • @DecimalMax(value = ??): value 초과인 경우를 허용하지 않음
    • @Email: 이메일 형식에 부합해야 함
    • @Range(min = 1, max = 5): 값이 1 이상 5 이하여야 함
    • 이 외에도 다양한 유효성 검사 어노테이션이 존재한다.
  • 문제점: 메시지 문자열이 하드코딩되어 있다. 모든 Dto 모든 필드에 다 하드코딩하기엔 번거롭지 않을까?

MessageUtil

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MessageUtil {

    // 유효성 검사 관련
    public static final String NOT_BLANK = "null 또는 공백이 입력되었습니다.";
    public static final String ONE_TO_FIVE = "1~5 사이의 값을 입력해주세요.";
    public static final String LARGER_THAN_ZERO = "0보다 큰 값을 입력해주세요.";
    public static final String INVALID_EMAIL_FORMAT = "이메일 형식이 올바르지 않습니다.";
}
  • 반복되는 유효성 검사 메시지를 통합적으로 관리하기 위해 MessageUtil이라는 클래스를 생성하였다. 이제 클래스에 추가된 문자열 필드를 사용함으로써 간단하게 메시지를 작성할 수 있다.

  • 유효성 검사를 위한 규칙에 어긋날 경우 message가 포함된 에러 메시지 코드가 클라이언트에게 전달되나, 클라이언트 입장에서 대응하기 좀 곤란한 상황이 나타난다.

{
    "timestamp": "2023-11-14T14:33:07.267+00:00",
    "status": 400,
    "error": "Bad Request",
    "path": "/api/hospital/list"
}
  • 클라이언트에게 자신의 요청데이터가 왜 유효성 검사에 실패했는지, 구체적으로 알려줄 수 없을까?
  • 유효성 검사 실패 시 MethodArgumentNotValidException이 발생한다는 점에서, 예외처리 경찰(?) GlobalExceptionHandler에게 유효성 검사 실패 예외에 대한 대응을 요구해보자.

GlobalExceptionHandler

@RestControllerAdvice
public class GlobalExceptionHandler {

		....

    @ExceptionHandler(MethodArgumentNotValidException.class) // 요청의 유효성 검사 실패 시
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 400 Bad Request로 응답 반환
    public ApiResponse<Map<String, String>> handleInValidRequestException(MethodArgumentNotValidException e) {
        // 에러가 발생한 객체 내 필드와 대응하는 에러 메시지를 map에 저장하여 반환
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(error -> {
            String fieldName = error.getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ApiResponse.fail(ResponseCode.BAD_REQUEST, errors);
    }
}
  • e.getBindingResult().getFieldErrors()를 통해 에러가 난 필드에 대한 정보(필드명, 에러메시지 등)를 얻어올 수 있다.
  • 예외처리가 발생한 1개 이상의 필드에 대해, key-value 형태의 필드명-해당 필드의 에러메시지로 Map을 구성하여 응답객체에 400과 함께 반환하는 메서드를 작성해주었다.
  • 이제 Postman으로 확인해보자.

  • 현재 정상적으로 데이터가 잘 조회된다. 이제 pageNum을 0으로 바꾸고 address2에 공백을 넣자.
    • address2는 @NotBlank가 존재하기에, 공백을 넣을 수 없다.
    • pageNum은 @DecimalMin(value = “1”)이 존재하기에 반드시 0보다 큰 정수여야 한다.

  • 이와 같이 클라이언트 입장에서 어떤 필드가 어떻게 잘못되었는지에 대한 명확한 데이터를 반환할 수 있다.
    • 꼭 Map이 아니라 다른 자료구조여도 상관없다.
  • 클라이언트가 서버로 전송할 RequestDto에 대해 @Valid를 통한 적극적인 유효성 검사 과정을 삽입하면, 떡잎부터 글러먹었는데 굳이 비즈니스 로직까지 비집고 들어간 Dto가 터뜨릴 Exception에 대한 부담을 줄일 수 있기에 서버 입장에서 리소스를 절약할 수 있다.
profile
Discover Tomorrow

0개의 댓글