๊ฒ์ฆ ๊ธฐ๋ฅ์ ์ง๊ธ์ฒ๋ผ ๋งค๋ฒ ์ฝ๋๋ก ์์ฑํ๋ ๊ฒ์ ์๋นํ ๋ฒ๊ฑฐ๋กญ๋คโน๏ธ
ํนํ ํน์ ํ๋์ ๋ํ ๊ฒ์ฆ ๋ก์ง์
๋๋ถ๋ถ ๋น ๊ฐ์ธ์ง ์๋์ง, ํน์ ํฌ๊ธฐ๋ฅผ ๋๋์ง ์๋์ง์ ๊ฐ์ด ๋งค์ฐ ์ผ๋ฐ์ ์ธ ๋ก์ง์ด๋ค.
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
//...
โฌ๏ธ ์ ์ฝ๋์ ๊ฐ์ด, ๊ฒ์ฆ ๋ก์ง์ ๋ชจ๋ ํ๋ก์ ํธ์ ์ ์ฉํ ์ ์๊ฒ ๊ณตํตํ, ํ์คํ ํ ๊ฒ์ ๋ฐ๋ก Bean Validation์ด๋ผ๊ณ ํ๋ค!
๐ก Bean Validation ์ด๋โ
๋จผ์ Bean Validation์ ํน์ ํ ๊ตฌํ์ฒด๊ฐ ์๋๋ผ Bean Validation 2.0(JSR-380)์ด๋ผ๋ ๊ธฐ์ ํ์ค์ด๋ค.
์ฝ๊ฒ ์ด์ผ๊ธฐํด์ ๊ฒ์ฆ ์ ๋ ธํ ์ด์ ๊ณผ ์ฌ๋ฌ ์ธํฐํ์ด์ค์ ๋ชจ์์ด๋ค.
๋ง์น JPA๊ฐ ํ์ค ๊ธฐ์ ์ด๊ณ ๊ทธ ๊ตฌํ์ฒด๋ก ํ์ด๋ฒ๋ค์ดํธ๊ฐ ์๋ ๊ฒ๊ณผ ๊ฐ๋ค!
๐ ํ์ด๋ฒ๋ค์ดํธ Validator ๊ด๋ จ ๋งํฌ
Bean Validation ๊ธฐ๋ฅ์ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์ฝ๋๋ก ์์๋ณด์!
๋จผ์ ์คํ๋ง๊ณผ ํตํฉํ์ง ์๊ณ , ์์ํ Bean Validation ์ฌ์ฉ๋ฒ ๋ถํฐ ํ
์คํธ ์ฝ๋๋ก ์์๋ณด์๐
build.gradle
โก๏ธ implementation 'org.springframework.boot:spring-boot-starter-validation'
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
๊ฒ์ฆ ์ ๋ ธํ ์ด์
@NotBlank
: ๋น๊ฐ + ๊ณต๋ฐฑ๋ง ์๋ ๊ฒฝ์ฐ๋ฅผ ํ์ฉํ์ง ์๋๋ค.@NotNull
: null ์ ํ์ฉํ์ง ์๋๋ค.@Range(min = 1000, max = 1000000)
: ๋ฒ์ ์์ ๊ฐ์ด์ด์ผ ํ๋ค. @Max(9999)
: ์ต๋ 9999๊น์ง๋ง ํ์ฉํ๋คpackage hello.itemservice.validation;
import hello.itemservice.domain.item.Item;
import org.junit.jupiter.api.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
public class BeanValidationTest {
@Test
void beanValidation() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Item item = new Item();
item.setItemName(" "); //๊ณต๋ฐฑ
item.setPrice(0);
item.setQuantity(10000);
Set<ConstraintViolation<Item>> violations = validator.validate(item);
for (ConstraintViolation<Item> violation : violations) {
System.out.println("violation = " + violation);
System.out.println("violation = " + violation.getMessage());
}
}
}
1๏ธโฃ ๊ฒ์ฆ๊ธฐ ์์ฑ
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
โฌ๏ธ ์์ ๊ฐ์ด ๊ฒ์ฆ๊ธฐ๋ฅผ ์์ฑํ๋ค!
(์ดํ ์คํ๋ง๊ณผ ํตํฉํ๋ ๊ณผ์ ์์ ์ด ์ฝ๋๋ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์, ๊ทธ๋ฅ ์ด๋ฐ๊ฒ ์๊ตฌ๋~ ์ ๋๋ก ์๊ณ ๋์ด๊ฐ์๐)
2๏ธโฃ ๊ฒ์ฆ ์คํ
Set<ConstraintViolation<Item>> violations = validator.validate(item);
โฌ๏ธ ๊ฒ์ฆ ๋์(item
)์ ์ง์ ๊ฒ์ฆ๊ธฐ์ ๋ฃ๊ณ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ๋๋ค. Set
์๋ ConstraintViolation
์ด๋ผ๋ ๊ฒ์ฆ ์ค๋ฅ๊ฐ ๋ด๊ธฐ๋๋ฐ, ๊ฒฐ๊ณผ๊ฐ ๋น์ด์๋ค๋ฉด, ๊ฒ์ฆ ์ค๋ฅ๊ฐ ์๋ ๊ฒ์ด๋ค!
์์ ๋ง๋ ๊ธฐ๋ฅ์ ์ ์งํ๊ธฐ ์ํด, ์ปจํธ๋กค๋ฌ์ ํ ํ๋ฆฟ ํ์ผ์ ๋ณต์ฌํ์!
1๏ธโฃ ValidationItemControllerV3
์ปจํธ๋กค๋ฌ ์์ฑ
hello.itemservice.web.validation.ValidationItemControllerV2
๋ณต์ฌhello.itemservice.web.validation.ValidationItemControllerV3
๋ถ์ฌ๋ฃ๊ธฐ2๏ธโฃ ํ ํ๋ฆฟ ํ์ผ ๋ณต์ฌ
validation/v2
๋๋ ํ ๋ฆฌ์ ๋ชจ๋ ํ
ํ๋ฆฟ ํ์ผ์ validation/v3
๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌ/resources/templates/validation/v3/
ํ์ 4๊ฐ ํ์ผ ๋ชจ๋ URL ๊ฒฝ๋ก ๋ณ๊ฒฝ3๏ธโฃ ์คํ
-http://localhost:8080/validation/v3/items
์คํ ํ ์น ๋ธ๋ผ์ฐ์ ์ URL์ด validation/v3
์ผ๋ก ์ ์ ์ง๋๋์ง ํ์ธ!
package hello.itemservice.web.validation;
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import hello.itemservice.domain.item.SaveCheck;
import hello.itemservice.domain.item.UpdateCheck;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.validation.Valid;
import java.util.List;
@Slf4j
@Controller
@RequestMapping("/validation/v3/items")
@RequiredArgsConstructor
public class ValidationItemControllerV3 {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "validation/v3/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v3/item";
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v3/addForm";
}
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
//๊ฒ์ฆ์ ์คํจํ๋ฉด ๋ค์ ์
๋ ฅ ํผ์ผ๋ก
if (bindingResult.hasErrors()) {
log.info("errors={} ", bindingResult);
return "validation/v3/addForm";
}
//์ฑ๊ณต ๋ก์ง
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v3/editForm";
}
// @PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute Item item, BindingResult bindingResult) {
//ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/editForm";
}
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
//ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/editForm";
}
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
}
๊ฒ์ฆ๊ธฐ ๋ถ๋ถ์ ๋นผ๊ณ , addItemV1~V5 ๋ฅผ ๋ชจ๋ ์ง์ด ๋ค,
addItemV6
โก๏ธ addItem
์ผ๋ก ๋ฐ๊พผ๋ค!!
๊ทธ๋ฐ๋ฐ ์คํํด๋ณด๋ฉด ๊ฒ์ฆ๊ธฐ ๋ถ๋ถ์ด ๋น ์ก์์๋ Bean Validator ๊ฐ ์ ๋์ํ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค๐ค
โก๏ธ ์คํ๋ง ๋ถํธ๊ฐ spring-boot-starter-validation
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ฃ์ผ๋ฉด ์๋์ผ๋ก Bean Validator๋ฅผ ์ธ์งํ๊ณ ์คํ๋ง์ ํตํฉํ๋คโ๏ธ
โก๏ธ LocalValidatorFactoryBean
์ ๊ธ๋ก๋ฒ Validator
๋ก ๋ฑ๋กํ๋ค.
โก๏ธ ์ด Validator
๋ @NotNull
๊ฐ์ ์ ๋
ธํ
์ด์
์ ๋ณด๊ณ ๊ฒ์ฆ์ ์ํํ๋ค.
โก๏ธ ์ด๋ ๊ฒ ๊ธ๋ก๋ฒ Validator
๊ฐ ์ ์ฉ๋์ด ์๊ธฐ ๋๋ฌธ์ @Valid
, @Validated
๋ง ์ ์ฉํ๋ฉด ๋๋ค.
1๏ธโฃ @ModelAttribute
๊ฐ๊ฐ์ ํ๋์ ํ์
๋ณํ ์๋
typeMismatch
๋ก FieldError
์ถ๊ฐ2๏ธโฃ Validator ์ ์ฉ
๐ก@ModelAttribute
โก๏ธ ๊ฐ๊ฐ์ ํ๋ ํ์
๋ณํ์๋ โก๏ธ ๋ณํ์ ์ฑ๊ณตํ ํ๋๋ง BeanValidation
์ ์ฉ๐ก
Bean Validation์ด ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ์ข ๋ ์์ธํ ๋ณ๊ฒฝํ๊ณ ์ถ์ผ๋ฉด ์ด๋ป๊ฒ ํ ๊น๐คโ
Bean Validation
์ ์ ์ฉํ๊ณ bindingResult
์ ๋ฑ๋ก๋ ๊ฒ์ฆ ์ค๋ฅ ์ฝ๋๋ฅผ ๋ณด์.typeMismatch
์ ์ ์ฌํ๋ค!NotBlank
๋ผ๋ ์ค๋ฅ ์ฝ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก MessageCodesResolver
๋ฅผ ํตํด ๋ค์ํ ๋ฉ์์ง ์ฝ๋๊ฐ ์์๋๋ก ์์ฑ๋๋ค.@NotBlank
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
@Range
- Range.item.price
- Range.price
- Range.java.lang.Integer
- Range
errors.properties
์์
#Bean Validation ์ถ๊ฐ
NotBlank={0} ๊ณต๋ฐฑX
Range={0}, {2} ~ {1} ํ์ฉ
Max={0}, ์ต๋ {1}
โฌ๏ธ {0}
์ ํ๋๋ช
์ด๊ณ , {1}
, {2}
...์ ๊ฐ ์ ๋
ธํ
์ด์
๋ง๋ค ๋ค๋ฆ
1๏ธโฃ ์์ฑ๋ ๋ฉ์์ง ์ฝ๋ ์์๋๋ก messageSource
์์ ๋ฉ์์ง ์ฐพ๊ธฐ
2๏ธโฃ ์ ๋
ธํ
์ด์
์ message
์์ฑ ์ฌ์ฉ โก๏ธ @NotBlank(message = "๊ณต๋ฐฑ! {0}")
3๏ธโฃ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ๊ฐ ์ฌ์ฉ โก๏ธ ๊ณต๋ฐฑ์ผ ์ ์์ต๋๋ค.
**Bean Validation์์ ํน์ ํ๋(FieldError
)๊ฐ ์๋ ํด๋น ์ค๋ธ์ ํธ ๊ด๋ จ ์ค๋ฅ(ObjectError
)๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ ์์๊น๐คโ
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
//...
}
โฌ๏ธ @ScriptAssert()
๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋คโ๏ธ
โฌ๏ธ ์คํํด๋ณด๋ฉด ์ ๋์ํ์ง๋ง, ์ค์ ๋ก ์ฌ์ฉํ๊ธฐ์๋ ์ ์ฝ์ด ๋ง๊ณ ๋ณต์กํ๋คโน๏ธ
๋ฐ๋ผ์ ์ค๋ธ์ ํธ ์ค๋ฅ(๊ธ๋ก๋ฒ ์ค๋ฅ)์ ๊ฒฝ์ฐ @ScriptAssert
์ ์ต์ง๋ก ์ฌ์ฉํ๋ ๊ฒ ๋ณด๋ค๋ ๋ค์๊ณผ ๊ฐ์ด ์ค๋ธ์ ํธ ์ค๋ฅ ๊ด๋ จ ๋ถ๋ถ๋ง ์ง์ ์๋ฐ ์ฝ๋๋ก ์์ฑํ๋ ๊ฒ์ ๊ถ์ฅํ๋ค!
.
.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
โฌ๏ธ ์คํ ๊ตฌ์ฐ์ฐ์
์์ ์๋ ๊ฒ์ฆ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์๐
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute Item item, BindingResult bindingResult) {
// ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/editForm";
}
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
edit()
: Item
๋ชจ๋ธ ๊ฐ์ฒด์ @Validated
๋ฅผ ์ถ๊ฐeditForm
์ผ๋ก ์ด๋ํ๋ ์ฝ๋ ์ถ๊ฐvalidation/v3/editForm.html
๋ณ๊ฒฝ<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
โฌ๏ธ .field-error
css ์ถ๊ฐ
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err :${#fields.globalErrors()}" th:text="${err}">๊ธ๋ก๋ฒ ์ค๋ฅ ๋ฉ์์ง</p>
</div>
โฌ๏ธ ๊ธ๋ก๋ฒ ์ค๋ฅ ๋ฉ์์ง
โฌ๏ธ ์คํ ๊ฒฐ๊ณผ ๐๐ป
quantity
์๋์ ์ต๋ 9999๊น์ง ๋ฑ๋กํ ์ ์์ง๋ง ์์ ์์๋ ์๋์ ๋ฌด์ ํ์ผ๋ก ๋ณ๊ฒฝํ ์ ์๋ค.id
๊ฐ์ด ์์ด๋ ๋์ง๋ง, ์์ ์์๋ id ๊ฐ์ด ํ์์ด๋ค.@Data
public class Item {
@NotNull // ์์ ์๊ตฌ์ฌํญ ์ถ๊ฐ
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
// @Max(9999) // ์์ ์๊ตฌ์ฌํญ ์ถ๊ฐ
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
id
: @NotNull
์ถ๊ฐquantity
: @Max(9999)
์ ๊ฑฐ์คํํด๋ณด๋ฉด ์์ ์ ์ ์ ์๋ํ๋๋ฐ, ๋ฑ๋ก์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๐
๋ฑ๋ก์ ํ๋ฉด์ด ๋์ด๊ฐ์ง ์์ผ๋ฉด์, 'id': rejected value [null];
์ด๋ฐ ์ค๋ฅ๊ฐ ๋์ค๋๋ฐ,
โก๏ธ ๋ฑ๋ก์์๋ id ์ ๊ฐ์ด ์๋๋ฐ @NotNull id
๋ฅผ ์ ์ฉํ ๊ฒ ๋๋ฌธ์ ๊ฒ์ฆ์ ์คํจํ๊ณ ๋ค์ ํผ ํ๋ฉด์ผ๋ก ๋์ด์จ๋ค,, ๊ฒฐ๊ตญ ๋ฑ๋ก ์์ฒด๋ ๋ถ๊ฐ๋ฅํ๊ณ , ์๋ ์ ํ๋ ๊ฑธ์ง ๋ชปํ๋ค๐ฅบ
์ด ๋ฌธ์ ๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ ์ง๋ ๋ค์ ์๊ฐ์ ๋ฐฐ์๋ณผ ์์ ์ด๋ค๐