DevLog ๐Ÿ˜ถ

[Spring] Bean Validation - ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ๊ฒ€์ฆ ์ง„ํ–‰ํ•˜๊ธฐ ๋ณธ๋ฌธ

Back-end/Spring

[Spring] Bean Validation - ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ๊ฒ€์ฆ ์ง„ํ–‰ํ•˜๊ธฐ

dolmeng2 2022. 8. 22. 22:58

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

 

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

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

www.inflearn.com


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

 

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

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

cl8d.tistory.com


 

| Bean Validation

- ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ๊ฒ€์ฆ ๋กœ์ง์„ ์„ค์ •ํ•ด๋ณด์ž.

- ์šฐ์„ , build.gradle์— ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์ž.

 

[SItem.java] - ์ˆ˜์ •

@Getter
@NoArgsConstructor
@Setter
public class SItem {
    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min=1000, max=1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;
    
    ...

- @Range์˜ ๊ฒฝ์šฐ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ validation์ธ๋ฐ ์‹ค๋ฌด์—์„œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํฌ๊ฒŒ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š๊ณ  ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค.

- ๊ทธ์™ธ ๋‚˜๋จธ์ง€๋Š” javax.validation์ด๋ผ ํŠน์ • ๊ตฌํ˜„์— ๊ด€๊ณ„์—†์ด ์ œ๊ณต๋œ๋‹ค.

 

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

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

- ์ง€๋‚œ ํฌ์ŠคํŒ…์—์„œ ์ž‘์„ฑํ•˜์˜€๋˜ WebBinder๋ฅผ ์ด์šฉํ•œ validation ์ถ”๊ฐ€ ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•˜์˜€๋‹ค.

- ์ด๋ ‡๊ฒŒ ํ•ด๋„ ์‹ค์ œ๋กœ๋Š” ์ž˜ ๋™์ž‘ํ•œ๋‹ค. ์–ด๋–ป๊ฒŒ ์ด๋Ÿฌ๋Š” ๊ฒƒ์ผ๊นŒ? (๋‹จ, ์•„์ง์€ ์šฐ๋ฆฌ๊ฐ€ ์ง€์ •ํ•œ ๋ฉ”์‹œ์ง€๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€๋Š” ์•Š๋Š”๋‹ค.)

 

- ๊ธฐ๋ณธ์ ์œผ๋กœ, ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” build.gradle์— ์žˆ๋Š” spring-boot-starter-validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™•์ธํ•˜๋ฉด Bean Validator๋ฅผ ์ž๋™์œผ๋กœ ์ธ์ง€ํ•œ๋‹ค.

- ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” ValidationAutoConfiguration ํด๋ž˜์Šค๋ฅผ ํ†ตํ•ด์„œ LocalValidatorFactoryBean์„ ๊ธ€๋กœ๋ฒŒ Validator๋กœ ๋“ฑ๋กํ•œ๋‹ค.

- ๋˜ํ•œ, ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ ๋ฆฌํ„ด ๊ฐ’ ๊ฒ€์ฆ์„ ์œ„ํ•ด MethodValidationPostProcessor ์—ญ์‹œ ์ž๋™์œผ๋กœ ์„ค์ •ํ•œ๋‹ค.

@AutoConfiguration
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    @ConditionalOnMissingBean(Validator.class)
    public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext) {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext);
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        return factoryBean;
    }

    @Bean
    @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
    public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
                                                                              @Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
        FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(
                excludeFilters.orderedStream());
        boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
        processor.setProxyTargetClass(proxyTargetClass);
        processor.setValidator(validator);
        return processor;
    }

}

- ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด @Bean์„ ํ†ตํ•ด 2๊ฐ€์ง€๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿšฉ ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค์— @Validated๋ฅผ ๋ถ™์ด๊ณ  ๊ฐ ๋ฉ”์„œ๋“œ์˜ @RequestParam์— @Min ๊ฐ™์€ ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜์„ ์„ค์ •ํ•˜๋ฉด,

MethodValidationPostProcessor์— ์˜ํ•ด์„œ validation์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ํ”„๋ก์‹œ ๊ฐ์ฒด๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค!

 

 

โœ” ๊ฒ€์ฆ ์ˆœ์„œ

