[Spring Boot][5] 4-1. ๊ฒ€์ฆ1 - Validation

sorzzzzyยท2021๋…„ 9์›” 18์ผ
0

Spring Boot - RoadMap 1

๋ชฉ๋ก ๋ณด๊ธฐ
39/46
post-thumbnail

๐Ÿท ๊ฒ€์ฆ ์š”๊ตฌ์‚ฌํ•ญ

์ƒํ’ˆ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์— ์ƒˆ๋กœ์šด ์š”๊ตฌ์‚ฌํ•ญ์ด ์ถ”๊ฐ€๋˜์—ˆ๋‹ค

โœ”๏ธ ์š”๊ตฌ์‚ฌํ•ญ: ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€

  • ํƒ€์ž… ๊ฒ€์ฆ

    • ๊ฐ€๊ฒฉ, ์ˆ˜๋Ÿ‰์— ๋ฌธ์ž๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ๊ฒ€์ฆ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
  • ํ•„๋“œ ๊ฒ€์ฆ

    • ์ƒํ’ˆ๋ช…: ํ•„์ˆ˜, ๊ณต๋ฐฑX
    • ๊ฐ€๊ฒฉ: 1000์› ์ด์ƒ, 1๋ฐฑ๋งŒ์› ์ดํ•˜
    • ์ˆ˜๋Ÿ‰: ์ตœ๋Œ€ 9999
  • ํŠน์ • ํ•„๋“œ์˜ ๋ฒ”์œ„๋ฅผ ๋„˜์–ด์„œ๋Š” ๊ฒ€์ฆ

    • ๊ฐ€๊ฒฉ * ์ˆ˜๋Ÿ‰์˜ ํ•ฉ์€ 10,000์› ์ด์ƒ


๐Ÿท ํ”„๋กœ์ ํŠธ ์„ค์ • V1

์ด์ „ ํ”„๋กœ์ ํŠธ์— ์ด์–ด์„œ โ—๏ธ๊ฒ€์ฆ(Validation) ๊ธฐ๋Šฅโ—๏ธ์„ ํ•™์Šตํ•  ์˜ˆ์ •.
์ด์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ์ผ๋ถ€ ์ˆ˜์ •ํ•œ validation-start ๋ผ๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค

๐Ÿ“Œ validation-start ์˜ ํด๋” ์ด๋ฆ„์„ validation ๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค!



๐Ÿท ๊ฒ€์ฆ ์ง์ ‘ ์ฒ˜๋ฆฌ - ์†Œ๊ฐœ

โœ”๏ธ ์ƒํ’ˆ ์ €์žฅ ์„ฑ๊ณต


โœ”๏ธ ์ƒํ’ˆ ์ €์žฅ ๊ฒ€์ฆ ์‹คํŒจ



๐Ÿท ๊ฒ€์ฆ ์ง์ ‘ ์ฒ˜๋ฆฌ - ๊ฐœ๋ฐœ

โœ”๏ธ ValidationItemV1Controller - addItem() ์ˆ˜์ •

@PostMapping("/add")
    public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {

        // ๊ฒ€์ฆ ์˜ค๋ฅ˜ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ด€
        Map<String, String> errors = new HashMap<>();

        // ๊ฒ€์ฆ ๋กœ์ง
        if (!StringUtils.hasText(item.getItemName())) {
            // itemName์ด ์—†๋‹ค๋ฉด ์˜ค๋ฅ˜๋ฉ”์„ธ์ง€ ์„ค์ •
            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 ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.");
        }

        // ํŠน์ • ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ๋ณตํ•ฉ ๋ฃฐ ๊ฒ€์ฆ
        // ํŠน์ • ํ•„๋“œ๋ฅผ ๋„˜์–ด์„œ๋Š” ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
        // ์ด๋•Œ๋Š” ํ•„๋“œ ์ด๋ฆ„์„ ๋„ฃ์„ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ globalError ๋ผ๋Š” key ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
        if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                errors.put("globalError", "๊ฐ€๊ฒฉ * ์ˆ˜๋Ÿ‰์˜ ํ•ฉ์€ 10,000์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ฐ’ = " + resultPrice);
            }
        }

        // ๊ฒ€์ฆ์— ์‹คํŒจํ•˜๋ฉด ๋‹ค์‹œ ์ž…๋ ฅ ํผ์œผ๋กœ
        // ๋งŒ์•ฝ ๊ฒ€์ฆ์—์„œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•˜๊ธฐ ์œ„ํ•ด model errors ๋ฅผ ๋‹ด๊ณ ,
        // ์ž…๋ ฅ ํผ์ด ์žˆ๋Š” ๋ทฐ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ๋ณด๋‚ธ๋‹ค.
        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}";
    }

