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

sorzzzzyยท2021๋…„ 9์›” 19์ผ
1

Spring Boot - RoadMap 1

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

๐Ÿท ์˜ค๋ฅ˜ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ1

์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋“ค๋„ ์ผ๊ด„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ํŽธํ•˜๋‹ค๐Ÿ˜Š
โžก๏ธ ์–ด๋””์„œ๋“  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—

์ง€๋‚œ ์‹œ๊ฐ„์— ๋ฐฐ์› ๋˜ ๋ฉ”์‹œ์ง€ ๊ธฐ๋Šฅ์„ ์ƒ๊ฐํ•ด๋ณด๋ฉฐ, errors ๋ฉ”์‹œ์ง€ ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด๋ณด์žโ—๏ธ
โžก๏ธ messages.properties ๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋˜์ง€๋งŒ, ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์‰ฝ๊ฒŒ errors.properties ๋ผ๋Š” ๋ณ„๋„์˜ ํŒŒ์ผ๋กœ ๊ด€๋ฆฌํ•ด๋ณด์ž.


1๏ธโƒฃ ์Šคํ”„๋ง ๋ถ€ํŠธ ๋ฉ”์‹œ์ง€ ์„ค์ • ์ถ”๊ฐ€ - application.properties
โžก๏ธ spring.messages.basename=messages,errors


2๏ธโƒฃ errors.properties ์ถ”๊ฐ€ - src/main/resources/errors.properties

required.item.itemName=์ƒํ’ˆ ์ด๋ฆ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. 
range.item.price=๊ฐ€๊ฒฉ์€ {0} ~ {1} ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. 
max.item.quantity=์ˆ˜๋Ÿ‰์€ ์ตœ๋Œ€ {0} ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. 
totalPriceMin=๊ฐ€๊ฒฉ * ์ˆ˜๋Ÿ‰์˜ ํ•ฉ์€ {0}์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ฐ’ = {1}

โœ”๏ธ ValidationItemControllerV2 - addItemV3() ์ถ”๊ฐ€

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

        //๊ฒ€์ฆ ๋กœ์ง
        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() >= 9999) {
            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()) {
            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}";
    }
  • codes : required.item.itemName ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๋ฅผ ์ง€์ •ํ•œ๋‹ค.
    • ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๋Š” ํ•˜๋‚˜๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฐฐ์—ด๋กœ ์—ฌ๋Ÿฌ ๊ฐ’์„ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ˆœ์„œ๋Œ€๋กœ ๋งค์นญํ•ด์„œ ์ฒ˜์Œ ๋งค์นญ๋˜๋Š” ๋ฉ”์‹œ์ง€๊ฐ€ ์‚ฌ์šฉ๋œ๋‹ค.
  • arguments : Object[]{1000, 1000000} ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ฝ”๋“œ์˜ {0} , {1} ๋กœ ์น˜ํ™˜ํ•  ๊ฐ’์„ ์ „๋‹ฌํ•œ๋‹ค.

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

โฌ†๏ธ ์ด๋ ‡๊ฒŒ๋งŒ ๋ณด๋ฉด, errors.properties์—์„œ ์ ์šฉ๋œ ๊ฑด ์ง€ ์ •ํ™•ํžˆ ์•Œ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด errors.properties ์˜ ๋‚ด์šฉ์„ ์ˆ˜์ • ํ›„ ๋‹ค์‹œ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž!

โฌ†๏ธ ๋ฉ”์‹œ์ง€ ๋’ค์— test์ถ”๊ฐ€
โฌ†๏ธ ์ž˜ ์ ์šฉ๋œ ๊ฒƒ์„ ํ™•์ธ๐Ÿ‘๐Ÿป



๐Ÿท ์˜ค๋ฅ˜ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ2

๊ทธ๋Ÿฐ๋ฐ FieldError , ObjectError ๋Š” ๋‹ค๋ฃจ๊ธฐ ๋„ˆ๋ฌด ๋ฒˆ๊ฑฐ๋กœ์šด๋ฐ,,
์˜ค๋ฅ˜ ์ฝ”๋“œ๋„ item.itemName ์ฒ˜๋Ÿผ ์ข€ ๋” ์ž๋™ํ™” ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ๐Ÿค”โ“

