1. 과거 프로젝트 build.gradle 오류
1) jdk 17 버전 수정
https://www.inflearn.com/questions/1232895/no-matching-variant-%EC%98%A4%EB%A5%98
2) gradle 버전 수정
[gradle-wrapper.properties]
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
2. 검증 요구사항
[상품등록]
1) 타입 검증
→ 가격, 수량에 문자가 들어가면 검증 오류 처리 필드 검증
2) 필드 검증
→ 상품명: 필수, 공백X
→ 가격: 1000원 이상, 1백만원 이하 수량: 최대 9999
→ 특정 필드의 범위를 넘어서는 검증
3) 복합 검증
→ 가격 * 수량의 합은 10,000원 이상
3. 서버 검증 과정
클라이언트에서는 사용자의 입력을 사전에 검증하여 사용자 경험을 향상시킬 수 있지만,
최종적으로 보안과 데이터 무결성을 위해서 서버 검증은 필수적이다.
API의 경우에는, API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 남겨주어야 한다.
4. 검증 V1
1) Map<String, String> errors = new HashMap<>();
검증 시 오류가 발생하면, 어떤 검증에서 오류가 발생했는지 정보를 담아둔다.
이 때, 어떤 필드에서 오류가 발생했는지 구분하기 위해 오류가 발생한 필드명을 Key로 사용한다.
2) StringUtils.hasText()
상품 이름이 입력되었는지 확인한다.
남은 문제점.
- Type 오류 처리가 안 된다.
→ Spring MVC에서 컨트롤러에 진입하기도 전에 예외가 발생하기 때문에, 400 예외가 발생하면서 오류 페이지가 나타난다.
- 사용자가 입력한 문자가 화면에 남지 않는다.
→ 고객이 입력한 값도 별도로 관리가 되어야 한다.
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
// 검증 오류 결과를 보관
Map<String, String> errors = new HashMap<>();
// 필드 검증
if (!StringUtils.hasText(item.getItemName())) {
errors.put("itemName", "상품 이름은 필수입니다.");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.put("price", "가격은 1,000 ~ 1,000,000 까지 허용합니다.");
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
errors.put("quantity", "수량은 최대 9,999 까지 허용합니다.");
}
// 복합 검증
if (item.getPrice() != null & item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.put("globalError", "가격*수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice);
}
}
// 검증에 실패하면 다시 입력 폼으로
if (hasError(errors)) {
log.info("errors = {} ", errors); // 로그
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
// 성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v1/items/{itemId}";
}
4-1. 검증 V2 [1]
- 오류 정보( `FieldError` )를 `BindingResult` 에 담아서 컨트롤러를 정상 호출한다.
1) BindingResult
검증 시 오류가 발생하면, 어떤 검증에서 오류가 발생했는지 정보를 담아둔다. (V1, HashMap 대체)
BindingResult bindingResult 파라미터 위치는 @ModelAttribute Item item 다음에 와야 한다.
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
2) 필드 오류 - FieldError
: 필드에 오류가 있으면 FieldError 객체를 생성해서, bindingResult에 담으면 된다.
[FieldError 생성자]
- objectName : @ModelAttribute 이름
- field : 오류가 발생한 필드 이름
- defaultMessage : 오류 기본 메시지
public FieldError(String objectName, String field, String defaultMessage) {}
// 필드 오류 → new FieldError()
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
3) 복합 오류 - ObjectError
: 특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서, bindingResult에 담으면 된다.
[ObjectError 생성자]
- objectName : @ModelAttribute 이름
- defaultMessage : 오류 기본 메시지
// 복합 오류 → new ObjectError()
if (item.getPrice() != null & item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", "가격*수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
}
}
4-2. 검증 V2 [2]
- FiledError에 더 많은 정보를 정의함으써, 사용자가 입력한 내용이 사라지지 않게 한다.
1) 필드 오류 - FieldError
: 필드에 오류가 있으면 FieldError 객체를 생성해서, bindingResult에 담으면 된다.
[FieldError 또 다른 생성자]
- objectName : @ModelAttribute 이름
- field : 오류가 발생한 필드 이름
- rejectedValue : 사용자가 입력한 값(거절된 값)
- bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
- codes : 메시지 코드
- arguments : 메시지에서 사용하는 인자
- defaultMessage : 오류 기본 메시지
public FieldError(String objectName, String field, String defaultMessage) {}
// 필드 오류 → new FieldError()
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
}
3) 복합 오류 - ObjectError
: 특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서, bindingResult에 담으면 된다.
[ObjectError 또 다른 생성자]
- objectName : @ModelAttribute 이름
- codes : 메시지 코드
- arguments : 메시지에서 사용하는 인자
- defaultMessage : 오류 기본 메시지
// 복합 오류 → new ObjectError()
if (item.getPrice() != null & item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", null, null, "가격*수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
}
}
4-3. 검증 V2 [3]
codes에 String[]을 넣어, resorce/errors.properties과 바인딩 될 수 있도록 메시지화 한다.
→ codes(메시지 코드)에 있는 값과 errors.properties 파일의 키 값과 맞는 value를 오류 메시지로 리턴한다.
required.item.itemName = 상품 이름은 필수입니다.
range.item.price = 가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity = 수량은 최대 {0} 까지 허용합니다.
totalPriceMin = 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
// 검증 오류 결과를 보관 → BindingResult
// 필드 오류 → new FieldError()
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]{9999}, null));
}
// 복합 오류 → new ObjectError()
if (item.getPrice() != null & item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[]{10000, resultPrice}, null));
}
}
...
}
4-4. 검증 V2 [4]
BindingResult 메소드인 rejectValue로 FieldError / ObjectError 생성 없이, 메시지 호출을 단순화하여 깔끔하게 검증 실행
[rejectValue 매개변수]
- field : 오류가 발생한 필드 이름
- errorCode : 루트 오류 코드(messageResolver 위함)
→ errorCode.target_object.field 메시지 코드를 탐색함
@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
// 검증 오류 결과를 보관 → BindingResult
// 필드 오류 → new FieldError()
if (!StringUtils.hasText(item.getItemName())) {
// bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
bindingResult.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
bindingResult.rejectValue("quantity,", "max", new Object[]{9999}, null);
}
// 복합 오류 → new ObjectError()
if (item.getPrice() != null & item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
...
}
4-5. 검증 V2 [5]
컨트롤러와 검증 로직을 ItemValidator로 따로 분리하여 실행한다.
public class ValidationItemControllerV2 {
private final ItemValidator itemValidator;
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
itemValidator.validate(item, bindingResult);
...
}
}
4-6. 검증 V2 [6]
별도의 검증 로직 호출 없이, @Validate 어노테이션으로 검증 로직 호출 자동화
// @Validated: 자동으로 검증기가 실행이 되어, 에러는 BindingResult에 담겨진다.
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
// 검증에 실패하면 다시 입력 폼으로
// BindingResult는 Spring에서 자동으로 View에 넘어가므로, model에 담지 않아도 된다.
if (bindingResult.hasErrors()) {
log.info("errors = {} ", bindingResult); // 로그
return "validation/v2/addForm";
}
// 성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
'Spring > MVC 2' 카테고리의 다른 글
6. 로그인 처리1 - 쿠키, 세션 (1) | 2024.05.02 |
---|---|
5. 검증 2 - Bean Validation (0) | 2024.05.01 |
3. 메시지, 국제화 (0) | 2024.04.29 |
2. 타임리프 - 스프링 통합과 폼 (0) | 2024.04.29 |
1. 타임리프 - 기본 기능 (0) | 2024.04.28 |