DevLog ๐ถ
[Spring] BindingResult๋ฅผ ํ์ฉํด์ ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐํ๊ธฐ, MessageCodesResolver ์์๋ณด๊ธฐ ๋ณธ๋ฌธ
[Spring] BindingResult๋ฅผ ํ์ฉํด์ ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐํ๊ธฐ, MessageCodesResolver ์์๋ณด๊ธฐ
dolmeng2 2022. 8. 22. 00:20๊น์ํ ๋์ '์คํ๋ง MVC 2ํธ - ๋ฐฑ์๋ ์น ๊ฐ๋ฐ ํ์ฉ ๊ธฐ์ '์ ๋ณด๊ณ ์ ๋ฆฌํ ๊ธ์ ๋๋ค ๐
- ์ง๋ ํฌ์คํ ๊ณผ ์ด์ด์ง๋๋ค :D
| ๊ฒ์ฆ ์ถ๊ฐํ๊ธฐ
- ์ง๋ ํฌ์คํ ์ ์ํ ๊ด๋ฆฌ ์์คํ ์ ๊ฐ ํ๋์ ๋ํ ๊ฒ์ฆ ์๊ตฌ์ฌํญ์ด ์ถ๊ฐ๋์๋ค.
- ๊ฐ๊ฒฉ, ์๋์ ๋ฌธ์๊ฐ ๋ค์ด๊ฐ๋ฉด ์ค๋ฅ
- ์ํ๋ช ์ ํ์๋ก
- ๊ฐ๊ฒฉ์ 1,000~1,000,000 ์ด๋ด์ ๊ฐ
- ์๋์ ์ต๋ 9999๊ฐ๊น์ง
- ๊ฐ๊ฒฉ * ์๋์ ๊ฐ์ ํญ์ 10,000 ์ด์์ด๋๋ก
- ๋ง์ฝ, ๊ฒ์ฆ์ ์คํจํ ๊ฒฝ์ฐ ๊ฒ์ฆ ์ค๋ฅ ๊ฒฐ๊ณผ๋ฅผ ํฌํจํ์ฌ ์ํ ๋ฑ๋ก ํผ์ ์ ๋ฌํด์ฃผ์.
[SItemController.java]
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class SItemController {
private final SItemService service;
@GetMapping
public String items(Model model) {
List<SItem> items = service.findAllItem();
model.addAttribute("items", items);
model.addAttribute("userName", "Spring");
return "basic/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model) {
SItem item = service.findByItemId(itemId);
model.addAttribute("item", item);
return "basic/item";
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new SItem());
return "basic/addForm";
}
@PostMapping("/add")
public String addItemV2 (@ModelAttribute("item") SItem item, Model model, RedirectAttributes redirectAttributes) {
// ๊ฒ์ฆ ์ค๋ฅ ๋ฉ์์ง ์ ์ฅ
Map<String, String> errors = service.verifyItemInfo(item);
if(!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
SItem item = service.findByItemId(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, SItem item, Model model) {
service.updateItem(itemId, item);
model.addAttribute("item", item);
return "redirect:/basic/items/{itemId}";
}
@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "์์ธ");
regions.put("BUSAN", "๋ถ์ฐ");
regions.put("JEJU", "์ ์ฃผ");
return regions;
}
@ModelAttribute("itemTypes")
public SItemType[] itemTypes() {
return SItemType.values();
}
@ModelAttribute("deliveryCodes")
public List<DeliveryCode> deliveryCodes() {
return CreateDeliveryCode.getCodes();
}
@NoArgsConstructor
static class CreateDeliveryCode {
private static final List<DeliveryCode> deliveryCodes
= Arrays.asList(new DeliveryCode("FAST", "๋น ๋ฅธ ๋ฐฐ์ก"),
new DeliveryCode("NORMAL", "์ผ๋ฐ ๋ฐฐ์ก"),
new DeliveryCode("SLOW", "๋๋ฆฐ ๋ฐฐ์ก"));
public static List<DeliveryCode> getCodes() {
return deliveryCodes;
}
}
// ํ
์คํธ ๋ฐ์ดํฐ ์ถ๊ฐ (์์กด๊ด๊ณ ์ฃผ์
์ดํ ์คํ)
@PostConstruct
public void init() {
service.saveItem(new SItem("itemA", 10000, 10));
service.saveItem(new SItem("itemB", 20000, 20));
}
}
- ์ฐ์ , ์ ๋ฐ์ ์ผ๋ก Controller -> Repository๋ก ๋ฐ๋ก ์ ๊ทผํ๊ฒ ํ์ง ์๊ณ Service๋จ์ ์ถ๊ฐ๋ก ๋์๋ค.
- addForm์ ๊ฒ์ฆ ๋ก์ง์ด ์ถ๊ฐ๋์๋ค. ๋ง์ฝ ์ค๋ฅ ๋ฉ์์ง๊ฐ ์กด์ฌํ๋ค๋ฉด model์ error๋ฅผ ๋ด์ ์ ๋ฌํ์๋ค.
[SItemService.java]
@Service
@RequiredArgsConstructor
public class SItemService {
private final SItemRepository repository;
public List<SItem> findAllItem() {
return repository.findAll();
}
public SItem findByItemId(Long itemId) {
return repository.findById(itemId);
}
public Map<String, String> verifyItemInfo(SItem item) {
Map<String, String> errors = new ConcurrentHashMap<>();
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() >= 10000) {
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 + "์");
}
}
return errors;
}
public SItem saveItem(SItem item) {
return repository.save(item);
}
public void updateItem(Long itemId, SItem item) {
repository.update(itemId, item);
}
}
- verifyItemInfo๊ฐ ๋ฐ๋ก ๊ฒ์ฆ ๋ก์ง์ด๋ค.
- ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ๊ฐ๊ฐ์ ๊ฒ์ฆํ ๋ค์, ์๋ฌ ๋ฉ์์ง๋ฅผ Map์ ๋ด์ ๋ฆฌํดํด์ฃผ์๋ค.
[addForm.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<title>Item Save Form</title>
<style>
.field-error{
border-color:#dc3545;
color:#dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="mt-5 py-2 text-center">
<h2 th:text="#{page.addItem}" class="font-monospace fw-bold">Item Save Form</h2>
</div>
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error text-center" th:text="${errors['globalError']}">
์ ์ฒด ์ค๋ฅ ๋ฉ์์ง</p>
</div>
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="itemName"
th:text="#{label.item.itemName}">Name</label>
<input type="text"
th:field="*{itemName}"
th:class="${errors?.containsKey('itemName')} ? 'form-control text-center field-error' : 'text-center form-control'"
class="form-control text-center"
placeholder="Please Enter an Item Name.">
<div class="field-error"
th:if="${errors?.containsKey('itemName')}"
th:text="${errors['itemName']}">
์ํ๋ช
์ค๋ฅ
</div>
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price"
th:text="#{label.item.price}">Price</label>
<input type="text"
th:field="*{price}"
th:class="${errors?.containsKey('price')} ? 'form-control text-center field-error' : 'text-center form-control'"
class="form-control text-center"
placeholder="Please Enter an Item Price.">
<div class="field-error"
th:if="${errors?.containsKey('price')}"
th:text="${errors['price']}">
๊ฐ๊ฒฉ ์ค๋ฅ
</div>
</div>
<div class="col text-center">
<label for="quantity"
th:text="#{label.item.quantity}">Quantity</label>
<input type="text"
th:field="*{quantity}"
th:class="${errors?.containsKey('quantity')} ? 'form-control text-center field-error' : 'text-center form-control'"
class="form-control text-center"
placeholder="Please Enter an Item Quantity.">
<div class="field-error"
th:if="${errors?.containsKey('quantity')}"
th:text="${errors['quantity']}">
์๋ ์ค๋ฅ
</div>
</div>
</div>
...
</form>
</div>
</body>
</html>
- ๋ณ๊ฒฝ ํ ๋ค์๊ณผ ๊ฐ์ด ๋ฉ์์ง๊ฐ ์ถ๋ ฅ๋๋ค.
- ๋ํ, ํ์ด์ง๊ฐ ๋์ด๊ฐ์ง ์๊ณ ๋ค์ saveForm์ด ๋์ค๋ ๊ฒ์ด๋ค.
- ๋ํ, ์์ ์ฝ๋์์ ํ ๊ฐ์ง ๋ฐ๊ฒฌํ ์ ์๋ ์ ์ด ์๋๋ฐ errors?.containsKey('globalError')}์ด๋ค.
์ฌ๊ธฐ์ ?์ ์จ์ค ์ด์ ๋ errors๊ฐ ์์ฑ๋์ง ์์์ ๋ ๋ด๋ถ ๋ฉ์๋์ธ .containsKey๋ฅผ ํธ์ถํ๋ฉด NPE๊ฐ ๋จ๊ธฐ ๋๋ฌธ!
(์๋๋ฉด errors๋ ์ฌ์ฉ์๊ฐ ์ ์ถ์ ํ์ ๋ ๊ฒ์ฆ ๋ก์ง์ ๋๋ฉด์ ์๊ธฐ๋๊น)
- ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด errors๊ฐ null์ด๋ผ๋ฉด null์ ๋ฐํํ๋๋ก ์ฒ๋ฆฌํ์๋ค. ์ด์ฐจํผ th:if์์ null์ด ๋ค์ด๊ฐ๋ฉด ์คํจ๋๊น.
โ ๋์ค์ ์ฐธ๊ณ ํ SpringEL ๋ฌธ๋ฒ ๋ชจ์
- ๊ธฐ๋ณธ ๊ฒ์ฆ์ ์๋ฃํ๋ค. ์ด์ , ํ์ ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํด๋ณด์.
- ํ์ฌ๋ ์๋ชป๋ ํ์ ์ ๋ฃ์ผ๋ฉด ์ปจํธ๋กค๋ฌ ์ง์ ์ด์ ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ ๋ณ๋ค๋ฅธ ์ฒ๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅํ๋ค.
- ์ฐ๋ฆฌ๋ ์ด๋ฅผ ์ํด ์คํ๋ง์์ ์ ๊ณตํ๋ ๊ฒ์ฆ ๋ฐฉ๋ฒ์ ์ด์ฉํด๋ณผ ๊ฒ์ด๋ค.
[SItemController.java] - ์์
@PostMapping("/add")
public String addItemV2 (@ModelAttribute("item") SItem item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "์ํ ์ด๋ฆ์ ํ์๋ก ์
๋ ฅํด์ฃผ์ธ์."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", "๊ฐ๊ฒฉ์ 1,000~1,000,000์๊น์ง ํ์ฉ๋ฉ๋๋ค."));
}
if (item.getQuantity() == null || item.getQuantity() >= 10000) {
bindingResult.addError(new FieldError("item", "quantity", "์๋์ ์ต๋ 9,999๊ฐ๊น์ง ํ์ฉ๋ฉ๋๋ค."));
}
//ํน์ ํ๋ ์์ธ๊ฐ ์๋ ์ ์ฒด ์์ธ
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", "์ด์ก(๊ฐ๊ฒฉ*์๋)์ 10,000์ ์ด์์ด์ด์ผ ํฉ๋๋ค. ํ์ฌ ๊ธ์ก = " + resultPrice + "์"));
}
}
if (bindingResult.hasErrors()) {
return "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
์ฝ๋๋ฅผ ์์ ํด์ฃผ์๋ค.
โญ ์ฌ๊ธฐ์ BindingResult๋ @ModelAttribute ์ด๋ ธํ ์ด์ ์ด ๋ถ์ ๊ฐ์ฒด ๋ค์์ ์์นํด์ผ ํ๋ค!
FieldError์๋ ์์ฐจ์ ์ผ๋ก objectName, field, defaultMessage๊ฐ ๋ค์ด๊ฐ๋ค.
objectName์๋ @ModelAttribute์ ์ด๋ฆ, field๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ ํ๋์ ์ด๋ฆ, defaultMessage๋ ์ค๋ฅ ๋ฉ์์ง์ด๋ค.
ObjectError์๋ ์ฌ๋ฌ ํ๋์ ๋ํ ์ค๋ฅ ๊ฒ์ฆ ์ ์ฌ์ฉํ๋ค. field๋ฅผ ์๋ตํ ์ ์๋ค. ๊ด๋ จํด์๋ ๋ค์์ ๋ ์ค๋ช ํ๊ฒ ๋ค.
[addForm.html]
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error text-center"
th:each="err: ${#fields.globalErrors()}"
th:text="${err}">
์ ์ฒด ์ค๋ฅ ๋ฉ์์ง</p>
</div>
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="itemName"
th:text="#{label.item.itemName}">Name</label>
<input type="text"
th:field="*{itemName}"
th:errorclass="field-error"
class="form-control text-center"
placeholder="Please Enter an Item Name.">
<div class="field-error"
th:errors="*{itemName}">
์ํ๋ช
์ค๋ฅ
</div>
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price"
th:text="#{label.item.price}">Price</label>
<input type="text"
th:field="*{price}"
th:errorclass="field-error"
class="form-control text-center"
placeholder="Please Enter an Item Price.">
<div class="field-error"
th:errors="*{price}">
๊ฐ๊ฒฉ ์ค๋ฅ
</div>
</div>
<div class="col text-center">
<label for="quantity"
th:text="#{label.item.quantity}">Quantity</label>
<input type="text"
th:field="*{quantity}"
th:errorclass ="field-error"
class="form-control text-center"
placeholder="Please Enter an Item Quantity.">
<div class="field-error"
th:errors="*{quantity}">
์๋ ์ค๋ฅ
</div>
</div>
</div>
...
- BIndingResult๊ฐ ์ ๊ณตํ๋ ์ค๋ฅ๋ #fields๋ก ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.
- th:each="err: ${#fields.globalErrors()} -> ๊ธ๋ก๋ฒ ์๋ฌ๊ฐ ์๋์ง boolean ๊ฐ์ผ๋ก ๋ฐํํ๋ค. ์๋ค๋ฉด ๋ฐ๋ณต.
- ์ฌ๊ธฐ์, ๊ธ๋ก๋ฒ ์ค๋ฅ๋ form๊ณผ ๊ด๋ จ๋ ์ค๋ฅ๋ก, #fields๋ฅผ ํตํด ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.
- ๋ชจ๋ ์ค๋ฅ๋ฅผ ํ์ธํ๊ณ ์ถ๋ค๋ฉด allErrors()๋ฅผ ์ฌ์ฉํด๋ ๋๋ค.
- th:errors๋ฅผ ์ด์ฉํด ํ๋ ๋จ์๋ก ์ค๋ฅ๊ฐ ์์ ๊ฒฝ์ฐ ํ๊ทธ ์ถ๋ ฅ์ ๊ฒฐ์ ํ ์ ์๋ค. (th:if์ ๊ฐ์ ๋๋)
- th:errors="*{itemName}": bindingResult์ ๋ด๊ธด field ๊ฐ ์ค 'ItemName'์ error๊ฐ ์๋์ง ํ์ธ.
- th:errorclass๋ฅผ ํตํด์ th:field์์ ์ง์ ํ ํ๋์ ์ค๋ฅ๊ฐ ์๋ค๋ฉด class ์ ๋ณด๋ฅผ ์ถ๊ฐํ ์ ์๋ค.
| BindingResult
- @ModelAttribute์ ๋ฐ์ธ๋ฉ ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ BindingResult์ ์ค๋ฅ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค.
- โญ ๋ง์ฝ BindingResult๊ฐ ์๋ค๋ฉด 400 ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ณ , ์ปจํธ๋กค๋ฌ ํธ์ถ ์์ด ๋ฐ๋ก ์ค๋ฅ ํ์ด์ง๋ก ์ด๋ํ๋ค.
- ์ฐ๋ฆฌ์ ์ฝ๋์ฒ๋ผ ์ง์ ์์ฑํ๊ฑฐ๋, ์์ฑํ์ง ์์ผ๋ฉด ์คํ๋ง์ด ์ง์ FieldErro๋ฅผ ๋ง๋ ๋ค.
- ํน์, Validator๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์๋ค.
- ์ฐธ๊ณ ๋ก, BindingResult๋ Errors๋ฅผ ์์๋ฐ๊ณ ์๊ธฐ ๋๋ฌธ์, Errors๋ฅผ ๋์ ์ฌ์ฉํด๋ ๋๋ค.
- ๊ทธ๋ฌ๋, BindingResult๊ฐ addError ๊ฐ์ ๋ถ๊ฐ ๊ธฐ๋ฅ์ ์ข ๋ ์ ๊ณตํ๋ค.
- ์ฐ๋ฆฌ๊ฐ ์ง ์ฝ๋์์ ์กฐ๊ธ ๋ ๋ฐ์ ์์ผ, ์๋ชป๋ ๊ฐ์ ์ ๋ ฅํ๋๋ผ๋ ๊ฐ์ด ๋จ์์๋๋ก ํด๋ณด์.
@PostMapping("/add")
public String addItemV2 (@ModelAttribute("item") SItem item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
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() > 10000) {
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()) {
return "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
FieldError์ ํ๋ผ๋ฏธํฐ๊ฐ ๋ง์์ง ๊ฒ์ ๋ณผ ์ ์๋ค.
objectName (์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฐ์ฒด ์ด๋ฆ - @ModelAttribute ์ด๋ฆ)
field (์ค๋ฅ ํ๋)
rejectedValue (์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ๊ฐ)
bindingFailure (๊ฒ์ฆ ์คํจ ์ ๊ตฌ๋ถ ๊ฐ - boolean์ผ๋ก ๊ตฌ๋ถ)
codes: ๋ฉ์์ง ์ฝ๋
arguments: ๋ฉ์์ง์์ ์ฌ์ฉํ๋ ์ธ์
defaultMessage: ๊ธฐ๋ณธ ์ค๋ฅ ๋ฉ์์ง
bindingFailure์ ๊ฒฝ์ฐ, ํ์ ์ค๋ฅ ๊ฐ์ ๋ฐ์ธ๋ฉ ์คํจ๋ผ๋ฉด true, ๊ฒ์ฆ ์คํจ๋ฉด false๋ฅผ ๋ฃ๋๋ค.
์ฐ๋ฆฌ๋ rejectedValue๋ฅผ ํตํด ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ๊ฐ์ ์ ์ฅํด๋ ์ ์๋ค!
๐ฉ ํ์๋ฆฌํ์ th:field๋ฅผ ์ฌ์ฉํ๋ฉด ์ค๋ฅ ๋ฐ์ ์ FieldError์์ ๋ณด๊ดํ ๊ฐ์ ์ฌ์ฉํ๋ค!
errorCode๋ ์ค๋ฅ ๋ฐ์ ์ ์ค๋ฅ ์ฝ๋๋ฅผ ํตํด ๋ฉ์์ง๋ฅผ ์ฐพ์ ์ ์๋ค.
์ฐ๋ฆฌ๋ errors.properties๋ผ๋ ํ์ผ๋ก ๊ด๋ฆฌ๋ฅผ ํด๋ณด์.
์ด๋, application.properties์ ๋ค์์ ์ถ๊ฐํด์ค๋ค.
spring.messages.basename=messages,errors
์ด๋ฌ๋ฉด ๋ ๋ค ์ธ์์ด ๊ฐ๋ฅํ๋ค. (๋ํดํธ๋ messages)
[errors.properties]
required.item.itemName=Please enter the product name as required.
range.item.price=Prices range from KRW {0} to KRW {1}.
max.item.quantity=Up to {0} quantities are allowed.
totalPriceMin=The total amount (price*quantity) must be at least KRW {0}! Current amount is KRW {1}
[errors_ja.properties]
required.item.itemName=ๅฟ
่ฆใซๅฟใใฆ่ฃฝๅๅใๅ
ฅๅใใฆใใ ใใใ
range.item.price=ไพกๆ ผใฏ{0}ใฆใฉใณ~{1}ใฆใฉใณใงใใ
max.item.quantity=ๆๅคง{0}ๅใพใง่จฑๅฏใใใพใ
totalPriceMin=ๅ่จ้้ก(ไพกๆ ผ*ๆฐ้)ใ{0}ใฆใฉใณไปฅไธใงใใๅฟ
่ฆใใใใพใใ็พๅจใฎ้้กใฏ{1}ใฆใฉใณใงใใ
[SItemController.java] - ์์
@PostMapping("/add")
public String addItemV2 (@ModelAttribute("item") SItem item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
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() > 10000) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false,
new String[]{"max.item.quantity"}, new Object[]{9999}, null));
}
//ํน์ ํ๋ ์์ธ๊ฐ ์๋ ์ ์ฒด ์์ธ
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));
}
}
if (bindingResult.hasErrors()) {
return "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
- ํ๋กํผํฐ์ ์ค์ ํ ๊ฐ์ ๊ฐ์ ธ์ฌ ์ ์๋๋ก ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝํ๋ค.
- new String[]{"range.item.price"}, new Object[]{1000, 1000000}
- ์์๋๋ก codes์ arguments ์๋ฆฌ์ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.
- ๋๋ ค๋ณด๋ฉด ๊ตญ์ ํ๊น์ง ์ ์ ์ฉ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
(ํฌ์คํ ์๋ ๋ด์ง ์์๋๋ฐ, ๋์ผํ ๋ฐฉ๋ฒ์ผ๋ก Item Status๋ Delivery ๊ฐ์ ๊ฒ๋ ๊ตญ์ ํ ์ฒ๋ฆฌ๋ฅผ ์งํํด์ฃผ์๋ค!)
- ํ์ง๋ง, ๊ฐ๋ฐ์๊ฐ ํญ์ FieldError์ ObjectError์ ๋ด๋ถ ์์๋ฅผ ์ง์ ์ ๋ ฅํ๋ ๊ฑด ์๋นํ ๊ท์ฐฎ๋ค.
- bindingResult์ ๋ด๋ถ ์์ -> .getObjectName(),.getTarget()์ ํด๋ณด๋ฉด, ์ด๋ฏธ bindingResult๋ ๊ฒ์ฆํด์ผ ํ๋ ๊ฐ์ฒด๋ฅผ ์๊ณ ์๋ค.
- ๋ํ, .rejectValue(), reject()๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ํธ๋ฆฌํ๊ฒ ๊ฒ์ฆ ์ค๋ฅ๋ฅผ ๋ค๋ฃฐ ์ ์๋ค.
[SItemController.java] - ์์
@PostMapping("/add")
public String addItemV2 (@ModelAttribute("item") SItem item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (!StringUtils.hasText(item.getItemName())) {
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() >= 10000) {
bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null);
}
//ํน์ ํ๋ ์์ธ๊ฐ ์๋ ์ ์ฒด ์์ธ
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()) {
return "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
bindingResult์ rejectValue๋ฅผ ์ดํด๋ณด์.
field : ์ค๋ฅ ํ๋๋ช
errorCode : ์ค๋ฅ ์ฝ๋ (๋ฉ์์ง ์ฝ๋ x)
errorArgs : ์ค๋ฅ ๋ฉ์์ง์์ ๋งค๊ฐ๋ณ์ ๊ฐ
defaultMessage: ๊ธฐ๋ณธ ์ค๋ฅ ๋ฉ์์ง
BindingResult๋ ์ด๋ฏธ target์ ์๊ณ ์๊ธฐ ๋๋ฌธ์, objectName์ ์์ฑํ์ง ์์๋ ๋๋ค.
๋ํ, ๊ธฐ์กด์๋ required.item.itemName๋ผ๊ณ ์์ฑํ๋ ๊ฒ์ ์ด๋ฒ์๋ itemName์ด๋ผ๊ณ ์ถ์ฝํ์ฌ ์์ฑํ์๋ค.
์ด๋ MessageCodesResolver ๋๋ถ์ด๋ค.
MessageCodesResolver๋ ์ธํฐํ์ด์ค๊ณ , ๊ตฌํ์ฒด์ธ DefaultMessageCodesResolver๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
| MessageCodesResolver
โ resolveMessageCodes()
- resolveMessageCodes(errorCode, objectName)
- resolveMessageCodes(errorCode, objectName, field, fieldType)
: ํ๋ผ๋ฏธํฐ๋ก ๋ค์ด์จ ์ ๋ณด๋กค ํ์ฉํด์ ๋ฉ์์ง ์ฝ๋๋ฅผ ์์ฑํ๋ค.
๋ฉ์์ง ์์ฑ ๊ท์น์ ๋ค์๊ณผ ๊ฐ๋ค.
โ ObjectError
- errorCode + "." + objectName
- errorCode
ex) codesResolver.resolveMessageCodes("required", "item");
- required.item
- required
โ FieldError
- errorCode + "." + objectName + "." + field
- errorCode + "." + field
- errorCode + "." + fieldType
- errorCode
ex) codesResolver.resolveMessageCodes("required","item", "itemName", String.class);
- required.item.itemName
- required.itemName
- required.java.lang.String
- required
์ด๊ฒ ๊ทธ๋์ ์ด๋์ ์ฐ์ด๋๋ฐ? ๋ฐ๋ก, .rejectValue()์ .reject()์ ๋ด๋ถ์์ ์ฌ์ฉ๋๋ค.
[AbstractBindingResult.java]
@Override
public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) {
addError(new ObjectError(getObjectName(), resolveMessageCodes(errorCode), errorArgs, defaultMessage));
}
@Override
public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs,
@Nullable String defaultMessage) {
if (!StringUtils.hasLength(getNestedPath()) && !StringUtils.hasLength(field)) {
reject(errorCode, errorArgs, defaultMessage);
return;
}
String fixedField = fixedField(field);
Object newVal = getActualFieldValue(fixedField);
FieldError fe = new FieldError(getObjectName(), fixedField, newVal, false,
resolveMessageCodes(errorCode, field), errorArgs, defaultMessage);
addError(fe);
}
@Override
public String[] resolveMessageCodes(String errorCode) {
return getMessageCodesResolver().resolveMessageCodes(errorCode, getObjectName());
}
@Override
public String[] resolveMessageCodes(String errorCode, @Nullable String field) {
return getMessageCodesResolver().resolveMessageCodes(
errorCode, getObjectName(), fixedField(field), getFieldType(field));
}
reject()์ rejectValue() ๋ด๋ถ์์ ๋ชจ๋ resolveMessageCodes๋ฅผ ์ฌ์ฉํ๋ ๊ฑธ ๋ณผ ์ ์๋ค!
ObjectError ์์ฑ ์์๋ errorCode์ ObjectName๋ง์, FieldError ์์ฑ ์์๋ errorCode, ObjectName, Field, FieldType๊น์ง ๋ชจ๋ ์ฌ์ฉํ๋ค.
MessageCodesResolver์ ๊ฒฝ์ฐ '๊ตฌ์ฒด์ ์ธ ๊ฒ์ ๋จผ์ ' ๋ง๋ค์ด์ฃผ๊ณ , ๋ ๊ตฌ์ฒด์ ์ธ ๊ฒ์ ๋์ค์ ๋ง๋ ๋ค.
ex) required.item -> required
๋ชจ๋ ์ค๋ฅ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ๋ณด๋ค๋, ์ค์ํ ๊ฒ๋ง ๊ตฌ์ฒด์ ์ผ๋ก ๋๊ณ ๋๋จธ์ง๋ ๋ ๊ตฌ์ฒด์ ์ธ ๊ฑธ๋ก ์ฌ์ฉํ๋ ๊ฒ ๋ ๋ซ๋ค.
๋ฉ์์ง๋ฅผ ์ฐพ์ ๋๋ ๊ตฌ์ฒด์ ์ธ ๊ฒ๋ถํฐ ์ฐพ๊ณ ์์ผ๋ฉด ๋ ๊ตฌ์ฒด์ ์ธ ๊ฑธ ์ฐพ๋ ํ์์ด๋ค!
์ถ๊ฐ์ ์ผ๋ก, ๊ฒ์ฆ ์ค๋ฅ ๋ก์ง์์ ๋ค์๊ณผ ๊ฐ์ด ์์ ๋ ๊ฐ๋ฅํ๋ค.
[SItemController.java]
@PostMapping("/add")
public String addItemV2 (@ModelAttribute("item") SItem item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
// if (!StringUtils.hasText(item.getItemName())) {
// bindingResult.rejectValue("itemName", "required");
// }
...
- ๋ค์๊ณผ ๊ฐ์ด ๊ฐ๋จํ ๊ณต๋ฐฑ ์ฒดํฌ๋ ValidationUtils์ ํ์ฉํด๋ ์ข์ ๊ฒ ๊ฐ๋ค :D
๐ฉ ์ ์ฒด ํ๋ฆ
- rejectValue() ํธ์ถ -> MessageCodesResolver๋ฅผ ํตํด ๋ฉ์์ง ์ฝ๋ ์์ฑ
-> new FieldError()๋ฅผ ์์ฑํ๋ฉด์ ์ฝ๋ ๋ณด๊ด -> th:errors์์ ๋ฉ์์ง ์ฝ๋๋ฅผ ํตํด ๋ฉ์์ง ์ฐพ๊ณ ๋ ธ์ถ์ํค๊ธฐ (์์ผ๋ฉด ๋ํดํธ ๋ฉ์์ง)
| ์คํ๋ง์ ๊ฒ์ฆ ์ค๋ฅ ์ ๋ต
- ์ฐ๋ฆฌ์ ์ํ ๋ฑ๋ก ํผ ์ค ๊ฐ๊ฒฉ ํ๋์ ๋ฌธ์๋ฅผ ์ ๋ ฅํ๋ฉด ์คํ๋ง์ ํ์ ์ค๋ฅ์ ๊ดํ ๋ฉ์์ง๋ฅผ ์์ฑํ๋ค.
- typeMismatch.item.price
- typeMismatch.price
- typeMismatch.java.lang.Integer
- typeMismatch
- ์ด๋ฅผ ์ด์ฉํด์ ์๋ฌ ํ๋กํผํฐ์ ์ถ๊ฐํด์ฃผ์.
[errors.properties]
typeMismatch.java.lang.Integer=Please enter 'Integer' Type.
typeMismatch=Type Mismatch Error. Check your field value.
- ๊ตญ์ ํ๊น์ง๋ ์งํํ์ง ์์๋ค. (์ด๋ฌํ ํ๋ ์ค๋ฅ๋ ๊ตญ์ ํ๋ณด๋ค๋ ๊ทธ๋ฅ ์์ด๋ก ์ ๋ฌํ๋ ๊ฒ ๋์ ๊ฒ ๊ฐ์์)
- ๋ค์๊ณผ ๊ฐ์ด ์ ๋จ๋ ๊ฑธ ๋ณผ ์ ์๋ค!
| Validator ๋ถ๋ฆฌํ๊ธฐ
- ๊ฒ์ฆ ๋ก์ง์ ์ฌ์ฌ์ฉํ ์ ์๋๋ก ๋ณ๋์ ํด๋์ค๋ก ๋ถ๋ฆฌํ์.
[SItemValidator.java]
@Component
public class SItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return SItem.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
SItem item = (SItem) 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);
}
}
}
}
- Validator ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ค.
- supports๋ฅผ ํตํด ํ๋ผ๋ฏธํฐ๋ก ๋์ด์จ ํด๋์ค๊ฐ SItem์ ์ง์ํ๋์ง. (SItem์ ์์ ํด๋์ค์ฌ๋ ๊ฐ๋ฅ)
- ๊ธฐ์กด์ bindingResult๊ฐ ๋ค์ด๊ฐ๋ ๋ถ๋ถ์ ํ๋ผ๋ฏธํฐ์ errors๋ฅผ ๋ฃ์ด์ค๋ค.
- ์ด์ฐจํผ bindingResult๋ Errors๋ฅผ ๊ตฌํํ ์ธํฐํ์ด์ค์ด๊ธฐ ๋๋ฌธ์ ๋ฃ์ด์ค ์ ์๋ค!
[SItemController.java] - ์์
private final SItemValidator validator;
@PostMapping("/add")
public String addItemV2 (@ModelAttribute("item") SItem item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
validator.validate(item, bindingResult);
if (bindingResult.hasErrors()) {
return "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
- ์ปจํธ๋กค๋ฌ ์ฝ๋๊ฐ ์ ๋ง ๊น๋ํด์ก๋ค!
- ๋ง์ง๋ง์ผ๋ก, validator๋ฅผ ํธ์ถํ์ง ์๊ณ @Validated๋ฅผ ์ฌ์ฉํด๋ณด์.
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(validator);
}
@PostMapping("/add")
public String addItemV2 (@Validated @ModelAttribute("item") SItem item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
@Validated๋ ๊ฒ์ฆ๊ธฐ๋ฅผ ์คํํ๋ผ๋ ์ด๋ ธํ ์ด์ ์ผ๋ก, WebDataBinder์ ๋ฑ๋กํ ๊ฒ์ฆ๊ธฐ๋ฅผ ์ฐพ์์ ์คํ๋๋ค.
@Valid๋ ๊ฐ์ ์๋ฏธ์ด๋ค. ์ด๊ฑด ๋ค์ ํฌ์คํ ์์ ์กฐ๊ธ ๋ ์์๋ณด์!
์ด๋, ์ฌ๋ฌ ๊ฒ์ฆ๊ธฐ๊ฐ ๋ฑ๋ก๋๋ฉด supports()์ ํต๊ณผ๋๋ ๊ฒ์ฆ๊ธฐ๊ฐ ์คํ๋๋ค.
ํด๋น ์ปจํธ๋กค๋ฌ ๋จ์์์ ๊ฒ์ฆ๊ธฐ๋ฅผ ์๋์ผ๋ก ์ ์ฉํ๊ธฐ ์ํด WebDataBinder๋ฅผ ์ฌ์ฉํ์๋ค. (@InitBinder)
@InitBinder๋ฅผ ์ฌ์ฉํ๋ฉด ํด๋น ์ปจํธ๋กค๋ฌ๊ฐ ํธ์ถ๋ ๋๋ง๋ค WebDataBinder์ ์ฐ๋ฆฌ๊ฐ ์ปค์คํ ํ validator๋ฅผ ์ถ๊ฐํ๊ฒ ๋๋ค.
์ฌ๊ธฐ์๋ ๋จ์ํ '์ถ๊ฐ' ์์
์ด๊ณ , ์ค์ ๋ก @Validated๊ฐ ๋ถ์ ๋ฉ์๋์์ validate๋ฅผ ์คํํ๋ ๊ฒ!
๐ฉ ๋ค๋ฅธ ๋ถ์ด ๋๋ฒ๊น ์ ํ์ จ๋ค๋ ๊ฑธ ๋ดค๋๋ฐ, @Validated๊ฐ ์์ด๋ ๊ฐ์ ๋งคํํ ๋๋ฉด @InitBinder๋ก ๋ฑ๋กํ Validator์ support() ๋ฉ์๋๊ฐ ๋์ํ๋ค๊ณ ํ๋ค...! ๊ทธ๋ผ SItem ์ธ์ ๋ค๋ฅธ ๊ฒ๋ค๋ ๊ฒ์ฆํ ํ ๋ฐ, ์์ธกํ์ง ๋ชปํ ๊ฒ์ฆ ์คํจ ์์ธ๊ฐ ๋ง์ด๋จ์ง ์์๊น?
๊ทธ๋์ ๋ณดํต์ @InitBinder("name") ์ด๋ ๊ฒ ์ด๋ฆ์ ์ง์ ํด์ ํด๋น ๊ฐ์ฒด์๋ง ์ํฅ์ ์ฃผ๋๋ก ์ง์ ํ๋ค๊ณ ํ๋ค!
๊ธ๋ก๋ฒํ๊ฒ ์ฌ์ฉํ๋ ค๋ฉด WebMvcConfigurer๋ฅผ ์์๋ฐ์ getValidator๋ฅผ ํตํด ์ค๋ฒ๋ผ์ด๋ฉ์ ํด์ฃผ๋ฉด ๋๋ค.
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
- ์ด๋ฐ ์์ผ๋ก ํ๋ฉด ๋๋ค! (๊ตณ์ด ์ถ๊ฐ๊น์ง๋ ํ์ง ๋ง๊ณ ์ด๋ฐ ๊ฒ ์๋ค๋ ๊ฒ ์ ๋๋ง ์ฒดํฌํ์)
- ๋ค์ ํฌ์คํ ์์๋ Bean Validation์ ์์๋ณด์. (์๋ง ์ค์ ๊ฒ์ฆ์์๋ ์ด๊ฑฐ๋ฅผ ํจ์ฌ ๋ง์ด ์ฌ์ฉํ ๊ฒ์ด๋ค!)