DevLog ๐Ÿ˜ถ

[Spring] BindingResult๋ฅผ ํ™œ์šฉํ•ด์„œ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ํ•˜๊ธฐ, MessageCodesResolver ์•Œ์•„๋ณด๊ธฐ ๋ณธ๋ฌธ

Back-end/Spring

[Spring] BindingResult๋ฅผ ํ™œ์šฉํ•ด์„œ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ํ•˜๊ธฐ, MessageCodesResolver ์•Œ์•„๋ณด๊ธฐ

dolmeng2 2022. 8. 22. 00:20

๊น€์˜ํ•œ ๋‹˜์˜ '์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ '์„ ๋ณด๊ณ  ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค ๐Ÿ˜Š

 

์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ  - ์ธํ”„๋Ÿฐ | ๊ฐ•์˜

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์›น ๊ธฐ์ˆ ์„ ๊ธฐ์ดˆ๋ถ€ํ„ฐ ์ดํ•ดํ•˜๊ณ , ์™„์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. MVC 2ํŽธ์—์„œ๋Š” MVC 1ํŽธ์˜ ํ•ต์‹ฌ ์›๋ฆฌ์™€ ๊ตฌ์กฐ ์œ„์— ์‹ค๋ฌด ์›น ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ชจ๋“  ํ™œ์šฉ ๊ธฐ์ˆ ๋“ค์„ ํ•™์Šตํ•  ์ˆ˜ ์žˆ

www.inflearn.com


- ์ง€๋‚œ ํฌ์ŠคํŒ…๊ณผ ์ด์–ด์ง‘๋‹ˆ๋‹ค :D

 

[Spring] ๋ฉ”์‹œ์ง€์™€ ๊ตญ์ œํ™”๋ฅผ ํ†ตํ•ด ์–ธ์–ด ์„ค์ • ์ปค์Šคํ…€ํ•˜๊ธฐ

๊น€์˜ํ•œ ๋‹˜์˜ '์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ '์„ ๋ณด๊ณ  ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค ๐Ÿ˜Š ์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ  - ์ธํ”„๋Ÿฐ | ๊ฐ•์˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์›น ๊ธฐ์ˆ ์„

cl8d.tistory.com


 

| ๊ฒ€์ฆ ์ถ”๊ฐ€ํ•˜๊ธฐ

- ์ง€๋‚œ ํฌ์ŠคํŒ…์˜ ์ƒํ’ˆ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์˜ ๊ฐ ํ•„๋“œ์— ๋Œ€ํ•œ ๊ฒ€์ฆ ์š”๊ตฌ์‚ฌํ•ญ์ด ์ถ”๊ฐ€๋˜์—ˆ๋‹ค.

- ๊ฐ€๊ฒฉ, ์ˆ˜๋Ÿ‰์— ๋ฌธ์ž๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ์˜ค๋ฅ˜ 

- ์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜๋กœ 

- ๊ฐ€๊ฒฉ์€ 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 ๋ฌธ๋ฒ• ๋ชจ์Œ

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

 

- ๊ธฐ๋ณธ ๊ฒ€์ฆ์€ ์™„๋ฃŒํ–ˆ๋‹ค. ์ด์ œ, ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ด๋ณด์ž.

- ํ˜„์žฌ๋Š” ์ž˜๋ชป๋œ ํƒ€์ž…์„ ๋„ฃ์œผ๋ฉด ์ปจํŠธ๋กค๋Ÿฌ ์ง„์ž… ์ด์ „์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋‹ค๋ฅธ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.

- ์šฐ๋ฆฌ๋Š” ์ด๋ฅผ ์œ„ํ•ด ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ฒ€์ฆ ๋ฐฉ๋ฒ•์„ ์ด์šฉํ•ด๋ณผ ๊ฒƒ์ด๋‹ค.

 

[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์„ ์•Œ์•„๋ณด์ž. (์•„๋งˆ ์‹ค์ œ ๊ฒ€์ฆ์—์„œ๋Š” ์ด๊ฑฐ๋ฅผ ํ›จ์”ฌ ๋งŽ์ด ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค!)

Comments