JiwonDev

Spring Bean Validation#2 ์Šคํ”„๋ง ๋นˆ ๊ฒ€์ฆ

by JiwonDev

์ด ๊ธ€์€ ์•„๋ž˜๊ธ€๊ณผ ์ด์–ด์ง€๋Š” ๊ธ€์ด๋‹ค.

2021.08.29 - [Backend/Spring MVC] - Spring Vaildation#1 ๊ฒ€์ฆ

 

Spring Vaildation#1 ๊ฒ€์ฆ

๊ฐ์ฒด๋ฅผ ๋งคํ•‘ํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ์˜ ์ค‘์š”ํ•œ ์—ญํ• ์ค‘ ํ•˜๋‚˜๋Š” HTTP ์š”์ฒญ์ด ์ •์ƒ์ธ์ง€ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์–ด์ฉŒ๋ฉด ์ •์ƒ ๋กœ์ง๋ณด๋‹ค ์ด๋Ÿฌํ•œ ์˜ˆ์™ธ, ๊ฒ€์ฆ ๋กœ์ง์„ ์„ค๊ณ„ํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ๋” ์–ด๋ ค์šธ ์ˆ˜ ์žˆ๋‹ค. ํ•ด๋‹น๊ธ€์€ Bea

jiwondev.tistory.com

 

# Bean Validation

WebDataBinder๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค ํ•œ๋“ค ์•ž์—์„œ ์„ค๋ช…ํ•œ Validation ๊ฐ์ฒด๋ฅผ ๋งค๋ฒˆ ์ž‘์„ฑํ•˜๋Š”๊ฑด ๋ฒˆ๊ฑฐ๋กญ๋‹ค. ์ด๋ฅผ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž๋™์œผ๋กœ ๋˜๊ฒŒ ๋งŒ๋“ค์ˆ˜๋Š” ์—†์„๊นŒ? → BeanValidation 2.0 (JSR-380)

@Data // @Get, @Set, @toStr, @Equals&Hash, @RequiredArg
public class Item {

  @NotNull(groups = UpdateCheck.class) //์ˆ˜์ • ์š”๊ตฌ์‚ฌํ•ญ ์ถ”๊ฐ€
  private Long id;

  @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
  private String itemName;

  @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
  @Range(min = 1000, max = 1000000, groups = {SaveCheck.class, UpdateCheck.class})
  private Integer price;

  @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
  @Max(value = 9999, groups = {SaveCheck.class})
  private Integer quantity;

  public Item() {
  }

  public Item(String itemName, Integer price, Integer quantity) {
    this.itemName = itemName;
    this.price = price;
    this.quantity = quantity;
  }
}

Bean Validation์€ ๊ฒ€์ฆ ๊ตฌํ˜„์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ, ์–ด๋…ธํ…Œ์ด์…˜๊ณผ ์—ฌ๋Ÿฌ ์ธํ„ฐํŽ˜์ด์Šค๋“ค์˜ ๋ชจ์Œ์ด๋‹ค. ๋งˆ์น˜ JPA - Hinbernate์˜ ๊ด€๊ณ„์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋ฉฐ ํ•„์š”์—๋”ฐ๋ผ ๊ตฌํ˜„์ฒด๋ฅผ ๋ฐ”๊ฟ”์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ณดํ†ต ๋Œ€๋ถ€๋ถ„ Hinbernate Validator ๊ตฌํ˜„์ฒด๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ์žฌ๋‹จ์—์„œ ๋งŒ๋“ค์–ด์„œ ๋ถ™์€ ์ด๋ฆ„์ด๋‹ค. JPA๋ž‘ ๊ธฐ์ˆ ์ ์ธ ์—ฐ๊ด€์€ ์—†๋‹ค.

 

๋‹ค๋งŒ ํ‘œ์ค€ ์ธํ„ฐํŽ˜์ด์Šค (javax.validation.constraints)์—์„œ ์ œ๊ณตํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์„ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ Validator๊ฐ€ ์ถ”๊ฐ€๋กœ ์ œ๊ณตํ•ด์ฃผ๊ธฐ๋„ ํ•œ๋‹ค. (org.hibernate.validator.constraints)

 

