2 분 소요


스프링과 통합

스프링 부트가 spring-boot-starter-validation라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합한다.

글로벌 Validator

스프링 부트는 자동으로 LocalValidatorFactoryBean을 글로벌 Validator로 등록하고, 이 Validator는 @NotNull같은 애노테이션을 보고 검증을 수행. 이렇게 글로벌 Validator가 적용되어 있기 때문에, @Valid, @Validated만 적용하면 사용이 가능하다.
검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담아준다.

검증 순서

  1. @ModelAttribute 각각의 필드에 타입 변환 시도
    1. 성공하면 다음으로.
    2. 실패하면 typeMismatchFieldError 추가
  2. Validator 적용

바인딩에 성공한 필드만 Bean Validation이 적용된다.


에러 코드

Bean Validation을 적용하고 bindingResult에 등록된 검증 오류 코드를 보면 typeMismatch와 유사하게 오류 코드가 애노테이션 이름으로 등록된다.

NotBlank,Range 등 오류 코드를 기반으로 MessageCodesResolver를 통해 다양한 메시지 코드가 생성된다.

@NotBlank

  • NotBlank.item.itemName
  • NotBlank.itemName
  • NotBlank.java.lang.String
  • NotBlank

메시지 등록

errors.properties

NotBlank={0} 공백X 
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}

{0}은 필드명이고, {1},{2}…는 각 애노테이션 마다 다르다.

Bean Validation 메시지 찾는 순서

  1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
  2. 애노테이션의 message 속성 사용 -> @NotBlank(message = "공백! {0}")
  3. 라이브러리가 제공하는 기본 값 사용


오브젝트 오류

오브젝트 오류는 다음과 같이 @ScriptAssert()를 사용해서 처리할 수 있다.

@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
     //...
}

메시지 코드

  • ScriptAssert.item
  • ScriptAssert

하지만 제약이 많고 복잡하며 검증 기능이 해당 객체의 범위를 넘어서는 경우도 종종 있는데, 이런 경우 대응이 어렵다.

따라서, 오브젝트 오류(글로벌 오류)의 경우 @ScriptAssert를 억지로 사용하는 것 보다는 이 부분만 직접 자바 코드로 작성하는 것이 좋다.

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


코드에 적용

상품 수정에 Bean Validation을 적용.

Controller

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute Item item, BindingResult bindingResult) {

    // 글로벌 오류 검증 코드

    //...
}

Item 모델 객체에 @Validated를 추가했다.


한계

데이터를 등록할 때와 수정할 때는 요구사항이 다를 수 있다.

요구 사항 변경

  • 등록: 수량 최대 9999 제한 -> 수정: 제한X
  • 등록: id에 값 X -> 수정: id값 필수

Item.class

@NotNull //수정 요구사항 추가
private Long id;

@NotBlank
private String itemName;

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

@NotNull
//@Max(9999) //수정 요구사항 추가
private Integer quantity;

//...
  • id: @NotNull 추가
  • quantity: @Max(9999) 제거

[참고]
구조상 수정시에는 itemid값은 항상 들어가 있도록 로직이 구성되어 있지만, HTTP 요청은 언제든 악의적으로 변경해서 요청할 수 있으므로 서버에서 항상 검증해야 한다!

문제점

  • 등록시 quantity 수량 제한 최대 값이 적용되지 않는다.
  • 등록시 화면이 넘어가지 않고 오류가 발생한다.
    • 등록시에는 id값이 없어 @NotNullid에 적용한 시점에서 검증에 실패하게 된다.

문제 해결 방법 2가지

  • Bean Validation의 groups 기능을 사용
  • Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용


groups

BeanValidation groups 기능 사용
등록시에 검증할 기능과 수정시에 검증할 기능을 각각 그룹으로 나누어 적용할 수 있다.

groups 적용

저장용

public interface SaveCheck {
}

수정용

public interface UpdateCheck {
}

Item.class

@Data
public class Item {

    @NotNull(groups = UpdateCheck.class) //수정시에만 적용
    private Long id;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String itemName;

    @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
    @Range(min = 1000, max = 1000000, groups = {SaveCheck.class, UpdateCheck.class})
    private Integer price;
    
    @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
    @Max(value = 9999, groups = SaveCheck.class) //등록시에만 적용
    private Integer quantity;

    // ...
}

Controller

// 등록 로직
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item,
                    BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    //...
}

// 수정 로직
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class)
                @ModelAttribute Item item, BindingResult bindingResult) {
    //...
}

[참고]
@Valid에는 groups를 적용할 수 있는 기능이 없기 때문에 사용하려면 @Validated를 사용해야 한다.

정리

groups 기능을 사용해 등록과 수정시에 각각 다르게 검증할 수 있었지만, Item은 물론이고, 전반적으로 복잡도가 올라갔다.
실무에서는 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용하기 때문에 groups 기능은 실제로 잘 사용되지 않는다.


<출처 : 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(김영한)>

댓글남기기