Bean Validation

YH·2023년 4월 24일
0

✅ Bean Validation 정의

  • Bean Validation은 특정한 구현체가 아니라, Bean Validation 2.0(JSR-380)이라는 기술 표준이다.
  • 검증 어노테이션과 여러 인터페이스 모음이다.
  • Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체가 Hibernate Validator이다. Hibernate는 이름만 붙었지 ORM과는 관련 없다.

💡Hibernate Validator 관련 링크

✅ Bean Validation 사용

✔️Bean Validation을 사용하기 위해 아래 의존관계를 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'

  • Jakarta Bean Validation
    • jakarta.validation-api : Bean Validation 인터페이스
    • hibernate-validator 구현체
@NotBlank
private String itemName;

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

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

✔️ 스프링 부트에 spring-boot-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합한다.

✔️ 스프링 부트는 LocalValidatorFactoryBean글로벌 Validator로 등록한다. 이 Validator가 @NotNull 같은 어노테이션을 보고 검증을 수행한다.
✔️ 이렇게 글로벌 Validator가 등록되어 있기 때문에 @Valid, @Validated 어노테이션만 적용하면 된다.
✔️ 검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담아준다.

주의

  • 글로벌 Validator를 개발자가 직접 등록해놓으면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않는다. 따라서 Bean Validator가 동작하지 않는다.

✔️ 검증 순서

  1. @ModelAttribute 각각 필드에 타입 변환 시도
  2. 성공하면 Validator 적용, 실패하면 typeMismatch로 FieldError 추가

💡 Bean Validator는 바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다. (검증 자체가 값이 정상적으로 들어와야 검증이 의미가 있기 때문)

✅ Bean Validation - 에러 코드

✔️ Bean Validation이 제공하는 기본 오류 메시지를 원하는대로 변경할 수도 있다.
✔️ Bean Validation을 적용하고 오류가 발생하면 BindingResult에 아래와 같이 오류 코드를 기반으로 메시지 코드가 생성된다.

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

✔️ .properties 파일에 원하는 메시지로 등록할 수 있다.

#Bean Validation 추가
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}

✔️ Bean Validation 메시지 찾는 순서

  1. 생성된 메시지 코드 순서대로 messageSource에서 메시지를 찾는다.
  2. 1번에 정의된 메시지 코드가 없으면, 어노테이션의 message 속성을 사용한다. (@NotBlank(message = "공백! {0}")
  3. 라이브러리가 제공하는 기본 값 사용 ( ex) 공백일 수 없습니다. )

✅ Bean Validation - 오브젝트 오류

✔️ Bean Validation에서 필드 오류(Field)가 아닌 오브젝트 관련 오류(ObjectError)를 어떻게 처리하는지 알아본다.

✔️ ScriptAssert() 사용

  • 아래와 같이 사용할 수 있으나, 제약이 많고 복잡하므로 실무에서는 잘 사용하지 않는다.
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
	//...
}

✔️ 오브젝트 오류는 관련 부분만 자바 코드로 직접 작성하는 것을 권장한다.

if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.reject("totalPriceMain", new Object[]{10000, resultPrice}, null);
	}
}

✅ Bean Validation - Groups

✔️ 동일한 모델 객체를 등록할 때수정할 때 각각 다르게 검증하는 방법을 알아본다.
✔️ 검증 방법 2가지

  • Bean Validation의 groups 기능을 사용
  • 모델 객체를 직접 사용하지 않고 폼 전송을 위한 별도 모델 객체를 만들어서 사용

1. Bean Validation의 groups 기능 사용

  • 각각 검증을 다르게 할 그룹을 만들어 사용한다.
//등록할 때 사용할 그룹
public interface SaveCheck {
}

//수정할 때 사용할 그룹
public interface UpdateCheck {
}
  • 정의한 그룹을 아래와 같이 사용한다.
//Item 엔티티

@NotNull(groups = UpdateCheck.class) //수정시에만 적용
private Long id;

@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Max(value = 9999, groups = SaveCheck.class) //등록시에만 적용
private Integer quantity;

// add 컨트롤러 메소드
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item,
	BindingResult bindingResult, RedirectAttributes redirectAttributes) {
	//...
}

✔️ groups 기능은 코드가 이것저것 추가되고 복잡도가 올라간다. 그래서 실무에서는 주로 다음에 정리할 Form 전송 객체를 분리해서 사용한다.

✅ Form 전송 객체 분리

  • 위에서 설명한 groups를 사용하지 않는 주된 이유는, 클라이언트에서 넘어오는 데이터도메인 객체완전 동일하지 않기 때문이다.
  • 실무에서는 회원 등록 시 회원 관련 데이터만 넘어오는게 아니라 수많은 부가적인 데이터들도 같이 넘어온다.
  • 따라서 컨트롤러에서 도메인 객체를 직접 받지 않고, 별도의 객체를 만들어서 전달하고, 추후에 도메인 객체를 만들어서 사용한다.

폼 데이터 전달에 Item 도메인 객체 사용

  • HTML Form -> Item -> Controller -> Item -> Repository
    • 장점: Item 도메인 객체를 컨트롤러, 리포지토리 까지 직접 전달해서 중간에 Item을 만드는 과정이 없어서 간단하다.
    • 단점: 간단한 경우에만 적용할 수 있다. 수정시 검증이 중복될 수 있고, groups를 사용해야 한다.

폼 데이터 전달을 위한 별도의 객체 사용

  • HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository
    • 장점: 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있다. 보통 등록과, 수정용으로 별도의 폼 객체를 만들기 때문에 검증이 중복되지 않는다.
    • 단점: 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정이 추가된다.

✔️ 아래와 같이 폼 전송 객체를 별도로 생성한다.

//ItemSaveForm 클래스
@Data
public class ItemSaveForm {
    @NotBlank
    private String itemName;

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

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

//컨트롤러 addItem 메소드
@PostMapping("/add")
    public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form,
                            BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        ...

        //성공 로직
        //전달받은 ItemSaveForm 객체 데이터를 가지고 Item 객체를 새로 생성해준다.
        Item item = new Item(form.getItemName(), form.getPrice(), form.getQuantity());

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

주의

  • 위의 @ModelAttribute에서 ItemSaveForm 으로 변경했기 때문에 타임리프의 item을 그대로 사용하려면 @ModelAttribute("item") 와 같이 변경해줘야 한다.
  • 그렇지 않으면 이름이 itemSaveForm 으로 담기게 된다.

✅ Bean Validation - HTTP MessageConverter

✔️ @Valid , @Validated 는 HttpMessageConverter ( @RequestBody )에도 적용할 수 있다.

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

✅ @ModelAttribute vs @RequestBody

  • @ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용 가능하다.
  • @RequestBody는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 파싱하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 즉, 컨트롤러도 호출되지 않고 Validator도 적용할 수 없다.
  • HttpMessageConverter 단계에서 실패하면 예외가 발생한다. 예외 발생 시 원하는 형태로 예외 처리를 할 수 있고 추후 예외 처리에서 정리한다.
profile
하루하루 꾸준히 포기하지 말고

0개의 댓글