์‚ฌ์šฉํ•˜๋Š” ๊ฒ€์ฆ ์• ๋…ธํ…Œ์ด์…˜๋“ค์€ ๊ณต์‹๋ฌธ์„œ๋งŒ ๋ด๋„ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ์ง๊ด€์ ์ด๋‹ค. ํ•œ๋ฒˆ ์ฝ์–ด๋ณด๋„๋ก ํ•˜์ž.

 

Index of /hibernate/validator/6.2/reference

 

docs.jboss.org

 

Hibernate Validator 6.2.0.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th

docs.jboss.org

 

 


# ๊ฒฐ๊ตญ์—๋Š” Validtor๋กœ ๋™์ž‘ํ•œ๋‹ค.

์Šคํ”„๋ง์˜ ๊ธฐ๋Šฅ์„ ์ด์šฉํ•ด์„œ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” ์ €๋ฒˆ ๊ธ€์—์„œ ๋ฐฐ์› ๋˜ Validtor ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ๊บผ๋‚ด ์‚ฌ์šฉํ• ์ผ์€ ์—†์ง€๋งŒ ๊ถ๊ธˆํ•˜๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ํ…Œ์ŠคํŠธํ•ด๋ด๋„ ๋œ๋‹ค.

@Test
void beanValidation() {
  // ์ด ์ฝ”๋“œ๋ฅผ ์Šคํ”„๋ง์—์„œ ์ง์ ‘ ์‚ฌ์šฉํ• ์ผ์€ ์—†์œผ๋‹ˆ ๊ฑฑ์ •๋ง์ž.
  ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
  Validator validator = factory.getValidator();

  Item item = new Item();
  item.setQuantity(10000);

  Set<ConstraintViolation<Item>> violations = validator.validate(item);
  
  for (ConstraintViolation<Item> violation : violations) {
    System.out.println("violation = " + violation);
    System.out.println("violation = " + violation.getMessage());
  }

}

 


# Bean Validation์˜๋‚ด๋ถ€๋™์ž‘

์Šคํ”„๋ง๋ถ€ํŠธ ํ•œ์ค„์ด๋ฉด ๋œ๋‹ค. implementation 'org.springframework.boot:spring-boot-starter-validation'

 

์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” LocalValidatorFactoryBean ์„ ๊ธ€๋กœ๋ฒŒ Validator ๊ฐ์ฒด๋กœ ๋“ฑ๋กํ•œ๋‹ค. ์ด ๊ฐ์ฒด๊ฐ€ ์–ด๋…ธํ…Œ์ด์…˜(@NotNull)์—  ๋”ฐ๋ผ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๋Š” ์•„๋ฌด๋Ÿฐ ์ถ”๊ฐ€์ž‘์—… ์—†์ด ๊ฒ€์ฆํ•˜๋ ค๋Š” ๊ณณ์— @Valid, @Validated๋งŒ ๋ถ™์—ฌ์ฃผ๋ฉด ๋œ๋‹ค.

//๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ(๊ธ€๋กœ๋ฒŒ)๋กœ ๋“ฑ๋ก๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ด๋Ÿฐ ์ฝ”๋“œ๋Š” ๋” ์ด์ƒ ํ•„์š”์—†๋‹ค.
//@InitBinder
//public void init(WebDataBinder dataBinder) {
// log.info("init binder {}", dataBinder);
// dataBinder.addValidators(itemValidator);
//}


@PostMapping("/add") // @Validated๋งŒ ๋ถ™์ด๋ฉด ์ ์šฉ์™„๋ฃŒ.
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult) {

}

 

