2 분 소요


Form 전송 객체 분리

실무에서 groups를 잘 사용하지 않는 이유는 등록시 폼에서 전달하는 데이터가 Item 도메인 객체와 딱 맞지 않기 때문이다.
실무에서는 회원 등록시 회원 관련 데이터 뿐만 아니라, 약관 정보 등 Item과 관계없는 부가 데이터가 넘어온다.
그래서 Item을 직접 전달받는 것이 아니라, 복잡한 폼의 데이터를 컨트롤러까지 전달할 별도의 객체를 만들어서 전달한다.

폼 데이터 전달에 Item 도메인 객체 사용

  • HTML Form -> Item -> Controller -> Item -> Repository
    • 장점: 중간에 Item을 만드는 과정이 없어서 간단하다.
    • 단점: 간단한 경우에만 적용할 수 있다. 수정시 검증이 중복될 수 있고, groups를 사용해야 한다.

폼 데이터 전달을 위한 별도의 객체 사용

  • HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository
    • 장점: 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있다. 검증이 중복되지 않는다.
    • 단점: 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정이 추가된다.


개발

Item.class

Item의 검증은 사용하지 않으므로 검증 코드를 제거해도 된다.

@Data
public class Item {
    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;
}

ItemSaveForm

@Data
public class ItemSaveForm {

    @NotBlank
    private String itemName;

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

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

ItemUpdateForm

@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) {

    // 코드 생략...

    //성공 로직
    Item item = new Item();
    item.setItemName(form.getItemName());
    item.setPrice(form.getPrice());
    item.setQuantity(form.getQuantity());
}

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

    // 코드 생략...

    //성공 로직
    Item item = new Item();
    item.setItemName(form.getItemName());
    item.setPrice(form.getPrice());
    item.setQuantity(form.getQuantity());
}

Item 대신에 ItemSaveForm/ItemUpdateForm을 전달 받고, @Validated로 검증을 수행 한 뒤 BindingResult로 검증 결과를 받는다.

폼 객체의 데이터를 기반으로 Item 객체를 생성한다.(성공 로직)
이렇게 폼 객체 처럼 중간에 다른 객체가 추가되면 변환하는 과정이 추가된다.


HTTP 메시지 컨버터

@Valid, @ValidatedHttpMessageConverter(@RequestBody)에도 적용 가능.

Controller

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

API의 경우 3가지 경우를 나누어야 한다.

  • 성공 요청: 성공
  • 실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
  • 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함

@ModelAttribute vs @RequestBody
HTTP 요청 파라미터를 처리하는 @ModelAttribute는 각각의 필드 단위로 세밀하게 적용된다. 그렇기 때문에 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리된다.
HttpMessageConverter@ModelAttribute와 다르게 필드 단위가 아닌 전체 객체 단위로 적용된다.
따라서, 메시지 컨버터의 작동이 성공해서 Item객체를 만들어야 @Valid, @Validated가 적용된다.

  • @ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.
  • @RequestBody 는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.


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

댓글남기기