1๏ธโƒฃ ๊ฒ€์ฆ ์˜ค๋ฅ˜ ๋ณด๊ด€

  • Map<String, String> errors = new HashMap<>();
  • ๋งŒ์•ฝ ๊ฒ€์ฆ์‹œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์–ด๋–ค ๊ฒ€์ฆ์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ์ •๋ณด๋ฅผ ๋‹ด์•„๋‘”๋‹ค.

2๏ธโƒฃ ๊ฒ€์ฆ ๋กœ์ง

  • ๊ฒ€์ฆ ์‹œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด errors ์— ๋‹ด์•„๋‘”๋‹ค.
  • ์ด๋•Œ ์–ด๋–ค ํ•„๋“œ์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ํ•„๋“œ๋ช…์„ key ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
  • ์ดํ›„ ๋ทฐ์—์„œ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ณ ๊ฐ์—๊ฒŒ ์นœ์ ˆํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค.

3๏ธโƒฃ ํŠน์ • ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ๋ณตํ•ฉ ๋ฃฐ ๊ฒ€์ฆ

  • ํŠน์ • ํ•„๋“œ๋ฅผ ๋„˜์–ด์„œ๋Š” ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
  • ์ด๋•Œ๋Š” ํ•„๋“œ ์ด๋ฆ„์„ ๋„ฃ์„ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ globalError ๋ผ๋Š” key ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

4๏ธโƒฃ ๊ฒ€์ฆ์— ์‹คํŒจํ•˜๋ฉด ๋‹ค์‹œ ์ž…๋ ฅ ํผ์œผ๋กœ

  • ๋งŒ์•ฝ ๊ฒ€์ฆ์—์„œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•˜๊ธฐ ์œ„ํ•ด model errors ๋ฅผ ๋‹ด๊ณ ,
  • ์ž…๋ ฅ ํผ์ด ์žˆ๋Š” ๋ทฐ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ๋ณด๋‚ธ๋‹ค.

โœ”๏ธ addForm.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    </style>
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2 th:text="#{page.addItem}">์ƒํ’ˆ ๋“ฑ๋ก</h2>
    </div>

    <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 type="text" id="itemName" th:field="*{itemName}"
                   th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
                   class="form-control" placeholder="์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”">
            <div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
                ์ƒํ’ˆ๋ช… ์˜ค๋ฅ˜
            </div>
        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">๊ฐ€๊ฒฉ</label>
            <input type="text" id="price" th:field="*{price}"
                   th:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control'"
                   class="form-control" placeholder="๊ฐ€๊ฒฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”">
            <div class="field-error" th:if="${errors?.containsKey('price')}" th:text="${errors['price']}">
                ๊ฐ€๊ฒฉ ์˜ค๋ฅ˜
            </div>
        </div>

        <div>
            <label for="quantity" th:text="#{label.item.quantity}">์ˆ˜๋Ÿ‰</label>
            <input type="text" id="quantity" th:field="*{quantity}"
                   th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control'"
                   class="form-control" placeholder="์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅํ•˜์„ธ์š”">
            <div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}">
                ์ˆ˜๋Ÿ‰ ์˜ค๋ฅ˜
            </div>

        </div>

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">์ƒํ’ˆ ๋“ฑ๋ก</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/validation/v1/items}'|"
                        type="button" th:text="#{button.cancel}">์ทจ์†Œ</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

๐Ÿ“Œ ๊ธ€๋กœ๋ฒŒ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€

<div th:if="${errors?.containsKey('globalError')}">
	<p class="field-error" th:text="${errors['globalError']}">์ „์ฒด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€</p>
</div>

โžก๏ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋Š” errors ์— ๋‚ด์šฉ์ด ์žˆ์„ ๋•Œ๋งŒ ์ถœ๋ ฅํ•˜๋ฉด ๋œ๋‹ค. ํƒ€์ž„๋ฆฌํ”„์˜ th:if ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์กฐ๊ฑด์— ๋งŒ์กฑํ•  ๋•Œ๋งŒ ํ•ด๋‹น HTML ํƒœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค!


โœ”๏ธ ์‹คํ–‰ ๊ฒฐ๊ณผ


โœ”๏ธ ์ •๋ฆฌ์™€ ๋‚จ์€ ๋ฌธ์ œ์ 