์šฐ๋ฆฌ๊ฐ€ ํ•˜๋Š”๊ฑด ์•„๋ฌด๊ฒƒ๋„ ์—†์ง€๋งŒ, ๋‚ด๋ถ€์—์„œ Bean Validation์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋™์ž‘ํ•œ๋‹ค.

  1. ์•ฑ์ด ์‹œ์ž‘ํ•  ๋•Œ BeanVaildaton(๊ตฌํ˜„์ฒด - LocalVaildatorFactoryBean)๊ฐ€ ์ „์—ญ์œผ๋กœ ์ปจํŠธ๋กค๋Ÿฌ์— ์ ์šฉ๋œ๋‹ค.
  2. @ModelAttribute ๊ฐ๊ฐ ํ•„๋“œ์— ํƒ€์ž… ๋ณ€ํ™˜(๋ฐ”์ธ๋”ฉ)์„ ์‹œ๋„ํ•œ๋‹ค.
    - ๋งŒ์•ฝ ํƒ€์ž… ๋ฐ”์ธ๋”ฉ์— ์‹คํŒจํ•˜๋ฉด FieldError('typeMismatch') ๋ฅผ ์ƒ์„ฑ
  3. ํƒ€์ž… ๋ฐ”์ธ๋”ฉ์— ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด Validator๋ฅผ ์ ์šฉ. (LocalValidatorFactoryBean๊ฐ€ @์–ด๋…ธํ…Œ์ด์…˜์„ Validator๋กœ ๋ณ€ํ™˜)
    - ์˜ค๋ฅ˜์ฝ”๋“œ๊ฐ€ ์• ๋…ธํ…Œ์ด์…˜ ์ด๋ฆ„์œผ๋กœ ์ ์šฉ๋œ๋‹ค.
  4. ํ•ด๋‹น ์˜ค๋ฅ˜์ฝ”๋“œ์— ํ•ด๋‹นํ•˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ MessageCodeResolver๋ฅผ ์ด์šฉํ•˜์—ฌ ์ฐพ๋Š”๋‹ค. 

ํƒ€์ž… ๋ณ€ํ™˜(๋ฐ”์ธ๋”ฉ)์— ์„ฑ๊ณตํ•œ ํ•„๋“œ๋งŒ Vaildator๋ฅผ ์ ์šฉํ•œ๋‹ค. ์ƒ๊ฐํ•ด๋ณด๋ฉด ๋‹น์—ฐํ•œ ์ผ

 

์šฐ๋ฆฌ๊ฐ€ ํ•  ์ผ์€ errors.properties๋งŒ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋์ด๋‹ค.

  • ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ชป์ฐพ์•˜๋‹ค๋ฉด @NotBlank(message = "๊ธฐ๋ณธ๊ฐ’")์„ ์“ฐ๊ณ , ๊ฑฐ๊ธฐ๋„ ์—†๋‹ค๋ฉด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.
# errors.properties

# Bean Validation ์ถ”๊ฐ€
NotBlank.item.itemNmae = ์ƒํ’ˆ ์ด๋ฆ„์„ ์ ์–ด์ฃผ์„ธ์š”.

NotBlank={0} ๊ณต๋ฐฑX 
Range={0}, {2} ~ {1} ํ—ˆ์šฉ
Max={0}, ์ตœ๋Œ€ {1}

 


# ๊ฐ์ฒด (Object) ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌํ•˜๊ธฐ

๋„๋ฉ”์ธ ๊ฐ์ฒด์— ์• ๋…ธํ…Œ์ด์…˜์— ์ง์ ‘ ๊ฒ€์ฆ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์œผ๋‚˜, ์ถ”์ฒœํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ๋‹ค.

@Data // ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ๊ฒ€์ฆ.
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 
10000")
public class Item {
 //...
}

 

๊ฐ์ฒด ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ (ObjectError)๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ปจํŠธ๋กค๋Ÿฌ์— ์ฝ”๋“œ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒŒ ๋‚˜์€ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.

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

    //ํŠน์ • ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ๋ณตํ•ฉ ๋ฃฐ ๊ฒ€์ฆ
    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/v3/addForm";
    }

    //์„ฑ๊ณต ๋กœ์ง
    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/validation/v3/items/{itemId}";
}

 


# Bean Validation - ๊ฐ๊ฐ ๋‹ค๋ฅธ ๊ฒ€์ฆ ์ ์šฉํ•˜๊ธฐ

