[스프링 MVC 2편] 검증2 - Bean Validation
포스트
취소

[스프링 MVC 2편] 검증2 - Bean Validation

Bean Validation - 소개

검증 기능을 항상 코드로 작성하는 것은 매우 번거롭다.
이런 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화된 것이 Bean Validation이다.
Bean Validation을 잘 활용하면, 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.

Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는
검증 애노테이션과 여러 인터페이스의 모음으로 이루어진 기술 표준이다.

Bean Validation - 시작

  • 우선 build.gradle에 아래 의존관계를 추가해주자.
    • implementation 'org.springframework.boot:spring-boot-starter-validation'
  • 해당 라이브러리를 추가하면 빈 검증 애노테이션을 사용할 수 있다.

Item 클래스에 적용해보자.

package hello.validation.domain.item;

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 Item {
    private Long id;

    @NotBlank
    private String itemName;

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

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {}
    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}
  • @NotBlank
    • 빈 값이나 공백만 있는 경우를 허용하지 않는다.
  • @NotNull
    • null을 허용하지 않는다.
  • @Range(min = 1000, max = 1000000)
    • 범위 안(min ~ max)의 값이어야 한다.
  • @Max(9999)
    • 최대 9999까지만 허용한다.

테스트 코드를 작성해보자.

package hello.validation;

import hello.validation.domain.item.Item;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import org.junit.jupiter.api.Test;

import java.util.Set;

public class BeanValidationTest {
    @Test
    void beanValidation() {
        //검증기 생성
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        
        //검증 대상 생성
        Item item = new Item();
        item.setItemName(" "); //공백
        item.setPrice(0);
        item.setQuantity(10000);
        
        //검증 실행
        Set<ConstraintViolation<Item>> violations = validator.validate(item);
        
        //실행 결과 출력
        for (ConstraintViolation<Item> violation : violations) {
            System.out.println("violation=" + violation);
            System.out.println("violation.message=" + violation.getMessage());
        }
    }
}

Bean Validation - 프로젝트 준비 V3

  • 이전 버전과의 차이점을 확인하기 위해 이번에는 v3를 생성해보자.
  • ValidationItemControllerV3 컨트롤러 생성
    1. hello.itemservice.web.validation.ValidationItemControllerV2 복사
    2. hello.itemservice.web.validation.ValidationItemControllerV3로 붙여넣기
    3. URL 경로를 validation/v2/에서 validation/v3/로 변경
  • 템플릿 파일 복사
    1. validation/v2 디렉토리의 모든 템플릿 파일을 validation/v3 디렉토리로 복사
  • ValidationItemControllerV3에서 리소스 경로를 모두 validation/v2/에서 validation/v3/로 변경

Bean Validation - 스프링 적용

불필요한 코드 제거

  • ValidationItemControllerV3에서 addItemV1() ~ addItemV5() 제거하기
  • ValidationItemControllerV3에서 addItemV6()를 addItem()로 변경하기
  • ValidationItemControllerV3에서 @InitBinder 제거하기
    • 제거하지 않으면 오류 검증기가 중복 적용된다.
  • http://localhost:8080/validation/v3/items로 접속해보면 빈 유효성 검사가 정상 동작하는 것을 확인할 수 있다.

스프링 MVC는 어떻게 Bean Validator를 사용하는 걸까?

  • 스프링 부트에 spring-boot-starter-validation에 대한 의존성이 있다면
    자동으로 Bean Validatior를 인지하고 스프링에 등록한다.
  • LocalValidatorFactoryBean글로벌 Validator로 자동으로 등록하기 때문에 애노테이션만 적용하면 된다.
  • 글로벌 Validator@NotNull같은 애노테이션을 보고 검증을 수행한다.
    • 이렇게 글로벌 Validator가 적용되어 있기 때문에 @Valid@Validated만 적용하면 된다.
    • 검증 오류가 발생하면 FieldErrorObjectError를 생성해서 BindingResult에 담아준다.
  • 주의점
    • 글로벌 Validator를 직접 등록하면 스프링 부트는 Bean Validatior를 글로벌 Validator로 등록하지 않는다.
    • 그래서 글로벌 Validator를 직접 등록하면 애노테이션 기반의 빈 검증기가 동작하지 않는다.