1๏ธโƒฃ ์ •๋ฆฌ

  • ๋งŒ์•ฝ ๊ฒ€์ฆ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ž…๋ ฅ ํผ์„ ๋‹ค์‹œ ๋ณด์—ฌ์ค€๋‹ค.
  • ๊ฒ€์ฆ ์˜ค๋ฅ˜๋“ค์„ ๊ณ ๊ฐ์—๊ฒŒ ์นœ์ ˆํ•˜๊ฒŒ ์•ˆ๋‚ดํ•ด์„œ ๋‹ค์‹œ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.
  • ๊ฒ€์ฆ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๊ณ ๊ฐ์ด ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ์ง€๋œ๋‹ค.

2๏ธโƒฃ ๋‚จ์€ ๋ฌธ์ œ์ 

  • ๋ทฐ ํ…œํ”Œ๋ฆฟ์—์„œ ์ค‘๋ณต ์ฒ˜๋ฆฌ๊ฐ€ ๋งŽ๋‹ค.
  • ํƒ€์ž… ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๊ฐ€ ์•ˆ๋œ๋‹ค
    -
    - Item์˜ price์— ๋ฌธ์ž๋ฅผ ์ž…๋ ฅํ•˜๋Š” ๊ฒƒ ์ฒ˜๋Ÿผ ํƒ€์ž… ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๊ณ ๊ฐ์ด ์ž…๋ ฅํ•œ ๋ฌธ์ž๋ฅผ ํ™”๋ฉด์— ๋‚จ๊ฒจ์•ผ ํ•œ๋‹ค.
    - ๋งŒ์•ฝ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋„ Item ์˜ price ๋Š” Integer ์ด๋ฏ€๋กœ ๋ฌธ์ž๋ฅผ ๋ณด๊ด€ํ•  ์ˆ˜๊ฐ€ ์—†๋‹ค.
    - ๊ฒฐ๊ตญ ๋ฌธ์ž๋Š” ๋ฐ”์ธ๋”ฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ๊ณ ๊ฐ์ด ์ž…๋ ฅํ•œ ๋ฌธ์ž๊ฐ€ ์‚ฌ๋ผ์ง€๊ฒŒ ๋˜๊ณ , ๊ณ ๊ฐ์€ ๋ณธ์ธ์ด ์–ด๋–ค ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ต๋‹ค.
    โžก๏ธ ๊ฒฐ๊ตญ ๊ณ ๊ฐ์ด ์ž…๋ ฅํ•œ ๊ฐ’๋„ ์–ด๋”˜๊ฐ€์— ๋ณ„๋„๋กœ ๊ด€๋ฆฌ๊ฐ€ ๋˜์–ด์•ผ ํ•œ๋‹คโ—๏ธ


๐Ÿท ํ”„๋กœ์ ํŠธ ์ค€๋น„ V2

์•ž์„œ ๋งŒ๋“  ๊ธฐ๋Šฅ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด, ์ปจํŠธ๋กค๋Ÿฌ์™€ ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ์„ ๋ณต์‚ฌํ•˜์žโ˜บ๏ธ

โœ”๏ธ ValidationItemControllerV2 ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ

- `hello.itemservice.web.validation.ValidationItemControllerV1` ๋ณต์‚ฌ
- `hello.itemservice.web.validation.ValidationItemControllerV2` ๋ถ™์—ฌ๋„ฃ๊ธฐ
  • URL ๊ฒฝ๋กœ ๋ณ€๊ฒฝ

    • validation/v1/ โžก๏ธ validation/v2/
  • ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ ๋ณต์‚ฌ

    • validation/v1 ๋””๋ ‰ํ† ๋ฆฌ์˜ ๋ชจ๋“  ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ์„ validation/v2 ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๋ณต์‚ฌ
    • /resources/templates/validation/v1/ โžก๏ธ /resources/templates/validation/v2/
  • /resources/templates/validation/v2/ ํ•˜์œ„ 4๊ฐœ ํŒŒ์ผ ๋ชจ๋‘ URL ๊ฒฝ๋กœ ๋ณ€๊ฒฝ

    • validation/v1/ โžก๏ธ validation/v2/

๋ณ€๊ฒฝ ํ›„, ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•ด ์ž˜ ๋™์ž‘ํ•˜๋Š” ์ง€ ํ™•์ธํ•˜๊ณ  ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜์–ด๊ฐ€๊ธฐ๐Ÿ˜€



๐Ÿท BindingResult1