๐Ÿ’ก BindingResult ๊ฐ€ ์ œ๊ณตํ•˜๋Š” rejectValue(), reject() ์„ ์‚ฌ์šฉํ•˜๋ฉด FieldError , ObjectError ๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ , ๊น”๋”ํ•˜๊ฒŒ ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋‹ค!!!!!!


โœ”๏ธ ValidationItemControllerV2 - addItemV4() ์ถ”๊ฐ€

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

        log.info("objectName={}", bindingResult.getObjectName());
        log.info("target={}", bindingResult.getTarget());

        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, 10000000}, null);
        }
        if (item.getQuantity() == null || item.getQuantity() >= 9999) {
            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()) {
            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}";
    }

โžก๏ธ ์ด๋ ‡๊ฒŒ ์‹คํ–‰ํ–ˆ๋Š”๋ฐ, errors.properties ์— ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•˜์ง€ ์•Š์•˜์Œ์—๋„ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์ •์ƒ ์ถœ๋ ฅ๋˜๋Š”๋ฐ ์–ด๋–ป๊ฒŒ ๋œ ๊ฒƒ์ผ๊นŒโ“

๐Ÿ“ŒrejectValue()

 void rejectValue(@Nullable String field, String errorCode,
        @Nullable Object[] errorArgs, @Nullable String defaultMessage)
  • field : ์˜ค๋ฅ˜ ํ•„๋“œ๋ช…
  • errorCode : ์˜ค๋ฅ˜ ์ฝ”๋“œ(์ด ์˜ค๋ฅ˜ ์ฝ”๋“œ๋Š” ๋ฉ”์‹œ์ง€์— ๋“ฑ๋ก๋œ ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๊ณ , ๋’ค์—์„œ ์„ค๋ช…ํ•  messageResolver๋ฅผ ์œ„ํ•œ ์˜ค๋ฅ˜ ์ฝ”๋“œ์ด๋‹ค)
  • errorArgs : ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์—์„œ {0} ์„ ์น˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ’
  • defaultMessage : ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€

โœ”๏ธ ์ถ•์•ฝ๋œ ์˜ค๋ฅ˜ ์ฝ”๋“œ

๐Ÿค” : FieldError() ๋ฅผ ์ง์ ‘ ๋‹ค๋ฃฐ ๋•Œ๋Š” ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ range.item.price ์™€ ๊ฐ™์ด ๋ชจ๋‘ ์ž…๋ ฅํ–ˆ์–ด! ๊ทธ๋Ÿฐ๋ฐ rejectValue() ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ๋ถ€ํ„ฐ๋Š” ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ range ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฐ”๊ฟจ๋Š”๋ฐ๋„ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ž˜ ์ถœ๋ ฅํ•˜๋Š”๋ฐ,, ์ด๊ฒŒ ์™œ ๊ทธ๋Ÿฐ ๊ฑฐ์•ผ?

๐Ÿ˜‡ : ์ด๊ฑธ ์ดํ•ดํ•˜๋ ค๋ฉด ์ผ๋‹จ MessageCodesResolver ๋ฅผ ๋จผ์ € ์ดํ•ดํ•ด์•ผ ํ•œ๋‹จ๋‹ค^^



๐Ÿท ์˜ค๋ฅ˜ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ3

  • required.item.itemName : ์ƒํ’ˆ ์ด๋ฆ„์€ ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค.
  • range.item.price : ์ƒํ’ˆ์˜ ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ์˜ค๋ฅ˜ ์ž…๋‹ˆ๋‹ค.
    โžก๏ธ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž์„ธํžˆ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ๊ณ ,

  • required : ํ•„์ˆ˜ ๊ฐ’ ์ž…๋‹ˆ๋‹ค.
  • range : ๋ฒ”์œ„ ์˜ค๋ฅ˜ ์ž…๋‹ˆ๋‹ค.
    โžก๏ธ ์ด์ฒ˜๋Ÿผ ๋งค์šฐ ๊ฐ„๋‹จํžˆ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ๋‹ค!