@Valid와 @Validated

  • 검증 시 @Valid@Validated 둘 다 사용가능하다.
  • @Valid를 사용하려면 build.gradle에 의존관계 추가가 필요하다.
  • @Valid는 자바 표준 검증 애노테이션이다.
  • @Validated는 스프링 전용 검증 애노테이션이다.
  • 둘 중 아무거나 사용해도 동일하게 동작한다.
  • 그저 차이점은 @Validated는 내부에 groups라는 기능을 포함하고 있다는 것 뿐이다.

Bean Validation - 에러 코드

  • Bean Validation이 기본으로 제공하는 오류 메시지를 좀 더 자세히 변경할 방법은 없을까?
  • Bean Validation을 적용하고 bindingResult 에 등록된 검증 오류 코드를 확인해보면
    마치 typeMismatch처럼 오류 코드가 애노테이션 이름으로 등록된다.
  • 그래서 MessageCodesResolver를 통한 다양한 메시지 코드가 순서대로 생성된다.

errors.properties에 추가해보자.

#Bean Validation 추가
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
  • {0} 은 필드명이고, {1} , {2} …은 각 애노테이션 마다 다르다.

BeanValidation 메시지 찾는 순서

  1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
  2. 애노테이션의 message 속성 사용
  3. 라이브러리가 제공하는 기본 값 사용

Bean Validation - 오브젝트 오류

  • Bean Validation에서 특정 필드(FieldError)가 아닌 해당 오브젝트 관련 오류(ObjectError)는 어떻게 처리해야 할까?
  • @ScriptAssert()를 사용하면 된다.
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
    //...
}
  • 실행해보면 메시지 코드가 아래와 같이 생성된다.
    • ScriptAssert.item
    • ScriptAssert

문제점

  • 실제 사용해보면 제약이 많고 복잡하다.
  • 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우들도 종종 등장하는데, 그런 경우 대응이 어렵다.

ValidationItemControllerV3에 글로벌 오류를 추가해보자.

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    //특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
        }
    }

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

Bean Validation - 수정에 적용

  • 상품 수정에도 빈 검증을 적용해보자.

ValidationItemControllerV3의 edit() 변경

//상품 수정
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute Item item, BindingResult bindingResult) {
    //특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000,
                    resultPrice}, null);
        }
    }

    if (bindingResult.hasErrors()) {
        log.info("errors={}", bindingResult);
        return "validation/v3/editForm";
    }
    
    itemRepository.update(itemId, item);
    return "redirect:/validation/v3/items/{itemId}";
}
  • Item 모델 객체에 @Validated를 추가하였다.
  • 검증 오류가 발생하면 editForm으로 이동하는 코드 추가하였다.