- 1์ฐจ์ ์œผ๋กœ @ModelAttribute์˜ ๊ฐ๊ฐ์˜ ํ•„๋“œ์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜์–ด์˜จ ๊ฐ’์„ ๋„ฃ์–ด ํƒ€์ž… ๋ณ€ํ™˜์„ ์‹œ๋„ํ•œ๋‹ค.

- ์‹คํŒจ ์‹œ typeMismatch๋กœ FieldError๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ์„ฑ๊ณตํ•˜๋ฉด ๋‹ค์Œ ๋กœ์ง์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค.

- โญ ์ด๋•Œ, ๋ฐ”์ธ๋”ฉ์— ์„ฑ๊ณตํ•œ ํ•„๋“œ๋งŒ Bean Validation์„ ์ ์šฉํ•œ๋‹ค.

- @Validated, @Valid๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ธ€๋กœ๋ฒŒ ๊ฒ€์ฆ๊ธฐ๋ฅผ ํ™œ์šฉํ•ด์„œ Validator๋ฅผ ์ฐพ๊ณ  ๊ฒ€์ฆ์„ ์‹คํ–‰ํ•œ๋‹ค. (Bean Validation)

 

- ๊ทธ๋ ‡๋‹ค๋ฉด, ์šฐ๋ฆฌ๊ฐ€ ์„ค์ •ํ•œ ๋ฉ”์‹œ์ง€๋กœ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜์˜ค๋„๋ก ํ•ด๋ณด์ž.

- ๊ธฐ๋ณธ์ ์œผ๋กœ, ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ ์• ๋…ธํ…Œ์ด์…˜ ์ด๋ฆ„ + ๊ทœ์น™์„ ํ†ตํ•ด ์„ค์ •๋œ๋‹ค.

@Range

- Range.item.price

- Range.price

- Range.java.lang.Integer

- Range

 

- ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์—, ์šฐ๋ฆฌ๊ฐ€ ๋”ฐ๋กœ ์ด์™€ ๊ด€๋ จํ•ด์„œ ํ”„๋กœํผํ‹ฐ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

[errors.properties]

NotBlank=Please enter the {0} as required.
Range={0} range from KRW {1} to KRW {2}.
Max=Up to {1}, {0} are allowed.

[errors_ja.properties]

NotBlank=ๅฟ…่ฆใซๅฟœใ˜ใฆ{0}ใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚
Range={0}ใฎ็ฏ„ๅ›ฒใฏ{1}ใ‚ฆใ‚ฉใƒณใ‹ใ‚‰{2}ใ‚ฆใ‚ฉใƒณใงใ™ใ€‚
Max=ๆœ€ๅคง{1}ใ€{0}ใŒ่จฑๅฏใ•ใ‚Œใพใ™ใ€‚

- ์ด๋ ‡๊ฒŒ ๋˜๋ฉด, ๋งŒ๋“ค์–ด์ง„ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ์— ๋Œ€ํ•ด ์ˆœ์„œ๋Œ€๋กœ messageSource์—์„œ ์ฐพ๋Š”๋‹ค.

- ๋งŒ์•ฝ ์—†์œผ๋ฉด ์–ด๋…ธํ…Œ์ด์…˜์˜ message ์†์„ฑ ๊ฐ’์„ ์ด์šฉํ•˜๊ฒŒ ๋œ๋‹ค. (@Range(message=""))

- ๊ทธ๋ž˜๋„ ์—†์œผ๋ฉด ๊ทธ๋ƒฅ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ๊ฐ’์ด ๋‚˜์˜จ๋‹ค.

- ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž˜ ๋™์ž‘ํ•˜๋Š” ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

- ๊ทธ๋Ÿผ, ํŠน์ • ํ•„๋“œ์— ๋Œ€ํ•œ ์˜ค๋ฅ˜๊ฐ€ ์•„๋‹Œ ์˜ค๋ธŒ์ ํŠธ ์˜ค๋ฅ˜(ObjectError)์˜ ๊ฒฝ์šฐ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์„๊นŒ?

@ScriptAssert๋ผ๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ƒ๋‹นํžˆ ๋ณต์žกํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์›ฌ๋งŒํ•˜๋ฉด ์ด ๋ถ€๋ถ„์€ ์ž๋ฐ” ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ๋‚ซ๋‹ค.

 

