JiwonDev

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>

์ƒ๋‹จ ์—๋Ÿฌ๋ฉ”์‹œ์ง€ / &lt;input class=field-error&gt; / ํ•˜๋‹จ ์ƒํ’ˆ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€๋‹ค.

 

์ฐธ๊ณ ๋กœ <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 ํŒŒ๋ผ๋ฉ”ํƒ€ ๊ฐ์ฒด์ด๋‹ค.

Errors๋Š” ๋‹จ์ˆœ ์˜ค๋ฅ˜ ์ €์žฅ์„, 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๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํƒ€์ž„๋ฆฌํ”„์—์„œ๋Š” ๊ฐ„๊ฒฐํ•œ ๋ฌธ๋ฒ•์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#validation-and-error-messages

<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() ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

์Šคํ”„๋ง์—์„œ 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)๊ฐ€ ์žˆ์–ด์„œ ์ง์ ‘ ์‚ฌ์šฉํ•  ์ผ์€ ์—†๋‹ค.

*์ฃผ์˜ ์ด๋ ‡๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด, BeanVaildaton์ด ํ•ด๋‹น ์ฝ”๋“œ๋กœ ์˜ค๋ฒ„๋ผ์ด๋”ฉ๋˜๋ฉฐ ๋” ์ด์ƒ ๋™์ž‘ํ•˜์ง€ ์•Š์Œ

 

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);
      }
    }
  }
}

 

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