Spring

[인프런/스프링 MVC 2편] 5. 검증 (2) - Bean Validation

주니어주니 2023. 5. 24. 20:46

 

 

1. Bean Validation 

 

 

public class Item {

    private Long id;
    
    @NotBlank
    private String itemName;
    
    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;
    
    @NotNull
    @Max(9999)
    private Integer quantity;
    //...
}

 

 


 

2. Bean Validation - 스프링 통합X, 직접 사용하는 방법 

 

1) Bean Validation 의존관계 추가

 

* build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

 

2) Bean Validation 어노테이션 적용

 

* Item 

package hello.itemservice.domain.item;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class Item {

    private Long id;

    @NotBlank
    private String itemName;

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

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {
    }

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

 

(1) 검증 어노테이션

  • @NotBlank : 빈값 + 공백만 있는 경우를 허용 X
  • @NotNull : null 허용 X
  • @Range(min = 1000, max = 1000000) : 범위 안의 값
  • @Max(9999) : 최대 9999

 

(2) Jakarta Bean Validation 

  • jakarta.validation-api : Bean Validation 인터페이스 (특정 구현에 관계없이 제공되는 표준 인터페이스)
  • hibernate-validator : 구현체 (하이버네이트 validator 구현체를 사용할 때만 제공되는 검증 기능 - 근데 대부분 하이버네이트 validator 사용)

 

 

3) 테스트 코드 작성 

 

* Bean ValidationTest

package hello.itemservice.validation;

import hello.itemservice.domain.item.Item;
import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class BeanValidationTest {

    @Test
    void beanValidation() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Item item = new Item();
        item.setItemName(" ");  // 공백
        item.setPrice(0);
        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());
        }
    }
}

 

 

(1) 검증기 생성 

스프링과 통합하면 직접 코드 작성 X -> 참고만 하기 

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

 

 

(2) 검증 실행 

검증 대상(item)을 직접 검증기에 넣고 Set으로 그 결과 받음

Set에는 ConstraintViolation이라는 검증 오류가 담김 

Set<ConstraintViolation<Item>> violations = validator.validate(item);

 

 

(3) 실행 결과 

검증 오류가 발생한 객체, 필드, 메시지 정보 등을 확인

 

 

--> Bean Validation을 직접 사용하는 방법

 

 

 


 

3. Bean Validation - 스프링 적용 

 

 

1) 라이브러리 추가

 

* build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

 

2) 어노테이션 적용

 

* item

package hello.itemservice.domain.item;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class Item {

    private Long id;

    @NotBlank
    private String itemName;

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

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {
    }

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

 

 

3) 컨트롤러 

 

* Controller 

  • ItemValidator 제거
package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;

@Slf4j
@Controller
@RequestMapping("/validation/v3/items")
@RequiredArgsConstructor
public class ValidationItemControllerV3 {

    private final ItemRepository itemRepository;

    ...

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

        // 검증에 실패하면 다시 입력 폼으로 (에러가 있으면)
        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}";
    }

    ...
}

 

 

4) 동작 방식

  1. 스프링 부트 라이브러리 추가 
  2. LocalValidatorFactoryBean을 글로벌 Validator로 등록 (스프링 부트에서 사용되는 Bean Validator 생성하는 객체)
  3. LocalValidatorFactoryBean이 @NotNull같은 어노테이션을 보고 검증 수행
  4. 글로벌 Validator이기 때문에 @Valid, @Validated만 적용하면 됨
  5. 검증 오류 발생 시 FieldError, ObjectError 생성 -> BindingResult에 담아줌

 

@Validated : 스프링 전용 검증 어노테이션 (groups 기능 포함)
@Valid : 자바 표준 검증 어노테이션

 

 

5) 검증 순서 

  1. @ModelAttribute 각각의 필드에 타입 변환 시도
    1. 성공하면 다음으로
    2. 실패하면 typeMismatch -> FieldError 추가
  2. 타입 변환(바인딩) 성공한 필드에만 Bean Validation 적용

 

