Validation
Spring Verification
컨νΈλ‘€λ¬μ μ€μν μν μ€ νλλ HTTP μμ²μ΄ μ μμΈμ§ κ²μ¦νλ κ²
ν΄λΌμ΄μΈνΈ κ²μ¦μ μ‘°μν μ μμΌλ―λ‘ λ³΄μμ μ·¨μ½
κ·Έλ λ€κ³ μλ²λ§μΌλ‘ κ²μ¦νλ©΄ μ¦κ°μ μΈ κ³ κ° μ¬μ©μ±μ΄ λΆμ‘±
μλ², ν΄λΌμ΄μΈνΈ κ²μ¦μ μ μ ν μμ΄μ μ¬μ©νλ μλ² κ²μ¦μ νμ
API λ°©μ μ¬μ© μ, API μ€νμ μ μ μν΄μ κ²μ¦ μ€λ₯λ₯Ό API μλ΅ κ²°κ³Όμ μ λ¨κ²¨μΌ ν¨
BindingResult
μ€νλ§μ΄ μ 곡νλ κ²μ¦ μ€λ₯λ₯Ό 보κ΄νλ κ°μ²΄
@PostMapping("/add")
public String addItem(@ModelAttribute Item item,
BindingResult bindingResult,
RedirectAttributes redirectAttributes,
Model model) {
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "μν μ΄λ¦μ νμ μ
λλ€."));
}
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 κΉμ§ νμ©ν©λλ€."));
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, null ,null, "μλμ μ΅λ 9,999 κΉμ§ νμ©ν©λλ€."));
}
// κΈλ‘λ² μμΈ
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));
}
}
// κ²μ¦ μ€ν¨ μ μ
λ ₯ νΌμΌλ‘
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}";
}
...
/**
* FieldError class
* νλμ μ€λ₯κ° μμΌλ©΄ FieldError κ°μ²΄λ₯Ό μμ±ν΄μ bindingResult μ λ΄μλμ.
*/
public FieldError(
String objectName, // @ModelAttribute μ΄λ¦
String field, // μ€λ₯κ° λ°μν νλ μ΄λ¦
String defaultMessage // μ€λ₯ κΈ°λ³Έ λ©μμ§
) {}
public FieldError(
String objectName, // μ€λ₯κ° λ°μν κ°μ²΄ μ΄λ¦
String field, // μ€λ₯ νλ
@Nullable Object rejectedValue, // μ¬μ©μκ° μ
λ ₯ν κ°(κ±°μ λ κ°)
boolean bindingFailure, // νμ
μ€λ₯ κ°μ λ°μΈλ© μ€ν¨μΈμ§, κ²μ¦ μ€ν¨μΈμ§ κ΅¬λΆ κ°
@Nullable String[] codes, // λ©μμ§ μ½λ
@Nullable Object[] arguments, // λ©μμ§μμ μ¬μ©νλ μΈμ
@Nullable String defaultMessage // κΈ°λ³Έ μ€λ₯ λ©μμ§
)
/**
* ObjectError
* νΉμ νλλ₯Ό λμ΄μλ μ€λ₯κ° μμΌλ©΄ ObjectError κ°μ²΄λ₯Ό μμ±ν΄μ bindingResult μ λ΄μλμ.
*/
public ObjectError(
String objectName, // @ModelAttribute μ μ΄λ¦
String defaultMessage // μ€λ₯ κΈ°λ³Έ λ©μμ§
) {}
BindingResult
@ModelAttribute κ°μ²΄ λ€μ μμΉ
BindingResult κ° μμΌλ©΄ @ModelAttribute μ λ°μ΄ν° λ°μΈλ© μ μ€λ₯κ° λ°μν΄λ 컨νΈλ‘€λ¬ νΈμΆ
BindingResult κ° μμΌλ©΄ 400 μ€λ₯ λ°μ ν, μ€λ₯ νμ΄μ§λ‘ μ΄λ
BindingResult κ° μμΌλ©΄ μ€λ₯ μ 보(FieldError)λ₯Ό BindingResult μ λ΄μμ 컨νΈλ‘€λ¬ μ μ νΈμΆ
Model μ μλμΌλ‘ ν¬ν¨
μ΄λ€ κ°μ²΄λ₯Ό λμμΌλ‘ κ²μ¦νλμ§ νκ²μ μκ³ μμ
.
BindingResult μ κ²μ¦ μ€λ₯λ₯Ό μ μ©νλ μΈ κ°μ§ λ°©λ²
@ModelAttribute κ°μ²΄μ νμ
μ€λ₯ λ±, λ°μΈλ© μ€ν¨ μ μ€νλ§μ΄ FieldError μμ± ν BindingResult μ μ½μ
κ°λ°μκ° μ§μ μ
λ ₯
Error Message
μ€λ₯ λ©μμ§ νμΌμ μΈμν μ μλλ‘ μ€μ μΆκ°
spring.messages.basename=messages,errors
errors.properties
required.item.itemName=μν μ΄λ¦μ νμμ
λλ€.
range.item.price=κ°κ²©μ {0} ~ {1} κΉμ§ νμ©ν©λλ€.
max.item.quantity=μλμ μ΅λ {0} κΉμ§ νμ©ν©λλ€.
totalPriceMin=κ°κ²© * μλμ ν©μ {0}μ μ΄μμ΄μ΄μΌ ν©λλ€. νμ¬ κ° = {1}
required.default=κΈ°λ³Έ μ€λ₯ λ©μμ§
required=νμ κ° μ
λλ€.
bindingResult.addError(
new FieldError("item", "itemName", item.getItemName(), false,
new String[]{"required.item.itemName"}, null, null)
);
bindingResult.addError(
new FieldError("item", "price", item.getPrice(), false,
new String[]{"range.item.price"}, // codes : λ©μμ§ μ½λ
new Object[]{1000, 1000000}, // arguments : λ©μμ§μμ μ¬μ©νλ μΈμ
null)
);
rejectValue()
, reject()
BindingResult λ κ²μ¦ν΄μΌ target κ°μ²΄λ₯Ό μκ³ μμ
BindingResult μ rejectValue()
, reject()
λ₯Ό μ¬μ©νλ©΄ FieldError, ObjectError λ₯Ό μ§μ μμ±νμ§ μκ³ , κΉλνκ² κ²μ¦ μ€λ₯λ₯Ό λ€λ£° μ μμ
bindingResult.rejectValue("itemName", "required");
bindingResult.rejectValue("price", "range", new Object[]{1000, 10000000}, "μν κ°κ²© μ€λ₯");
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
...
void rejectValue(
@Nullable String field, // μ€λ₯ νλλͺ
String errorCode, // μ€λ₯ μ½λ(λ©μμ§μ λ±λ‘λ μ½λκ° μλ messageResolver λ₯Ό μν μ€λ₯ μ½λ)
@Nullable Object[] errorArgs, // λ©μμ§μμ μ¬μ©νλ μΈμ
@Nullable String defaultMessage // κΈ°λ³Έ λ©μμ§(μ€λ₯ λ©μμ§λ₯Ό μ°Ύμ μ μμ λ μ¬μ©)
);
void reject(
String errorCode,
@Nullable Object[] errorArgs,
@Nullable String defaultMessage
);
Apply Thymeleaf
νλ μ€λ₯ μ²λ¦¬
rejectedValue()
: μ€λ₯ λ°μ μ μ¬μ©μ μ
λ ₯ κ°μ μ μ₯
bindingResult.rejectValue("price", "range", new Object[]{1000, 10000000}, "μν κ°κ²© μ€λ₯");
<input
type="text"
id="price"
th:field="*{price}"
th:errorclass="field-error"
class="form-control"
placeholder="κ°κ²©μ μ
λ ₯νμΈμ"
/>
<div class="field-error" th:errors="*{price}">κ°κ²© μ€λ₯</div>
th:field
λ νμμλ λͺ¨λΈ κ°μ²΄μ κ°μ μ¬μ©νμ§λ§, μ€λ₯κ° λ°μνλ©΄ FieldError μμ 보κ΄ν κ°μ μ¬μ©ν΄μ κ°μ μΆλ ₯
κΈλ‘λ² μ€λ₯ μ²λ¦¬
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
<div th:if="${#fields.hasGlobalErrors()}">
<p
class="field-error"
th:each="err : ${#fields.globalErrors()}"
th:text="${err}"
>
μ 체 μ€λ₯ λ©μμ§
</p>
</div>
Validation and Error Messages
MessageCodesResolver
κ²μ¦ μ€λ₯ μ½λλ‘ λ©μμ§ μ½λλ€μ μμ±
MessageCodesResolver
λ μΈν°νμ΄μ€, DefaultMessageCodesResolver
λ κΈ°λ³Έ ꡬν체
ObjectError
, FieldError
μ μ£Όλ‘ ν¨κ» μ¬μ©
DefaultMessageCodesResolver κΈ°λ³Έ λ©μμ§ μμ± κ·μΉ
νλ μ€λ₯
νλ μ€λ₯μ κ²½μ° λ€μ μμλ‘ 4κ°μ§ λ©μμ§ μ½λ μμ±
1. code + "." + ObjectName + "." + field
2. code + "." + field
3. code + "." + field type
4. code
---
(example)
error code: required
ObjectName: item
field: itemName
field type: String
1. "required.item.itemName"
2. "required.itemName"
3. "required.java.lang.String"
4. "required"
κ°μ²΄ μ€λ₯
κ°μ²΄ μ€λ₯μ κ²½μ° λ€μ μμλ‘ 2κ°μ§ μμ±
1. code + "." + objectName
2. code
---
(example)
error code: totalPriceMin
ObjectName: item
1. "totalPriceMin.item"
2. "totalPriceMin"
λμ λ°©μ
rejectValue()
, reject()
λ λ΄λΆμμ MessageCodesResolver
μ¬μ© β μ¬κΈ°μ λ©μμ§ μ½λ μμ±
FieldError, ObjectError μμ±μλ₯Ό 보면 μ μ μλ―μ΄, μ¬λ¬ μ€λ₯ μ½λλ₯Ό κ°μ§ μ μμ
MessageCodesResolver
λ₯Ό ν΅ν΄ μμ±λ μμλλ‘ μ€λ₯ μ½λ 보κ΄
1. rejectValue() νΈμΆ
2. MessageCodesResolver λ₯Ό μ¬μ©ν΄μ κ²μ¦ μ€λ₯ μ½λλ‘ λ©μμ§ μ½λλ€μ μμ±
3. new FieldError() λ₯Ό μμ±νλ©΄μ λ©μμ§ μ½λλ€μ 보κ΄
4. th:erros μμ λ©μμ§ μ½λλ€λ‘ λ©μμ§λ₯Ό μμλλ‘ λ©μμ§μμ μ°Ύκ³ μΆλ ₯
μ€λ₯ μ½λ κ΄λ¦¬ μ λ΅
ꡬ체μ μΈ κ² β λ ꡬ체μ μΈ κ²μΌλ‘κ° ν΅μ¬
MessageCodesResolver λ ꡬ체μ μΈ κ²(required.item.itemName)μ λ¨Όμ μμ±νκ³ , λ ꡬ체μ μΈ κ²(required)μ λμ€μ μμ±
μ€μνμ§ μμ λ©μμ§λ λ²μ©μ± μλ κ°λ¨ν λ©μμ§(requried)λ‘, μ€μν λ©μμ§λ νμν λ ꡬ체μ μΌλ‘ μ¬μ©νλ λ°©μμ΄ ν¨κ³Όμ
errors.properties
#==ObjectError==
#Level1
totalPriceMin.item=μνμ κ°κ²© * μλμ ν©μ {0}μ μ΄μμ΄μ΄μΌ ν©λλ€. νμ¬ κ° = {1}
#Level2 - μλ΅
totalPriceMin=μ 체 κ°κ²©μ {0}μ μ΄μμ΄μ΄μΌ ν©λλ€. νμ¬ κ° = {1}
#==FieldError==
#Level1
required.item.itemName=μν μ΄λ¦μ νμμ
λλ€.
range.item.price=κ°κ²©μ {0} ~ {1} κΉμ§ νμ©ν©λλ€.
max.item.quantity=μλμ μ΅λ {0} κΉμ§ νμ©ν©λλ€.
#Level2 - μλ΅
#Level3
required.java.lang.String = νμ λ¬Έμμ
λλ€.
required.java.lang.Integer = νμ μ«μμ
λλ€.
min.java.lang.String = {0} μ΄μμ λ¬Έμλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.
min.java.lang.Integer = {0} μ΄μμ μ«μλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.
range.java.lang.String = {0} ~ {1} κΉμ§μ λ¬Έμλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.
range.java.lang.Integer = {0} ~ {1} κΉμ§μ μ«μλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.
max.java.lang.String = {0} κΉμ§μ μ«μλ₯Ό νμ©ν©λλ€.
max.java.lang.Integer = {0} κΉμ§μ μ«μλ₯Ό νμ©ν©λλ€.
#Level4
required = νμ κ° μ
λλ€.
min= {0} μ΄μμ΄μ΄μΌ ν©λλ€.
range= {0} ~ {1} λ²μλ₯Ό νμ©ν©λλ€.
max= {0} κΉμ§ νμ©ν©λλ€.
ValidationUtils
ValidationUtils μ¬μ© μ
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required", "κΈ°λ³Έ: μν μ΄λ¦μ νμμ
λλ€.");
}
ValidationUtils μ¬μ© ν
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
Spring κ²μ¦ μ€λ₯ λ©μμ§
Spring μ νμ
μ€λ₯κ° λ°μνλ©΄ typeMismatch μ€λ₯ μ½λλ₯Ό μ¬μ©
μ£Όλ‘ νμ
μ λ³΄κ° λ§μ§ μμ κ²½μ° Spring μ§μ κ²μ¦
typeMismatch
μ€λ₯ μ½λκ° MessageCodesResolver
λ₯Ό ν΅ν΄ 4κ°μ§ λ©μμ§ μ½λλ‘ μμ±
typeMismatch.java.lang.Integer
errors.properties
typeMismatch.java.lang.Integer=μ«μλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.
typeMismatch=νμ
μ€λ₯μ
λλ€.
Validator
μ€νλ§μ 체κ³μ μΌλ‘ κ²μ¦ κΈ°λ₯μ μ 곡νκΈ° μν΄ Validator
μΈν°νμ΄μ€ μ 곡
public interface Validator {
boolean supports(Class<?> clazz); // ν΄λΉ κ²μ¦κΈ°λ₯Ό μ§μνλ μ¬λΆ νμΈ
void validate(Object target, Errors errors); // κ²μ¦ λμ κ°μ²΄μ BindingResult
}
κ²μ¦λ‘μ§ λΆλ¦¬μ νΈμΆ
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
}
...
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
itemValidator.validate(item, bindingResult);
...
}
WebDataBinder
WebDataBinder
λ μ€νλ§μ νλΌλ―Έν° λ°μΈλ© μν μ ν΄μ£Όκ³ , κ²μ¦ κΈ°λ₯λ λ΄λΆμ ν¬ν¨
/**
* @InitBinder
* νΉμ 컨νΈλ‘€λ¬μλ§ μ μ©
*/
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
...
/**
* Application.class
* κΈλ‘λ² μ€μ (λͺ¨λ 컨νΈλ‘€λ¬μ λ€ μ μ©)
* μ§μ μ¬μ©νλ κ²½μ°λ λλ¬Ύ
*/
@Override
public Validator getValidator() {
return new ItemValidator();
}
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
...
}
WebDataBinder
μ κ²μ¦κΈ° μΆκ° μ ν΄λΉ 컨νΈλ‘€λ¬μμλ κ²μ¦κΈ°λ₯Ό μλ μ μ©
WebDataBinder
λ₯Ό ν΅ν΄ ItemValidator
νΈμΆ
@Validated
κ²μ¦κΈ°λ₯Ό μ€ννλΌλ μ λ
Έν
μ΄μ
WebDataBinder
μ λ±λ‘ν κ²μ¦κΈ°λ₯Ό μ°Ύμμ μ€ν
μ¬λ¬ κ²μ¦κΈ°κ° λ±λ‘λλ€λ©΄ Validator.supports() λ₯Ό ν΅ν΄ ꡬλΆ
μ¬κΈ°μλ Validator.supports(Item.class) νΈμΆ ν, κ²°κ³Όκ° true μ΄λ―λ‘ ItemValidator μ validate() νΈμΆ
bindingResult μ κ²μ¦ κ²°κ³Όκ° λ΄κΉ
Bean Validation
κ²μ¦ λ‘μ§μ λͺ¨λ νλ‘μ νΈμ μ μ©ν μ μκ² κ³΅ν΅ννκ³ , νμ€ν
μ λ
Έν
μ΄μ
νλλ‘ κ²μ¦ λ‘μ§μ λ§€μ° νΈλ¦¬νκ² μ μ©
νΉμ ν ꡬνμ²΄κ° μλλΌ Bean Validation 2.0(JSR-380)μ΄λΌλ κΈ°μ νμ€
κ²μ¦ μ λ
Έν
μ΄μ
κ³Ό μ¬λ¬ μΈν°νμ΄μ€μ λͺ¨μ
λ§μΉ JPA κ° νμ€ κΈ°μ μ΄κ³ κ·Έ ꡬνμ²΄λ‘ νμ΄λ²λ€μ΄νΈκ° μλ κ²κ³Ό μ μ¬
μΌλ°μ μΌλ‘ μ¬μ©νλ ꡬν체λ Hibernate Validator
Hibernate Validator Reference
Hibernate Validator
Hibernate Validator Guide
Jakarta Bean Validation constraints
dependence
implementation 'org.springframework.boot:spring-boot-starter-validation'
Jakarta Bean Validation
jakarta.validation-api
: Bean Validation μΈν°νμ΄μ€
hibernate-validator
: ꡬν체
κ²μ¦ μ λ
Έν
μ΄μ
μ μ©
@Data
@NoArgsConstructor
public class Item {
private Long id;
@NotBlank // λΉκ° + 곡백 νμ© X
private String itemName;
@NotNull // null νμ© X
@Range(min = 1000, max = 1000000) // λ²μ μμ κ°μ΄μ΄μΌ νμ©
private Integer price;
@NotNull
@Max(9999) // μ΅λ 9999κΉμ§λ§ νμ©
private Integer quantity;
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
...
@Test
void beanValidation() {
/**
* κ²μ¦κΈ° μμ±
*/
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Item item = new Item();
item.setItemName(" ");
item.setPrice(0);
item.setQuantity(10000);
/**
* κ²μ¦ μ€ν
*
* κ²μ¦ λμ(item)μ κ²μ¦κΈ°μ μ½μ
* Set μλ ConstraintViolation μ΄λΌλ κ²μ¦ μ€λ₯κ° λ΄κΉ
* κ²°κ³Όκ° λΉμ΄μμΌλ©΄ κ²μ¦ μ€λ₯κ° μλ κ²
*/
Set<ConstraintViolation<Item>> violations = validator.validate(item);
for (ConstraintViolation<Item> violation : violations) {
System.out.println("violation=" + violation);
System.out.println("violation.message=" + violation.getMessage() + "\n");
}
}
violation=ConstraintViolationImpl{interpolatedMessage='κ³΅λ°±μΌ μ μμ΅λλ€', propertyPath=itemName, rootBeanClass=class com.conquest.spring.validation.domain.Item, messageTemplate='{jakarta.validation.constraints.NotBlank.message}'}
violation.message=κ³΅λ°±μΌ μ μμ΅λλ€
violation=ConstraintViolationImpl{interpolatedMessage='9999 μ΄νμ¬μΌ ν©λλ€', propertyPath=quantity, rootBeanClass=class com.conquest.spring.validation.domain.Item, messageTemplate='{jakarta.validation.constraints.Max.message}'}
violation.message=9999 μ΄νμ¬μΌ ν©λλ€
violation=ConstraintViolationImpl{interpolatedMessage='1000μμ 1000000 μ¬μ΄μ¬μΌ ν©λλ€', propertyPath=price, rootBeanClass=class com.conquest.spring.validation.domain.Item, messageTemplate='{org.hibernate.validator.constraints.Range.message}'}
violation.message=1000μμ 1000000 μ¬μ΄μ¬μΌ ν©λλ€
Apply Bean Validation in Spring
μ€νλ§ λΆνΈλ spring-boot-starter-validation
λΌμ΄λΈλ¬λ¦¬κ° μΆκ°λλ©΄ μλμΌλ‘ Bean Validator
λ₯Ό μΈμ§νκ³ μ€νλ§μ ν΅ν©
Spring Boot λ μλμΌλ‘ LocalValidatorFactoryBean
μ Global Validator
λ‘ λ±λ‘
LocalValidatorFactoryBean
μ Annotation κΈ°λ° κ²μ¦
@Valid
(μλ° νμ€), @Validated
(μ€νλ§ μ μ©) λ§ μ μ©νμ¬ κ²μ¦ μ¬μ© κ°λ₯
κ²μ¦ μ€λ₯ λ°μ μ FieldError, ObjectError λ₯Ό μμ±ν΄μ BindingResult μ λ΄μ μ€λ€.
ItemValidator κ° λ±λ‘λμ΄ μλ€λ©΄ μ€λ₯ κ²μ¦κΈ° μ€λ³΅μ λ§κΈ° μν΄ μ κ±°κ° νμ
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// ..
}
κ²μ¦ μμ
\1. @ModelAttribute κ° νλμ νμ
λ³ν μλ
λ³νμ μ±κ³΅ν νλλ§ BeanValidation μ μ©
μ€ν¨νλ©΄ typeMismatch λ‘ FieldError μΆκ°
\2. BeanValidation
λ°μΈλ©μ μ±κ³΅ν νλλ§ BeanValidation μ μ©
λ°μΈλ©μ μ€ν¨ν νλλ μ μ©νμ§ μμ
μλ¬ μ½λ
μ λ
Έν
μ΄μ
μ€λ₯ μ½λλ₯Ό κΈ°λ°μΌλ‘ MessageCodesResolver λ₯Ό ν΅ν΄ λ€μν λ©μμ§ μ½λκ° μμλλ‘ μμ±
@NotBlank
NotBlank.java.lang.String
@Range
NotBlank.item.itemName=μν μ΄λ¦μ μ
λ ₯ν΄μ£ΌμΈμ.
NotBlank={0} 곡백X
Range={0}, {2} ~ {1} νμ©
Max={0}, μ΅λ {1}
BeanValidation λ©μμ§ μ°Ύλ μμ
μμ±λ λ©μμ§ μ½λ μμλλ‘ messageSource μμ λ©μμ§ μ°ΎκΈ°
μ λ
Έν
μ΄μ
μ message μμ± μ¬μ© β @NotBlank(message = "곡백μ μ
λ ₯ν μ μμ΅λλ€.")
λΌμ΄λΈλ¬λ¦¬κ° μ 곡νλ κΈ°λ³Έ κ° μ¬μ© β "κ³΅λ°±μΌ μ μμ΅λλ€"
κΈλ‘λ² μ€λ₯
@ScriptAssert()
μ¬μ©μ μ μ½μ΄ λ§κ³ κ²μ¦ κΈ°λ₯μ΄ ν΄λΉ κ°μ²΄ λ²μλ₯Ό λμ΄μ€ κ²½μ° λμμ΄ μ΄λ ΅λ€.
κΈλ‘λ² μ€λ₯ κ΄λ ¨ λΆλΆλ§ μλ° μ½λλ‘ μμ±νλ κ²μ κΆμ₯
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
groups
λ±λ‘μμ κ²μ¦ν κΈ°λ₯κ³Ό μμ μμ κ²μ¦ν κΈ°λ₯μ κ°κ° κ·Έλ£ΉμΌλ‘ λλμ΄ μ μ©
groups κΈ°λ₯μ μ€μ μ μ¬μ©λμ§ μμ
λμ μ€λ¬΄μμλ μ£Όλ‘ λ±λ‘μ© νΌ κ°μ²΄(ItemSaveForm)μ μμ μ© νΌ κ°μ²΄(ItemUpdateForm)λ₯Ό λΆλ¦¬ν΄μ μ¬μ©
μ°Έκ³ . @Valid μλ groups μ μ© κΈ°λ₯μ μ 곡νμ§ μμΌλ―λ‘, groups μ¬μ© μ @Validated λ₯Ό μ¬μ©νμ.
groups μμ±
/**
* μ μ₯μ© groups
*/
public interface SaveCheck {
}
...
/**
* μμ μ© groups
*/
public interface UpdateCheck {
}
...
@Data
@NoArgsConstructor
public class Item {
@NotNull(groups = UpdateCheck.class) // μμ μμλ§ μ μ©
private Long id;
@NotBlank(message = "{0} 곡백μ μ
λ ₯ν μ μμ΅λλ€.", 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(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
validation interface λͺ
μ
@PostMapping("/add")
public String addItem(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//...
}
...
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
//...
}
Form μ μ‘ κ°μ²΄ λΆλ¦¬ π
groups κΈ°λ₯μ μ¬μ©νλ©΄ μ λ°μ μΈ λ³΅μ‘λκ° μμΉν΄μ μ€λ¬΄μμλ μ£Όλ‘ νΌ κ°μ²΄λ₯Ό λΆλ¦¬ν΄μ μ¬μ©
λ±λ‘κ³Ό μμ μ λ€λ£¨λ λ°μ΄ν° λ²μμ μ°¨μ΄κ° μλ€λ³΄λ μμ ν λ€λ₯Έ λ°μ΄ν°κ° λμ΄μ¨λ€.
λ°λΌμ, Save/Update λ³λμ κ°μ²΄λ‘ λ°μ΄ν°λ₯Ό μ λ¬λ°λ κ²μ΄ μ’λ€.
νΌ λ°μ΄ν° μ λ¬μ μν λ³λμ κ°μ²΄λ₯Ό μ¬μ©νλ©΄ λ±λ‘, μμ μ΄ μμ ν λΆλ¦¬λκΈ° λλ¬Έμ groups μ μ©μ΄ λΆνμ
Form μ μ‘ κ°μ²΄ λΆλ¦¬
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
}
...
@Data
public class ItemSaveForm {
@NotBlank(message = "{0} 곡백μ μ
λ ₯ν μ μμ΅λλ€.")
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(message = "{0} 곡백μ μ
λ ₯ν μ μμ΅λλ€.")
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
private Integer quantity;
}
λΆλ¦¬λ μ μ‘ κ°μ²΄ μ μ©
MVC Model μ item μΌλ‘ λ΄κΈ°λλ‘ νκΈ° μν΄ @ModelAttribute("item") μ μ©
@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());
...
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
...
Item itemParam = new Item();
itemParam.setItemName(form.getItemName());
itemParam.setPrice(form.getPrice());
itemParam.setQuantity(form.getQuantity());
...
}
HTTP Message Converter
@Valid
, @Validated
λ HttpMessageConverter(@RequestBody
)μλ μ μ© κ°λ₯
@ModelAttribute
λ HTTP μμ² νλΌλ―Έν°(URL 쿼리 μ€νΈλ§, POST Form)λ₯Ό λ€λ£° λ μ¬μ©
@RequestBody
λ HTTP Bodyμ λ°μ΄ν°λ₯Ό κ°μ²΄λ‘ λ³νν λ μ¬μ©(μ£Όλ‘ API JSON μμ²)
@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@PostMapping("/add")
public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult) {
// ...
if (bindingResult.hasErrors()) {
return bindingResult.getAllErrors();
}
// ...
}
}
API μμ² μΌμ΄μ€
μ€ν¨ μμ²: JSON μ κ°μ²΄λ‘ μμ±νλ κ² μμ²΄κ° μ€ν¨
JSON parse error: Cannot deserialize value of type `java.lang.Integer` from String "A": not a valid `java.lang.Integer` value]
{
"timestamp": "2023-07-07T00:00:00.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/validation/api/items/add"
}
κ²μ¦ μ€λ₯ μμ²: JSONμ κ°μ²΄λ‘ μμ±νλ κ²μ μ±κ³΅νμ§λ§, κ²μ¦μμ μ€ν¨
μ€λ¬΄μμλ νμν λ°μ΄ν°λ§ λ½μ λ³λ API μ€νμ μ μνκ³ κ°μ²΄λ₯Ό λ§λ€μ΄μ μ€λ₯ μ 보 λ°ν
Field error in object 'itemSaveForm' on field 'quantity': rejected value [10000]; codes [Max.itemSaveForm.quantity,Max.quantity,Max.java.lang.Integer,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [itemSaveForm.quantity,quantity]; arguments []; default message [quantity],9999]; default message [9999 μ΄νμ¬μΌ ν©λλ€]
[
{
"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"
}
]
@ModelAttribute VS @RequestBody
HttpMessageConverter
λ κ° νλ λ¨μλ‘ μ μ©λλ κ²μ΄ μλλΌ, μ 체 κ°μ²΄ λ¨μ
λ‘ μ μ©
λ°λΌμ, λ©μμ§ μ»¨λ²ν° μλμ΄ μ±κ³΅ν΄μ κ°μ²΄λ₯Ό μμ±ν΄μΌ @Valid
, @Validated
κ° μ μ©
@ModelAttribute
HTTP μμ² ν리미ν°λ₯Ό μ²λ¦¬
ModelAttribute λμ κ°μ²΄λ Setter
λ©μλ νμ
νλ λ¨μλ‘ μ κ΅νκ² λ°μΈλ©μ΄ μ μ©
νΉμ νλκ° λ°μΈλ© λμ§ μλλΌλ λλ¨Έμ§ νλλ μ μ λ°μΈλ© λκ³ , @Validator λ₯Ό μ¬μ©ν κ²μ¦λ μ μ© κ°λ₯
@RequestBody
HttpMessageConverter λ¨κ³μμ JSON λ°μ΄ν°λ₯Ό κ°μ²΄λ‘ λ³κ²½ μ€ν¨νλ©΄ μ΄ν λ¨κ³κ° μ§νλμ§ μκ³ 400 Bad Request μμΈ λ°μ
νμ
μ€λ₯κ° λ°μν κ²½μ° μ»¨νΈλ‘€λ¬λ νΈμΆλμ§ μκ³ , Validator λ μ μ© λΆκ°
{
"timestamp": "2023-07-07T04:36:11.489+00:00",
"status": 400,
"error": "Bad Request",
"path": "/validation/api/items/add"
}
Ideal result code..