๋‹ค์Œ๊ณผ ๊ฐ™์€ ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณด์ž.

  • ํƒ€์ž… ๊ฒ€์ฆ
    - ๊ฐ€๊ฒฉ, ์ˆ˜๋Ÿ‰์— ๋ฌธ์ž๊ฐ€ ๋“ค์–ด๊ฐ€์žˆ์œผ๋ฉด ๊ฒ€์ฆ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
  • ํ•„๋“œ ๊ฒ€์ฆ
    - ์ƒํ’ˆ๋ช…: ํ•„์ˆ˜. ๊ณต๋ฐฑ ๋ถˆ๊ฐ€๋Šฅ
    - ๊ฐ€๊ฒฉ: ์ตœ์†Œ 1000์›, 1๋ฐฑ๋งŒ์› ์ดํ•˜
    - ์ˆ˜๋Ÿ‰: ์ตœ๋Œ€ 9999๊ฐœ
  • ๋ณตํ•ฉ์ ์ธ ๊ฒ€์ฆ (ObjectError)
    - ์ตœ์†Œ์ฃผ๋ฌธ๊ธˆ์•ก: ๊ฐ€๊ฒฉ * ์ˆ˜๋Ÿ‰์˜ ํ•ฉ์€ 10000์› ์ด์ƒ์ด์–ด์•ผ ๊ฑฐ๋ž˜๊ฐ€ ์Šน์ธ๋จ.

 

ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ ๊ธฐํš์ž๋Š” ๋“ฑ๋ก๋ง๊ณ  ์ˆ˜์ •๋•Œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๋‹ค๋ฅธ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๊ณ  ํ•œ๋‹ค.

  • ๋“ฑ๋ก๋ง๊ณ  ์ˆ˜์ •์‹œ์—๋Š” ์ˆ˜๋Ÿ‰์— ์ œํ•œ์ด ์—†๋‹ค.
  • ๋“ฑ๋ก์€ ์ƒ๊ด€์—†์ง€๋งŒ ์ˆ˜์ •์‹œ์—๋Š” id๊ฐ’์ด ํ•„์ˆ˜์ด๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์ฝ”๋“œ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•˜๋ฉด ๋ ๊นŒ?

@Data // @Get, @Set, @toStr, @Equals&Hash, @RequiredArg
public class Item {

  @NotNull //์ˆ˜์ • ์š”๊ตฌ์‚ฌํ•ญ ๋ฐ˜์˜ - NotNull ์ถ”๊ฐ€
  private Long id;

  @NotBlank
  private String itemName;

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

  @NotNull
  //  @Max(value = 9999) ์ˆ˜์ • ์š”๊ตฌ์‚ฌํ•ญ ๋ฐ˜์˜- MAX ์‚ญ์ œ
  private Integer quantity;

  public Item() {
  }

  public Item(String itemName, Integer price, Integer quantity) {
    this.itemName = itemName;
    this.price = price;
    this.quantity = quantity;
  }
}

๋‹น์—ฐํžˆ ์ด๋Ÿฌ๋ฉด ๋“ฑ๋ก์—์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ๋“ฑ๋ก, ์ˆ˜์ •์— ๊ฐ๊ฐ ๋‹ค๋ฅธ ๊ฒ€์ฆ์„ ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ?

 


@ ์„ ํƒ์ง€1 - BeanValidation groups

๊ทธ๋ฃน์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์‚ฌ์šฉํ• ๋•Œ๋Š” @Vaildated(value = Group.class) ์ฐธ๊ณ ๋กœ @Vaild์—๋Š” ์—†๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

ํ•˜์ง€๋งŒ ์‹ค๋ฌด์—์„œ๋Š” ์ž˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ๊ทธ ์ด์œ ๋Š” @ ์„ ํƒ์ง€2 ๋ฅผ ๋ณด์ž.

package hello.itemservice.domain.item;
public interface SaveCheck {// ์ €์žฅ์šฉ ๊ทธ๋ฃน
}

