[Spring] 검증2 - Bean Validation

·2023년 11월 24일

Spring

목록 보기
3/26
post-thumbnail

💡Bean Validation

검증 애노테이션과 여러 인터페이스를 모은 기술 표준으로, 여러 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고 표준화한 것 이다.

Bean Validation 사용하기

bulid.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'
  • Bean Validation을 사용하려면 bulid.gradle에 의존관계를 추가해주어야한다.

Item.java

@NotBlank
private String itemName;

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

@NotNull
@Max(value = 9999)
private Integer quantity;
  • @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
  • @NotNull : null을 허용하지 않는다.
  • @Range(min=1000, max=1000000) : 범위 안의 값이여야 한다.
  • @Max(9999) : 최대 9999까지만 허용한다.

📌참고
javax.validation.constraints.NotNull
org.hibernate.validator.constraints.Range
@NotNull, @NotBlank의 경우 javax가 제공하는 표준 인터페이스이므로 어떤 구현체에서도 동작한다.
@Range의 경우 하이버네이트 validator 구현체를 사용할 떄만 제공되는 검증 기능이다.
하지만 실무에서는 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용해도 된다.

controller

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {...}
  • controller에 적용 시에는 검증 대상 객체에 @Valid 또는 @Validated만 적용하면 검증기가 실행된다.

📌@Valid@Validated
@Valid : 자바 표준 검증 애노테이션
@Validated : 스프링 전용 검증 애노테이션
현재로써는 둘 중 아무거나 사용해도 동일하게 작동하지만, 뒤에 나올 groups기능을 사용하려면 @Validated를 써야한다.

검증 순서

  1. @ModelAttribute가 각각의 필드에 타입 변환을 시도
    1-1. 성공하면 다음으로
    1-2. 실패하면 typeMismatchFieldError추가
  2. Validator 적용

바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다.
즉, 타입 변환에 성공해서 바인딩에 성공한 필드여야만 BeanValidation 적용이 의미가 있다.

  • itmeName에 문자 "A" 입력 -> 타입 변환 성공 -> itemName필드에 BeanValidation 적용
  • price에 문자 "A" 입력 -> 숫자 타입 변환 시도 실패 -> typeMismatch FieldError추가 -> price필드에 BeanValidation 적용 불가

📗Bean Validation - 에러 코드

  • Bean Validation을 적용하고 bindingResult에 등록된 검증 오류 코드는 MessageCodesResolver에 의해 생성된다.
  • 해당 오류 코드를 기반으로 원하는 오류 메시지를 등록할 수 있다.
#오류 코드
@NotBlank
NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank

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

첫번째 방법 : 메시지 등록(errors.properties)

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

두번째 방법 : 애노테이션의 message 사용(errors.properties)

@NotBlank(message = "공백은 입력할 수 없습니다.") 
private String itemName;

📌BeanValidation 메시지 찾는 순서
1. 생성된 메시지 코드 순서대로 messageSource에서 찾기
2. 애노테이션의 message속성 사용
3. 라이브러리가 제공하는 기본 값 사용


📗Bean Validation - 오브젝트 오류

특정 필드가 아닌 해당 오브젝트 관련 오류는 @ScriptAssert()를 사용하면 된다.

@Data
@ScriptAssert(lang="javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 만원이 넘어야합니다.")
public class Item{
	...
}

@ScriptAssert()는 제약이 많고 복잡하며, 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우도 종종 등장하는데, 그런 경우 대응이 어렵다.
따라서 오브젝트 오류의 경우 @ScriptAssert()를 사용하기 보다 이전과 같이 오브젝트 오류 관련 부분만 자바코드로 작성하는 것을 권장한다.

//특정 필드 예외가 아닌 전체 예외
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 - 한계

데이터 등록 시와 수정 시 요구사항이 다를 수 있다.

  • 등록 시에는 quantity수량을 최대 9999까지 등록할 수 있지만, 수정 시에는 수량을 무제한으로 변경할 수 있다.
  • 등록 시에는 id에 값이 없어도 되지만 수정시에는 id 값이 필수이다.

이 경우, 수정기능은 잘 작동하지만 등록시에는 id값이 없고 quantity최대 값인 9999도 적용되지 않는다.
이러한 문제점을 해결 할 수 있는 방법은 크게 두가지이다.

📗대안1 - Groups

  • Groups를 사용하면 등록시에 검증할 기능과 수정시에 검증할 기능을 각각 그룹으로 나누어 적용할 수 있다.

1. 저장용/수정용 groups 인터페이스 생성

package hello.itemservice.domain.item;

public interface SaveCheck {
}
package hello.itemservice.domain.item;

public interface UpdateCheck {
}

2. Item - groups 적용

@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)
private Integer price;

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

3. 각 로직에 Groups 적용

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

📌참고
@Valid에는 groups를 적용할 수 있는 기능이 없다.
groups를 사용하려면 @Validated를 사용해야 한다.

groups를 사용하면 등록, 수정 시에 각각 다르게 검증을 할 수 있다.
하지만 전반적으로 복잡도가 올라갔으며, 실무에서는 groups 기능은 잘 사용하지 않고 다음에 등장할 Form 전송 객체 분리를 주로 사용한다.

📗대안2 - Form 전송 객체 분리

등록과 수정을 각각 처리하는 전용 객체를 @ModelAttribute로 사용해 컨트롤러에서 데이터를 전달받고, 필요한 데이터를 사용해 Item 객체를 생성하는 방법을 사용해보자.

1.저장용/수정용 Item 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;
}

2-1. 등록 로직 변경

 @PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
	...
	//성공로직
	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}";
    }

2-2. 수정 로직 변경

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
	...
	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}";
 }
  • Item대신 각각 ItemSaveForm, ItemUpdateForm을 전달받는다.
  • 폼 객체의 데이터를 기반으로 Item객체를 생성한다. 폼 객체처럼 중간에 다른 객체가 추가되면 변환하는 과정이 추가된다.

📌주의
@ModelAttributeitem이름을 넣어주지 않으면 자동으로 itemSaveForm이라는 이름으로 Model에 담기게 된다.
이렇게 되면 뷰 템플릿에서 접근하는 th:object도 함께 변경해 주어야한다.

profile
배우고 기록하며 성장하는 백엔드 개발자입니다!

0개의 댓글