예)

  • itemName(String 타입) -> 문자 "A" 입력 -> 타입 변환 성공 -> itemName 필드에 BeanValidation 적용
  • price(Integer 타입) -> 문자 "A" 입력 -> 타입 변환 실패 -> typeMismatch FieldError 추가 -> price 필드는 BeanValidation 적용 X

 

가격 필드 : Validation 에러 메시지가 아닌, typeMismatch 에러 메시지만 출력

 

 

 

 


 

4. Bean Validation - 에러 코드 

 

 

1) Bean Validation이 기본으로 제공하는 오류 메시지 

 

 

 

2) BindingResult에 등록된 검증 오류 코드 확인

- 상품명 검증 실패 시

 

 

콘솔 메시지의 codes 

Field error in object 'item' on field 'itemName': rejected value []; codes [NotBlank.item.itemName,NotBlank.itemName,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.itemName,itemName]; arguments []; default message [itemName]]; default message [공백일 수 없습니다]

 

-> 오류 코드어노테이션 이름으로 등록됨 (@NotBlank)

-> 오류 코드를 기반으로 MessageCodesResolver를 통해 메시지 코드 순서대로 생성

-> 에러 메시지 등록하면 됨

 

  • @NotBlack
    • NotBlank.item.itemName
    • NotBlank.itemName
    • NotBlank.java.lang.String
    • NotBlank

  • @Range
    • Range.item.price
    • Range.price
    • Range.java.lang.Integer
    • Range

 

 

3) 메시지 등록 

  • {0} -> 필드명
    {1}, {2} -> 각 어노테이션마다 다름
#Bean Validation 추가
NotBlank={0} 공백X 
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}

 

 

 

4) BeanValidation 메시지 찾는 순서

  1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
  2. 어노테이션의 message 속성 -> @NotBlank(message = "공백은 입력할 수 없습니다.")
  3. 라이브러리가 제공하는 기본값 -> 공백일 수 없습니다.

 

 


 

5. Bean Validation - 오브젝트 오류 

 

특정 필드(FieldError)가 아닌 해당 오브젝트 관련 오류(ObjectError) 해결 방법 

 

 

1) @ScrpitAssert() - 비추 

제약 많고 복잡함

@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 10000원 넘게 입력해주세요.")
public class Item {
	...
}

 

 

2) 컨트롤러에 글로벌 오류 추가 

 

* Controller

특정 필드가 아닌 복합 룰 검증 (글로벌 오류) 추가

 

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

 

 

 

📌 정리

필드 오류 : 어노테이션 
오브젝트 오류 : 컨트롤러에 따로 검증 추가 

 


 

6. Bean Validation - 상품 수정에도 적용

 

 

1) 컨트롤러 

  • ModelAttribute 앞에 @Validated 추가
  • BindingResult 추가
  • 검증 로직 추가
  • 검증 오류 발생 시 editForm 으로 이동하는 경로로 수정
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute Item 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()) {
        log.info("errors = {}", bindingResult);
        return "validation/v3/editForm";
    }

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

 

 

2) editForm.html 

  • .field-error css 추가 
  • 글로벌 오류 메시지 추가 
  • 각 필드에 검증 기능, 오류 메시지 추가 
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    </style>
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2 th:text="#{page.updateItem}">상품 수정</h2>
    </div>

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

        <div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">글로벌 오류 메시지</p>
        </div>

        <div>
            <label for="id" th:text="#{label.item.id}">상품 ID</label>
            <input type="text" id="id" th:field="*{id}" class="form-control" readonly>
        </div>
        <div>
            <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control">
            <div class="field-error" th:errors="*{itemName}">
                상품명 오류
            </div>
        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">가격</label>
            <input type="text" id="price" th:field="*{price}" th:errorclass="field-error" class="form-control">
            <div class="field-error" th:errors="*{price}">
                가격 오류
            </div>
        </div>
        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}" th:errorclass="field-error" class="form-control">
            <div class="field-error" th:errors="*{quantity}">
                수량 오류
            </div>
        </div>

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">저장</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='item.html'"
                        th:onclick="|location.href='@{/validation/v3/items/{itemId}(itemId=${item.id})}'|"
                        type="button" th:text="#{button.cancel}">취소</button>
            </div>
        </div>

    </form>

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

 

 

 

 

 


 

 