package hello.itemservice.domain.item;
public interface UpdateCheck {// ์ˆ˜์ •์šฉ ๊ทธ๋ฃน
}
@Data // @Get, @Set, @toStr, @Equals&Hash, @RequiredArg
public class Item {

  @NotNull(groups = UpdateCheck.class) //์ˆ˜์ • ์š”๊ตฌ์‚ฌํ•ญ ์ถ”๊ฐ€
  private Long id;

  @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
  private String itemName;

  @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
  @Range(min = 1000, max = 1000000, groups = {SaveCheck.class, UpdateCheck.class})
  private Integer price;

  @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
  @Max(value = 9999, groups = {SaveCheck.class})
  private Integer quantity;

  public Item() {
  }

  public Item(String itemName, Integer price, Integer quantity) {
    this.itemName = itemName;
    this.price = price;
    this.quantity = quantity;
  }
}

์ด๋ ‡๊ฒŒ ์ •ํ•œ ๊ทธ๋ฃน์€ ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId,
    @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {...}
    
@PostMapping("/add")
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item,
    BindingResult bindingResult, RedirectAttributes redirectAttributes) {...}

 


@ ์„ ํƒ์ง€2 - ๋“ฑ๋ก์šฉ ํผ, ์ˆ˜์ •์šฉ ํผ ๊ฐ์ฒด๋ถ„๋ฆฌ

์œ„์™€ ๊ฐ™์ด groups๋กœ ๊ตฌ๋ถ„ํ•˜๊ธฐ์—๋Š” ์ฝ”๋“œ๊ฐ€ ๋„ˆ๋ฌด ์ง€์ €๋ถ„ํ•ด์ง„๋‹ค. ๋˜ํ•œ ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” Item ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์ •๋ณด ๋ง๊ณ ๋„ ์•ฝ๊ด€ ์ •๋ณด, ๋ถ€๊ฐ€๋ฐ์ดํ„ฐ๋“ค์„ DB์—์„œ ์ถ”๊ฐ€๋กœ ๋ฐ›๋Š”๋ฐ ์ด๊ฒƒ๋“ค์„ ๊ฐ™์ด ๊ฒ€์ฆํ•  ์ˆ˜ ์—†๋‹ค.

 

๊ทธ๋ž˜์„œ ์‹ค๋ฌด์—์„œ๋Š” ItemSaveForm, ItemUpdateForm ์ฒ˜๋Ÿผ ์ „์†ก ๊ฐ์ฒด๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ ๋‹ค.

  • (๊ธฐ์กด์˜ ๋ฐฉ๋ฒ•) [HTML Form -> Item -> Controller -> Item -> Repository]
    - ์žฅ์  : ๊ณผ์ •์ด ๊ฐ„๋‹จํ•˜๋‹ค.
    - ๋‹จ์  : ์ˆ˜์ •์‹œ ๊ฒ€์ฆ์ด ์ค‘๋ณต๋  ์ˆ˜ ์žˆ๊ณ , groups๋กœ ๋ณต์žกํ•œ ์ฝ”๋“œ๊ฐ€ ๋‚˜์˜จ๋‹ค.

  • (ํผ ๊ฐ์ฒด ์‚ฌ์šฉ) [HTML Form -> ItemSaveForm -> Controller -> Item ์ƒ์„ฑ -> Repository]
    - ์žฅ์  : ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณต์žกํ•ด๋„, ๋ณ„๋„์˜ Form ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ์— ๊ด€๋ฆฌํ•˜๊ธฐ ์‰ฝ๋‹ค. ์ค‘๋ณต์„ ๋ฐฉ์ง€ํ•œ๋‹ค.
    - ๋‹จ์  : ํผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ Item ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ & ๋ฐ˜ํ™˜ํ•ด์•ผํ•œ๋‹ค.

์ฐธ๊ณ ๋กœ ๋น„์Šทํ•˜๋‹ค๊ณ ํ•ด์„œ ๋“ฑ๋ก, ์ˆ˜์ •์šฉ HTML ํ…œํ”Œ๋ฆฟ์„ ํ•ฉ์ณ์„œ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๋‚˜์ค‘์— ์œ ์ง€๋ณด์ˆ˜์—์„œ ๊ณ ํ†ต๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

 

