5. 검증 2 - Bean Validation

2024. 5. 1. 10:38·Spring/MVC 2

1. 복습

이전 Chapter에서 컨트롤러와 검증 로직을 분리하고

WebDataBinder에 Validator를 등록하고 @Validate 어노테이션을 사용해, 별도의 검증 로직 호출 없이 동작하는 모습을 보였다.

private final ItemValidator itemValidator;

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

 

// @Validated: 자동으로 검증기가 실행이 되어, 에러는 BindingResult에 담겨진다.
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

    // 검증에 실패하면 다시 입력 폼으로
    // BindingResult는 Spring에서 자동으로 View에 넘어가므로, 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}";
}
@Component
public class ItemValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        // Item의 자식 클래스들도 전부 통과
        return Item.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors bindingResult) {
        Item item = (Item) target;


        // 검증 오류 결과를 보관 → BindingResult

        // 필드 오류 → new FieldError()
        if (!StringUtils.hasText(item.getItemName())) {
            // bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
            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() >= 9999) {
            bindingResult.rejectValue("quantity,", "max", new Object[]{9999}, null);
        }

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

 

 


 

2. Bean Validation

하지만 이러한 특정 필드에 대한 검증 로직은 단순하긴 하지만 매번 작성하기 번거롭다.

이러한 일반적인 로직을 Spring에서 @Bean Validation으로, 클래스(모델) 내 필드에서 제약 조건을 정의할 수 있다.

 

Bean Validation

Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다.

구현체로는 java에서 지원하는 jakarta.validation과, hibernate에서 지원하는 org.hibernate.validator가 있다.

cf. ORM 과는 관련이 없다.

예를 들어 DB 스키마에 제약 조건을 걸고자 할 때는, @Column(nullable =false) 와 같이 걸어주어야 한다.

 

 

1) 라이브러리를 추가하여, 자동으로 LocalValidatorFactoryBean이 글로벌 Validator로 등록한다.

 

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

 

 

cf. 기존 생성한 ItemValidator는 삭제해주어야 한다.

public class ValidationItemControllerV2 {

//    private final ItemValidator itemValidator;

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

}
@Component
public class ItemValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) { 
        return Item.class.isAssignableFrom(clazz); // 검증 객체인지 판단
    }


    @Override
    public void validate(Object target, Errors bindingResult) {
    	// 검증 로직
    }   
}

 

 

3) 글로벌 Validator이 LocalValidatorFactoryBean이 어노테이션 검증을 실행한다. → Bean Validation

이 때, 검증 오류가 발생하면 FieldError / ObjectError를 생성해서 BindingResult에 담아준다.

 

@Valid, @Validated : 검증 대상임을 명시

@NotNull, @Min, @Max : 입력값에 대한 검증로직 관리

@Data
public class Item {

    @NotNull(groups = UpdateCheck.class)
    private Long id;

    @NotBlank(message = "공백 X")
    private String itemName;

    @NotNull(message = "1천 이상 1백만 이하")
    @Range(groups = {SaveCheck.class, UpdateCheck.class}, min = 1000, max = 1000000)
    private Integer price;

    @NotNull(message = "최대 9999")
    @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. Bean Validation - 에러 코드

Bean Validation이 기본으로 제공하는 오류 메시지를 변경하고 싶을 때

message.properties 에 에러 코드에 맞는 메시지를 정의하면 된다.

 

 

[ex. 에러 코드]

@NotBalnk

- NotBlank.item.itemName

- NotBlank.itemName

- NotBlank.java.lang.String

- NotBlank

@Range

- Range.item.price

- Range.price

- Range.java.lang.Integer

- Range

 

 


 

4. Bean Validation - 오브젝트 오류

특정 필드(FieldError)가 아닌, 복합 필드(ObjectError) 검증 오류와 같은 경우는

클래스(모델) 내 필드에서 정의하기 어려우므로, 기존 작성 방식대로 직접 자바 코드로 작성하는 것을 권장한다.

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

 

 


 

동일한 모델 객체를 등록할 때와 수정할 때의 제약 조건을 다르게 설정할 수 있다.

(1) groups

(2) Form 전송 객체 분리

 

5. 제약 조건 분리 - groups

 

1) 저장용 / 수정용 groups을 생성한다.

public interface SaveCheck {
}
public interface UpdateCheck {
}

 

 

2) 모델 객체에 제약 조건을 적용할 그룹을 적용한다.

@Data
public class Item {

    @NotNull(groups = UpdateCheck.class)
    private Long id;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class}, message = "공백 X")
    private String itemName;

    @NotNull(groups = {SaveCheck.class, UpdateCheck.class}, message = "1천 이상 1백만 이하")
    @Range(groups = {SaveCheck.class, UpdateCheck.class}, min = 1000, max = 1000000)
    private Integer price;

    @NotNull(groups = {SaveCheck.class}, message = "최대 9999")
    @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) 컨트롤러에서 어떤 제약 조건을 적용할 것인지 정의한다.

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

 

 


 