[SItemController.java] - ์ˆ˜์ •

@PostMapping("/add")
public String addItem (@Validated @ModelAttribute("item") SItem item, BindingResult bindingResult,
                       RedirectAttributes redirectAttributes) {

    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
        }
    }
    ...

| Bean Validation - ์ „๋ถ€ ์ ์šฉํ•˜๊ธฐ

- ์ง€๊ธˆ๊นŒ์ง€๋Š” ๋“ฑ๋ก ํผ์—๋งŒ ์ ์šฉํ–ˆ์ง€๋งŒ, ์ด๋ฒˆ์—๋Š” ์ˆ˜์ • ํผ์—๋„ ์ ์šฉํ•ด์ฃผ์ž!

[SItemController.java]

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId,
                   @Validated @ModelAttribute("item") SItem item,
                   BindingResult bindingResult) {

    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/editForm";
    }

    service.updateItem(itemId, item);
    return "redirect:/basic/items/{itemId}";
}

 

[editForm.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 Edit Form</title>
    <style>
        .field-error{
            border-color:#dc3545;
            color:#dc3545;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2 class="font-monospace fw-bold"
            th:text="#{page.updateItem}">Item Edit Form</h2>
    </div>

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

        <div class="row fw-bold font-monospace">
            <div class="col text-center">
                <label for="id"
                       th:text="#{label.item.id}">Id</label>
                <input type="text"
                       th:field="*{id}"
                       class="form-control text-center"
                       readonly>
            </div>
            <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>
        ...
    </form>
</div>
</body>
</html>

 


| Bean Validation - ํ•œ๊ณ„์ 

- ๋ฐ์ดํ„ฐ๋ฅผ ๋“ฑ๋กํ•  ๋•Œ์™€ ์ˆ˜์ •ํ•  ๋•Œ์˜ ์š”๊ตฌ์‚ฌํ•ญ์ด ๋‹ค๋ฅด๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ• ๊นŒ?

- ์ˆ˜์ •ํ•  ๋•Œ๋Š” ์ˆ˜๋Ÿ‰์„ ๋ฌด์ œํ•œ์œผ๋กœ ์ฒดํฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝํ•ด๋ณด์ž.

- ๋˜ํ•œ, ์ˆ˜์ • ์‹œ์—๋Š” id ๊ฐ’์ด ํ•„์ˆ˜๋กœ ๋“ค์–ด๊ฐ„๋‹ค.

 

์ด๋ฅผ ์œ„ํ•ด Bean Validation์˜ groups ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋“ฑ๋ก์šฉ ์ธํ„ฐํŽ˜์ด์Šค์™€ ์ˆ˜์ •์šฉ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋”ฐ๋กœ ์ƒ์„ฑํ•˜์—ฌ ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜์˜ ๊ทธ๋ฃน ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

ex)

๋„๋ฉ”์ธ ํ•„๋“œ - @NotNull(groups=UpdateChecking.class)

์ปจํŠธ๋กค๋Ÿฌ - @Validated(UpdateChecking.class)

cf) ์ด๋•Œ, @Valid๋Š” groups ๊ธฐ๋Šฅ์ด ์—†๊ณ  @Validated๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค!

 

๊ทธ๋Ÿฌ๋‚˜, ์ด ๋ฐฉ๋ฒ•๋ณด๋‹ค๋Š” ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์„ ์œ„ํ•œ ๋ณ„๋„์˜ ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค.

์™œ๋ƒ๋ฉด, ๋ณดํ†ต์€ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด ์ƒ์„ฑ/์ˆ˜์ • ์‹œ ๊ฒ€์ฆ๋„ groups๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ณ , ๊ฐ„๋‹จํ•œ ๊ฒฝ์šฐ๋งŒ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋œ๋‹ค.

๋ณดํ†ต, DTO๋ฅผ ํ†ตํ•ด์„œ ์ด๋Ÿฌํ•œ ์ •๋ณด๋ฅผ ๋งŽ์ด ๊ด€๋ฆฌํ•˜๋ฉฐ, ์ด๋ฆ„์€ ๊ฐ ์—ญํ• ์„ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ๋„๋ก '์˜๋ฏธ์žˆ๊ฒŒ' ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

 

