Search
Duplicate

5. 검증2 - Bean Validation

목차

Previous

validation.zip
245.4KB
이번 챕터를 학습하며 작성했던 Item 관련 프로젝트 혹시라도 이해가 잘 되지 않는다면 코드를 import하여 참고하자.

Bean Validation이란?

특정 구현체가 아닌 Bean Validation 2.0(JSR-380)이라는 기술 표준으로 여러 검증 애노테이션과 여러 인터페이스의 모음이다. (ex: JPA라는 표준 기술에 구현체로 하이버네이트가 있다.)
이러한 Bean Validation을 구현한 기술중 일반적으로 사용하는 구현체는 하이버네이트 Validator이다.
(이름이 하이버네이트지만 ORM과는 관련없다. )
즉, Bean Validation 를 활용하면 애노테이션 기반으로 우리가 이전에 구현해봤던 각종 구현로직들을 간단하게 적용할 수 있다.
우리가 이전에 사용했던 검증방식은 직접 Validator 인터페이스를 구현한 뒤 InitBinder로 구현한 검증기를 등록해서 사용하는 식이였다.
@Component public class ItemValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Item.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { Item item = (Item) target; ValidationUtils.rejectIfEmpty(errors, "itemName", "required"); if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1_000_000) { errors.rejectValue("price", "range", new Object[]{1000, 1_000_000}, null); } if (item.getQuantity() == null || item.getQuantity() >= 9999) { errors.rejectValue("quantity", "max", new Object[]{9999}, null); } //복합 룰 검증 if (item.getPrice() != null && item.getQuantity() != null) { int resultPrice = item.getPrice() * item.getQuantity(); if (resultPrice < 10000) { errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null); } } } }
Java
복사
Validator 인터페이스를 구현한 검증기
@Controller @RequiredArgsConstructor public class ValidationItemControllerV2 { private final ItemValidator itemValidator; @InitBinder public void init(WebDataBinder dataBinder){ dataBinder.addValidators(itemValidator); } @PostMapping("/add") public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) { //검증 실패시 다시 입력 폼으로 이동해야 한다. if (bindingResult.hasErrors()) { log.info("errors = {}", bindingResult); return "validation/v2/addForm"; } ... } }
Java
복사
직접 구현한 검증기를 사용하는 컨트롤러
이 방식도 처음보다는 많이 간결해진 코드지만, 이마저도 Bean Validation을 사용하면 훨씬 간단해진다. 다음은 간단하게 위 유효성 검증을 Bean Validation을 적용한 코드이다. 도메인에서 검증이 필요한 필드에 바로 적용을 해준다.
@Data public class Item { private Long id; @NotBlank private String itemName; @NotNull @Range(min = 1000, max = 1_000_000) 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; } }
Java
복사
짧은 애노테이션 몇 가지로 기존의 작성한 방대한 검증 로직들이 대부분 대치된다.
공식 API doc을 보면 정말 이런것도 있을까 싶은 검증 애노테이션도 모두 제공되니 참고하면 된다.

하이버네이트 Validator 관련 링크

Bean Validation 사용하기

의존성 추가및 애노테이션 확인

Bean Validation 기능은 라이브러리를 추가해서 사용해야 한다.
Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
Groovy
복사
Maven
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>2.5.2</version> </dependency>
XML
복사
이렇게 설정을 추가해 라이브러리를 추가하면 다음과 같은 라이브러리들이 생성되어야 한다.
External Libraries에 추가된 검증 라이브러리
이제 위에서 작성했던 Item 객체를 다시 보며 각 필드에 붙은 애노테이션을 확인해보자.
@Data public class Item { private Long id; @NotBlank private String itemName; @NotNull @Range(min = 1000, max = 1_000_000) 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; } }
Groovy
복사
@NotBlank : 빈 값 + 공백만 있는 경우를 허용하지 않는다.
@NotNull: null을 허용하지 않는다.
@Max(최대값): 최대값 초과를 허용하지 않는다.
@Range(min, max): 범위 안의 값이여야 한다.
여기서 @Range 애노테이션은 org.hibernate.validator 에 있는 검증기능인데, 실무에서는 대부분 하이버네이트 validator를 사용하기에 사용을 자유롭게 해도 된다.

동작 확인해보기