6. 제약 조건 분리 - Form 전송 객체 분리

1) Item 저장 폼

@Data
public class ItemSaveForm {
    @NotBlank
    private String itemName;

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

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

 

public String addItem(@Validated @ModelAttribute("item") ItemSaveForm itemSaveForm, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    // ItemSaveForm을 Item 객체로 바꾸는 과정이 필요하다.
    Item item = new Item(itemSaveForm.getItemName(), itemSaveForm.getPrice(), itemSaveForm.getQuantity());

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

 

 

2) Item 수정 폼

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

    @NotBlank
    private String itemName;

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

    // 수정 시에는 수량을 자유롭게 설정할 수 있다.
    private Integer quantity;
}

 

public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm itemUpdateForm, BindingResult bindingResult) {
    // ItemUpdateForm을 Item 객체로 바꾸는 과정이 필요하다.
    Item item = new Item(itemUpdateForm.getItemName(), itemUpdateForm.getPrice(), itemUpdateForm.getQuantity());

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

 

 


 

7. @Validated - HTTP 메시지 컨버터(@RequestBody)

@ModelAttribute : URL, 쿼리스트링, Post Form

@RequestBody : Http body

 

지금까지 @ModelAttribute 어노테이션으로 매핑시킬 때의 모델 객체를

@Valid, @Validate와 같은 BeanValidation 방식으로 검증해보았다.

 

그럼 API 통신을 할 때, JSON 데이터를 Http Message Converter로 매핑시킬 때도 똑같이 검증 로직이 잘 실행될까??

cf. Http Message Converter → https://wch-0625.tistory.com/119

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

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

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

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

}

 

 

1) API 검증 경우의 수

- 성공

- 실패 : JSON 객체로 생성하는 것 자체가 실패함

- 검증 오류 : JSON 객체로 생성하는 것은 성공했지만, 검증에서 실패함

 

 

 

2) @ModelAttribute  vs. @RequestBody

@ModelAttribute는 각각의 필드 단위로 세밀하게 적용된다.

그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있었다.

typeMismatch 에러와, 필드 검증 에러가 같이 발생한 모습이다.

 

 

@RequestBody는 Http Message Converter가 동작하면서 모델 객체와 매핑을 하게 되는데,

이 때 @ModelAttribute와 다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용된다.

따라서 Http Message Converter 동작이 성공해서 모델 객체와 정확히 매핑되어 생성이 되어야 Validater의 검증 로직이 적용된다.

HttpMessageConverter에서 매핑이 실패하면 400 error가 발생한다.

 

저작자표시 (새창열림)

'Spring > MVC 2' 카테고리의 다른 글

7. 로그인 처리2 - 필터, 인터셉터  (0) 2024.05.04
6. 로그인 처리1 - 쿠키, 세션  (1) 2024.05.02
4. 검증 1 - Validation  (0) 2024.04.29
3. 메시지, 국제화  (0) 2024.04.29
2. 타임리프 - 스프링 통합과 폼  (0) 2024.04.29
'Spring/MVC 2' 카테고리의 다른 글
  • 7. 로그인 처리2 - 필터, 인터셉터
  • 6. 로그인 처리1 - 쿠키, 세션
  • 4. 검증 1 - Validation
  • 3. 메시지, 국제화
wch_t
wch_t
  • wch_t
    끄적끄적(TIL)
    wch_t
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (171)
      • Architecture (0)
      • Algorithm (67)
        • Math (5)
        • Simulation (1)
        • Data Structure (4)
        • DP (7)
        • Brute Fource (10)
        • Binary Search (6)
        • Greedy (2)
        • Graph (11)
        • Mst (1)
        • Shortest path (10)
        • Two Pointer (1)
        • Tsp (3)
        • Union Find (2)
        • Mitm (1)
      • CS (2)
        • 데이터베이스 (5)
        • 네트워크 (5)
      • DB (6)
      • DevOps (17)
        • AWS (9)
        • Docker (1)
        • CI-CD (5)
      • Error (1)
      • Project (0)
        • kotrip (0)
      • Spring (59)
        • 끄적끄적 (5)
        • 기본 (9)
        • MVC 1 (7)
        • MVC 2 (11)
        • ORM (8)
        • JPA 1 (7)
        • JPA 2 (5)
        • Spring Data Jpa (7)
      • Test (2)
      • TIL (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    spring-cloud-starter-bootstrap
    spring-cloud-starter-aws-secrets-manager-config
    Sxssf
    view algorithm
    백준 3015 파이썬
    form_post
    response_mode
    백준 17289 파이썬
    aws secrets manager
    scope
    Jenkins
    TempTable
    docker: not found
    백준 17299 파이썬
    Merge
    애플
    apache poi
    docker
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
wch_t
5. 검증 2 - Bean Validation
상단으로

티스토리툴바