validation/v3/editForm.html를 변경

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
        <link th:href="@{/css/bootstrap.min.css}" href="../css/bootstrap.min.css" rel="stylesheet">
        <style>
            .container {
                max-width: 560px;
            }

            .field-error {
                border-color: #dc3545;
                color: #dc3545;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="py-5 text-center">
                <h2 th:text="#{page.updateItem}">상품 수정</h2>
            </div>
            <form action="item.html" th:action th:object="${item}" method="post">
                <div th:if="${#fields.hasGlobalErrors()}">
                    <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">글로벌 오류 메시지</p>
                </div>
                <div>
                    <label for="id" th:text="#{label.item.id}">상품 ID</label>
                    <input type="text" id="id" th:field="*{id}" class="form-control"
                           readonly>
                </div>
                <div>
                    <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
                    <input type="text" id="itemName" th:field="*{itemName}"
                           th:errorclass="field-error" class="form-control"
                           placeholder="이름을 입력하세요">
                    <div class="field-error" th:errors="*{itemName}">
                        상품명 오류
                    </div>
                </div>
                <div>
                    <label for="price" th:text="#{label.item.price}">가격</label>
                    <input type="text" id="price" th:field="*{price}"
                           th:errorclass="field-error" class="form-control"
                           placeholder="가격을 입력하세요">
                    <div class="field-error" th:errors="*{price}">
                        가격 오류
                    </div>
                </div>
                <div>
                    <label for="quantity" th:text="#{label.item.quantity}">수량</label>
                    <input type="text" id="quantity" th:field="*{quantity}"
                           th:errorclass="field-error" class="form-control"
                           placeholder="수량을 입력하세요">
                    <div class="field-error" th:errors="*{quantity}">
                        수량 오류
                    </div>
                </div>
                <hr class="my-4">
                <div class="row">
                    <div class="col">
                        <button class="w-100 btn btn-primary btn-lg" type="submit"
                                th:text="#{button.save}">저장
                        </button>
                    </div>
                    <div class="col">
                        <button class="w-100 btn btn-secondary btn-lg"
                                onclick="location.href='item.html'"
                                th:onclick="|location.href='@{/validation/v3/items/{itemId}(itemId=${item.id})}'|"
                                type="button" th:text="#{button.cancel}">취소
                        </button>
                    </div>
                </div>
            </form>
        </div> <!-- /container -->
    </body>
</html>
  • .field-error css를 추가하였다.
  • 글로벌 오류 메시지를 추가하였다.
  • 상품명, 가격, 수량 필드에 검증 기능을 추가하였다.

Bean Validation - 한계

  • 상품을 등록하거나 수정할 때 현재는 동일한 모델 객체를 사용하고 있다.
  • 그런데 상품을 등록할 때와 수정할 때의 요구사항이 다를 수가 있다.
  • 두 경우의 요구사항이 다를 때 한 쪽에만 맞추게 된다면 다른 한 쪽에서 문제가 발생할텐데 이런 경우에는 어떻게 해야 할까?

Bean Validation - groups

  • 동일한 모델 객체를 사용할 때 등록할 때와 수정할 떄 각각 다르게 검증하는 방법은 2가지가 있다.
    • BeanValidation의 groups 기능을 사용한다.
    • Item을 직접 사용하지 않고, ItemSaveForm나 ItemUpdateForm처럼
      폼 전송을 위한 별도의 모델 객체를 만들어서 사용한다.

groups

  • 검증할 기능을 그룹으로 나누어서 적용하는 기술이다.

groups 적용해보기

  • 등록용 groups 생성
package hello.validation.domain.item;

public interface SaveCheck {
}
  • 수정용 groups 생성
package hello.validation.domain.item;

public interface UpdateCheck {
}
  • Item에 groups를 적용해보자.
package hello.validation.domain.item;

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

    public Item() {}
    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}
  • addItem()를 복사해서 addItemV2() 생성 후 SaveCheck.class를 적용해보자.
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    //특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
        }
    }

    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}";
}
  • edit()를 복사해서 editV2() 생성 후 UpdateCheck.class를 적용해보자.
//상품 수정
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
    //특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000,
                    resultPrice}, null);
        }
    }

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

    itemRepository.update(itemId, item);
    return "redirect:/validation/v3/items/{itemId}";
}

알아야 하는 점

  • groups 기능을 사용해서 등록과 수정시에 각각 다르게 검증을 할 수 있었다.
    • 그런데 groups 기능을 사용하니 Item은 물론이고, 전반적으로 복잡도가 올라갔다.
  • 그래서 실무에서는 보통 groups를 사용하지는 않고, 그냥 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용한다.

Form 전송 객체 분리 - 프로젝트 준비 V4

  • 이전 버전과의 차이점을 확인하기 위해 이번에는 v4를 생성해보자.
  • ValidationItemControllerV4 컨트롤러 생성
    1. hello.itemservice.web.validation.ValidationItemControllerV3 복사
    2. hello.itemservice.web.validation.ValidationItemControllerV4로 붙여넣기
    3. URL 경로를 validation/v3/에서 validation/v4/로 변경
  • 템플릿 파일 복사
    1. validation/v3 디렉토리의 모든 템플릿 파일을 validation/v4 디렉토리로 복사
  • ValidationItemControllerV4에서 리소스 경로를 모두 validation/v3/에서 validation/v4/로 변경
  • http://localhost:8080/validation/v4/items로 접속해서 잘 동작하는지 확인해보자.

