V4
์ ์ฅ์ฉ๊ณผ ์์ ์ฉ DTO๋ฅผ ๊ฐ๊ฐ ๋ง๋ ๋ค.
Item
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() { }
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}์ ์ฅ์ฉ ๊ฐ์ฒด
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}์์ ์ฉ ๊ฐ์ฒด
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1_000_000)
private Integer price;
//์์ ์์๋ ์๋์ ์์ ๋กญ๊ฒ ๋ณ๊ฒฝํ ์ ์๋ค.
private Integer quantity;
}์ปจํธ๋กค๋ฌ
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
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}";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable("itemId") 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}";
}Bean Validation - HTTP ๋ฉ์์ง ์ปจ๋ฒํฐ
@Valid,@Validated๋HttpMessageConverter(@RequestBody)์๋ ์ ์ฉํ ์ ์๋ค.
์ปจํธ๋กค๋ฌ
@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;
}
}API์ ๊ฒฝ์ฐ 3๊ฐ์ง ๊ฒฝ์ฐ๋ฅผ ์๊ฐํด์ผ ํ๋ค.
์ฑ๊ณต ์์ฒญ (์ฑ๊ณต)
์คํจ ์์ฒญ : JSON์ ๊ฐ์ฒด๋ก ์์ฑํ๋ ๊ฒ ์คํจ (JSON parser error)
๊ฒ์ฆ ์ค๋ฅ ์์ฒญ : JSON์ ๊ฐ์ฒด๋ก ์์ฑํ๋ ๊ฒ์ ์ฑ๊ณตํ์ผ๋ ๊ฒ์ฆ์์ ์คํจ
์คํจ ์์ฒญ : price์ ๋ฌธ์๋ฅผ ์ ๋ ฅํ์ ๋
{
"timestamp": "2024-01-20T15:52:55.588+00:00",
"status": 400,
"error": "Bad Request",
"path": "/validation/api/items/add"
}๊ฒ์ฆ ์ค๋ฅ ์์ฒญ : @Max์ ๋ฒ์๋ฅผ ์ด๊ณผํ์ ๋
[
{
"codes": [
"Max.itemSaveForm.quantity",
"Max.quantity",
"Max.java.lang.Integer",
"Max"
],
"arguments": [
{
"codes": [
"itemSaveForm.quantity",
"quantity"
],
"arguments": null,
"defaultMessage": "quantity",
"code": "quantity"
},
9999
],
"defaultMessage": "9999 ์ดํ์ฌ์ผ ํฉ๋๋ค",
"objectName": "itemSaveForm",
"field": "quantity",
"rejectedValue": 10000,
"bindingFailure": false,
"code": "Max"
}
]bindingResult.getAllErrors()๋ objectError์ FieldError๋ฅผ ๋ฐํํ๋ค. ์คํ๋ง์ ์ด ๊ฐ์ฒด๋ฅผ JSON์ผ๋ก ๋ณํํด์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌํ๋ค. ์ค์ ๊ฐ๋ฐํ ๋๋ ์ด ๊ฐ์ฒด๋ค์ ๊ทธ๋๋ก ์ฌ์ฉํ์ง ๋ง๊ณ ํ์ํ ๋ฐ์ดํฐ๋ง ๋ฝ์์ ๋ณ๋์ API ์คํ์ ์ ์ํด์ผ ํ๋ค.
@ModelAttribute vs @RequestBody
HTTP ์์ฒญ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฒ๋ฆฌํ๋
@ModelAttribute๋ ํ๋ ๋จ์๋ก ์ ๊ตํ๊ฒ ๋ฐ์ธ๋ฉ์ด ์ ์ฉ๋๊ธฐ ๋๋ฌธ์ ํน์ ํ๋๊ฐ ๋ฐ์ธ๋ฉ ๋์ง ์์๋ ๋๋จธ์ง ํ๋๋ ์ ์ ๋ฐ์ธ๋ฉ ๋๊ณValidator๋ฅผ ์ฌ์ฉํ ๊ฒ์ฆ๋ ์ ์ฉํ ์ ์๋ค.
@RequestBody๋HttpMessageConverter๋จ๊ณ์์ JSON ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ๋ณ๊ฒฝํ์ง ๋ชปํ๋ฉด ์ปจํธ๋กค๋ฌ๋ฅผ ํธ์ถํ์ง ๋ชปํ๊ณ ์์ธ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์Validator๋ ์ ์ฉํ ์ ์๋ค.
์คํ๋ง๊ณผ Bean Validation
์คํ๋ง์์๋ ์ด๋ ธํ ์ด์ ๊ธฐ๋ฐ ๊ฒ์ฆ์ ์ํด
@Valid์@Validated์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ ์ ์์ผ๋ฉฐ ์ฌ์ฉ ๋ฐฉ์์ ์์ด ์ฝ๊ฐ ์ฐจ์ด๊ฐ ์๋ค.@Valid๋jakarta.validation์ ํฌํจ๋์ด ์๊ณ ,@Validated๋org.springframework.validation.annotation์ ํฌํจ๋์ด ์์ผ๋ฉฐ@Valid๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋spring-boot-starter-validation์์กด์ฑ์ด ํ์ํ๋ค.๋ ์ด๋ ธํ ์ด์ ๋ชจ๋ ๊ฐ์ฒด ํ์ ์๋ง ์ฌ์ฉํ ์ ์๋ค. ๋ํ ๊ฒ์ฆํ ๊ฐ์ฒด ๋ฐ๋ก ์์ ์์นํด์ผ ํ๋ฉฐ ๊ฒ์ฆ๋ ๊ฒฐ๊ณผ๋
BindingResult์ ๋ด๊ธด๋ค.๊ฒ์ฆ์ ๋ฐ์ธ๋ฉ์ ๊ฐ์ฅ ๋ง์ง๋ง ์ฒ๋ฆฌ ๊ณผ์ ์ด๋ฉฐ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฐ์ธ๋ฉ์ ์ฑ๊ณตํ ํ๋๋ ๊ฒ์ฆ์ด ์ด๋ฃจ์ด์ง๋ค.
๋ง์ฝ ํ๋์ ํ์ ๋ณํ์ด ์คํจํ๋ฉด ์คํจ ๊ฒฐ๊ณผ๊ฐ
FieldError๊ฐ์ฒด์ ๋ด๊ธฐ๊ณBindingResult์ ๋ณด๊ด๋๋ค.ํ์ ๋ณํ์ ์คํจํ ํ๋๋ ๊ธฐ๋ณธ ๊ฐ์ด ์ ์ฅ๋ ์ํ์์ ๊ฒ์ฆ์ด ์ด๋ฃจ์ด์ง์ง๋ง
Validator๊ตฌํ์ฒด์ ๋ฐ๋ผ ์์ธ๊ฐ ๋ฐ์ํ ์๋ ์๊ณ ๊ธฐ๋ณธ ๊ฒ์ฆ์ด ์ด๋ฃจ์ด์ง ์ ์๋ค.
ํด๋์ค ๊ตฌ์กฐ