๋‹จ์ˆœํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค๋ฉด, ๋ฒ”์šฉ์„ฑ์ด ์ข‹์•„ ์—ฌ๋Ÿฌ๊ณณ์—์„œ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ๐Ÿ˜€, ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ž‘์„ฑํ•˜๊ธฐ๋Š” ์–ด๋ ต๋‹ค๐Ÿฅบ
์ž์„ธํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค๋ฉด, ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜๋Š” ์žˆ์ง€๋งŒ๐Ÿ˜€, ๋ฒ”์šฉ์„ฑ์ด ๋–จ์–ด์ง„๋‹ค๐Ÿฅบ

๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€
โ—๏ธ๋ฒ”์šฉ์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•˜๋‹ค๊ฐ€, ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์„ธ๋ฐ€ํ•œ ๋‚ด์šฉ์ด ์ ์šฉ๋˜๋„๋ก ๋ฉ”์‹œ์ง€์— ๋‹จ๊ณ„๋ฅผ ๋‘๋Š” ๋ฐฉ๋ฒ•โ—๏ธ์ด๋‹ค


์˜ˆ๋ฅผ ๋“ค์–ด

#Level1
required.item.itemName: ์ƒํ’ˆ ์ด๋ฆ„์€ ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค. 

#Level2
required: ํ•„์ˆ˜ ๊ฐ’ ์ž…๋‹ˆ๋‹ค.

์ด์™€ ๊ฐ™์ด, required ๋ผ๋Š” ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ,

1๏ธโƒฃ required ๋งŒ ์žˆ๋‹ค๋ฉด "ํ•„์ˆ˜ ๊ฐ’ ์ž…๋‹ˆ๋‹ค" ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ,
2๏ธโƒฃ required.item.itemName ์™€ ๊ฐ™์ด ๊ฐ์ฒด๋ช…๊ณผ ํ•„๋“œ๋ช…์„ ์กฐํ•ฉํ•œ ์„ธ๋ฐ€ํ•œ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๋ฉด "์ƒํ’ˆ ์ด๋ฆ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค" ๋ผ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

โžก๏ธ ์Šคํ”„๋ง์€ ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ MessageCodesResolver ๋กœ ์ง€์›ํ•œ๋‹ค ๐Ÿ‘๐Ÿป



๐Ÿท ์˜ค๋ฅ˜ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ4

โœ”๏ธ MessageCodesResolverTest

package hello.itemservice.validation;

import org.junit.jupiter.api.Test;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;

import static org.assertj.core.api.Assertions.*;

public class MessageCodesResolverTest {

    MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();

    @Test
    // ๊ฐ์ฒด ์—๋Ÿฌ
    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",
                "required.itemName",
                "required.java.lang.String",
                "required"
        );
    }

}

โฌ†๏ธ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๐Ÿ‘๐Ÿป


MessageCodesResolver

  • ๊ฒ€์ฆ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋กœ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๋“ค์„ ์ƒ์„ฑํ•œ๋‹ค.
  • MessageCodesResolver ์ธํ„ฐํŽ˜์ด์Šค์ด๊ณ  DefaultMessageCodesResolver ๋Š” ๊ธฐ๋ณธ ๊ตฌํ˜„์ฒด์ด๋‹ค.
  • ์ฃผ๋กœ ObjectError , FieldError ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•œ๋‹ค.

โœ”๏ธ DefaultMessageCodesResolver ์˜ ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ๊ทœ์น™

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

๊ฐ์ฒด ์˜ค๋ฅ˜์˜ ๊ฒฝ์šฐ ๋‹ค์Œ ์ˆœ์„œ๋กœ 2๊ฐ€์ง€ ์ƒ์„ฑ 
1.: code + "." + object name 
2.: code

์˜ˆ) ์˜ค๋ฅ˜ ์ฝ”๋“œ: required, object name: item 
1.: required.item
2.: required

โžก๏ธ ObjectError reject("totalPriceMin")
๋‹ค์Œ 2๊ฐ€์ง€ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑ

  • totalPriceMin.item
  • totalPriceMin

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