Form 전송 객체 분리 - 소개

폼 데이터를 받는 방법

  • 데이터 전달에 Item 도메인 객체 사용
    • HTML Form -> Item -> Controller -> Item -> Repository
    • 장점
      • Item 도메인 객체를 컨트롤러, 리포지토리 까지 직접 전달한다.
      • 중간에 Item을 만드는 과정이 없어서 간단하다.
    • 단점
      • 간단한 경우에만 적용할 수 있다.
    • 수정시 검증이 중복될 수 있고, groups를 사용해야 한다.
  • 폼 데이터 전달을 위한 별도의 객체 사용
    • HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository
    • 장점
      • 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있다.
      • 보통 등록과, 수정용으로 별도의 폼 객체를 만들기 때문에 검증이 중복되지 않는다.
    • 단점
      • 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정이 추가된다.

폼 데이터를 받는 객체를 분리하는 이유

  • 정말 간단한 단일 서비스거나 등록과 수정이 동일한 경우면 굳이 분리하지는 않아도 된다.
  • 하지만 실무에서는 복잡한 케이스가 많고, 등록과 수정이 다른 경우가 많은 편이라 어지간하면 분리하는게 좋다.

폼 데이터 객체 작명하기

  • 사실 의미가 있는 이름이면 크게 상관없다.
  • ItemSaveForm이든 ItemSaveDto든 뭐든 상관없다.
  • 다만 해당 회사나 프로젝트에 업무 규칙이 있다면 그건 반드시 지켜야 한다. (중요★)

폼 데이터가 발생하는 뷰 템플릿은 분리해야 할까?

  • 단순한 페이지면 분리하지 않아도 상관없다.
  • 다만 조건문이 좀 많아지기 시작했다? 그러면 분리하는게 정신건강에 이롭다.

Form 전송 객체 분리 - 개발

Item 원복

  • 이제 Item에서는 검증을 사용하지 않으니 검증 코드를 제거하자.

Item 등록용 폼 생성하기

package hello.validation.domain.item;

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

Item 수정용 폼 생성하기

package hello.validation.domain.item;

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

ValidationItemControllerV4에서 기존 코드 제거하기

  1. addItem()
  2. addItemV2()
  3. edit()
  4. editV2()

ValidationItemControllerV4에 신규 코드 추가하기

  • addItem()
    • Item 대신에 ItemSaveform을 전달 받는다.
    • @Validated로 검증을 수행한다.
    • BindingResult 로 검증 결과를 받는다.
@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("errors={}", bindingResult);
        return "validation/v4/addForm";
    }
    
    //성공 로직 (폼 객체를 Item으로 변환)
    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}";
}
  • edit()
@PostMapping("/{itemId}/edit")
public String edit(@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 "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”)에 item이라고 이름을 넣어준 부분을 주의하자.
  • 이것을 넣지 않으면 경우 규칙에 의해 자동으로 itemSaveForm이라는 이름으로 MVC Model에 담기게 된다.
  • 이렇게 되면 뷰 템플릿에서 접근하는 th:object 이름도 함께 변경해주어야 한다.

Bean Validation - HTTP 메시지 컨버터

  • @Valid, @Validated는 HttpMessageConverter(@RequestBody)에도 적용할 수 있다.

ValidationItemApiController 생성

package hello.validation.web.item.validation;

import hello.validation.domain.item.ItemSaveForm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@ModelAttribute vs @RequestBody

  • HTTP 요청 파리미터를 처리하는 @ModelAttribute는 각각의 필드 단위로 세밀하게 적용된다.
    • 그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있었다.
  • HttpMessageConverter@ModelAttribute와 다르게 각각의 필드 단위로 적용되는 것이 아니라,
    전체 객체 단위로 적용된다.
    • 따라서 메시지 컨버터의 작동이 성공해서 ItemSaveForm 객체를 만들어야 @Valid@Validated가 적용된다.
  • @ModelAttribute는 필드 단위로 정교하게 바인딩이 적용된다.
    • 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.
  • @RequestBodyHttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면
    이후 단계 자체가 진행되지 않고 예외가 발생한다.
    • 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.

출처

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.