Spring 검증 4 Bean Validation

바그다드·2023년 5월 15일
0

검증

목록 보기
4/5
  • 앞선 포스팅에서 검증 기능을 구현해보았다. 그런데 이런 코드를 일일이 구현하는 것도 복잡하고, 이런 검증 로직은 대부분의 사람들이 겪는 사항이다. 때문에 Spring에서는 Bean Validation이라는 기능을 지원한다.

Bean Validation이란?

검증 애노테이션과 인터페이스의 모음으로 구현체가 아닌 Bean Validation2.0(JSR-380)이라는 기술 표준이다. 구현체로는 하이버네이트 Validator가 있고, ORM 하이버네이트와는 관련이 없다.

  • 그럼 Bean Validator를 적용해보자

의존성 주입

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

필드 에러

domain 생성

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class Item {

    private Long id;

    @NotBlank //(message = "공백X")
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}
  • @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
    @NotNull : null 을 허용하지 않는다.
    @Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.
    @Max(9999) : 최대값 이하여야 한다.

컨트롤러 수정

    @PostMapping("/add")
    public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        // 검증에 실패하면 다시 입력 폼으로
        if (bindingResult.hasErrors()) {
            log.info("errors = {}", bindingResult);
            return "validation/v3/addForm";
        }

        // 성공 로직
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v3/items/{itemId}";
    }

Bean Validator 등록?

  • 위에서 한 것처럼 의존 관계를 추가해주면 스프링 부트가 자동으로 Bean Validator를 스프링에 통합해준다.
    LocalValidatorFactoryBean을 글로벌 Validator로 등록하고, 위의 도메인에서 했던 것처럼 @NotNull이나 @NotBlank등의 어노테이션을 인지하고 검증을 수행한다.
  • 컨트롤러에서는 타겟 객체 앞에 @Validated를 명시해주면 된다
    - @Valid라는 어노테이션도 비슷한 기능을 하는데 이건 자바 표준 어노테이션이다.
  • 글로벌 Validator를 직접 등록하면 Bean Validator가 등록되지 않기 때문에 주의하자!

검증 순서

  1. @ModelAttribute를 이용한 바인딩을 시도
    • 성공하면 다음 과정 진행
    • 실패하면 typeMismatch로 FieldError에 추가
  2. Validator 적용
  • Bean Validator는 바인딩에 성공한 필드에만 적용한다.

에러 코드 생성

  • Bean Validator도 마찬가지로 MessageResolver를 이용해 메세지 코드를 생성한다. 아래의 예시로 확인하자
  • @NotBlank, item(Object), itemName(Field)일 때,
    1순위 : NotBlank.item.itemName
    2순위 : NotBlank.itemName
    3순위 : NotBlank.java.lang.String
    4순위 : NotBlank
    위 우선순위대로 메세지 코드를 생성해준다.
  • @Range면 최상위 키가 Range가 된다.

메세지 등록

  • errors.properties가 등록이 되어 있다고 했을 때
#Bean Validation 추가

NotBlank.item.itemName=상품명을 입력해주세요

NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
  • 위의 코드를 추가하면 메세지 코드로 등록이 되는데, 앞서 봤듯이
    NotBlank.item.itemName
    NotBlank
    이 두가지 코드가 있을 때 우선순위는 NotBlank.item.itemName이 더 높다.
  • {0}, {1}, {2}는 파라미터를 뜻하는데,
    {0}는 필드명을,
    {1}, {2} 각 어노테이션의 파라미터를 뜻한다.
  • 만약 NotBlank.item.itemName과 NotBlank 둘 다 없다면?
    라이브러리가 제공하는 기본 값을 사용한다!!

오브젝트 에러

  • 이제 오브젝트 에러 메세지를 띄워보자

도메인 수정

@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 10000이상이 되도록 입력해주세요")
public class Item {
  • @ScriptAssert를 추가해주면 아래와 같이 오브젝트 에러 메세지가 뜨는 것을 확인할 수 있다.
  • 오브젝트 에러도 마찬가지로 아래와 같이 에러 메세지를 자동으로 생성해준다.
    ScriptAssert.item
    ScriptAssert
  • 다만 이 방법은 제약이 많고 복잡하기 때문에 오브젝트 에러는 아래처럼 자바 코드를 이용하자!!
    @PostMapping("/add")
    public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        // 오브젝트 에러
        if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
            }
        }

        // 검증에 실패하면 다시 입력 폼으로
        if (bindingResult.hasErrors()) {
            log.info("errors = {}", bindingResult);
            return "validation/v3/addForm";
        }

        // 성공 로직
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v3/items/{itemId}";
    }

한계

같은 도메인에 대해 기능에 따라 요구사항이 차이가 있을 때
예를 들어, 상품(Item)을 등록할 때는 수량(quantity)이 1~9999개 사이로만 설정이 가능한 반면에, 상품을 수정할 때는 수량에 대한 제한이 없을 때를 생각해보자.
현재 상품에는 아래 코드와 같이 설정이 되어 있기 때문에 수정을 할 때 요구사항을 충족할 수 없다.

    @NotNull
    @Max(9999)
    private Integer quantity;
  • 이런 문제에 대한 해결책을 다음 포스팅에서 알아보자

출처: 김영한의 스프링MVC2편

profile
꾸준히 하자!

0개의 댓글