ํ•„๋“œ ์˜ค๋ฅ˜์˜ ๊ฒฝ์šฐ ๋‹ค์Œ ์ˆœ์„œ๋กœ4๊ฐ€์ง€ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ ์ƒ์„ฑ
1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code

์˜ˆ) ์˜ค๋ฅ˜ ์ฝ”๋“œ: typeMismatch, object name "user", field "age", field type: int 
1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"
4. "typeMismatch

โžก๏ธ FieldError rejectValue("itemName", "required")
๋‹ค์Œ 4๊ฐ€์ง€ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑ

  • required.item.itemName
  • required.itemName
  • required.java.lang.String
  • required

3๏ธโƒฃ ๋™์ž‘ ๋ฐฉ์‹

  • rejectValue() , reject() ๋Š” ๋‚ด๋ถ€์—์„œ MessageCodesResolver ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์—ฌ๊ธฐ์—์„œ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๋“ค์„ ์ƒ์„ฑํ•œ๋‹ค.
  • FieldError , ObjectError ์˜ ์ƒ์„ฑ์ž๋ฅผ ๋ณด๋ฉด, ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ํ•˜๋‚˜๊ฐ€ ์•„๋‹ˆ๋ผ ์—ฌ๋Ÿฌ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.
  • MessageCodesResolver ๋ฅผ ํ†ตํ•ด์„œ ์ƒ์„ฑ๋œ ์ˆœ์„œ๋Œ€๋กœ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ๋ณด๊ด€ํ•œ๋‹ค.

4๏ธโƒฃ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ

  • ํƒ€์ž„๋ฆฌํ”„ ํ™”๋ฉด์„ ๋ Œ๋”๋ง ํ•  ๋•Œ th:errors ๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
  • ๋งŒ์•ฝ ์ด๋•Œ ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋‹ค๋ฉด ์ƒ์„ฑ๋œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋Œ์•„๊ฐ€๋ฉด์„œ ๋ฉ”์‹œ์ง€๋ฅผ ์ฐพ๋Š”๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  ์—†์œผ๋ฉด ๋””ํดํŠธ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.


๐Ÿท ์˜ค๋ฅ˜ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ5

โœ”๏ธ์˜ค๋ฅ˜ ์ฝ”๋“œ ๊ด€๋ฆฌ ์ „๋žต

ํ•ต์‹ฌ์€ ๊ตฌ์ฒด์ ์ธ ๊ฒƒ์—์„œโ—๏ธ ๋œ ๊ตฌ์ฒด์ ์ธ ๊ฒƒ์œผ๋กœโ—๏ธ

MessageCodesResolver ๋Š” required.item.itemName ์ฒ˜๋Ÿผ ๊ตฌ์ฒด์ ์ธ ๊ฒƒ์„ ๋จผ์ € ๋งŒ๋“ค์–ด์ฃผ๊ณ , required ์ฒ˜๋Ÿผ ๋œ ๊ตฌ์ฒด์ ์ธ ๊ฒƒ์„ ๊ฐ€์žฅ ๋‚˜์ค‘์— ๋งŒ๋“ ๋‹ค.
โžก๏ธ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์•ž์„œ ๋งํ•œ ๊ฒƒ ์ฒ˜๋Ÿผ ๋ฉ”์‹œ์ง€์™€ ๊ด€๋ จ๋œ ๊ณตํ†ต ์ „๋žต์„ ํŽธ๋ฆฌํ•˜๊ฒŒ ๋„์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค๐Ÿ‘๐Ÿป


โœ”๏ธ errors.properties ์ˆ˜์ •

#required.item.itemName=์ƒํ’ˆ ์ด๋ฆ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.
#range.item.price=๊ฐ€๊ฒฉ์€ {0} ~ {1} ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
#max.item.quantity=์ˆ˜๋Ÿ‰์€ ์ตœ๋Œ€ {0} ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
#totalPriceMin=๊ฐ€๊ฒฉ * ์ˆ˜๋Ÿ‰์˜ ํ•ฉ์€ {0}์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ฐ’ = {1}

#==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} ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.