@Data // Item์˜ ๊ฒ€์ฆ์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๊ฒ€์ฆ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•ด์ฃผ์ž
public class Item {
  private Long id;
  private String itemName;
  private Integer price;
  private Integer quantity;
}
// ์ฐธ๊ณ ๋กœ form์€ ์ปจํŠธ๋กค๋Ÿฌ์—๋งŒ ์“ฐ๋Š” ๊ฐ์ฒด๋ผ ํ”„๋กœ์ ํŠธ๊ฒฝ๋กœ/web/form/... ์— ๋งŒ๋“ ๋‹ค.
@Data
public class ItemSaveForm {

  @NotBlank
  private String itemName;

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

  @NotNull
  @Max(value = 9999)
  private Integer quantity;
}
@Data
public class ItemUpdateForm {

  @NotNull
  private Long id;

  @NotBlank
  private String itemName;

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

  //์ˆ˜์ •์—์„œ๋Š” ์ˆ˜๋Ÿ‰์€ ์ž์œ ๋กญ๊ฒŒ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.
  private Integer quantity;
}

 

๊ทธ๋ฆฌ๊ณ  ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

  • ๋‹จ ๋ทฐ์—์„œ ๋ชจ๋ธ ์ด๋ฆ„์„ item์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด @ModelAttribute("item")์œผ๋กœ ์ง€์ •ํ•ด์ค˜์•ผํ•œ๋‹ค.
@Slf4j
@Controller
@RequestMapping("/validation/v4/items")
@RequiredArgsConstructor
public class ValidationItemControllerV4 {

  private final ItemRepository itemRepository;
  
  @GetMapping("/{itemId}") // prg -> post redirect get
  public String item(@PathVariable long itemId, Model model) {
    Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);
    return "validation/v4/item";
  }
  ...

  @PostMapping("/add") // ๋“ฑ๋ก์—๋Š” ItemSaveForm ์‚ฌ์šฉ
  public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form,
      BindingResult bindingResult, RedirectAttributes redirectAttributes) {

    //ํŠน์ • ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ๋ณตํ•ฉ ๋ฃฐ ๊ฒ€์ฆ
    if (form.getPrice() != null && form.getQuantity() != null) {
      int resultPrice = form.getPrice() * form.getQuantity();
      if (resultPrice < 10000) {
        bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
      }
    }

    //๊ฒ€์ฆ์— ์‹คํŒจํ•˜๋ฉด ๋‹ค์‹œ ์ž…๋ ฅ ํผ์œผ๋กœ
    if (bindingResult.hasErrors()) {
      log.info("errors={} ", bindingResult);
      return "validation/v4/addForm";
    }

    //์„ฑ๊ณต ๋กœ์ง, ํผ์—์„œ ๊ฐ€์ ธ์™€์„œ Item์„ ์ƒ์„ฑํ•จ
    Item item = new Item();
    item.setItemName(form.getItemName());
    item.setPrice(form.getPrice());
    item.setQuantity(form.getQuantity());

    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/validation/v4/items/{itemId}";
  }


  @PostMapping("/{itemId}/edit") // ์ˆ˜์ •์—๋Š” ItemUpdateForm ์‚ฌ์šฉ
  public String edit(@PathVariable Long itemId,
      @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {

    //ํŠน์ • ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ๋ณตํ•ฉ ๋ฃฐ ๊ฒ€์ฆ
    if (form.getPrice() != null && form.getQuantity() != null) {
      int resultPrice = form.getPrice() * form.getQuantity();
      if (resultPrice < 10000) {
        bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
      }
    }

    if (bindingResult.hasErrors()) {
      log.info("errors={}", bindingResult);
      return "validation/v4/editForm";
    }

    Item itemParam = new Item();
    itemParam.setItemName(form.getItemName());
    itemParam.setPrice(form.getPrice());
    itemParam.setQuantity(form.getQuantity());

    itemRepository.update(itemId, itemParam);
    return "redirect:/validation/v4/items/{itemId}";
  }

}

 