- ์šฐ๋ฆฌ์˜ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด๋ณด์ž. ์šฐ์„ , ๊ธฐ์กด์˜ SItem์—์„œ ์ ์šฉํ•˜์˜€๋˜ ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜๋“ค์€ ๋‹ค ์‚ญ์ œํ•œ๋‹ค.

[SItem.java]

@Getter
@NoArgsConstructor
@Setter
public class SItem {
    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    private Boolean open;
    private List<String> regions;
    private SItemType itemType;
    private String deliveryCode;

    public SItem(String itemName, Integer price, Integer quantity, Boolean open, List<String> regions, SItemType itemType, String deliveryCode) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
        this.open = open;
        this.regions = regions;
        this.itemType = itemType;
        this.deliveryCode = deliveryCode;
    }

    public void changeId(Long id) {
        this.id = id;
    }

    public void changeItemInfo(String itemName, Integer price, Integer quantity,
                               Boolean open, List<String> regions, SItemType itemType,
                               String deliveryCode) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
        this.open = open;
        this.regions = regions;
        this.itemType = itemType;
        this.deliveryCode = deliveryCode;
    }

}

- ๋“ฑ๋ก, ์ˆ˜์ •์šฉ๋„ DTO ๊ฐ์ฒด๋ฅผ ์ƒˆ๋กญ๊ฒŒ ๋งŒ๋“ ๋‹ค.

- Setter๋ฅผ ์‚ญ์ œํ•˜์˜€์œผ๋ฉฐ, ์ƒ์„ฑ์ž ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ˆ˜์ •ํ•˜์˜€๋‹ค.

 

[SItemSaveRequest.java]

@Data
@AllArgsConstructor
public class SItemSaveRequest {
    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Max(value = 9999)
    private Integer quantity;

    @NotNull
    private Boolean open;
    @NotNull
    private List<String> regions;
    @NotNull
    private SItemType itemType;
    @NotNull
    private String deliveryCode;

}

[SItemUpdateRequest.java]

@Data
public class SItemUpdateRequest {
    @NotNull
    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    private Integer quantity;

    @NotNull
    private Boolean open;
    @NotNull
    private List<String> regions;
    @NotNull
    private SItemType itemType;
    @NotNull
    private String deliveryCode;
}

[SItemRepository.java] - ์ผ๋ถ€ ์ˆ˜์ •

public void update(Long itemId, String itemName, Integer price, Integer quantity,
                   Boolean open, String deliveryCode, List<String> regions, SItemType itemType) {
    SItem findItem = store.get(itemId);
    findItem.changeItemInfo(itemName, price, quantity, open, regions, itemType, deliveryCode);
}

- Dto๊ฐ€ ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ ๋‹จ๊ณ„๊นŒ์ง€ ๋‚ด๋ ค๊ฐ€๋Š” ๊ฑด ๋ณ„๋กœ ์ข‹์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ์—ˆ๋‹ค.

 

[SItemService.java] - ์ผ๋ถ€ ์ˆ˜์ •

public SItem saveItem(SItemSaveRequest itemDto) {
    SItem item = new SItem(itemDto.getItemName(), itemDto.getPrice(), itemDto.getPrice(), itemDto.getOpen(),
            itemDto.getRegions(), itemDto.getItemType(), itemDto.getDeliveryCode());
    return repository.save(item);
}

public void updateItem(Long itemId, SItemUpdateRequest item) {
    repository.update(itemId, item.getItemName(), item.getPrice(), item.getQuantity(),
            item.getOpen(), item.getDeliveryCode(), item.getRegions(), item.getItemType());
}

 

[SItemController.java]

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class SItemController {
    private final SItemService service;

    ...

    @PostMapping("/add")
    public String addItem (@Validated @ModelAttribute("item") SItemSaveRequest item,
                           BindingResult bindingResult, RedirectAttributes redirectAttributes) {

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

    ...

    @PostMapping("/{itemId}/edit")
    public String edit(@PathVariable Long itemId,
                       @Validated @ModelAttribute("item") SItemUpdateRequest item,
                       BindingResult bindingResult) {

        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/editForm";
        }

        service.updateItem(itemId, item);
        return "redirect:/basic/items/{itemId}";
    }

    ...
    // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (์˜์กด๊ด€๊ณ„ ์ฃผ์ž… ์ดํ›„ ์‹คํ–‰)
    @PostConstruct
    public void init() {
        service.saveItem(new SItemSaveRequest("itemA", 10000, 10, true, null, SItemType.BOOK, null));
        service.saveItem(new SItemSaveRequest("itemB", 20000, 20, false, null, SItemType.FOOD, null));
    }
}

