본문 바로가기
백엔드/Spring

[Java] Bean Validation에 대해서 알아보자 (JSR-303, JSR-380, 파라미터 유효성 체크)

by RoJae 2022. 11. 6.
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;
 
해당 글은 여기까지 입니다.
심화 내용은 다음 포스팅에서 다루겠습니다. 😁
 
 

 

댓글