์ง€๊ธˆ๋ถ€ํ„ฐ ์Šคํ”„๋ง์ด ์ œ๊ณตํ•˜๋Š” ๊ฒ€์ฆ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผํ…๋ฐ ์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ BindingResult์ด๋‹คโ—๏ธ


โœ”๏ธ ValidationItemControllerV2 - addItemV1

    @PostMapping("/add")
    public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        //๊ฒ€์ฆ ๋กœ์ง
        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() >= 9999) {
            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()) {
            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 โžก๏ธ ObjectError ๋กœ ๋ณ€๊ฒฝ

๐Ÿ“Œ ํŠน์ • ํ•„๋“œ๋ฅผ ๋„˜์–ด์„œ๋Š” ์˜ค๋ฅ˜๊ฐ€ ์žˆ์œผ๋ฉด ObjectError ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์„œ bindingResult ์— ๋‹ด์•„๋‘”๋‹ค

  • objectName : @ModelAttribute ์˜ ์ด๋ฆ„
  • defaultMessage : ์˜ค๋ฅ˜ ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€

โœ”๏ธ validation/v2/addForm.html ์ˆ˜์ •

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    </style>
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2 th:text="#{page.addItem}">์ƒํ’ˆ ๋“ฑ๋ก</h2>
    </div>

    <form action="item.html" th:action th:object="${item}" method="post">

        <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 type="text" id="itemName" th:field="*{itemName}"
                   th:errorclass="field-error" class="form-control" placeholder="์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”">
            <div class="field-error" th:errors="*{itemName}">
                ์ƒํ’ˆ๋ช… ์˜ค๋ฅ˜
            </div>
        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">๊ฐ€๊ฒฉ</label>
            <input type="text" id="price" th:field="*{price}"
                   th:errorclass="field-error" class="form-control" placeholder="๊ฐ€๊ฒฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”">
            <div class="field-error" th:errors="*{price}">
                ๊ฐ€๊ฒฉ ์˜ค๋ฅ˜
            </div>
        </div>

        <div>
            <label for="quantity" th:text="#{label.item.quantity}">์ˆ˜๋Ÿ‰</label>
            <input type="text" id="quantity" th:field="*{quantity}"
                   th:errorclass="field-error" class="form-control" placeholder="์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅํ•˜์„ธ์š”">
            <div class="field-error" th:errors="*{quantity}">
                ์ˆ˜๋Ÿ‰ ์˜ค๋ฅ˜
            </div>

        </div>

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">์ƒํ’ˆ ๋“ฑ๋ก</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/validation/v2/items}'|"
                        type="button" th:text="#{button.cancel}">์ทจ์†Œ</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

ํƒ€์ž„๋ฆฌํ”„ ์Šคํ”„๋ง ๊ฒ€์ฆ ์˜ค๋ฅ˜ ํ†ตํ•ฉ ๊ธฐ๋Šฅ

  • ํƒ€์ž„๋ฆฌํ”„๋Š” ์Šคํ”„๋ง์˜ BindingResult ๋ฅผ ํ™œ์šฉํ•ด์„œ ํŽธ๋ฆฌํ•˜๊ฒŒ ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.
  • #fields : #fields ๋กœ `BindingResult`` ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ฒ€์ฆ ์˜ค๋ฅ˜์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
  • th:errors : ํ•ด๋‹น ํ•„๋“œ์— ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์— ํƒœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค. th:if ์˜ ํŽธ์˜ ๋ฒ„์ „!
  • th:errorclass : th:field ์—์„œ ์ง€์ •ํ•œ ํ•„๋“œ์— ์˜ค๋ฅ˜๊ฐ€ ์žˆ์œผ๋ฉด class ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

1๏ธโƒฃ ๊ธ€๋กœ๋ฒŒ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

<div th:if="${#fields.hasGlobalErrors()}">
      <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="$
{err}">์ „์ฒด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€</p> 
</div>

2๏ธโƒฃ ํ•„๋“œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

<input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="์ด๋ฆ„์„
์ž…๋ ฅํ•˜์„ธ์š”">
<div class="field-error" th:errors="*{itemName}">
์ƒํ’ˆ๋ช… ์˜ค๋ฅ˜
</div>


๐Ÿท BindingResult2

์Šคํ”„๋ง์ด ์ œ๊ณตํ•˜๋Š” ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ๋ณด๊ด€ํ•˜๋Š” ๊ฐ์ฒด์ด๋‹ค.
๊ฒ€์ฆ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์—ฌ๊ธฐ์— ๋ณด๊ด€ํ•˜๋ฉด ๋œ๋‹ค.
BindingResult ๊ฐ€ ์žˆ์œผ๋ฉด @ModelAttribute ์— ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์‹œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค!

@ModelAttribute์— ๋ฐ”์ธ๋”ฉ ์‹œ ํƒ€์ž… ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด๐Ÿค”โ“

  • BindingResult ๊ฐ€ ์—†์œผ๋ฉด 400 ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด์„œ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๊ณ , ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋กœ
    ์ด๋™ํ•œ๋‹ค.
  • BindingResult ๊ฐ€ ์žˆ์œผ๋ฉด ์˜ค๋ฅ˜ ์ •๋ณด(FieldError)๋ฅผ BindingResult ์— ๋‹ด์•„์„œ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ
    ์ •์ƒ ํ˜ธ์ถœํ•œ๋‹ค.

โœ”๏ธ BindingResult์— ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ์ ์šฉํ•˜๋Š” 3๊ฐ€์ง€ ๋ฐฉ๋ฒ•

1๏ธโƒฃ @ModelAttribute ์˜ ๊ฐ์ฒด์— ํƒ€์ž… ์˜ค๋ฅ˜ ๋“ฑ์œผ๋กœ ๋ฐ”์ธ๋”ฉ์ด ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ ์Šคํ”„๋ง์ด FieldError ์ƒ์„ฑํ•ด์„œ
BindingResult ์— ๋„ฃ์–ด์ค€๋‹ค.
2๏ธโƒฃ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘! ๋„ฃ์–ด์ค€๋‹ค.
3๏ธโƒฃ Validator์„ ์‚ฌ์šฉํ•œ๋‹ค.


์ฃผ์˜๐Ÿคš๐Ÿป
BindingResult ๋Š” ๊ฒ€์ฆํ•  ๋Œ€์ƒ ๋ฐ”๋กœ ๋‹ค์Œ์— ์™€์•ผํ•œ๋‹ค! (์ˆœ์„œ ์ค‘์š”)

  • ์˜ˆ๋ฅผ ๋“ค์–ด์„œ @ModelAttribute Item item , ๋ฐ”๋กœ ๋‹ค์Œ์— BindingResult ๊ฐ€ ์™€์•ผ ํ•œ๋‹ค.
  • BindingResult ๋Š” Model์— ์ž๋™์œผ๋กœ ํฌํ•จ๋œ๋‹ค.

โœ”๏ธ BindingResult์™€ Errors

  • org.springframework.validation.Errors

  • org.springframework.validation.BindingResult

  • BindingResult ๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ด๊ณ , Errors ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์†๋ฐ›๊ณ  ์žˆ๋‹ค.

  • ์‹ค์ œ ๋„˜์–ด์˜ค๋Š” ๊ตฌํ˜„์ฒด๋Š” BeanPropertyBindingResult ๋ผ๋Š” ๊ฒƒ์ธ๋ฐ, ๋‘˜๋‹ค ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ BindingResult ๋Œ€์‹ ์— Errors ๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค.

  • Errors ์ธํ„ฐํŽ˜์ด์Šค๋Š” ๋‹จ์ˆœํ•œ ์˜ค๋ฅ˜ ์ €์žฅ๊ณผ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

  • BindingResult ๋Š” ์—ฌ๊ธฐ์— ๋”ํ•ด์„œ ์ถ”๊ฐ€์ ์ธ ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•œ๋‹ค.

  • addError() ๋„ BindingResult ๊ฐ€ ์ œ๊ณตํ•˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ๋Š” BindingResult ๋ฅผ ์‚ฌ์šฉํ•˜์žโ—๏ธ
    ์ฃผ๋กœ ๊ด€๋ก€์ƒ BindingResult ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค.



๐Ÿท FieldError, ObjectError

์šฐ๋ฆฌ์˜ ๋ชฉํ‘œ : ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ํ™”๋ฉด์— ๋‚จ๋„๋ก ๋งŒ๋“ค์–ด๋ณด์ž๐Ÿค—
โžก๏ธ ์˜ˆ) ๊ฐ€๊ฒฉ์„ 1000์› ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •์‹œ ์ž…๋ ฅํ•œ ๊ฐ’์ด ๋‚จ์•„์žˆ์–ด์•ผ ํ•œ๋‹ค!