테스트코드를 작성해서 Bean Validator가 동작하는지 확인해보자.
@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>> validate = validator.validate(item); for (ConstraintViolation<Item> violation : validate) { System.out.println("violation = " + violation); System.out.println("violation.getMessage() = " + violation.getMessage()); } }
Groovy
복사
물론 실무에서 위와 같이 Validator를 꺼내서 검증을 수행하진 않고 테스트 목적으로 임의로 validator를 꺼내서 검증을 해보았다. 코드는 아이템의 모든 필드의 유효성을 어겼기 때문에 이에 해당하는 결과가 출력될 것인데 이는 다음과 같다.
violation={interpolatedMessage='공백일 수 없습니다', propertyPath=itemName, rootBeanClass=class hello.itemservice.domain.item.Item, messageTemplate='{javax.validation.constraints.NotBlank.message}'} violation.message=공백일 수 없습니다 violation={interpolatedMessage='9999 이하여야 합니다', propertyPath=quantity, rootBeanClass=class hello.itemservice.domain.item.Item, messageTemplate='{javax.validation.constraints.Max.message}'} violation.message=9999 이하여야 합니다 violation={interpolatedMessage='1000에서 1000000 사이여야 합니다', propertyPath=price, rootBeanClass=class hello.itemservice.domain.item.Item, messageTemplate='{org.hibernate.validator.constraints.Range.message}'} violation.message=1000에서 1000000 사이여야 합니다
Plain Text
복사
스프링부트에서는 검증실패시 위와같은 violation 인스턴스를 이용해 결과를 반환한다.
이렇게 실제로 Bean Validation이 돌아가는 것도 확인을 해보았다면 이제 프로젝트에 실제로 적용해서 사용해보아야 한다. 이제 필드 검증, 객체 검증, 비즈니스 로직 검증등을 하나씩 알아보자.

참고 - 스프링 부트는 자동으로 글로벌 Validator로 등록한다.

LocalValidatorFactoryBean이 글로벌 Validator로 등록되며 위에서 사용해봤던 @NotNull과 같은 애노테이션 검증을 수행한다. 또한 검증 오류 발생시 FieldError, ObjectError를 생성해 BindingResult에 담아준다.

주의 - 임의로 글로벌 Validator를 등록하는것을 주의하자.

이전에 직접 글로벌 Validator를 추가해본적이 있을 것이다. 그런데 이처럼 임의로 글로벌 Validator를 등록해주면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않기에 우리가 위에서 사용했던 검증 애노테이션들이 동작하지 않는다. 그러니 이 부분을 주의하도록 하자.

필드 검증하기

상품 엔티티의 각각의 필드에 대해서 다음과 같은 요구사항이 있다고 하자.
이름은 공백이여선 안된다.
가격은 1000원 이상 100만원 이하여야 한다.
수량은 9999개까지만 가능하다.
이 요구사항을 상품 엔티티(Item)에 Bean Validation을 적용하면 다음과 같다.
@Data public class Item { private Long id; @NotBlank private String itemName; @NotNull @Range(min = 1000, max = 1_000_000) 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; } }
Java
복사
Item 엔티티
사실 위에서 두 번이나 등장한 엔티티 코드로 검증 애노테이션을 통해 필드의 제약을 작성해줬다.
그리고 이러한 검증 애노테이션을 실제로 동작해서 검증하려면 컨트롤러에서 받고자 하는 Request 객체에 검증 애노테이션을 붙혀주면 된다.
@PostMapping("/add") public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) { ... }
Java
복사
등록할 상품 객체에 @Validated 검증 애노테이션을 붙혀주고 검증결과를 담기위해 BindingResult 클래스를 바로 다음 위치에 매개변수로 받아주고 있다. 이처럼 컨트롤러를 작성해주면 스프링에서는 자동으로 엔티티에 적용된 검증 애노테이션을 수행한다.

검증 순서

1.
@ModelAttribute 각각의 필드에 타입 변환 시도
a.
성공하면 다음 필드 진행
b.
실패하면 typeMismatchFieldError 추가
2.
Validator 적용
즉, 각각의 필드에 바인딩이 된 필드만 Bean Validation이 적용된다.
예를들어 Item의 price 필드는 Integer타입이다. 그런데 웹 페이지에서 A 라는 문자열을 전송해 타입 변환을 시도할 경우 typeMismatch가 발생하여 FieldError가 추가된다.

검증 메세지 수정