ํฌ๊ฒŒ ๊ฐ์ฒด ์˜ค๋ฅ˜์™€ ํ•„๋“œ ์˜ค๋ฅ˜๋ฅผ ๋‚˜๋ˆ„์—ˆ๊ณ , ๋ฒ”์šฉ์„ฑ์— ๋”ฐ๋ผ ๋ ˆ๋ฒจ์„ ๋‚˜๋ˆ„์–ด๋‘์—ˆ๋‹ค!

itemName ์˜ ๊ฒฝ์šฐ required ๊ฒ€์ฆ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋‹ค์Œ ์ฝ”๋“œ ์ˆœ์„œ๋Œ€๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค!
1๏ธโƒฃ required.item.itemName
2๏ธโƒฃ required.itemName
3๏ธโƒฃ required.java.lang.String
4๏ธโƒฃ required

์ด๋ ‡๊ฒŒ ์ƒ์„ฑ๋œ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ˆœ์„œ๋Œ€๋กœ MessageSource ์—์„œ ๋ฉ”์‹œ์ง€์—์„œ ์ฐพ๋Š”๋ฐ,

๊ตฌ์ฒด์ ์ธ ๊ฒƒ์—์„œ ๋œ ๊ตฌ์ฒด์ ์ธ ์ˆœ์„œ๋Œ€๋กœ ์ฐพ๋Š”๋‹คโ—๏ธ

  • ๋ฉ”์‹œ์ง€์— 1๋ฒˆ์ด ์—†์œผ๋ฉด 2๋ฒˆ์„ ์ฐพ๊ณ ,
  • 2๋ฒˆ์ด ์—†์œผ๋ฉด 3๋ฒˆ์„ ์ฐพ๋Š”๋‹ค.
  • ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ๋งŒ์•ฝ์— ํฌ๊ฒŒ ์ค‘์š”ํ•˜์ง€ ์•Š์€ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋Š” ๊ธฐ์กด์— ์ •์˜๋œ ๊ฒƒ์„ ๊ทธ๋ƒฅ ์žฌํ™œ์šฉ ํ•˜๋ฉด ๋œ๋‹ค!

โœ”๏ธ ์‹คํ–‰

1๏ธโƒฃ Level1,2 ์ „๋ถ€ ์ฃผ์„ํ•ด๋ณด์ž

2๏ธโƒฃ Level3 ์ „๋ถ€ ์ฃผ์„ํ•ด๋ณด์ž

3๏ธโƒฃ Level4 ์ „๋ถ€ ์ฃผ์„ํ•ด๋ณด์ž โžก๏ธ ๋ชป์ฐพ์œผ๋ฉด ์ฝ”๋“œ์— ์ž‘์„ฑํ•œ ๋””ํดํŠธ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

4๏ธโƒฃ Object ์˜ค๋ฅ˜๋„ Level1, Level2๋กœ ์žฌํ™œ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.



๐Ÿท ์˜ค๋ฅ˜ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ6

โœ”๏ธ ์Šคํ”„๋ง์ด ์ง์ ‘ ๋งŒ๋“  ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ

๊ฒ€์ฆ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋Š” โฌ‡๏ธ์•„๋ž˜์™€ ๊ฐ™์ดโฌ‡๏ธ 2๊ฐ€์ง€๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ๋‹ค.
1๏ธโƒฃ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์„ค์ •ํ•œ ์˜ค๋ฅ˜ ์ฝ”๋“œ โžก๏ธ rejectValue() ๋ฅผ ์ง์ ‘ ํ˜ธ์ถœ
2๏ธโƒฃ ์Šคํ”„๋ง์ด ์ง์ ‘ ๊ฒ€์ฆ ์˜ค๋ฅ˜์— ์ถ”๊ฐ€ํ•œ ๊ฒฝ์šฐ(์ฃผ๋กœ ํƒ€์ž… ์ •๋ณด๊ฐ€ ๋งž์ง€ ์•Š์Œ)


์ง€๊ธˆ๊นŒ์ง€ ํ•™์Šตํ•œ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ ์ „๋žต์˜ ๊ฐ•์ ์„ ํ™•์ธํ•ด๋ณด์ž!

