김영한 선생님의 스프링 MVC 2편 강의를 듣고 정리한 내용이다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
를 gradle에 추가해주어야 한다.
아래와 같이 제한 조건들을 간단하게 나타낼 수 있다.
@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;
}
@NotBlank
: 빈값 + 공백만 있는 경우를 허용하지 않는다.
@NotNull
: null
을 허용하지 않는다.
@Range(min = 1000, max = 1000000)
: 범위 안의 값이어야 한다.
@Max(9999)
: 최대 9999까지만 허용한다.
LocalValidatorFactoryBean
을 글로벌 Validator
로 등록한다. 이 Validator
는 @NotNull
같은 애노테이션을 보고 검증을 수행한다. 이렇게 글로벌 Validator
가 적용되어 있기 때문에, @Valid
, @Validated
만 적용하면 된다.
검증 오류가 발생하면, FieldError
, ObjectError
를 생성해서 BindingResult
에 담아준다.
타입 변환에 성공해서 바인딩해 성공해야만 적용된다.
NotBlank 라는 오류 코드를 기반으로 MessageCodesResolver 를 통해 다양한 메시지 코드가 순서대로 생성된다.
@NotBlank
NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank
BeanValidation
메시지 찾는 순서messageSource
에서 메시지 찾기message
속성 사용 @NotBlank(message = "공백! {0}")
공백일 수 없습니다.
message
사용 예@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;
상품수정에 적용하는 과정이다. 앞에서 수정한 내용과 맞게 적절하게 수정하면 된다.
등록할 때와 수정할 때의 요구사항이 다른 경우 문제가 발생한다. 등록에서는 ID를 입력받는 칸이 없지만, 수정할 때에는 ID가 필수인 경우가 그 예시이다.
위의 한계를 해결하기 위한 방법이다.
각각 등록과 수정을 위해서 따로 인터페이스를 만든다.
이후에 Validated에 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 = 1000000, groups = {SaveCheck.class,
UpdateCheck.class})
private Integer price;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Max(value = 9999, groups = SaveCheck.class) //등록시에만 적용
private Integer quantity;
}
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item,
BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//...
}
groups 기능을 사용해서 등록과 수정시에 각각 다르게 검증을 할 수 있었다. 그런데 groups 기능을 사용하니 Item
은 물론이고, 전반적으로 복잡도가 올라갔다.
사실 groups 기능은 실제 잘 사용되지는 않는데, 그 이유는 실무에서는 주로 다음에 등장하는 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용하기 때문이다.
실무에서는 groups 를 잘 사용하지 않는데, 그 이유가 다른 곳에 있다. 바로 등록시 폼에서 전달하는 데이터가 Item
도메인 객체와 딱 맞지 않기 때문이다.
소위 "Hello World" 예제에서는 폼에서 전달하는 데이터와 Item
도메인 객체가 딱 맞는다. 하지만 실무에서는 회원 등록시 회원과 관련된 데이터만 전달받는 것이 아니라, 약관 정보도 추가로 받는 등 Item
과 관계없는 수 많은 부가 데이터가 넘어온다.
그래서 보통 Item
을 직접 전달받는 것이 아니라, 복잡한 폼의 데이터를 컨트롤러까지 전달할 별도의 객체를 만들어서 전달한다.
예를 들면 ItemSaveForm
이라는 폼을 전달받는 전용 객체를 만들어서 @ModelAttribute
로 사용한다. 이것을 통해 컨트롤러에서 폼 데이터를 전달 받고, 이후 컨트롤러에서 필요한 데이터를 사용해서 Item
을 생성한다.
//기존의 Item 클래스에서 검증 대신
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
//수정에서는 수량은 자유롭게 변경할 수 있다.
private Integer quantity;
}
// 아래와 같이 폼을 두개 만들어서 따로 적용한다. ModelAttriute에 이름도 제대로 적용하여야 한다. (폼 객체를 item 객체로 변환하는 과정이다.)
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
//...
}
Item
대신에 ItemSaveform
을 전달받고 Validated
로 검증 수행 후 BindgingResult
로 결과도 받는다.item
으로 넣지 않을 경우 MVC model
에 itemSaveForm
으로 담기게 된다.@Validated
는 HTTP 메시지 컨버터에도 적용이 가능하다.
참고
@ModelAttribute
는 HTTP 요청 파라미터(URL 쿼리 스트링, POST Form)를 다룰 때 사용한다.@RequestBody
는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용한다.
API의 경우 3가지 경우를 나누어 생각해야 한다.
성공
JSON을 객체로 생성하는 것 자체가 실패함
객체를 만들지 못하기 때문에 컨트롤러 자체가 호출되지 않고 그 전에 예외가 발생한다. 물론 Validator도 실행되지 않는다.
JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함
검증 오류가 정상적으로 동작한다.