Bean_Validation (3)

JIWOO YUN·2024년 2월 16일
0

SpringMVC2

목록 보기
16/26
post-custom-banner

Form 전송 객체 분리

폼 데이터 전달에 Item 도메인 객체를 사용하는 경우
  • HTML form -> Item -> Controller -> Item -> Repository
  • 장점 : Item 도메인 객체를 컨트롤러, 레포지토리 까지 직접 전달 -> 중간에 Item 만드는 과정이 없어서 간단
  • 단점 : 간단한 경우에만 적용 가능, 수정시 중복될 수 있으며 groups를 사용해야한다.
폼 데이터 전달에 별도의 객체 사용
  • HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository에 저장
  • 장점 : 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있다.
  • 단점 : 폼 데이터 기반으로 컨트롤러에서 Item 객체를 생성하는 변환과정 추가됨.
수정과 등록은 완전히 다른 데이터가 넘어가기 때문에 둘을 별도의 폼을 만들어서 돌리는게 효율적이다.
  • 회원가입시 다루는 데이터와 회원 수정시 다루는 데이터의 범위차이가 존재.
    • 회원가입 등록시 로그인 id, 주민번호 등등을 받지만, 수정시에는 이런부분을 받지 않아도됨.
    • 로직 검증에서도 많이 달라짐.
    • ItemSaveForm 과 ItemUpdateForm이라는 별도의 폼으로 나눠서 진행

Form 전송 객체를 분리할 것이기 때문에 Item 객체를 원복시켜준다.

Save 폼을 담당할 ItemSaveForm

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Range;

@Data
public class ItemSaveForm {

    @NotBlank
    private String itemName;

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

    @NotNull
    @Max(value = 9999)
    private Integer quantity;

}

Update 폼을 담당할 ItemUpdateForm

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Range;

@Data
public class ItemUpdateForm {

    @NotNull
    private Long id;

    @NotBlank
    private String itemName;

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


    private Integer quantity;

}

폼을 만들어놨으니 이제 Controller에 넣어서 적용시키자.

    @PostMapping("/add")
    public String addItem(@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("target = {}",bindingResult.getTarget());
            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}";
    }



    @GetMapping("/{itemId}/edit")
    public String editForm(@PathVariable("itemId") Long itemId, Model model){
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item",item);

        return "validation/v4/editForm";
    }

    @PostMapping("/{itemId}/edit")
    public String edit(@PathVariable("itemId")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 "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}";
    }

@ModelAttribute("item") 을 넣는 부분을 주의

  • 넣지 않을 경우 ItemSaveForm을 쓸경우 ItemSaveForm의 이름으로 담기게 된다.

HTTP 메시지 컨버터

  • @Valid, @Validated 는 HttpMessageConverter 에도 적용이 가능하다.

@ModelAttribute는 HTTP 요청 파라미터를 다룰 때 사용된다

@RequestBody 는 Http body의 데이터를 객체로 변환할 떄 사용됨 -> 주로 API JSON 요청을 다룰 때 사용된다.


apiController를 만들어서 PostMan을 통해서 테스트 진행

@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {

    @PostMapping("/add")
    public Object addItem(@RequestBody @Validated ItemSaveForm form,
                          BindingResult bindingResult){
        log.info("API 컨트롤러 호출");

        if (bindingResult.hasErrors()) {
            log.info("검증 오류 발생 errors ={}",bindingResult);
            return bindingResult.getAllErrors();
        }

        log.info("성공 로직 실행");
        return form;
    }
}

HttpMessageConverter에서 요청 Json을 ItemSaveForm 객체로 생성한다.

  • 만약 ItemSaveForm에 맞는 JSon이 들어오지않는다면 객체를 생성하느데 실패함.
    • 이 경우에는 ItemSaveForm 객체를 만들지 못하기 때문에 컨트롤러 자체가 호출되지 않고 그전에 예외가 발생.

검증 오류 요청시

HttpMessageConverter는 성공하지만 검증에서 오류가 발생하는 경우

itemSaveForm의 경우 quantity가 최대 9999기 때문에 10000으로 넘겨주게 되면 검증오류가 발생하여 bindingresult.getAllErrorer() 가 ObejctError와 FieldError를 반환해준다.

  • 반환해준 것을 스프링이 Json으로 변환해서 클라이언트에 전달함.
  • 실제 개발시에는 필요한 데이터만 뽑아서 별도의 API 스펙을 정의하고 그에 맞는 객체를 만들어서 반환해야함.
[
    {
        "codes": [
            "Max.itemSaveForm.quantity",
            "Max.quantity",
            "Max.java.lang.Integer",
            "Max"
        ],
        "arguments": [
            {
                "codes": [
                    "itemSaveForm.quantity",
                    "quantity"
                ],
                "arguments": null,
                "defaultMessage": "quantity",
                "code": "quantity"
            },
            9999
        ],
        "defaultMessage": "9999 이하여야 합니다",
        "objectName": "itemSaveForm",
        "field": "quantity",
        "rejectedValue": 10000,
        "bindingFailure": false,
        "code": "Max"
    }
]

@ModelAttribute vs @RequestBody

  • Http 요청 파라미터를 처리하는 @ModelAttribute의 경우 각각의 필드 단위로 세세하게 적용
    • 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리가능

HttpMessageConverter는 @ModelAttribute와 다르게 각각의 필드 단위로 적용되지 않고 전체 객체 단위로 적용됨.

  • 메시지 컨버터의 작동이 성공하여 ItemSaveForm 객체를 만들어야 @Valid, @Validated가 적용된다.
  • @RequestBody는 HttpMessageConverter 단계에서 Json 데이터를 객체로 변경하지 못할 경우 이후 단계 자체가 진행되지않고 예외 발생 -> 컨트롤러도 호출되지 않고, Validator 도 적용이 불가능하다.
profile
열심히하자
post-custom-banner

0개의 댓글