7. Bean Validation - 한계 

 

등록할 때의 요구사항 ≠ 수정할 때의 요구사항 일 때

 

* 등록시 기존 요구사항 

  • 타입 검증
    • 가격, 수량에 문자가 들어가면 검증 오류 처리
  • 필드 검증
    • 상품명: 필수, 공백X
    • 가격: 1000원 이상, 1백만원 이하
    • 수량: 최대 9999
  • 특정 필드의 범위를 넘어서는 검증
    • 가격 * 수량의 합은 10,000원 이상

 

* 수정시 요구사항

  • 수량 : 무제한으로 변경 
  • id 값 : 필수

 

* Item 객체에서 검증 어노테이션을 적용하면

  • id : @NotNull 추가
  • quantity : @Max(9999) 제거 
@Data
public class Item {

    @NotNull //수정 요구사항 추가
    private Long id;
    
    @NotBlank
    private String itemName;
    
    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;
    
    @NotNull
    //@Max(9999) //수정 요구사항 추가
    private Integer quantity;
    
}

 

-> 수정은 적용되지만 등록에서 문제 발생

-> 등록과 수정은 같은 BeanValidation 적용 X

 

 


 

8. Bean Validation - 등록, 수정 별도 검증

 

등록할 때와 수정할 때 각각 다르게 검증하는 방법 2가지 

  1. BeanValidation의 groups 기능 
  2. Item 객체를 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm으로 별도의 모델 객체를 만들어서 사용

 

 

8-1. groups 기능 

 

 

1) groups 인터페이스 생성 

 

* 저장용 groups

package hello.itemservice.domain.item;

public interface SaveCheck {
}

 

* 수정용 groups

package hello.itemservice.domain.item;

public interface UpdateCheck {
}

 

 

2) Item 객체에 groups 적용

 

package hello.itemservice.domain.item;

import lombok.Data;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.ScriptAssert;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

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

 

 

3) 컨트롤러에 적용

 

* 저장 로직에 SaveCheck Groups 적용

  • 등록시에는 SaveCheck 그룹의 요구사항만 적용
@PostMapping("/add")
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
}

 

 

* 수정 로직에 UpdateCheck Groups 적용

  • 수정시에는 UpdateCheck 그룹의 요구사항만 적용
@PostMapping("/{itemId}/edit")
public String edit2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
}

 

 

 

등록 시에는 최대 9,999개, 수정 시에는 무제한

 

 

-> 복잡함

 


 

8-2. Form 전송 객체 분리 

 

등록 시 : Item 관련 데이터 + 약관 정보 등 Item 객체와 관련 없는 수많은 부가 데이터도 넘어옴 

수정 시 : 등록 시 요구사항과 다를 수 있음

-> Item 객체가 아닌 별도의 객체 만들어서 전달

-> HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository

 

 

1) Item 객체 원복

 

@Data
public class Item {

    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;
	
}

 

 

 

2) Item 폼

 

(1) 저장용 폼 - ItemSaveForm

package hello.itemservice.web.validation.form;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class ItemSaveForm {

    @NotBlank
    private String itemName;

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

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

 

 

(2) 수정용 폼 - ItemUpdateForm

package hello.itemservice.web.validation.form;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class ItemUpdateForm {

    @NotNull
    private Long id;

    @NotBlank
    private String itemName;

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

    // 수정할 때는 수량을 자유롭게 변경 가능
    private Integer quantity;
}

 

 

 

3) 컨트롤러 

 

(1) 등록 로직

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

    // 특정 필드가 아닌 복합 룰 검증
    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 = 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}";
}

 

  • 폼 객체 바인딩
    • ItemSaveForm을 전달받음
    • @ModelAttribute("item") 이름 주의 -> 이름 안넣으면 바인딩 된 객체 itemSaveForm이라는 이름으로 모델에 담김 -> 뷰 템플릿에서 접근하는 th:object 이름도 변경해야 함 
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
}

 

  • 폼 객체를 Item으로 변환
    • 전달받은 저장 객체 ItemSaveForm 검증 -> 새로운 Item 객체 생성 -> ItemSaveForm 값을 Item에 옮기기 -> Item 객체를 저장
//성공 로직
Item item = new Item();
item.setItemName(form.getItemName());
item.setPrice(form.getPrice());
item.setQuantity(form.getQuantity());

Item savedItem = itemRepository.save(item);

 

 

 

(2) 수정 로직 

@PostMapping("/{itemId}/edit")
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}";
}

 

  • 폼 객체 바인딩
    • 수정 정보를 입력한 ItemUpdateForm 전달받음
    • @ModelAttribute("item") 모델에 담기는 이름 주의 

  • 수정 폼 객체를 Item 객체로 변환
    • 수정 폼 객체 데이터 -> 새로운 Item 객체로 복사 -> update 로직에 전달 

 

 

 


 

9. Bean Validation - HTTP 메시지 컨버터 

 

@ModelAttribute : HTTP 요청 파라미터 (쿼리스트링, POST Form)
@RequestBody : HTTP Body의 데이터를 객체로 변환할 때 (API JSON

 

 

* HTTP 메시지 컨버터(@RequestBody)에 @Validated 적용 테스트

 

 

1) 컨트롤러 ValidationItemApiController 

package hello.itemservice.web.validation;

import hello.itemservice.web.validation.form.ItemSaveForm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {

    @PostMapping("/add")
    public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult) {
        log.info("API 컨트롤러 호출");

        if (bindingResult.hasErrors()) {
            log.info("검증 오류 발생 errors={}", bindingResult);
            return bindingResult.getAllErrors();
        }

        log.info("성공 로직 실행");
        return form;
    }
}

 

 

 

2) Postman으로 테스트

 

  • 성공 요청 : 성공
  • 실패 요청 : JSON을 객체로 생성하는 것 자체가 실패
  • 검증 오류 요청 : JSON을 객체로 생성하는 것은 성공, 검증은 실패

 

 

 

(1) 성공 요청

 

 

성공 요청 로그 

  • 컨트롤러 호출 성공
  • 로직 성공

 

 

 

 

(2) 실패 요청

 

 

실패 요청 로그 

  • Http 메시지 컨버터에서 요청 JSON을 ItemSaveForm 객체로 생성하는 것 실패
    -> 예외 발생
    -> 컨트롤러 자체가 호출 X
    -> Validator 실행 X

 

 

 

 

(3) 검증 오류 요청

 

 

검증 오류 요청 로그 

  • 컨트롤러 호출
  • 검증 오류 발생 -> FieldError 발생 

 

 

 

 

3) @ModelAttribute vs @RequestBody

 

  • @ModelAttribute
    • 필드 단위로 세밀하게 바인딩 적용 
    • 특정 필드에 바인딩 오류가 발생해도 나머지 필드는 정상 바인딩 O, 검증 O
  • @RequestBody
    • HttpMessageConverter 단계에서 전체 객체 단위로 적용
    • JSON 데이터를 객체로 변경 실패 -> 예외 발생 -> 컨트롤러 호출 X, 검증 X
    • 메시지 컨버터 작동 성공 -> ItemSaveForm 객체 만들어야 -> @Validated 적용