잘 생각해보면 Bean Validation을 사용하면서 따로 messages.properties를 설정해주거나 작성해준적이 없는데도, 무엇인가 메세지가 출력되고 있다. 이는 해당 라이브러리에서 지정한 기본 메세지인데, 만약 이를 임의로 바꾸고 싶다면 어떻게 해야할까?
메세지 설정에서 MessageCodeResolver는 다음과 같이 각각의 애노테이션에 대한 메세지코드가 생성된다.
@NotBlank
⇒ NotBlank.item.itemName
⇒ NotBlank.itemName
⇒ NotBlank.java.lang.String
⇒ NotBlank
@Range
⇒ Range.item.price
⇒ Range.price
⇒ Range.java.lang.Integer
⇒ Range
그럼 이제 이 메세지코드에 메세지를 직접 등록해주면 내가 적용한 메세지가 적용될 것이다.
NotBlank={0} 공백은 유효하지 않습니다. Range={0}, {2}~{1}만 허용됩니다. Max={0}, 최대{1}까지만 허용됩니다.
YAML
복사
⇒ {0}은 필드명이고 {1}, {2},...은 각 애노테이션마다 다르다(보통 arguments)
적용 결과
물론 properties에 설정하는게아니라 직접 애노테이션에 message속성으로 지정할수도 있다.
@NotBlank(message = "공백은 입력할 수 없습니다.") private String itemName;
Java
복사

BeanValidation 메세지 찾는 순서

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

객체 검증하기

각각의 필드에 대해서 검증을 했다면 이번에는 객체를 검증해보자.
객체에 대한 검증은 다음과 같은 예를 말한다.
가격과 수량의 합은 10000원 이상이어야 한다.
하나의 필드에 붙힐 수 없는 이런 로직상의 검증은 두 가지 방법으로 해결할 수 있다.

검증 애노테이션 @ScriptAssert()를 사용하기

클래스레벨에 @ScriptAssert 애노테이션을 활용하여 이런 객체 로직도 검증할 수 있다.
사용하는 방법은 다음과 같다.
@Data @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000") public class Item { //... }
Java
복사
실제로 수행해보면 제대로 나오는것을 확인할 수 있으며 다음과 같은 순서로 메세지 코드도 찾는다.
ScriptAssert.item
ScriptAssert
하지만, 이 방식은 다음과 같은 이유로 실무에서 잘 사용되지 않는다.
애노테이션의 기능자체가 강하지 않아 제약이 많고 복잡하다.
실무에선 검증 기능이 해당 객체의 범위를 벗어나는 경우도 있는데 이 경우 대응이 어렵다.
제약조건이 많아질수록 코드가 길어지는데 속성에 로직을 넣기엔 가독성이 너무 떨어지게 된다.

직접 코드로 구현하기

@PostMapping("/add") public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) { if (item.getPrice() != null && item.getQuantity() != null) { int resultPrice = item.getPrice() * item.getQuantity(); if (resultPrice < 10000) { bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null); } } //... }
Java
복사
이처럼 객체 검증에 대한 부분은 직접 코드로 작성하여 놓는게 대부분 더 좋다.
만약 코드 중복이 걱정된다면 따로 메서드로 분리하도록 하자!

정리

간단한 필드검증에는 Bean Validation을 이용하여 검증 애노테이션을 활용하자
복잡한 객체검증에는 제약이 많은 애노테이션을 활용하기보단 코드로 직접 구현하자.
⇒ 코드의 재사용성이 높다면 모듈화를 진행해라!

Bean Validation의 한계

상황에 따라 달라지는 검증 조건

우리가 지금까지 해 본 코드는 상품 등록(POST)에 대한 부분이였고, 검증까지 무사히 완료했다.
그렇다면 상품 수정(Fetch or Put)은 어떨까? 실제로 실무에서는 상품에 대한 제약사항이 등록일 경우와 수정일경우에 달라질 수 있다. 뿐만 아니라 상품 등록시 전송될 내용과 수정시 전송될 내용도 상이할 확률이 높다.
예를들어, 상품 등록시에는 아직 등록이 되지 않았기에 아이디(id)가 존재하지 않지만, 수정시에는 이미 등록된 상품을 수정하는 것이기에 id가 null이여서는 안된다(NotNull) 또한, 상품 등록시에는 수량을 1~9999개까지만 허용했지만, 등록후에는 그 외의 값으로 수정을 해도 제약이 없도록 할 수도 있다.
하지만 이런 변경된 제약조건은 지금 기존에 작성된 상품 엔티티에서는 적용이 불가능하다.
그렇다고 수정에 맞춰서 아이디 필드에 @NotNull 검증 애노테이션을 붙히고 수량 필드에 @Max 애노테이션을 지우면 수정은 의도한대로 동작할지 몰라도 상품 등록시 아직 존재하지 않는게 당연한 아이디가 null이기에 검증 오류가 날 것이고 수량도 9999개를 넘는 숫자를 넣어도 문제가 발생하지 않을 것이다. 이처럼 상황에 따라 달라지는 검증 조건은 어떻게 적용해야 할까?
스프링에서는 다음과 같이 두 가지 방법으로 이를 해결할 수 있다.
1.
Bean Validation의 groups 기능을 사용하기
2.
전송 객체 분리하기(ItemSaveForm, ItemUpdateForm)
사실 결론부터 얘기하자면 groups는 한계가 명확하기에 전송 객체 분리가 일반적으로 옳은 선택지가 된다. 하지만, 어째서 한계가 명확한지도 파악하기위해 하나하나 소개해본다.