- ๋“ฑ๋ก์— ๋Œ€ํ•ด์„œ๋Š” ItemSaveRequest๋ฅผ, ์ˆ˜์ •์— ๋Œ€ํ•ด์„œ๋Š” ItemUpdateRequest๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ์—ˆ๋‹ค.

- ๋ชจ๋‘ ๋งค์šฐ ์ž˜ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

 


| Bean Validation - HTTP Message Converter

- @Valid, @Validated๋Š” @RequestBody์—๋„ ์ ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

cf) @RequestBody๋Š” HTTP  Body์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉ.

- ์ด ๊ฒฝ์šฐ, ํƒ€์ž… ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์• ์ดˆ๋ถ€ํ„ฐ 'SItemSaveRequest ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜'ํ•˜๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ์‹คํŒจํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ปจํŠธ๋กค๋Ÿฌ ์ž์ฒด๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค.

- ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ผ๋ฉด (์ˆ˜๋Ÿ‰์ด 9999๊ฐœ ์ด์ƒ ๊ฐ™์€ ์˜ค๋ฅ˜) ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์‹คํ–‰๋˜๋ฉฐ ๊ฒ€์ฆ ์˜ค๋ฅ˜ ์ž์ฒด๊ฐ€ ์ž˜ ์‹คํ–‰๋œ๋‹ค.

 

 

โœ” @ModelAttribute vs @RequestBody

- HttpMessageConverter์˜ ๊ฒฝ์šฐ ์ „์ฒด ๊ฐ์ฒด ๋‹จ์œ„๋กœ ์ ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ๊ฐ์ฒด ์ƒ์„ฑ์ด ๋˜์–ด์•ผ @Valid, @Validated๊ฐ€ ์ ์šฉ๋œ๋‹ค.

- @ModelAttribute๋Š” ๊ฐ๊ฐ์˜ ํ•„๋“œ ๋‹จ์œ„๋กœ ๋ฐ”์ธ๋”ฉ์ด ์ ์šฉ๋œ๋‹ค. ๊ทธ๋ž˜์„œ ํŠน์ • ํ•„๋“œ๊ฐ€ ๋ฐ”์ธ๋”ฉ ์•ˆ ๋˜์–ด๋„ ๋‹ค๋ฅธ ํ•„๋“œ๋Š” ์ž˜ ๋ฐ”์ธ๋”ฉ๋œ๋‹ค.

 

๐Ÿšฉ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์ฐจ์ด์ 

@ModelAttribute : Http ํŒŒ๋ผ๋ฏธํ„ฐ, Http Body ๋‚ด์šฉ์„ Setter๋ฅผ ํ†ตํ•ด 1:1๋กœ ๊ฐ์ฒด์— ๋ฐ”์ธ๋”ฉ 

- ๋ฐ”์ธ๋”ฉ ์‹œ ๊ฒ€์ฆ ์ž‘์—… ์ง„ํ–‰ 

- Http Body๋Š” multipart/form-data ํ˜•ํƒœ์ด๋‹ค.

 

@RequestBody : Http Body์— application/json ํ˜•ํƒœ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๋ฉด ํ•ด๋‹น ๋‚ด์šฉ์„ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ์—ญํ•  ์ˆ˜ํ–‰

- ๊ฐ์ฒด ๋ณ€ํ™˜์€ MappingJackson2HttpMessageConverter ์ด์šฉ

- Body ๋‚ด์šฉ์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ์ฒด์— @Setter ์—†์–ด๋„ ๋ฌด๊ด€, POST์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (GET์€ header ๊ฐ’ ์‚ฌ์šฉ)

 

@RequestParam : HTTP ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ @RequestParam์ด ์“ฐ์ธ ๋ณ€์ˆ˜๋กœ mapping

 

- ๋‹ค์Œ ํฌ์ŠคํŒ…๋ถ€ํ„ฐ๋Š” ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž!

Comments