โœ”๏ธ ValidationItemControllerV2 - addItemV2

    @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}";
    }

์ฃผ์˜๐Ÿคš๐Ÿป - addItemV1() ์˜ @PostMapping("/add") ์„ ์ฃผ์„ ์ฒ˜๋ฆฌํ•˜์ž!

์‚ฌ์‹ค FieldError ๋Š” ๋‘ ๊ฐ€์ง€ ์ƒ์„ฑ์ž๋ฅผ ์ œ๊ณตํ•œ๋‹คโ—๏ธ

 public FieldError(String objectName, String field, String defaultMessage);
 
 public FieldError(String objectName, String field, @Nullable Object
  rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
  Object[] arguments, @Nullable String defaultMessage)
  • objectName : ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฐ์ฒด ์ด๋ฆ„
  • field : ์˜ค๋ฅ˜ ํ•„๋“œ
  • rejectedValue : ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’(๊ฑฐ์ ˆ๋œ ๊ฐ’)
  • bindingFailure : ํƒ€์ž… ์˜ค๋ฅ˜ ๊ฐ™์€ ๋ฐ”์ธ๋”ฉ ์‹คํŒจ์ธ์ง€, ๊ฒ€์ฆ ์‹คํŒจ์ธ์ง€ ๊ตฌ๋ถ„ ๊ฐ’
  • codes : ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ
  • arguments : ๋ฉ”์‹œ์ง€์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ธ์ž
  • defaultMessage : ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€