Bean Validation - groups를 사용해 검증 분리

Bean Validation은 위와 같은 검증 모델이 상황에따라 달라지는것에 맞춰 적용될 수 있게 groups라는 기능을 제공하는데, 각각의 group을 인터페이스로 만들어서 groups 라는 속성을 사용하면 된다.
우선 상품 등록과 상품 수정을 구분할 것이기에 ItemSave, ItemUpdate 인터페이스를 작성하자.
1.
ItemSave, ItemUpdate Inteface
public interface SaveCheck {} public interface UpdateCheck {}
Java
복사
2.
Item 객체에 groups 속성 설정
@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 = 1_000_000) 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; } }
Java
복사
groups는 다수의 그룹도 설정할 수 있으며 필요에따라 맞는 그룹을 선택해 검증할 수 있다.
3.
컨트롤러에서 필요한 검증 group 선택
@PostMapping("/add") public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) { //... } @PostMapping("/{itemId}/edit") public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) { //... }
Java
복사
addItem에서는 상품 저장이기에 @Validated 애노테이션에 속성으로 SaveCheck.class를 사용했다.
editV2에서는 상품 갱신이기에 @Validated 애노테이션 속성으로 UpdateCheck.class를 사용했다.
Item 객체에서는 각각 @Validated 애노테이션에 작성된 인터페이스가 선언된 검증만 수행한다.
이 방식을 사용하면 상품 등록시 검증 애노테이션에는 SaveCheck가 작성되었기에 Item 객체에서는 groups 속성으로 SaveCheck가 선언된 검증 애노테이션만 수행된다. 그렇기에 아이디(id)는 UpdateCheck만 작성되어있어 제품 등록시에는 검증이 수행되지 않는 것이다.
하지만, 이 방식은 사실 잘 사용되지 않는다.
그 이유는 해당 애노테이션자체가 문제가 있는것은 아니고 등록,수정시 전달되는 내용이 상품 도메인 객체(Item)과 딱 일치하지 않기 때문이다.
예를 들어, 회원 가입을 한다고 할 땐 회원 정보에 더해 약관정보같은 추가 정보가 있을 수 있고 아직 등록하지 않기에 존재하지 않는 정보들도 있을 것이다. 그리고, 이런 엔티티를 사용자에게 노출시키는 것은 보안상으로도 문제가 많다. 그렇기에 노출시켜도 되는 필드를 모아 View 객체를 만들어 이를 통해 데이터를 주고받고는 한다.

주의 - groups는 @Valid에서는 사용할 수 없다.

@Valid 검증 애노테이션은 groups라는 속성이 없기 때문에 해당 기능을 사용할 수 없다.
그렇기에 이 기능을 사용하기 위해서는 @Validated를 사용해야 한다.

Form 전송 객체 분리를 이용한 검증 분리

이 방식은 요약하면 다음과 같다.
상품 등록과 상품 수정시 사용자와 주고받을 전용 폼 전달 객체를 만들어서 사용하자.
즉, 각각에 상황에맞는 전용 폼 객체를 따로 만들어서 상황에 맞는 검증을 하고, 전송 객체이기에 사용자에게 노출해도 상관이 없는 객체가 된다. 물론, 이렇게 구현 할 경우 도메인 객체로 한번 더 변환을 해서 등록이든 수정이든 해야한다는 추가 과정이 생기지만, 이 과정을 줄이고자 엔티티를 그대로 사용하는 것보다 장점이 더 크다.

구현 코드

ItemSaveForm
@Data public class ItemSaveForm { @NotBlank private String itemName; @NotNull @Range(min = 1000, max = 1_000_000) private Integer price; @NotNull @Max(9999) private Integer quantity; }
Java
복사
ItemUpdateForm
@Data public class ItemUpdateForm { @NotNull private Long id; @NotBlank private String itemName; @NotNull @Range(min = 1000, max = 1_000_000) private Integer price; //수정일 경우 제약은 사라진다. private Integer quantity; }
Java
복사
ValidationController
@PostMapping("/add") public String addItemV2(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) { //... } @PostMapping("/{itemId}/edit") public String editV2(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) { //... }
Java
복사
이 역시 동작해보면 제대로 동작할 것이다.
뿐만 아니라 상품 등록시와 수정시 각각 상황에 맞는 검증도 제대로 분리되어 검증된다.
근데 몇가지 주의점이 있다.
@ModelAttribute에 추가되는 value 속성
: 이전과 다르게 컨트롤러에서 @ModelAttribute에 item 이라는 value 속성을 작성해줬다. 만약 이를 작성해주지 않으면 규칙에 따라 MVC Model에는 itemSaveForm라는 이름으로 담기게 된다. 그렇게되면 기존에 뷰 템플릿에서 th:object 이름을 item으로 선언해줬는데 이를 itemSaveForm으로 수정해줘야 한다.
Form 객체의 도메인 객체 변환 작업
:폼 객체를 기반으로 Item 객체를 생성 및 수정해야 하기 때문에 변환 과정이 작성되야하는데,
폼 객체와 도메인 객체간의 커플링을 최소한으로 할 수 있도록 설계에 주의해야 한다.
보통 폼 객체와 같은 DTO 에서 도메인을 의존하는것은 괜찮지만 반대의 경우는 괜찮지 않다.
의존의 방향은 변경이 많은곳에서 변경이 적은곳으로 향하는게 바람직하다.

정리

실무에선 상황에따라 같은 도메인이라도 검증 조건이 달라지는 경우가 생긴다.
검증 조건을 분리하는 방법은 Bean Validation의 groups 기능과 DTO를 이용한 전송객체 분리가 있다.
프론트의 데이터와 서버의 엔티티간의 데이터가 불일치하는 경우와 보안상의 이유로 groups 보다는 전송객체 분리를 권장한다.

Bean Validation - HTTP 메세지 컨버터

지금까지는 Form을 이용한 페이지 이동 방식에서 검증을 했다.
하지만, ajax, fetch, axios 등등 프론트 영역에서 API JSON을 요청하는경우는 어떨까?
@Valid, @ValidatedHttpMessageConvert(@RequestBody)에서도 사용할 수 있다.

참고- @ModelAttribute, @RequestBody

@ModelAttribute는 HTTP 요청 파라미터(URL 쿼리 스트링, POST Form)을 다룰 때 사용.
@RequestBody는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용.
ValidationItemApiController
@Slf4j @RestController @RequestMapping("/validation/api/items") public class ValidationItemApiController { @PostMapping("/add") public Object addItem(@Validated @RequestBody ItemSaveForm form, BindingResult bindingResult) { log.info("API 컨트롤러 호출"); if (bindingResult.hasErrors()) { log.info("검증 오류 발생 errors={}", bindingResult); return bindingResult.getAllErrors(); } log.info("성공 로직 실행"); return form; } }
Java
복사
테스트용 요청 정보
POST http://localhost:8080/validation/api/items/add Content-Type: application/json {"itemName":"hello", "price": 1000,"quantity": 10}
Java
복사
API의 경우 다음과 같은 3가지 경우가 발생할 수 있다.
성공 요청: 성공
실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함
성공하는 상황은 원래 문제가 아니기에 괜찮다.
검증 오류 요청은 내가 의도한 검증에 걸려 실패한 것이기에 괜찮다. 검증 실패 내역도 BindingResult 클래스에 들어있기 때문에 적절히 꺼내 담아 반환하면 된다. 이는 폼 전송객체를 이용한 방식에서도 동일하기에 문제될 것이 없다. 그런데 HttpMessageConverter에서 요청 JSON을 객체로 생성하는 것 자체가 실패하는 경우는 문제다. 지정한 객체(ex: Item)로 만들지 못하기 때문에 컨트롤러 호출이 되지 않기 때문에 Validator도 실행되지 않는다.

@ModelAttribute vs @RequestBody

어째서 폼 전송방식으로 할 때 @ModelAttribute를 사용할 때는 타입이 불일치해도 발생하지 않는 문제가 @RequestBody를 사용할때는 발생하는 것일까?
HTTP 요청 파라미터를 처리하는 @ModelAttribute는 각각의 필드 단위로 세밀하게 적용되기에 특정 필드가 타입이 맞지 않더라도 나머지 필드를 정상 처리할 수 있다.
하지만, HttpMessageConverter는 @ModelAttribute과는 다르게 필드 단위가 아닌 객체 전체 단위로 적용되기 때문에 메세지 컨버팅이 성공해서 객체가 만들어진 다음에나 검증 애노테이션(@Valid, @Validated)이 적용된다.
그래서 다음 포스팅에서는 이런 경우에 대처하는 방식에 대해 알아보자.

다음 챕터로

이전 챕터로