1๏ธโƒฃ ์ƒํ’ˆ ์ž…๋ ฅ ํผ price ํ•„๋“œ์— ์ˆซ์ž๊ฐ€ ์•„๋‹Œ 'A'๋ฅผ ๋„ฃ์–ด๋ณด์ž

2๏ธโƒฃ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด BindingResult ์— FieldError ๊ฐ€ ๋‹ด๊ฒจ์žˆ๊ณ , ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๋“ค์ด ์ƒ์„ฑ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

  • typeMismatch.item.price
  • typeMismatch.price
  • typeMismatch.java.lang.Integer
  • typeMismatch

3๏ธโƒฃ ์Šคํ”„๋ง์€ ํƒ€์ž… ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด typeMismatch ๋ผ๋Š” ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
์ด ์˜ค๋ฅ˜ ์ฝ”๋“œ๊ฐ€ MessageCodesResolver ๋ฅผ ํ†ตํ•˜๋ฉด์„œ 4๊ฐ€์ง€ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๊ฐ€ ์ƒ์„ฑ๋œ ๊ฒƒ์ด๋‹ค.

4๏ธโƒฃ error.properties ์— ๋‹ค์Œ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•˜์ž!

#์ถ”๊ฐ€
typeMismatch.java.lang.Integer=์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. typeMismatch=ํƒ€์ž… ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค.

5๏ธโƒฃ ๋‹ค์‹œ ์‹คํ–‰ํ•ด๋ณด๋ฉด, ์†Œ์Šค์ฝ”๋“œ๋ฅผ ํ•˜๋‚˜๋„ ๊ฑด๋“ค์ง€ ์•Š๊ณ , ์›ํ•˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋‹จ๊ณ„๋ณ„๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค!



๐Ÿท Validation ๋ถ„๋ฆฌ1

๋ณต์žกํ•œ ๊ฒ€์ฆ ๋กœ์ง์„ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•ด๋ณด์ž๐Ÿ˜Š
โžก๏ธValidator ๋ผ๋Š” ๊ฒ€์ฆ ๋กœ์ง์„ ์ „์šฉ์œผ๋กœ ๋ชจ์•„๋‘๋Š” ํด๋ž˜์Šค๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์„œ ๋ถ„๋ฆฌํ•˜์ž!


โœ”๏ธ ItemValidator ์ƒ์„ฑ

package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class ItemValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        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);
            }
        }
    }
}

๐Ÿ“Œ ์Šคํ”„๋ง์€ ๊ฒ€์ฆ์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜์˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•œ๋‹ค

public interface Validator {
    boolean supports(Class<?> clazz);
    void validate(Object target, Errors errors);
}
  • supports() {} : ํ•ด๋‹น ๊ฒ€์ฆ๊ธฐ๋ฅผ ์ง€์›ํ•˜๋Š” ์—ฌ๋ถ€ ํ™•์ธ
  • validate(Object target, Errors errors) : ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์™€ BindingResult

โœ”๏ธ ItemValidator ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ธฐ โžก๏ธ ValidationItemControllerV2 - addItemV5() ์ถ”๊ฐ€

@PostMapping("/add")
    public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
        // target : item
        // errors : bindingResult
        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}";
    }

๐Ÿ’ก ItemValidator ๋ฅผ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ์ฃผ์ž… ๋ฐ›์•„์„œ ์ง์ ‘ ํ˜ธ์ถœํ–ˆ์Œ!


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

โฌ†๏ธ ์ •์ƒ์ ์œผ๋กœ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์ž˜ ์ถœ๋ ฅ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธ๐Ÿ‘๐Ÿป ๊ฒ€์ฆ๊ณผ ๊ด€๋ จ๋œ ๋ถ€๋ถ„์ด ๊น”๋”ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜์—ˆ๋‹ค!



๐Ÿท Validation ๋ถ„๋ฆฌ2