โœ”๏ธ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’ ์œ ์ง€

new FieldError("item", "price", item.getPrice(), false, null, null, "๊ฐ€๊ฒฉ์€ 1,000 ~ 1,000,000 ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.")
  • FieldError ๋Š” ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’์„ ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.
  • ์—ฌ๊ธฐ์„œ rejectedValue ๊ฐ€ ๋ฐ”๋กœ ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’์„ ์ €์žฅํ•˜๋Š” ํ•„๋“œ๋‹ค.
  • bindingFailure ๋Š” ํƒ€์ž… ์˜ค๋ฅ˜ ๊ฐ™์€ ๋ฐ”์ธ๋”ฉ์ด ์‹คํŒจํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ์ ์–ด์ฃผ๋ฉด ๋œ๋‹ค.
  • ์—ฌ๊ธฐ์„œ๋Š” ๋ฐ”์ธ๋”ฉ์ด ์‹คํŒจํ•œ ๊ฒƒ์€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— false๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

โœ”๏ธ ํƒ€์ž„๋ฆฌํ”„์˜ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’ ์œ ์ง€

โžก๏ธ th:field="*{price}"

  • ํƒ€์ž„๋ฆฌํ”„์˜ th:field ๋Š” ๋งค์šฐ ๋˜‘๋˜‘ํ•˜๊ฒŒ ๋™์ž‘ํ•จ!
  • ์ •์ƒ ์ƒํ™ฉ์—๋Š” ๋ชจ๋ธ ๊ฐ์ฒด์˜ ๊ฐ’์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด FieldError ์—์„œ ๋ณด๊ด€ํ•œ ๊ฐ’์„ ์‚ฌ์šฉํ•ด์„œ ๊ฐ’์„ ์ถœ๋ ฅํ•œ๋‹ค.

โœ”๏ธ ์Šคํ”„๋ง์˜ ๋ฐ”์ธ๋”ฉ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

  • ํƒ€์ž… ์˜ค๋ฅ˜๋กœ ๋ฐ”์ธ๋”ฉ์— ์‹คํŒจํ•˜๋ฉด ์Šคํ”„๋ง์€ FieldError ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’์„ ๋„ฃ์–ด๋‘”๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น ์˜ค๋ฅ˜๋ฅผ BindingResult ์— ๋‹ด์•„์„œ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
    โžก๏ธ ๋”ฐ๋ผ์„œ ํƒ€์ž… ์˜ค๋ฅ˜ ๊ฐ™์€ ๋ฐ”์ธ์‹ฑ ์‹คํŒจ์‹œ์—๋„ ์‚ฌ์šฉ์ž์˜ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ •์ƒ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค.

โœ”๏ธ ์‹คํ–‰ ๊ฒฐ๊ณผ

โฌ†๏ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋– ๋„ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’์ด ๊ทธ๋Œ€๋กœ ๋‚จ์•„์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธ


๋‚ด์šฉ์ด ๊ธธ์–ด์ง€๋‹ˆ 2ํƒ„์œผ๋กœ ๋Œ์•„์˜ค๊ฒ ์Šด๋‹ค!

profile
Backend Developer

0๊ฐœ์˜ ๋Œ“๊ธ€