ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Bean Validation에 대해서 알아보자 (JSR-303, JSR-380, 파라미터 유효성 체크)
    백엔드/Spring 2022. 11. 6. 21:57
    Jakarta bean validation
    서비스에서 중요한 기술 중 하나가, Validation을 체크하는 기술입니다.
    클라이언트에서 서버사이드로 값을 전달할 때, 올바른 값이 전달되었는지, Validation을 체크 할 수 있어야 합니다.
    이러한 기술을 위해서, Java Community Process (JCP)에서는 “Bean Validation”이라는 스펙을 공개했습니다.
     
    이와 같은 기술을 통해서, 개발자는 Annotation을 사용하여, Validation을 체크할 수 있었습니다.
    이 방법의 Specification을 기술한 라이브러리가 Jakarta Bean Validation 이고
    이를 구현한 라이브러리가 바로, Hibernate Validator입니다.
     

    🔆 Specification

    Java Bean Validation Specification
    • JSR303 
    • JSR380
     
    Java Bean Validation’s Name
    • “javax.validation” (Spring Boot 2.x)
    • “jakarta.validation” (Spring Boot 3.x)
     
    Reference Implementation

    🧩 Dependency

    Spring Boot에서 “spring-boot-starter-validation”은 어떤 구조로 되어 있는지 보겠습니다.
     
    다음과 같은 구조를 통해서, 의존성을 가져옵니다.
    • spring-boot-starter-validation
      • hibernate Validation (Implementation)   
        • jakarta.validation (Specification)
     
    코드 레벨에서는 아래와 같은 의존성을 가질 수 있습니다.
     
    Gradle
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    
     
    Maven
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
     
     

    ✏️ Usage

    이제 간단한 예시를 토대로, 확인해보겠습니다.
     
    [POST] 영화 정보를 추가하는 API를 예시로 들었습니다.
     

    1. Request에 대한 DTO를 생성

    MovieAddRequest.java
    @Data
    public class MovieAddRequest {
        @NotBlank(message = "MovieAddRequest.name은 빈 값일 수 없습니다")
        private String name;
    
        @NotBlank(message = "MovieAddRequest.category 빈 값일 수 없습니다")
        private String category;
    
        @NotBlank(message = "MovieAddRequest.useYn 빈 값일 수 없습니다")
        private String useYn;
    }
    
     

    2. Response에 대한 DTO 생성

    기본이 될 Base DTO 입니다.
    ApiBase.java
    @Builder
    @Getter
    public class ApiBase<T> {
        private String message;
        private T response;
    }
    
     
    영화 추가에 대한 응답 DTO 입니다.
    MovieAddResponse.java
    @Data
    public class MovieAddResponse {
        private String name;
    }
    
     
     

    3. Validation를 확인할 Controller 생성

    ValidController.java
    @RestController
    public class ValidController {
    
        @PostMapping("/")
        public ResponseEntity<ApiBase> addMovie(@RequestBody @Valid MovieAddRequest request){
            MovieAddResponse response = new MovieAddResponse();
            response.setName(request.getName());
            return ResponseEntity.status(HttpStatus.CREATED).body(ApiBase.builder().message("").response(response).build());
        }
    
    }
    
     

     

    4. 전역으로 에러를 핸들링할, GlobalErrorHandler 생성

    실제로 Validation 체크에 실패한 경우, Exception이 발생하는 정보에 대해서
    Custom Exception 처리를 해줘야 합니다.
     
    GlobalErrorHandler.java
    @ControllerAdvice
    @Slf4j
    public class GlobalErrorHandler {
    
        @ExceptionHandler({MethodArgumentNotValidException.class})
        public ResponseEntity<ApiBase> handleResponseBodyError(MethodArgumentNotValidException ex) {
            log.error("Exception Caught in handleRequestBodyError : {}", ex.getMessage());
    
            var error = ex.getBindingResult().getAllErrors().stream()
                    .map(DefaultMessageSourceResolvable::getDefaultMessage)
                    .sorted()
                    .collect(Collectors.joining(", "));
    
            log.error("Error is : {}", error);
    
            return ResponseEntity.badRequest()
                    .body(ApiBase.builder().message(error).response("").build());
        }
    
    }
    
     
    Validation 체크에 실패한 경우 GlobalErrrorHandler에서, Http Status는 400 (Bad Request)가 되며
    Annotation에 설정한 메시지들이 Body에 출력이 되어야 합니다.
     
     

    ⏰ Execute

    이제 완성이 되었으면, localhost:8080/에 아래를 던져봅시다.
     
    curl --location --request POST 'http://localhost:8080/' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "name": "제목",
        "category": "",
        "useYn": "N"
    }'
    
     
    아래처럼, Validation 체크를 통과하지 못했고, 오류가 발생했습니다. (category가 빈 값으로 @NotBlank, 예외발생)
    위에서 설정한 GlobalErrorHandler에 의해서 핸들링 된 값입니다.
     
    ojaeseocBookPro:valid ojaeseong$ curl --location --request POST 'http://localhost:8080/' \
    > --header 'Content-Type: application/json' \
    > --data-raw '{
    >     "name": "제목",
    >     "category": "",
    >     "useYn": "N"
    > }'
    {"message":"MovieAddRequest.category 빈 값일 수 없습니다","response":""}
    
     

    📎 Github Link

    이번 포스팅에서 개발한 예제 코드에 대한 Github 링크입니다.
     

    🚀 Next Step

    우리가 작성했던 MovieAddRequest.java에서 불만인 부분이 있습니다.
    movieCategory와 useYn은 정해진 값만 들어와야 합니다.
    (movieCategory는 열거형 타입이며, useYn은 "Y"이거나 "N"이여야만 합니다)
     
    @NotBlank(message = "MovieAddRequest.category 빈 값일 수 없습니다")
    private String category;
    
    @NotBlank(message = "MovieAddRequest.useYn 빈 값일 수 없습니다")
    private String useYn;
     
    물론 Enum으로 설정하고, 기동을 하면 오류가 발생하기야 하지만..
    Validation 체크가 아니고 맵핑, 파싱 오류입니다. 😰
    (Servlet Container's Mapping → Validation 순서로 인한 예상 밖의 에러 발생)
     
    그렇다면, 어떻게 하면 좋을까요? 🥺
     
    저의 해결법은, String으로 설정하고 자체적으로 Validator를 개발하는 방법입니다.
    이는 Bean Validation에서 제공하는 인터페이스를 통해서 구현이 가능합니다. (ConstraintValidator 사용)
    이 과정을 거치면, 아래의 코드로 변환이 됩니다.

     

    @NotBlank(message = "MovieAddRequest.name은 빈 값일 수 없습니다")
        private String name;
    
        @MovieCategoryValid(message = "MovieAddRequest.category이 유효한 값이 아닙니다")
        private String category;
    
        @StringValid(acceptedList = {"Y", "N"}, message = "MovieAddRequest.useYn이 유효한 값이 아닙니다")
        private String useYn;
    
     
    해당 글은 여기까지 입니다.
    심화 내용은 다음 포스팅에서 다루겠습니다. 😁
     
     

     

    반응형
Designed by Tistory.