# Bean Validation - HTTP ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ (API @RequestBody)

  • @ModelAttribute ๋Š” HTTP ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ(URL ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง, POST Form)๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
  • @RequestBody ๋Š” HTTP Body์˜ ๋ฐ์ดํ„ฐ ์ž์ฒด๋ฅผ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. (API - JSON ์š”์ฒญ)

ํฌ๊ฒŒ ๋‹ฌ๋ผ์ง€๋Š”๊ฑด ์—†๋‹ค. ๊ทธ๋ƒฅ @ModelAttribute๊ฐ€ @RequestBody๋กœ ๋ฐ”๋€Œ์—ˆ์„ ๋ฟ์ด๋‹ค.

@Slf4j
@RestController // @ResponseBody๊ฐ€ ๋ถ™์Œ. ๋ทฐ๋ฆฌ์กธ๋ฒ„ ๋Œ€์‹  Str, Json์œผ๋กœ ๋ณ€ํ™˜
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {

  @PostMapping("/add")
  public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
      log.info("๊ฒ€์ฆ ์˜ค๋ฅ˜ ๋ฐœ์ƒ errors={}", bindingResult);
      return bindingResult.getAllErrors();
    }

    //...์„ฑ๊ณต ๋กœ์ง ์‹คํ–‰...

    return form; // MappingJacksonHttpMessageConverter์— ์˜ํ•ด Object -> Json์œผ๋กœ ๋ณ€ํ™˜๋จ.
  }
}

 

ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์—๋Š” ํฐ ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค. Json์„ ์ฃผ๊ณ ๋ฐ›๋‹ค๊ฐ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ, ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๊ณ  ์˜ˆ์™ธ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.์ฆ‰ ์•„์˜ˆ form ๊ฐ์ฒด๊ฐ€ ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š๋Š”๋‹ค → ๊ฒ€์ฆ ๋ถˆ๊ฐ€๋Šฅ!

  • @ModelAttribute ์—์„œ๋Š” ํƒ€์ž…์ด ๋‹ฌ๋ผ์„œ ๋ณ€ํ™˜์ด ์•ˆ๋˜๋„ ํ•„๋“œ ๋‹จ์œ„(FieldError)๋กœ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ–ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ @RequestBody๋Š” ๊ฐ๊ฐ์˜ ํ•„๋“œ๋‹จ์œ„๋กœ ์ ์šฉํ•  ์ˆ˜ ์—†๋‹ค. ์ „์ฒด๋ฅผ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•œ๋‹ค.

 

์ฆ‰ ์šฐ๋ฆฌ๋Š” API๋ฅผ ๋งŒ๋“ค ๋•Œ ์•„๋ž˜ 3๊ฐ€์ง€ ์ƒํ™ฉ์— ๋Œ€ํ•œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ์™€ ์•Œ๋งž์€ ์‘๋‹ต์„ ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค. ์Šคํ”„๋ง์˜ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋Š” ๋‚˜์ค‘์— ๋”ฐ๋กœ ๋‹ค๋ฃฐ์˜ˆ์ •์ด๋‹ˆ, ๊ทธ๋•Œ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

  • ์„ฑ๊ณต ์š”์ฒญ : ์„ฑ๊ณต ์‹œ ๋กœ์ง
  • ์‹คํŒจ ์š”์ฒญ : Json -> ๊ฐ์ฒด ๋ณ€ํ™˜ ์ž์ฒด๊ฐ€ ์‹คํŒจํ•จ ( form ๊ฐ์ฒด ์ƒ์„ฑ ์‹คํŒจ, Vaildator ์‚ฌ์šฉ ๋ถˆ๊ฐ€ )
  • ๊ฒ€์ฆ ์˜ค๋ฅ˜ ์š”์ฒญ : Json -> ๊ฐ์ฒด ๋ณ€ํ™˜์€ ์„ฑ๊ณตํ–ˆ์œผ๋‚˜ ๊ฒ€์ฆ์€ ์‹คํŒจํ•จ.

 

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

JiwonDev

JiwonDev

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