일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 벌크 연산
- 스프링 데이터 JPA
- 페이징
- JPQL
- 타임리프 문법
- 기본문법
- 프로젝트 환경설정
- 트위터
- Bean Validation
- 스프링 mvc
- 일론머스크
- 불변 객체
- jpa 활용
- JPA 활용 2
- QueryDSL
- 김영한
- 예제 도메인 모델
- API 개발 고급
- 스프링
- 값 타입 컬렉션
- JPA 활용2
- 검증 애노테이션
- 임베디드 타입
- 컬렉션 조회 최적화
- 스프링MVC
- JPA
- 로그인
- Spring Data JPA
- 타임리프
- 실무활용
- Today
- Total
RE-Heat 개발자 일지
스프링 MVC 2편 - [5] 검증2- Bean Validation(하편) 본문
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
인프런 김영한님의 스프링 MVC 2편 강의를 토대로 정리한 내용입니다.
[7] Bean Validation - 한계
한계점 : 등록할 때와 수정할 때 요구사항이 다르면 기존의 Bean Validation 방식은 적용하기 어렵다.
■ 요구사항의 변화 (등록 -> 수정)
① 등록 시 기존 요구사항
- 타입 검증
- 가격, 수량에 문자가 들어가면 검증 오류 처리 - 필드 검증
- 상품명: 필수, 공백 X
- 가격: 1000원 이상, 1백만원 이하
- 수량: 최대 9999 - 특정 필드의 범위를 넘어서는 검증
- 가격 * 수량의 합은 10,000원 이상
② 수정 시 요구사항
- 등록 시에는 quantity 수량을 최대 9999까지 등록할 수 있지만, 수정 시에는 수량을 무제한으로 변경할 수 있다.
- 등록 시에는 id에 값이 없어도 되지만, 수정 시에는 id 값이 필수이다.
수정을 위해 Item을 수정
id: @NotNull 추가
quantity: @Max(9999) 제거
이러면 같은 Item 객체를 쓰는 등록에서 문제가 발생한다.
등록 시 id값이 없으며, 수량 제한도 적용되지 않는다.
해결방안
① BeanValidation의 groups 기능을 사용한다.
② Item을 직접 사용하지 않고 ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어 사용한다.
[8] Bean Validation - groups
① 저장용/수정용 groups 인터페이스 생성
② 인터페이스를 Item 모델 객체에 필드마다 적용
@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;
}
}
ex) id는 수정 시에만 필수 값으로 요구됐으므로 UpdateCheck.class 적용
itemName @NotBlank는 저장용/수정용 둘 다 적용돼야 함 ...
③ 해당 컨트롤러의 저장 로직/수정 로직에 각각의 groups를 적용한다.
참고] @Valid는 groups 기능이 없다. 따라서 groups 기능을 활용하려면 @Validated를 써야 한다.
사실 groups기능은 잘 사용되지 않는다. groups 기능을 쓰면 전반적으로 복잡도가 올라가고, 폼에서 전달하는 데이터가 Item 도메인 객체와 딱 맞지 않은 경우가 많기 때문이다.
그래서 실무에선 등록용 폼 객체/수정용 폼 객체를 분리해서 사용한다.
[9] Form 전송 객체 분리 - 소개
■ 별도 객체 여부 관련 장단점
1. 폼 데이터 전달에 Item 도메인 객체 사용(분리 X)
HTML Form → Item → Controller → Item → Repository
- 장점: Item 도메인 객체를 컨트롤러, 리포지토리까지 직접 전달해서 중간에 Item을 만드는 과정이 없어서 간단하다.
- 단점: 간단한 경우에만 적용할 수 있다. 수정 시 검증이 중복될 수 있고, groups를 사용해야 한다.
2. 폼 데이터 전달을 위한 별도 객체 사용
HTML Form → ItemSaveForm → Controller → Item 생성 → Repository
- 장점: 전송하는 폼 데이터가 복잡해도 그 상황에 맞춘 별도의 폼 객체를 사용해 데이터를 전달받을 수 있다.
보통 등록과, 수정용으로 별도의 폼 객체를 만들기 때문에 검증이 중복되지 않는다. - 단점: 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정이 추가된다.
[10] Form 전송 객체 분리 - 개발
ItemSaveForm(저장용) & ItemUpdateForm(수정용)
ValidationItemControllerV4
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//특정 필드가 아닌 복합 룰 검증
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 = 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}";
}
① @ModelAttribute("item") 이름을 넣어주지 않으면 itemSaveForm이라는 이름으로 MVC Model에 담긴다. 그러면 뷰 템플릿에 접근하는 th:object이름도 일일이 다 바꿔주어야 하므로 동작하기 편하게 "item"이란 이름을 붙여준다.
② ItemSaveForm을 받아온 후 폼 객체를 Item으로 변환해 넘기는 과정이 필요하다.
수정도 위와 같은 방식을 적용하면 된다.
@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}";
}
[11] Bean Validation - HTTP 메시지 컨버터
@Valid, @Validated는 HttpMessageConverter(@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(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult){
log.info("API 컨트롤러 호출");
if (bindingResult.hasErrors()){
log.info("검증오류 발생", bindingResult);
return bindingResult.getAllErrors();
}
log.info("성공 로직 실행");
return form;
}
}
@ModelAttribute를 @RequestBody로 대체한 것을 확인할 수 있다.
API 3가지 상황
■ 성공 요청
■ 실패 요청 (Price에 문자를 넣음)
HttpMessageConverter에서 요청 JSoN을 ItemSaveFrom 객체로 생성하는 데 실패한다. 이러면 Validator를 거칠 필요가 없기 때문에 컨트롤러가 호출되지 않고, 곧바로 400번 예외가 발생한다.
■ 검증 오류 요청
HttpMessageConverter가 성공하지만, 검증에서 오류가 발생할 때
Postman 입력 값 :
POST http://localhost:8080/validation/api/items/add
{"itemName":"hello", "price":1000, "quantity": 10000}
return bindingResult.getAllErrors();는 FieldError와 ObjectError를 반환한다. 스프링이 이 객체를 JSON으로 변환해 전달한 게 위 사진이다.
물론 실제로 개발할 땐 이 객체를 그대로 사용하지 않고 필요한 데이터만 뽑아서 API 스펙을 정의하고 그에 맞는 객체를 만들어 반환해야 한다.
타입 오류가 뜰 때 JSON은 Form과는 달리 바인딩돼서 잘 넘어가지 않는다. 이유는 @ModelAttribute와 @RequestBodydml 차이 때문이다.
@ModelAttribute vs @RequestBody
- @ModelAttribute는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩되지 않아도 나머지 필드는 정상 바인딩 돼 Validator를 사용한 검증도 적용할 수 있다.
- 반면 @RequestBody는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 그래서 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.
영한님 : JSON으로 하면 예외가 발생할 수밖에 없는데, 예외 처리와 관련한 부분은 섹션 8·9에서 설명할 예정이다.
'백엔드 > 스프링' 카테고리의 다른 글
스프링 MVC 2편 - [6] 로그인 처리 1 - 쿠키·세션(하편) (0) | 2023.07.21 |
---|---|
스프링 MVC 2편 - [6] 로그인 처리 1 - 쿠키·세션(상편) (0) | 2023.07.20 |
스프링 MVC 2편 - [5] 검증2- Bean Validation(상편) (0) | 2023.07.19 |
스프링 MVC 2편 - [4] 검증 1 - Validation(하편) (1) | 2023.07.16 |
스프링 MVC 2편 - [4] 검증 1 - Validation(상편) (0) | 2023.07.14 |