GitHub

https://github.com/Choidongjun0830

Spring

검증2 - Bean Validation

gogi masidda 2024. 1. 6. 13:27

앞에서 검증1에서 한 것을 검증 Annotation을 이용하여 간단하게 할 수 있는 방식이다.

@Data
public class Item {

    private Long id;

    @NotBlank
    private String itemName;
    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;
    @NotNull
    @Max(9999)
    private Integer quantity;
    ...
@PostMapping("/add")
    public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        //검증에 실패하면 다시 입력 폼으로
        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}";
    }
  • 이렇게 애노테이션을 붙여서 검증을 할 수 있다. addItem에서 @Validated(@Valid)가 있어야 동작한다.
  • 바인딩이 되어야 BeanValidation이 동작한다. 만약 price나 quantity에 문자를 입력하면 BeanValidation이 동작하지 않는다.

검증1에서와 마찬가지로 

@NotBlank

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

@Range

  • Range.item.price
  • Range.price
  • Range.java.lang.Integer
  • Range

이런 순서를 가지고 메시지 코드가 생성된다. 이를 이용해 'errors.properties'에 메시지를 정의할 수 있다!


오브젝트 오류의 경우에는 

@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 10000원 넘게 입력해주세요")
public class Item {
	...

@ScriptAssert를 이용하여 할 수 있지만 Java 표현식으로 

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

이렇게 작성하는 것이 더 낫다.


BeanValidation - groups : 동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법

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

  • SaveCheck와 UpdateCheck같이 그룹화할 인터페이스를 각각 만든다.
  • 객체 클래스에 아래와 같이 그룹을 적는다.
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)
    private Integer price;

    @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
    @Max(value = 9999, groups = SaveCheck.class) //수정 요구사항 추가
    private Integer quantity;
...
  • 컨트롤러에 아래와 같이 @Validated(value = 그룹)을 적는다. 여기서 value는 생략가능하다.
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes)
...
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult)
...

하지만 이런 방식은 복잡도가 올라가서 잘 사용되지 않는다. 실무에서는 회원가입 시에 약관에 대한 정보도 추가로 받는 등 수 많은 데이터가 날아와서 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용한다. 폼을 분리해서 사용하면 group를 사용할 일이 드물다.


등록용 폼 객체와 수정용 폼 객체 분리

등록용 폼

@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 addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        //ModelAttribute("item")을 하지않으면 model.addAttribute()에 "itemSaveForm"이라는 이름으로 등록됨. "item"을 적지 않으려면 뷰 템플릿에서 item을 바꿔주어야함.

        //특정 필드가 아닌 복합 룰 검증
        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 "validation/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:/validation/v4/items/{itemId}";
    }

수정용 폼

@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 editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @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 "validation/v4/editForm";
        }

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

        itemRepository.update(itemId, itemParam);
        return "redirect:/validation/v4/items/{itemId}";
    }
  • Form 형태로 받기 때문에 Item객체로 변환해야 한다.
  • @ModelAttribute("item") ItemSaveForm form에서 "item"을 따로 적어야 뷰 컨트롤러에 itemSaveForm으로 넘어가지 않도록 할 수 있다.

Bean Validation - HTTP 메시지 컨버터

@ModelAttribute는 HTTP 요청 파라미터(URL쿼리 스트링, POST Form)을 다룰 때 사용한다.

@RequestBody는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용한다.

API의 경우 3가지 경우로 나누어 생각해야 한다.

  • 성공 요청: 성공
  • 실패 요청: JSON을 객체로 생성하는 것 자체가 실패 (price나 quantity에 문자 넣었을 때)
    • 이때는 컨트롤러 요청도 되지 않음.
  • 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함

 

@ModelAttribute vs @RequestBody

  • @ModelAttribute는 각각의 필드 단위로 적용되어 한 필드가 오류가 나도 다른 필드들은 정상처리 될 수 있다.
  • @RequestBody의 HttpMessageConvertor 단계는 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용된다. 따라서 메시지 컨버터의 작동이 성공해서 Item이 만들어져야 다음 단계인  컨트롤러가 호출되거나, @Valid나 @Validated가 작동할 수 있다.
    • HttpMessageConvertor 단계에서 실패하면 예외로 넘어가는데 이는 예외처리 챕터에서 배운다!
728x90

'Spring' 카테고리의 다른 글

서블릿 필터  (0) 2024.01.09
쿠키와 세션  (1) 2024.01.08
검증1-Validation  (2) 2024.01.04
메시지, 국제화  (0) 2024.01.01
thymeleaf-1  (0) 2023.12.31