[스프링 웹 MVC 2편] 007. Bean Validation
검증코드를 이전과 같이 컨트롤러에 모두 넣는 방식DataBinder
를 쓴다고 해도 ItemValidator
를 따로 만들어서 검증 로직을 구성하기는 번거롭다
따라서 다음과 같은 방법으로 스프링은 검증을 위한 편의 기능을 제공한다
아래는 강의를 듣고 완성한 코드를 모두 첨부한 것이다.
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range; // Hibernate Validator에서만 동작
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank; // 표준 제공 (어떤 구현체에서도 동작)
import javax.validation.constraints.NotNull;
@Data
// @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "10000원 넘게 입력해주세요") // 이거까지 가는건 좀 오버
// 검증 기능이 해당 객체를 넘어서는 경우가 많음 억지로 사용하는 것보다 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장
// 그냥 복합 룰 검증으로 사용하자!
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;
}
}
보면 @NotBlank
, @NotNull
@Range
와 같은 Annotation 기반의 검증 방식을 제공하는 것을 알 수 있다.
Bean Validation은 특정한 구현체가 아니라 기술 표준에 해당한다!
-> 기술 표준 : 검증 애노테이션과 여러 인터페이스의 모음
Like JPA - 표준 기술 / Hibernate - 그 구현체!
Bean Validation이라는 표준 기술의 구현체는 Hibernate Validator인데, ORM과는 연관이 없다!
본격적인 Bean Validation 사용 설정
build.gradle
에 다음과 같이 내용을 추가하자
implementation 'org.springframework.boot:spring-boot-starter-validation'
테스트 코드 작성
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range; // Hibernate Validator에서만 동작
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank; // 표준 제공 (어떤 구현체에서도 동작)
import javax.validation.constraints.NotNull;
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 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)
: 값의 범위를 1000에서 1000000 사이만 허용함@Max(9999)
: 최대 9999까지만 허용함
()
안에 message = "쓸 메세지"
식으로 넣으면 해당 오류 메세지가 errors
에 들어간다.
package hello.itemservice.validation;
import hello.itemservice.domain.item.Item;
import org.junit.jupiter.api.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
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.setQuantity(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.getMessage() = " + violation.getMessage());
// Set에는 ConstratintViolation이라는 검증 오류가 담김.
// 비어있으면 오류가 없는 것임.
}
}
}
![[Pasted image 20230815211651.png]]
이미 실습을 거치고 다음단계로 넘어가서 테스트 코드 결과가 나오지 않는다....
여기서 Item
객체는 왜 Bean
으로 등록되는지 궁금했는데 (애노테이션이 없음에도...)rootBeanClass
에 Item
객체에 @Data
어노테이션이 있어도 스프링 빈으로 등록한다는 답변이 있었다
![[Pasted image 20230815211935.png]]
스프링 Validator 직접 적용
package hello.itemservice.web.validation;
import com.sun.jdi.Field;
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import hello.itemservice.domain.item.SaveCheck;
import hello.itemservice.domain.item.UpdateCheck;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Controller
@RequestMapping("/validation/v3/items")
@RequiredArgsConstructor
public class ValidationItemControllerV3 {
// LocalValidatorFactoryBean - 애노테이션들을 보고 검증 로직을 구성해줌
// Global Validator로 등록되어있어 @Validated만 사용해주면 검증이 됨!
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "validation/v3/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v3/item";
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v3/addForm";
}
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, // Bean Validation이 그냥 적용됨.
BindingResult bindingResult, // ModelAttribute 바로 뒤에 와야 함! (순서 중요)
RedirectAttributes redirectAttributes,
Model model) {
if(item.getPrice() != null && item.getQuantity() != null){ // 오브젝트 관련된 건 따로 처리하고 Method Extraction으로 가독성 향상
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}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v3/editForm";
}
//@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute Item item, BindingResult bindingResult) {
if(item.getPrice() != null && item.getQuantity() != null){ // 오브젝트 관련된 건 따로 처리하고 Method Extraction으로 가독성 향상
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}";
}
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
if(item.getPrice() != null && item.getQuantity() != null){ // 오브젝트 관련된 건 따로 처리하고 Method Extraction으로 가독성 향상
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}";
}
}
@Validated
애노테이션을 검증 대상인 Item
객체 앞에 붙여주었다.
이 때, @Valid
는 build.gradle
에 아까 추가한 의존관계를 작성해주어야 사용가능하다.
두 개의 차이는 전자는 스프링 전용 검증 애노테이션이고 후자는 자바 표준 검증 애노테이션인데
@Validated
의 경우 groups
기능을 지원한다
이 기능은 Item
객체에 붙인 검증기 애노테이션은 global하게 모든 @Validated
가 붙은 Item
객체에서 동일한 검증 로직을 적용하는데, 상품 추가 / 수정 등 다른 비즈니스 로직에서 다른 검증 룰을 적용해야하는 경우 인터페이스를 생성하고 조건을 붙임으로써 사용이 가능한데 자세한 이야기는 추후에 나온다!
Bean Validator 검증 순서
@ModelAttribute
에 각각의 필드에 타입 변환을 시도함- 성공하면 다음으로
- 실패하면
typeMismatch
로FieldError
를 추가한다.
Validator
를 적용하여 룰 검증을 시작한다.
!!중요!!
Bean Validator
의 경우 바인딩이 성공한 필드에만 적용된다- 실패하는 경우
BindingResult
에FieldError
가 추가된다.
Bean Validator 에러 코드
예시
@NotBlank
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
@Range
- Range.item.price
- Range.price
- Range.java.lang.Integer
- Range
errors.properties
NotBlank = {0} 공백 X
Range = {0}, {2} ~ {1} 허용
Max = {0}, 최대 {1}
{0}
은 필드명에 해당하고, {1}, {2}
는 각 애노테이션마다 상이함.
Bean Validation 메세지 찾는 순서
- 생성된 메세지 코드 순서대로
messageSource
에서 메세지 찾기 - 애노테이션
message
속성 사용 ->@NotBlank(message = "공백! {0}")
- 라이브러리가 제공하는 기본 값 사용 -> 예를 들면, 공백일 수 없습니다
용례
@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;
여기까지는 모두 필드 오류에 해당하는데, 오브젝트 오류는 다루지 않았따
Bean Validation - 오브젝트 오류
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range; // Hibernate Validator에서만 동작
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank; // 표준 제공 (어떤 구현체에서도 동작)
import javax.validation.constraints.NotNull;
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "10000원 넘게 입력해주세요") // 이거까지 가는건 좀 오버
// 검증 기능이 해당 객체를 넘어서는 경우가 많음 억지로 사용하는 것보다 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장
// 그냥 복합 룰 검증으로 사용하자!
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;
}
}
일단 groups
는 무시합시다, 나중에 더 작성할 예정입니다.
ScriptAssert
는 생각보다 제약이 많고 복잡하고 실무에서는 검증기능이 해당 객체 범위를 벗어나는 경우도 많아서 해당 부분만 BindingResult를 사용하여 자바코드로 직접 작성하는것을 권장한다.
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "10000원 넘게 입력해주세요")
if(item.getPrice() != null && item.getQuantity() != null){ // 오브젝트 관련된 건 따로 처리하고 Method Extraction으로 가독성 향상
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
Bean Validation의 한계와 Groups / 별도의 폼 객체 생성
이전에 언급한대로, 데이터 등록시와 수정시 다른 검증룰을 적용하고 싶을 수 있는데
지금 그대로면 문제가 발생한다.
id
에 초장부터 NOT NULL을 추가해버리면 id값이 초반에는 없는데 (itemRepository
에 의해 생성)id
값을 요구하기 때문에.... 오류가 나버린다!
Groups와 별도의 폼 객체 생성
- Bean Validation의
groups
기능을 사용한다. Item
을 직접 사용하지 않고,ItemSaveForm
,ItemUpdateForm
같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용한다.
groups 사용방법
package hello.itemservice.domain.item;
public interface SaveCheck {
}
package hello.itemservice.domain.item;
public interface UpdateCheck {
}
두 개의 인터페이스를 만들고
다음과 같이 도메인 코드를 수정한다
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range; // Hibernate Validator에서만 동작
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank; // 표준 제공 (어떤 구현체에서도 동작)
import javax.validation.constraints.NotNull;
@Data
// @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "10000원 넘게 입력해주세요") // 이거까지 가는건 좀 오버
// 검증 기능이 해당 객체를 넘어서는 경우가 많음 억지로 사용하는 것보다 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장
// 그냥 복합 룰 검증으로 사용하자!
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;
}
}
그리고 컨트롤러 코드를 다음과 같이 수정한다
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, // Bean Validation이 그냥 적용됨.
BindingResult bindingResult, // ModelAttribute 바로 뒤에 와야 함! (순서 중요)
RedirectAttributes redirectAttributes,
Model model) {
if(item.getPrice() != null && item.getQuantity() != null){ // 오브젝트 관련된 건 따로 처리하고 Method Extraction으로 가독성 향상
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}";
}
먼저 SaveCheck.class
와 UpdateCheck.class
두 인터페이스를 선언하고
엔티티에 다음과 같이 @NotNull(groups = UpdateCheck.class)
식으로 추가한다.
그리고 컨트롤러의 @Validated
에 ()
안에 UpdateCheck.class
를 그대로 써주면 된다.
별도 폼 객체 전송
위와 같이 groups
를 사용하면 복잡도가 많이 올라간다, 그 대신 드디어 DTO
활용에 대해서 공부할 기회가 생겨서 기분이 좋다.
평소에 뭐지 싶었던 내용들이었는데 드디어 배우네요.....
등록시 폼에서 전달하는 데이터가 Item
도메인 객체 필드와 일치하지 않아서 별도의 객체를 생성합니다.
HTML FORM -> Item -> Controller -> Item -> Reposistory
장점 : Item
도메인 객체를 컨트롤러, 리포지토리까지 직접 전달해서 별도의 객체를 만드는 과정이 없다
단점 : 간단한 경우에만 사용 가능하고 수정시 검증이 중복될 수 있으며 groups
를 사용해야 한다.
HTML FORM -> ItemSaveForm -> Controller -> Item 생성 -> Repository
여기서 Item
생성은 Controller
에서 일어난다.
장점 : 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 활용하여 데이터 전달 가능
등록, 수정용으로 폼별로 객체를 별도로 만들기 때문에 검증 중복 X
단점 : 폼 데이터 기반의 컨트롤러에서 Item
객체를 생성하는 변환 과정이 추가됨(컨트롤러가 헤비해짐)
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range; // Hibernate Validator에서만 동작
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank; // 표준 제공 (어떤 구현체에서도 동작)
import javax.validation.constraints.NotNull;
@Data
// @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "10000원 넘게 입력해주세요") // 이거까지 가는건 좀 오버
// 검증 기능이 해당 객체를 넘어서는 경우가 많음 억지로 사용하는 것보다 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장
// 그냥 복합 룰 검증으로 사용하자!
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;
}
}
Item
객체에 붙은 모든 애노테이션을 빼고 순수하게(?) 만들었다.@Data
는 남겨두었다
그리고 각 폼 별로 DTO 객체를 생성했다 그 코드는 아래와 같다
package hello.itemservice.web.validation.form;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
// 수정에서 값은 자유롭게 변경 가능
private Integer quantity;
}
package hello.itemservice.web.validation.form;
import hello.itemservice.domain.item.SaveCheck;
import hello.itemservice.domain.item.UpdateCheck;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
각각 저장을 위한 폼 객체, 수정을 위한 폼 객체에 해당하며 Bean Validation 검증 룰이 각각 다르기 때문에 그 차이를 두었다.
@PostMapping("/add")
public String addItemV2(@Validated @ModelAttribute("item") ItemSaveForm form, // Bean Validation이 그냥 적용됨.
BindingResult bindingResult, // ModelAttribute 바로 뒤에 와야 함! (순서 중요)
RedirectAttributes redirectAttributes,
Model model) {
if(form.getPrice() != null && form.getQuantity() != null){ // 오브젝트 관련된 건 따로 처리하고 Method Extraction으로 가독성 향상
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.setQuantity(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}";
}
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
if(form.getPrice() != null && form.getQuantity() != null){ // 오브젝트 관련된 건 따로 처리하고 Method Extraction으로 가독성 향상
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 item = new Item();
item.setItemName(form.getItemName());
item.setPrice(form.getPrice());
item.setQuantity(form.getQuantity());
itemRepository.update(itemId, item);
return "redirect:/validation/v4/items/{itemId}";
}
}
각각 @ModelAttribute
에 사용할 객체 형태가 바뀌었기 때문에 Item
객체로 받아오던 것을 Form
객체로 변경해주었는데 이때 주의할 것이 기존에 작성된 thymeleaf에 .addAttribute
가 여전히 item
이다.
문제는 이걸 ItemSaveForm
과 ItemUpdateForm
으로 바꾸면 그 key
값도 변경된다는 것인데 이때 @ModelAttribute
에 ()
안에 이름을 따로 기술해줄 수 있기 때문에 그 방식을 사용한다.
Bean Validation - HTTP 메세지 컨버터
package hello.itemservice.web.validation;
import hello.itemservice.web.validation.form.ItemSaveForm;
import lombok.extern.java.Log;
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("검증 오류 발생 = {}", bindingResult);
return bindingResult.getAllErrors();
}
log.info("성공 로직 실행");
return form;
}
}
@ModelAttribute
는 각각의 필드 단위로 세세하게 적용되어서 특정 필드에 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리가 가능하나, HttpMessageConverter
는 @ModelAttribute
와 다르게 필드 단위로 적용되는 것이 아니라 객체 단위로 적용된다.
따라서 메세지 컨버터의 작동이 일단 성공해서 ItemSaveForm
객체가 제대로 생성이 되었다는 전제가 있어야 @Valid
, @Validated
가 적용될 수 있다.
@RequestBody
에서 예외를 처리하는 방법은 예외처리부분에서 추후에 다루게 된다.