Spring Vaildation#1 ๊ฒ์ฆ
by JiwonDev๊ฐ์ฒด๋ฅผ ๋งคํํ๋ ์ปจํธ๋กค๋ฌ์ ์ค์ํ ์ญํ ์ค ํ๋๋ HTTP ์์ฒญ์ด ์ ์์ธ์ง ๊ฒ์ฆํ๋ ๊ฒ์ด๋ค. ์ด์ฉ๋ฉด ์ ์ ๋ก์ง๋ณด๋ค ์ด๋ฌํ ์์ธ, ๊ฒ์ฆ ๋ก์ง์ ์ค๊ณํ๋ ๊ฒ์ด ํจ์ฌ ๋ ์ด๋ ค์ธ ์ ์๋ค.
ํด๋น๊ธ์ Bean Validation์ด ์ด๋ป๊ฒ ๋์ํ๋์ง ์๊ธฐ์ํด ์คํ๋ง์์ ์๋์ผ๋ก ์ง์ ๊ฒ์ฆ๊ฐ์ฒด๋ฅผ ๋ง๋ค ๊ฒ์ด๋ค. @Valid์ @NotNull ๋ฑ ์ด๋ ธํ ์ด์ ์ ์ด์ฉํ Validation ์ฌ์ฉ๋ฒ๋ง ์๊ณ ์ถ๋ค๋ฉด, ๋ค์ ๊ธ์ ์ฝ์ด๋ณด๋๋ก ํ์.
# Vaildation, ๊ฒ์ฆ ๊ธฐ๋ฅ์ด๋?
๊ฐ์ด ์ฌ๋ฐ๋ฅธ ํํ์ธ์ง ๊ฒ์ฆํ๋ ๊ฒ์ ์๋ฏธํ๋ค. ์น ์๋น์ค๋ ๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ๊ณ ๊ฐ์ด ์ ๋ ฅํ ๋ฐ์ดํฐ๋ฅผ ์ ์งํ ์ํ๋ก ์ด๋ค ์ค๋ฅ๊ฐ ๋ฐ์ํ๋์ง ์๋ ค์ฃผ์ด์ผ ํ๋ค.
- ํด๋ผ์ด์ธํธ์์ ์๋ฐ์คํฌ๋ฆฝํธ, HTML์ ์กฐ์ํ ์ ์์ผ๋ฏ๋ก ๋ณด์์ ์ทจ์ฝํ๋ค.
- ๊ทธ๋ ๋ค๊ณ Only ์๋ฒ๋ก ๊ฒ์ฆํ๋ฉด ์ฆ๊ฐ์ ์ธ ๊ณ ๊ฐ ์ฌ์ฉ์ฑ(UX)์ด ๋ต๋ตํด์ง๋ค.
- ๋์ ์ ์ ํ ์์ด์ ์ฌ์ฉํ๋, ์ต์ข ์ ์ผ๋ก ์๋ฒ ๊ฒ์ฆ์ ํ์
- API ๋ฐฉ์์ ์ฌ์ฉํ๋ฉด API ์คํ์ ์ ์ ์ํด์ ๊ฒ์ฆ ์ค๋ฅ๋ฅผ API ์๋ต ๊ฒฐ๊ณผ์ ์ ๋จ๊ฒจ์ฃผ์ด์ผ ํ๋ค.
# Vaildation์ ์ง์ ์ฒ๋ฆฌํ๊ธฐ
์ ๋ง ๊ฐ๋จํ ํ์ด์ง์ธ๋ฐ๋ ๊ฒ์ฆ ์คํจ๋ฅผ ์ฒ๋ฆฌํ๋ ์ปจํธ๋กค๋ฌ ๊ฐ์ฒด๊ฐ ๋งค์ฐ ๋ณต์กํด์ง๋ค.
- ๊ณ ๊ฐ์๊ฒ ์ด๋ค ๊ฐ์ด ๊ฒ์ฆ์ ์คํจํ๋์ง ์๋ ค์ฃผ๊ธฐ์ํด Map<String, String> errors์ ๊ธฐ๋กํ๋ค.
- ๊ฐ๊ฐ์ ํ๋์ ํด๋นํ๋ ๊ฒ์ฆ, ์ฌ๋ฌ ํ๋๋ฅผ ๋ณตํฉ์ ์ผ๋ก ์ ๊ฒํ๋ ๊ฒ์ฆ์ด ๋ฐ๋ก ํ์ํ๋ค.
- ์คํจํ์ ๋, ์ฑ๊ณตํ์ ๋ ์ ๋ฌํ๋ View๊ฐ ๋ฌ๋ผ์ ธ์ผ ํ๋ค. (items์ errors๋ฅผ ์ ๋ฌ)
@Slf4j
@Controller
@RequestMapping("/validation/v1/items")
@RequiredArgsConstructor
public class ValidationItemControllerV1 {
private final ItemRepository itemRepository;
@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 (!errors.isEmpty()) {
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}";
}
}
ํ์๋ฆฌํ์์ item๊ณผ erros๋ฅผ ๋ฐ์ ์๋์ ๊ฐ์ด ์ค๋ฅ๋ฅผ ๋ณด์ฌ์ค ์ ์๋ค.
- errors.method() ๋์ errors?.method()๋ฅผ ์ฌ์ฉํ๋ฉด ํด๋น ๊ฐ์ฒด๊ฐ null์ผ ๋ ๋ฌด์ํ ์ ์๋ค.
- map.containsKey('value')๋ ํด๋น ํค์ ์ฌ๋ถ์ ๋ฐ๋ผ true, false๋ฅผ ๋ฐํํ๋ค. => th:if ์กฐ๊ฑด๋ฌธ ์ฌ์ฉ.
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">์ ์ฒด ์ค๋ฅ ๋ฉ์์ง</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">์ํ๋ช
</label>
<input class="form-control" id="itemName" placeholder="์ด๋ฆ์ ์
๋ ฅํ์ธ์"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
th:field="*{itemName}" type="text">
<div class="field-error" th:if="${errors?.containsKey('itemName')}"
th:text="${errors['itemName']}">
์ํ๋ช
์ค๋ฅ (์ํ์ด๋ฆ์ ํ์์
๋๋ค)
</div>
</div>
...
</form>
์ฐธ๊ณ ๋ก <input class=filed-error>๋ ์๋์ ๊ฐ์ด th:classappend๋ฅผ ์ฌ์ฉํ ์๋ ์๋ค.
<input type="text"
th:classappend="${errors?.containsKey('itemName')} ? 'field-error' : _"
class="form-control">
@ ์ง์ ์ฒ๋ฆฌํ์ ๋ ๋ฌธ์ ์
- ๋ทฐ ํ ํ๋ฆฟ์์ ์ค๋ณต์ด ๋๋ฌด ๋ง๋ค. input ํ๋๋ง๋ค ๋ฐ๋ณตํด์ ์ฒ๋ฆฌํด์ผ ํ๋ค.
- ๊ฐ์ด ์๋๋ผ ํ์ ์ค๋ฅ ์ฒ๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅํ๋ค. ์๋ฅผ ๋ค์ด ์ซ์ ํ์ (Integer)์ ๋ฌธ์('one')๊ฐ ๋ค์ด์จ๋ค๋ฉด ์คํ๋ง MVC์ ์ง์ ํ๊ธฐ๋ ์ ์ HTTP 400 ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์๋ค.
- ํ์ ์ด ๋ค๋ฅธ ์ด์ํ ๊ฐ์ด ๋ค์ด์ค๋๋ผ๋ ๊ณ ๊ฐ์๊ฒ ์๋ชป๋ ๊ฐ์์ ์๋ ค์ค์ผ ํ๋ค.
# ์คํ๋ง์ด ์ ๊ณตํ๋ Validation ์ฒ๋ฆฌ
์คํ๋ง Vaildation์ ํต์ฌ์ ์ปจํธ๋กค๋ฌ์์ ์ฌ์ฉํ ์ ์๋ BindingResult ํ๋ผ๋ฉํ ๊ฐ์ฒด์ด๋ค.
- ๋จ BindingResult ํ๋ผ๋ฉํ๋ @ModelAttribute Item ๋ค์์ ์์ผ ํ๋ค.
- ๊ธฐ์กด์ errors ๋์ ์ bindingResult๋ฅผ ์ฌ์ฉํ๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฐ์ฒด FieldError์ ObjectError๋ฅผ ๋ด๋๋ค.
- bindingResult ๊ฐ์ฒด๋ ์๋์ผ๋ก View์ ๋์ด๊ฐ๋ค. ๋ํ .hasErrors() .addError(~) ๊ฐ์ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค.
public FieldError(String objectName, String field, String defaultMessage) {}
// ์ค๋ฅ๊ฐ ๋ฐ์ํ ๋ชจ๋ธ ๊ฐ์ฒด ์ด๋ฆ, ํ๋ ์ด๋ฆ, ๊ธฐ๋ณธ ์ค๋ฅ ๋ฉ์์ง
public ObjectError(String objectName, String defaultMessage) {}
// ์ค๋ฅ๊ฐ ๋ฐ์ํ ๋ชจ๋ธ ๊ฐ์ฒด ์ด๋ฆ, ๊ธฐ๋ณธ ์ค๋ฅ ๋ฉ์์ง
์ด์ ํ๋์ฝ๋ฉ ํ์ง์๊ณ , ์๋ฌ๋ฅผ bindingResult ๊ฐ์ฒด๋ก ๊ด๋ฆฌํ ์ ์๊ฒ ๋์๋ค.
@PostMapping("/add")
public String addItemV2(@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}";
}
# ํ์๋ฆฌํ(thymeleaft) ์คํ๋ง ์ค๋ฅ ์์ธ์ฒ๋ฆฌ ๊ธฐ๋ฅ
์คํ๋ง์ bindingErrors๋ฅผ ์ฌ์ฉํ๋ฉด ํ์๋ฆฌํ์์๋ ๊ฐ๊ฒฐํ ๋ฌธ๋ฒ์ ์ด์ฉํ ์ ์๋ค.
<form action="item.html" method="post" th:action th:object="${item}">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">๊ธ๋ก๋ฒ ์ค๋ฅ
๋ฉ์์ง</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">์ํ๋ช
</label>
<input class="form-control" id="itemName" placeholder="์ด๋ฆ์ ์
๋ ฅํ์ธ์"
th:errorclass="field-error" th:field="*{itemName}" type="text">
<div class="field-error" th:errors="*{itemName}">
์ํ๋ช
์ค๋ฅ
</div>
</div>
...
</form>
@ FieldError ์ ObjectError ๋ ์์๋ณด๊ธฐ
์ด ๊ฐ์ฒด๋ค์๊ฒ ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ฅผ ์ค์ ์์ธ๊ฐ ๋ฐ์ํ๋๋ผ๋ ์ฌ์ฉ์ ์ ๋ ฅ๊ฐ์ ์ ์ง์ํฌ ์ ์๋ค.
public FieldError(String objectName, String field, String defaultMessage)
public FieldError(String objectName, String field,
@Nullable Object rejectedValue, // ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ๊ฐ(๊ฑฐ์ ๊ฐ)
boolean bindingFailure, // ํ์
๋ฐ์ธ๋ฉ ์คํจ(true), ๊ฒ์ฆ ์คํจ(false)
@Nullable String[] codes, // ๋ฉ์์ง ์ฝ๋, ๋ฌธ์์ด
@Nullable Object[] arguments, // ๋ฉ์์ง์์ ์ฌ์ฉํ๋ ์ธ์
@Nullable String defaultMessage)
//๊ฒ์ฆ ๋ก์ง
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(
new FieldError("item", "itemName",
item.getItemName(), false, null, null,"์ํ ์ด๋ฆ์ ํ์ ์
๋๋ค."));
}
- codes์ arguments๋ฅผ ์ด์ฉํ๋ฉด ์ค๋ฅ ๋ฉ์์ง๋ ์คํ๋ง์ ๊ตญ์ ํ, ๋ฉ์์ง ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์๋ค.
๋ง์ฝ errors.properties๋ก ์ค๋ฅ ํ๋กํผํฐ๋ฅผ ๋ถ๋ฆฌํ๊ณ ์ถ๋ค๋ฉด ์ด๋ ๊ฒ ์ฌ์ฉ์ถ๊ฐํ๋ฉด ๋๋ค.
(application.properties ํ์ผ) spring.messages.basename= messages, errors
# errors.properties
required.item.itemName = ์ํ ์ด๋ฆ์ ํ์์
๋๋ค.
range.item.price = ๊ฐ๊ฒฉ์ {0} ~ {1} ๊น์ง ํ์ฉํฉ๋๋ค.
max.item.quantity = ์๋์ ์ต๋ {0} ๊น์ง ํ์ฉํฉ๋๋ค.
totalPriceMin = ๊ฐ๊ฒฉ * ์๋์ ํฉ์ {0}์ ์ด์์ด์ด์ผ ํฉ๋๋ค. ํ์ฌ ๊ฐ = {1}
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));
// String[] codes์ ์๋ ๋ฉ์์ง๋ฅผ ์์๋๋ก ์ฐพ๋ค๊ฐ ์ ์ผ ๋จผ์ ๋ฐ๊ฒฌ๋ ๋ฉ์์ง๋ฅผ ์ฌ์ฉํ๋ค.
}
์ค๋ฅ์ฝ๋๋ฅผ ๋ฐฐ์ด๋ก ๋ฐ์ ๊ณ์ธต์ ์ผ๋ก ์ฌ์ฉํ๋ ์ด์ ๋ MessageCodesResolver๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ด๋ค.
@ BindingResult๋ฅผ ์ฌ์ฉํ์ ๋ ๋ฌ๋ผ์ง์
- ์คํ๋ง์ด ์ ๊ณตํด์ฃผ๋ ๊ฐ์ฒด๋ฅผ ์ด์ฉํด์ ์๋ฐ์ฝ๋์ ํ์๋ฆฌํ ์ฝ๋๊ฐ ๊ฐ๊ฒฐํด์ก๋ค.
- ํ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋๋ผ๋ BindingResult๊ฐ ์์ด์ HTTP 400 ์๋ฌ๊ฐ ์ํฐ์ง๊ณ ์ปจํธ๋กค๋ฌ๊ฐ ํธ์ถ๋๋ค.
# ์คํ๋ง์ ์ค๋ฅ์ฝ๋ ์ถ์ํ
์์์ ์ค๋ช ํ FieldError ๋ ObjectError๋ฅผ ๋งค๋ฒ new๋ก ์์ฑํด์ฃผ๊ธฐ์๋ ๋๋ฌด ๋ฒ๊ฑฐ๋กญ๋ค.
์กฐ๊ธ ๋ ์ถ์ํํด์ ์ ๋ ฅ ๊ฐ๋ง ์ฃผ๋ฉด ์์์ ๋์ํ๋๋ก ๋ง๋ค ์ ์์๊น? → rejectValue(), reject()
- BindingResult์๋ rejectValue(), reject() ๋ผ๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค.
(๋ด๋ถ์์ FieldError, ObjectError ๋ฅผ ๋์ ๋ง๋ค์ด์ค๋ค.)
# errors.properties
required.item.itemName = ์ํ ์ด๋ฆ์ ํ์์
๋๋ค.
range.item.price = ๊ฐ๊ฒฉ์ {0} ~ {1} ๊น์ง ํ์ฉํฉ๋๋ค.
max.item.quantity = ์๋์ ์ต๋ {0} ๊น์ง ํ์ฉํฉ๋๋ค.
totalPriceMin = ๊ฐ๊ฒฉ * ์๋์ ํฉ์ {0}์ ์ด์์ด์ด์ผ ํฉ๋๋ค. ํ์ฌ ๊ฐ = {1}
if (resultPrice < 10000) { // ๊ธฐ์กด ์ฝ๋
bindingResult.addError(
new ObjectError("item", new String[]{"totalPriceMin"},
new Object[]{10000, resultPrice}, null));
}
if (resultPrice < 10000) { // reject() ์ฌ์ฉ
bindingResult.reject(
"totalPriceMin",
new Object[]{10000, resultPrice}, null);
}
// range.item.price ๊ฐ ๋งค์นญ๋์ด ๋ฉ์์ง๊ฐ ์ฝ์
๋๋ค.
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null)
@ MessageCodeResolver
reject์ rejectValue ์ ์๋ ์ค๋ฅ์ฝ๋(errorCode)๋ MessageCodeResolver ๊ฐ์ฒด๋ฅผ ํตํด ์ค๋ฅ๋ฉ์์ง๊ฐ ์์ฑ๋๋ค.
๋ฉ์์ง ๋ฆฌ์กธ๋ฒ๋ฅผ ์ฌ์ฉํด์ ๋ง๋ ๋ฒ์ฉ์ ์ธ errors.properties
๊ธฐ์กด์ errors.properties, ๋ชจ๋ ์ค๋ฅ์ฝ๋๋ฅผ ์ง์ ํด์ค์ผ ํ๋ค. (์์ผ๋ฉด ์์ธ ๋ฐ์)
required.item.itemName=์ํ ์ด๋ฆ์ ํ์์
๋๋ค.
range.item.price=๊ฐ๊ฒฉ์ {0} ~ {1} ๊น์ง ํ์ฉํฉ๋๋ค.
max.item.quantity=์๋์ ์ต๋ {0} ๊น์ง ํ์ฉํฉ๋๋ค.
totalPriceMin=๊ฐ๊ฒฉ * ์๋์ ํฉ์ {0}์ ์ด์์ด์ด์ผ ํฉ๋๋ค. ํ์ฌ ๊ฐ = {1}
๋ฒ์ฉ์ ์ธ 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} ๊น์ง ํ์ฉํฉ๋๋ค
#Level1
required.item.itemName: ์ํ ์ด๋ฆ์ ํ์ ์
๋๋ค.
#Level2
required: ํ์ ๊ฐ ์
๋๋ค.
public class MessageCodesResolverTest {
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
@Test
@DisplayName("(required)๋ 1. required.item 2. required ์์ผ๋ก ์ง์ ๋๋ค.")
void messageCodesResolverObject() {
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
assertThat(messageCodes).containsExactly("required.item", "required");
}
@Test
void messageCodesResolverField() {
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName",
String.class);
assertThat(messageCodes).containsExactly(
"required.item.itemName", // 1์์. ์๋ฒฝํ๊ฒ ์ผ์น
"required.itemName", // 2์์. ํ๋๋ช
์ผ์น
"required.java.lang.String", // 3์์ ํ์
์ผ์น
"required" // 4์์ ์๋ฌ์ฝ๋ ์ผ์น
// 5์์ - reject()์ ์ ์ defaultMessage
);
}
}
FieldError ๊ฐ์ฒด๊ฐ ์๋ฌ์ฝ๋๋ฅผ ์ String[]๋ก ๋ฐ๋์ง์ ๋ํ ์ด์ ๊ฐ ์ฌ๊ธฐ์ ๋์จ๋ค.
→ MessageCodeResolver๊ฐ ํด๋น ๋ฉ์์ง๋ฅผ ์ ์ฐํ๊ฒ ์ฐพ์ ๋ฐํํ๊ธฐ ๋๋ฌธ์ด๋ค. ๋ง์ฝ ์ค๋ฅ์ฝ๋๊ฐ ๋ฉ์์ง ํ๋กํผํฐ์ ์๋ ๊ฒฝ์ฐ, ์ผ์น๋๋ฅผ ๋น๊ตํด์ ๋ค๋ฅธ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ๋์ ์ฌ์ฉํ๊ฒ ๋๋ค.
// DefaultMessageCodesResolver ์ ๋์
๊ฐ์ฒด ์ค๋ฅ์ ๊ฒฝ์ฐ ๋ค์ ์์๋ก 2๊ฐ์ง ์์ฑ
1. code + "." + object name
2. code
์) { errorCode: "required", object-name: "item" }
1. required.item
2. required
ํ๋ ์ค๋ฅ์ ๊ฒฝ์ฐ ๋ค์ ์์๋ก 4๊ฐ์ง ๋ฉ์์ง ์ฝ๋ ์์ฑ
1. code + "." + object name + "." + field
2. code + "." + field
3. code + "." + field type
4. code
์) { errorCode: "typeMismatch", object-name:"user", field:"age", field-type: int }
1. typeMismatch.user.age
2. typeMismatch.age
3. typeMismatch.int
4. typeMismatch
์๋ฅผ ๋ค์ด item ๊ฐ์ฒด์ itemName ํ๋์์ ์๋ฌ๊ฐ ๋ฌ๋ค๋ฉด, bindingResult.reject() ๋ด๋ถ์์๋ ๋ค์๊ณผ ๊ฐ์ด ์๋ํ๋ค.
# ValidationUtils
๋์ค์ ์ฌ์ฉํ ํ ์ง๋ง, ์์์ ์ ์ ์กฐ๊ฑด๋ฌธ์ ValidationUtils๋ฅผ ์ด์ฉํ๋ฉด JUnit Test์ฒ๋ผ ๋ฉ์๋๋ก ๋ฐ๊ฟ ์ ์๋ค.
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required", "๊ธฐ๋ณธ: ์ํ ์ด๋ฆ ํ์");
}
// ๊ฐ๋จํ ์กฐ๊ฑด์ VaildationUtils๋ก ๋๊ฐ์ด ๊ตฌํ๊ฐ๋ฅ.
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
# ์ ๋ฆฌ - ์คํ๋ง ์ค๋ฅ ๋ฉ์์ง ์ฒ๋ฆฌ
์คํ๋ง์์ FieldError ๊ฐ์ฒด๋ฅผ ๋ง๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋ฉ์์ง๋ฅผ ์์ฑํด๋ฒ๋ฆฐ๋ค.
- ๋ฒ์ฉ์ ์ธ ๋ฉ์์ง(error.properties)๋ฅผ ์ ๊ณ MessageCodeResolver๋ฅผ ์ฌ์ฉํ๋ฉด ์๋์ ๊ฐ์ ์ผ์ ๋ฐฉ์งํ ์ ์๋ค.
@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()) {
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}";
}
# ํ๊ฑธ์ ๋, Validator ๋ถ๋ฆฌ
ํ์ง๋ง ์ฌ์ ํ ์ปจํธ๋กค๋ฌ๊ฐ ๋๋ฌด ๋ง์ ๋ก์ง์ ๋ด๋นํ๊ณ ์๋ค. ํ ๊ฐ์ฒด๊ฐ ๋๊ฐ์ง ์ฑ ์์ ์ง๊ณ ์์ผ๋ (OCP) ๋ณต์กํ ๊ฒ์ฆ ๋ก์ง์ Vaildator ๊ฐ์ฒด๋ก ๋ง๋ค์ด ์ฑ ์์ ๋ถ๋ฆฌํ์.
- ์คํ๋ง์์๋ Validator ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํด์ค๋ค. ์ด๋ฅผ ์ด์ฉํด ๊ฒ์ฆ ๊ฐ์ฒด๋ฅผ ์ฝ๊ฒ ๋ถ๋ฆฌํ ์ ์๋ค.
๊ทธ๋ฐ๋ฐ ์ฌ์ค Validator๋ฅผ ์ฌ์ฉํ์ง ์๊ณ , ๊ทธ๋ฅ ์๋ฌด ๊ฐ์ฒด๋ ์คํ๋ง๋น์ผ๋ก ๋ฑ๋กํด ์ฌ์ฉํด๋ ๋๋ค. ์ด๋ฅผ ์ฌ์ฉํ๋ ์ด์ ๋ ์๋์์ ์์๋ณด์.
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
// return (item == clazz) ์ ๊ฐ์ ์ฝ๋์ง๋ง, ์ด๋ ๊ฒ ์ฐ๋ฉด ์์ ๊ฐ์ฒด๋ true.
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
if (!StringUtils.hasText(item.getItemName())) {
errors.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range", new Object[]{1000, 10000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
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);
}
}
}
}
์ด๋ ๊ฒ ๋ง๋ ์คํ๋ง ๋น์ Controller์ ์ฌ์ฉํ๋ฉด ๋๋ค.
- Controller์์๋ ์ฌ์ฉ๋ ๊ฒ์ฆ ์ฝ๋๋ฅผ itemValidator๋ก ๋ถ๋ฆฌํ์๋ค.
public class ValidationItemControllerV2 {
private final ItemRepository itemRepository;
private final ItemValidator itemValidator;
...
@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes, Model model) {
itemValidator.validate(item, bindingResult);
//๊ฒ์ฆ์ ์คํจํ๋ฉด ๋ค์ ์
๋ ฅ ํผ์ผ๋ก
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}";
}
}
# Validator ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ ์ด์
์คํ๋ง์ Vaildator ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์คํ๋ง์ ์ถ๊ฐ์ ์ธ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์๋ค.
@ WebDataBinder ํตํด์ ์ฌ์ฉํ๊ธฐ
์ด๋ ๊ฐ @RequsetMapping์ด ์์๋ ๋ ๋ฐ์ธ๋ฉ ํ๋ผ๋ฏธํฐ(BinderingResult)๋ฅผ ์ ๋ฌํ๋ ์ญํ ์ ํด์ฃผ๊ณ , ๊ฒ์ฆ ๊ฐ์ฒด๋ฅผ ๋ฑ๋กํ์ฌ ์ฌ์ฉํ ์ ์๋ค.
// ๊ฐ ๋ฉ์๋์์ itemValidator.validate(item, bindingResult); ๋ฅผ ํธ์ถํด์ฃผ๋ ์ญํ .
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
์ฐธ๊ณ ๋ก @InitBinder๋ ํด๋น ์ปจํธ๋กค๋ฌ์๋ง ๊ฒ์ฆ ๊ฐ์ฒด๋ฅผ ๋ง๋ ๋ค. ๋ชจ๋ ์ปจํธ๋กค๋ฌ์ ์ ์ญ์ผ๋ก ํ๋๋ฐฉ๋ฒ์ ์๊ธดํ๋ฐ, ์คํ๋ง์์ ๋น ๊ฒ์ฆ์ ํ ๋ ๋ฑ๋ก๋๋ ๊ธ๋ก๋ฒ Validator(LocalValidatorFactoryBean)๊ฐ ์์ด์ ์ง์ ์ฌ์ฉํ ์ผ์ ์๋ค.
itemValidator.validate(item, bindingResult)๋ฅผ ํธ์ถํ๋ ๋์ ์ @Validated๋ฅผ ๋ถ์ด๋ฉด ์๋ํ๋ค.
- ๋๋ javax.validation.@Vaild๋ฅผ ์ฌ์ฉํด๋ ๋๋๋ฐ, ์คํ๋ง์ @Validated๊ฐ ์ฌ์ฉ๋ฒ์ด ๋ ๋ค์ํ๋ค.
@Valid๋ ์๋ฐ ํ์ค ์ด๋ ธํ ์ด์ ์ด๋ฏ๋ก ์ฌ์ฉํ๋ ค๋ฉด ์๋์ ์์กด์ฑ์ ์ถ๊ฐํด์ฃผ์ด์ผ ํ๋ค.
implementation 'org.springframework.boot:spring-boot-starter-validation'
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes, Model model) {...}
๊ทธ๋ฌ๋ฉด @Validated๊ฐ ๋ถ์ ๊ฐ์ฒด๋ฅผ dataBinder์ ๋ฑ๋ก๋ ๊ฒ์ฆ๊ธฐ(Validator)๋ค์ support(~)์ ๋ค ๋ฃ์ด๋ณด๋ฉฐ ๋ด๋น ๊ฒ์ฆ ๊ฐ์ฒด๋ฅผ ์ฐพ๋๋ค. ์ฐพ์๋ค๋ฉด validate() ๋ฅผ ์คํ์์ผ ๊ฒ์ฆํ๊ฒ๋๋ค.
if ( validator.support(Item.class) == true ) { validator.validate() }
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
// return (item == clazz) ์ ๊ฐ์ ์ฝ๋์ง๋ง, ์ด๋ ๊ฒ ์ฐ๋ฉด ์์ ๊ฐ์ฒด๋ true.
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
if (!StringUtils.hasText(item.getItemName())) {
errors.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range", new Object[]{1000, 10000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
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);
}
}
}
}
'๐ฑ Spring Framework > Spring MVC' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Spring Login#1 ์ฟ ํค์ ์ธ์ (0) | 2021.08.29 |
---|---|
Spring Bean Validation#2 ์คํ๋ง ๋น ๊ฒ์ฆ (0) | 2021.08.29 |
Spring ๋ฉ์์ง, ๊ตญ์ ํ ๊ธฐ๋ฅ (0) | 2021.08.28 |
Thymeleaf#2 Spring๊ณผ HTML Form ์ฒ๋ฆฌ (0) | 2021.08.28 |
Thymeleaf#1 ๊ธฐ๋ณธ๊ธฐ๋ฅ (0) | 2021.08.28 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev