RestApi에 BeanValidation 적용하기

@ModelAttribute에 BeanValidation 적용에서 ModelAttribute로 적용은 매우 쉽게 했다. 그런데, RestApi에 적용은 어떤 식으로 하면 좋을까? RestApi는 매우 복잡한 방식으로 메시지 소스를 적용해 주고 있었다.

바로 ValidationUtils라는 빈을 만들어서 처리해주고 있었다. 그런데, BeanValidation과 Validator 동시 사용이 가능할까? 결론은 가능하다. 조금 복잡해지고, 기존 메시지 소스를 불편하게 사용해야 하는 단점이 있긴하다.

간단한 작업(필수 값, 최소 길이)와 같은 검증은 BeanValidation으로, 복잡한 중복된 isbn같은 검증은 Validator로 하도록 하자.

Dto에 애노테이션 처리

public class NewBookDto {

    @NotBlank
    @Size(min = 5)
    private String bookName;
    @NotBlank
    @Size(min = 5)
    private String isbn;
}

Controller

기존에는, BindingResult bindingResult = new BeanPropertyBindingResult(book, "book"); 와 같은 코드로 objectName을 지정해주었으나, BeanValidation을 RestApi로 처리할 경우, 바인딩 단계에서 처리하기 때문에, objectName을 변경할 수 없으므로, objectName은 NewBookDto로 하도록 하자.

그 외에는 @Validated를 붙여주는 것 외에 변경점이 없다.

@ResponseBody
@PostMapping("/books/add")
public ResponseEntity<JsonResponse> addBook(HttpSession session, @Validated @RequestBody NewBookDto book, BindingResult bindingResult) {

    if (adminUtils.isDefault(session)) {
        return new ResponseEntity<>(ErrorResponse.builder()
                .code("roleError")
                .message("권한이 없습니다.")
                .build(), HttpStatus.FORBIDDEN);
    }

    log.debug("bindingResult.objectName={}", bindingResult.getObjectName());
    log.debug("bindingResult.target={}", bindingResult.getTarget());

    log.debug("book={}", book);
    log.debug("book.bookName={}", book.getBookName());
    log.debug("book.isbn={}", book.getIsbn());

    bookAddValidator.validate(book, bindingResult);

    if (bindingResult.hasErrors()) {

        log.debug("errors={}", bindingResult);
        return validationUtils.handleValidationErrors(bindingResult);
    }

    adminService.addBook(book);

    return new ResponseEntity<>(JsonResponse.builder()
            .message("정상 등록되었습니다.")
            .build(), HttpStatus.OK);
}

FeildValidationHandler

기존에 ValidationUtils를 만들어두기 참 잘했다고 느낀 대목이다. 역시 가능하다면 추상화를 해두는 것이 좋은 방향인 것 같다.

BeanValidation에서 바인딩이 실패할 경우, MethodArgumentNotValidException 예외가 발생하는데, @ControllerAdvice에서 이를 처리하는 방식이다.

@ControllerAdvice
@RequiredArgsConstructor
public class FieldValidationHandler {

    private final ValidationUtils validationUtils;

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<JsonResponse> handleError(MethodArgumentNotValidException e) {
        return validationUtils.handleValidationErrors(e);
    }
}

errors.properties

가장 큰 단점은, objectName을 임의로 바꿔서 쓸 수 없다는 점이다. 하지만, 대부분의 웹 서비스에서는 국제화를 하지 않으므로, 메시지 소스를 쓰지 않고 직접 명시할 것 같다.

Size.user.username=유저이름은 {2}자 이상이어여 합니다.
Size.user.password=피스워드는 {2}자 이상이어여 합니다.
Size.newBookDto.bookName=책 이름은 최소 {2}자 이상 입력하세요.
Size.newBookDto.isbn=isbn은 최소 {2}자 이상 입력하세요.
Size.modifyBookDto.bookName=책 이름은 최소 {2}자 이상 입력하세요.
Size.modifyBookDto.isbn=isbn은 최소 {2}자 이상 입력하세요.

NotBlank.user.username=유저이름은 필수입니다.
NotBlank.user.password=패스워드는 필수입니다.
NotBlank.newBookDto.bookName=책 이름은 필수입니다.
NotBlank.newBookDto.isbn=ISBN은 필수입니다.
NotBlank.modifyBookDto.bookName=책 이름은 필수입니다.
NotBlank.modifyBookDto.isbn=ISBN은 필수입니다.

댓글

개발자  김철준

백엔드 개발자 김철준의 블로그입니다.

주요 프로젝트