1. groups
동일한 모델 객체를 상품 등록과 상품 수정 두 군데에서 사용하기 위해서, Bean Validation의 검증 기능 중 하나인 groups를 적용해보자. 구분을 위한 interface, SaveCheck, UpdateCheck를 2개 만들어준다.
그리고 Item 객체에는 groups 속성을 주면된다.
java/hello/itemservice/domain/item/Item.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@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;
}
}
|
cs |
다음으로 각 컨트롤러에서 @Validated에 구분자 인터페이스를 참조해주면 된다. @Validated에서만 적용이 되고, @Valid 어노테이션에서는 제공하지 않는 기능이다.
java/hello/itemservice/web/validation/ValidationItemControllerV3.java
1
2
3
4
5
6
7
8
|
@PostMapping("/add")
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
....
}
|
cs |
테스트를 해보면 잘 적용되는 것을 확인할 수 있다. 상품 수정 시에 수량을 9999 이상으로 수정할 수 있게 되었다.
그러나 앞선 글에서도 설명한 바와 같이, groups는 번잡한 느낌이 있는데다, groups 대신 각 View를 위한 객체를 따로 만드는 것이 일반적이다. 다음 섹션에서 객체를 따로 만들어서 적용해보자.
2. 객체 분리 적용하기
상품 등록과 상품 수정에 따로 쓰일 수 있도록 Item 객체를 분리해보자. v2 -> v3에서 했던 것처럼 v4로 복사하여 컨트롤러 및 View를 추가하자.
그리고 객체 분리 시, Item 객체를 ItemSaveForm, ItemUpdateForm의 2가지 파일로 분리한다. 아래와 같은 프로세스로 진행될 것이다. HTML Form으로 데이터를 받아온 뒤, Controller에서 Item 객체를 생성하는 부분을 추가해주어야 한다.
HTML -> ItemSaveForm -> Controller -> Item 객체 생성 -> Repository
우선 Item 객체에 등록해뒀던 개별 검증 어노테이션을 주석처리한다. 그리고 validation 패키지 안에 form 패키지를 만들고 각 View에 필요한 Form 데이터를 만든다.
java/hello/itemservice/web/validation/form/ItemSaveForm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
|
cs |
java/hello/itemservice/web/validation/form/ItemUpdateForm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
//수정 시에는 수량 자유롭게 변경 가능
private Integer quantity;
}
|
cs |
모델 객체 정보 바꾸기
그리고 컨트롤러에서 Model 객체를 전달하는 부분을 바꿔주어야 한다. 기존 Item 객체를 넘기던 부분에 ItemSaveForm, ItemUpdateForm 타입을 전달한다. 객체 이름은 form으로 변경하는데, item 객체에 커서를 두고 [Shift+F6]을 눌러서 refactoring(renaming)을 할 수 있다. 그리고 View에서는 여전히 item 이라는 이름으로 객체를 참조하므로, @ModelAttribute의 속성값을 "item"으로 주어야 한다. 이를 생략하면 객체 타입명을 첫글자를 소문자로 바꾸어서 itemSaveForm이라는 이름으로 전달된다는 것을 배웠었다.
item 정보 전달하기
item 객체로 View에 전달해야하기 때문에, item 객체를 만들고 getter, setter를 통해 필드값들을 넣어준다. 이후 26번줄과 같이 item 객체를 전달해주면 정상적으로 동작한다.
java/hello/itemservice/web/validation/ValidationItemControllerV4.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@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());
//글로벌 검증
if (form.getPrice() != null && form.getQuantity() != null) {
int totalPrice = form.getQuantity() * form.getPrice();
if (totalPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, totalPrice}, null);
}
}
log.info("bindingResult ={}", bindingResult);
//검증 로직 실패 시, 다시 입력 폼으로
//이중 부정이므로 리팩토링으로 hasError 등으로 바꾸는게 좋다.
if (bindingResult.hasErrors()) {
return "validation/v3/addForm";
}
//검증 로직 통과 시, 상품 저장
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v4/items/{itemId}";
}
|
cs |
3. API 메시지 적용
API 방식으로 사용해보자. JSON 객체를 전송하고, 전송된 데이터를 받기 위한 컨트롤러를 작성한다.
java/hello/itemservice/web/validation/ValidationItemApiController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@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;
}
}
|
cs |
포스트맨으로 @Post 요청을 보내면, 에러 발생 시 에러 객체가 출력되는 것을 확인할 수 있다.
그런데 아예 타입이 맞지 않는 데이터를 보낸다면, 컨트롤러 호출 자체가 안되는 것을 볼 수 있다.
@ModelAttribute VS @HttpMessageConverter
이것은 HttpMessageConverter가 JSON 객체를 받아서 우리가 받기로 지정한 ItemSaveForm 객체의 형태로 변환(파싱)해주어야 되는데, 파싱 자체가 실패한 것이다. 이 문제는 @ModelAttribute와 @RequestBody의 차이에 그 원인이 있다. @ModelAttribute는 요청 파라미터를 각각의 필드별로 적용하므로 특정 필드에 타입이 맞지 않더라도 나머지 필드는 정상처리되어 컨트롤러 자체는 호출이 된다. 그러나 HttpMessageConverter의 @RequestBody 등은 전체 객체 단위로 적용되므로 컨트롤러 호출에 실패하고 검증단계로 넘어가지 못하는 것이다.
HttpMessageConverter 단계에서 실패 시 위와 같이 예외가 발생한다. 이런 예외가 발생했을 때 예외 메시지를 바꿀 수는 있다. 이 내용은 나중에 다룬다.
참조
1. 인프런_스프링 MVC 2편 - 백엔드 웹개발 핵심 기술_김영한 님 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
'Programming-[Backend] > Spring' 카테고리의 다른 글
[스프링 웹MVC-2] 11. 로그인 - 세션 활용 (0) | 2021.08.29 |
---|---|
[스프링 웹MVC-2] 10. 로그인 - 기본 기능, 쿠키 적용 (0) | 2021.08.29 |
[스프링 웹MVC-2] 8. 검증(Validation) - Bean Validation 사용하기 (0) | 2021.08.24 |
[스프링 웹MVC-2] 7. 검증(Validation) - 오류코드, 메시지 처리 (0) | 2021.08.22 |
[스프링 웹MVC-2] 6. 검증(Validation) 개선 : BindingError, FieldError, ObjectError (0) | 2021.08.22 |