๊ฒ์ฆ ์ฒ๋ฆฌ ํ๋ฆ๋

์ฌ๊ธฐ์ ConstraintValidator์๋ ๋ค์ํ ๊ฒ์ฆ๊ธฐ๋ค์ด ์ ์๋์ด ์๋ค.

์ปค์คํ
๊ฒ์ฆ ์ ๋
ธํ
์ด์
๋ง๋ค๊ธฐ
ConstraintValidator ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ์ฌ ์ปค์คํ
๊ฒ์ฆ ์ ๋
ธํ
์ด์
์ ๋ง๋ค ์ ์๋ค.

์ฃผ์ด์ง ์ ์ฝ ์กฐ๊ฑด A์ ๋ํด ๊ฒ์ฆ ๋์ ํ์
T๋ฅผ ๊ฒ์ฆํ๋ ์ธํฐํ์ด์ค์ด๋ค.
isValid(): ๊ฒ์ฆ ์์ ์ ์ํํ๋ค. ๊ฒ์ฆ ๋์ ๊ฐ์ฒดT๋ ๋ณ๊ฒฝํ์ง ์๊ณ ๊ทธ๋๋ก ์ ์งํด์ผ ํ๋ค.initialize(): ๊ฒ์ฆ๊ธฐ์isValid()ํธ์ถ์ ์ค๋นํ๊ธฐ ์ํด ์ด๊ธฐํํ๋ค. ๊ฒ์ฆ์ ์ฌ์ฉ๋๊ธฐ ์ ์ ๋จผ์ ํธ์ถ ๋๋ค.
/*--------------์ปค์คํ
์ด๋
ธํ
์ด์
--------------*/
@Documented
@Constraint(validatedBy = PasswordValidator.class) //๊ฒ์ฆ๊ธฐ๋ฅผ ์ง์
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPassword {
String message() default "์๋ชป๋ ๋น๋ฐ๋ฒํธ ์
๋๋ค. ๋น๋ฐ๋ฒํธ๋ ์ต์ 8์ ์ด์์ด์ด์ผ ํ๊ณ , ๋๋ฌธ์, ์๋ฌธ์, ์ซ์๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.";
int minLength() default 8;
Class<?>[] groups() default {}; //ํ์
Class<? extends Payload>[] payload() default {}; //ํ์
}
/*--------------์ปค์คํ
๊ฒ์ฆ๊ธฐ--------------*/
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
private int minLength;
@Override
public void initialize(ValidPassword constraintAnnotation) {
this.minLength = constraintAnnotation.minLength();
}
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (!StringUtils.hasText(password)) {
return false;
}
boolean length = password.length() >= minLength;
boolean upperCase = password.chars().anyMatch(Character::isUpperCase);
boolean lowerCase = password.chars().anyMatch(Character::isLowerCase);
boolean digit = password.chars().anyMatch(Character::isDigit);
//์ต์ ๊ธธ์ด, ๋๋ฌธ์, ์๋ฌธ์, ์ซ์๋ฅผ ํฌํจํ๋์ง ๊ฒ์ฌ
return length && upperCase && lowerCase && digit;
}
}Last updated