์Šคํ”„๋ง์ด Validator ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ณ„๋„๋กœ ์ œ๊ณตํ•˜๋Š” ์ด์œ ๋Š” ์ฒด๊ณ„์ ์œผ๋กœ ๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ ๋„์ž…ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ด๋‹ค!
๊ทธ๋Ÿฐ๋ฐ ๋ฐ”๋กœ ์•ž์—์„œ๋Š” ๊ฒ€์ฆ๊ธฐ๋ฅผ ์ง์ ‘ ๋ถˆ๋Ÿฌ์„œ ์‚ฌ์šฉํ–ˆ๋‹ค.
์ด๋ ‡๊ฒŒ ๊ทธ๋Œ€~๋กœ ์‚ฌ์šฉํ•ด๋„ ๋˜์ง€๋งŒ, Validator ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฒ€์ฆ๊ธฐ๋ฅผ ๋งŒ๋“ค๋ฉด ์Šคํ”„๋ง์˜ ์ถ”๊ฐ€์ ์ธ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค๐Ÿ˜ฎ


โœ”๏ธ ValidationItemControllerV2 ์ถ”๊ฐ€

@InitBinder
    public void init(WebDataBinder dataBinder) {
        dataBinder.addValidators(itemValidator);
    }

๐Ÿ’ก WebDataBinder๋ฅผ ํ†ตํ•ด์„œ ์‚ฌ์šฉํ•˜๊ธฐ
โžก๏ธ WebDataBinder ๋Š” ์Šคํ”„๋ง์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ์˜ ์—ญํ• ์„ ํ•ด์ฃผ๊ณ  ๊ฒ€์ฆ ๊ธฐ๋Šฅ๋„ ๋‚ด๋ถ€์— ํฌํ•จํ•œ๋‹ค!!

์ด์ฒ˜๋Ÿผ WebDataBinder ์— ๊ฒ€์ฆ๊ธฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” ๊ฒ€์ฆ๊ธฐ๋ฅผ ์ž๋™์œผ๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๐Ÿ‘๐Ÿป
(@InitBinder ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ์—๋งŒ ์˜ํ–ฅ์„ ์ค€๋‹ค)


โœ”๏ธ @Validated ์ ์šฉ โžก๏ธ ValidationItemControllerV2 - addItemV6() ์ถ”๊ฐ€

@PostMapping("/add")
    // @Validated ์• ๋…ธํ…Œ์ด์…˜ ์ถ” : Item์— ๋Œ€ํ•ด ์ž๋™์œผ๋กœ ๊ฒ€์ฆ๊ธฐ๊ฐ€ ์‹คํ–‰๋จ
    public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        // ๊ฒ€์ฆ์— ์‹คํŒจํ•˜๋ฉด ๋‹ค์‹œ ์ž…๋ ฅ ํผ์œผ๋กœ
        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๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๋ถ€๋ถ„์ด ์‚ฌ๋ผ์ง€๊ณ , ๋Œ€์‹ ์— ๊ฒ€์ฆ ๋Œ€์ƒ ์•ž์— @Validated ๋ผ๋Š” ์• ๋…ธํ…Œ์ด์…˜์ด ๋ถ™์—ˆ๋‹คโ—๏ธ

  • @Validated ๋Š” ๊ฒ€์ฆ๊ธฐ๋ฅผ ์‹คํ–‰ํ•˜๋ผ๋Š” ์• ๋…ธํ…Œ์ด์…˜์ด๋‹ค.
  • ์ด ์• ๋…ธํ…Œ์ด์…˜์ด ๋ถ™์œผ๋ฉด ์•ž์„œ WebDataBinder ์— ๋“ฑ๋กํ•œ ๊ฒ€์ฆ๊ธฐ๋ฅผ ์ฐพ์•„์„œ ์‹คํ–‰ํ•œ๋‹ค.

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

โฌ†๏ธ ์ •์ƒ์ ์œผ๋กœ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์ž˜ ์ถœ๋ ฅ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธ ๐Ÿ‘๐Ÿป


๊ฒ€์ฆ2 ๊นŒ์ง€๋งŒ ํ–ˆ๋Š”๋ฐ ์–˜ ํ˜ผ์ž ๋ฒ„์ „6 ๊นŒ์ง€ ์žˆ๋‹ค๋‡จ,,๐Ÿ˜‚

profile
Backend Developer

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