kiwi

Bean Validation

by 키위먹고싶다

validation을 코드로 모두 작성하는것은 매우 번거롭다. 

이런 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이 바로 BeanValidation이다.

 

이것은 특정 구현체가 아니라 검증 애노테이션과 여러 인터페이스의 모음이다. 

어노테이션 하나로 매우 편리하게 검증할 수 있게 한다. 

 

먼저 build.gradle에

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

추가 해야 한다.

 

라이브러리에  jakarta.validation-api, hibernate-validator가 추가된다.

 


@Data
public class Item {

    private Long id;

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

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

    @NotNull
    @Max(999)
    private Integer quantity;

라이브러리 추가 하면 @NotBlank, @NotNull, @Range, @Max등의 어노테이션 사용이 가능하다. 

 

 

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

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

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

@Validated추가

스프링부트는 라이브러를 추가하면 자동으로 BeanValidator를 인지하고 스프링에 통합한다. 

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

 

그런데 BeanValidator는 바인딩에 성공한 필드만 BeanValidation을 적용한다. 

만약 @ModelAttribute로 각각 필드에 타입 변환을 시도 해서 성공하면 Validator를 적용하고

실패하면 typeMismatch로 FieldError를 추가한다. 

 

갑자기 어리둥절 할 정도로 스프링부트가 알아서 다 해준다??

 

NotBlank.item.itemName=상품명을 적어주세요

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

 

 

BeanValidation이 기본으로 제공하는 오류 메시지를 더 자세하게 변경하고 싶을 경우

 

@NotBlank를 기반으로 MessageCodesResolver가 

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

 

순으로 메시지코드를 만든다. 

만약 errors.properties에 추가할 경우 우선순위에 따라 메시지가 출력될 수 있다.

 

 

 

그런데 BeanValidation도 한계점이 있다. 

 

만약 등록 검증 요구사항과 수정 검증 요구사항이 다르다면 어떻게 할까? 

 

Validation의 groups기능을 살펴보자

package hello.itemservice.domain.item;

public interface SaveCheck {
}
public interface UpdateCheck {
}

 

저장용, 수정용 체크 인터페이스 만들기

@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;

어노테이션 안에 해당 기능이 적용되야 하는 인터페이스 넣어주기

 

@PostMapping("/add")
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

 

컨트롤러에 @Validated 에 인터페이스 넣어주기 (등록용)

 

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

 

  

수정용

 

이렇게 하면 등록할 시 검증 따로 , 수정할 시 검증 따로 가 잘 적용된다. 

그러나 실무에서 이거 잘 사용안한다. 

 

왜냐하면 실제 상품 등록과 상품 수정을 할때를 생각해보자. 

실제 상품 등록하기 위해서는 상품 객체 정보 말고도 회원정보와 부가 정보가 필요하고

또 수정할때의 상품 객체 정보 말고 다른 부가 정보가 필요할때가 많다. 

결론은 등록과 수정시 각각 필요한 객체정보가 다를 수 있다. 

그래서 등록폼 객체 따로 수정폼 객체 따로 만들어서 사용한다. 

 

package hello.itemservice.web.basic.form;

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 ItemSaveForm {

    @NotBlank
    private String itemName;

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

    @NotNull
    @Max(value = 9999)
    private Integer quantity;
}
@PostMapping("/add")
public String addItem2(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

    //특정 필드 예외가 아닌 전체 예외
    if (form.getPrice() != null && form.getQuantity() != null) {
        int resultPrice = form.getPrice() * form.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000,
                    resultPrice}, null);
        }
    }

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

    //성공 로직

    Item item = new Item();
    item.setItemName(form.getItemName());
    item.setPrice(form.getPrice());
    item.setQuantity(form.getQuantity());

    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/basic/v4/items/{itemId}";
}

Item대신 컨트롤러에서 ItemSaveForm객체를 받아서 item객체를 만들어서 Repository로 간다. 

이때 주의해야 할 점은 @ModelAttribute에 이름을 설정 안하면 규칙에 의해 

model.addAttribute.에 itemSaveForm이라는 이름으로 담기는데 그러면 템플릿의 th:object 이름을 item이 아니라 itemSaveForm으로 변경해야 한다.   그래서 @ModelAttribute에 이름을 item으로 넣어줘야한다. 

 

@Data
public class ItemUpdateForm {

    @NotNull
    private Long id;

    @NotBlank
    private String itemName;

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

    //수정에서는 자유롭게 등록
    private Integer quantity;
}
@PostMapping("/{itemId}/edit")
public String editItemV2(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult){

    //특정 필드 예외가 아닌 전체 예외
    if (form.getPrice() != null && form.getQuantity() != null) {
        int resultPrice = form.getPrice() * form.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000,
                    resultPrice}, null);
        }
    }

    if(bindingResult.hasErrors()){
        log.info("errors={}", bindingResult);
        return "basic/v4/editForm";
    }

    Item itemParam = new Item();
    itemParam.setItemName(form.getItemName());
    itemParam.setPrice(form.getPrice());
    itemParam.setQuantity(form.getQuantity());

    itemRepository.update(itemId, itemParam);
    return "redirect:/basic/v4/items/{itemId}";
}

이것은 수정용

 

 

 

'spring' 카테고리의 다른 글

로그인 - 필터, 인터셉터  (0) 2022.02.05
로그인-cookie, session  (0) 2022.02.04
Validation  (0) 2022.02.03
메시지 국제화  (0) 2022.02.01
타임리프 기본 문법  (0) 2022.01.28

블로그의 정보

kiwi

키위먹고싶다

활동하기