[Spring] Bean Validation

hi·2022년 12월 5일
0

Bean Validation

  • 특정한 구현체가 아닌 Bean Validation 2.0(JSR-380) 이라는 기술 표준
  • 검증 애노테이션과 여러 인터페이스의 모음
  • 일반적으로 사용하는 구현체는 하이버네이트 Validator

하이버네이트 Validator

공식 메뉴얼 https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/
검증 애노테이션 모음 https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec

순수 Bean Validation 사용

  • 의존관계 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'

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

💡 참고

javax.validation.constraints.NotNull
org.hibernate.validator.constraints.Range

javax.validation ~
: 특정 구현에 관계없이 제공되는 표준 인터페이스
org.hibernate.validator ~
: 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증 기능.
실무에서 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용해도 된다.

검증기 직접 사용

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

//검증 실행
Set<ConstraintViolation<Item>> violations = validator.validate(item);

=> 검증 대상( item )을 직접 검증기에 넣고 그 결과를 받는다
Set 에는 ConstraintViolation 이라는 검증 오류가 담긴다
결과가 비어있으면 검증 오류가 없는 것

스프링 통합 Bean Validation

  • 스프링 부트가 spring-boot-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합
  • 스프링 부트는 자동으로 LocalValidatorFactoryBean 을 글로벌 Validator로 등록
    • @NotNull 같은 애노테이션을 보고 검증을 수행.
      따라서 @Valid, @Validated 만 적용하면 된다.
    • 오류 발생시 FieldError , ObjectError 를 생성해서 BindingResult 에 담는다

검증 순서

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

  2. Validator 적용

👉 바인딩에 성공한 필드만 Bean Validation 적용


Bean Validation 에러 코드

  • 오류 코드가 애노테이션 이름으로 등록
@NotBlank
NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank

@Range
Range.item.price
Range.price
Range.java.lang.Integer
Range
  • 애노테이션 message 속성 사용 가능
    `@NotBlank(message = "공백 입력 불가")

오브젝트 오류 처리 @ScriptAssert( )

@Data
@ScriptAssert(lang = "javascript", script = "_this.price * this.quantity >= 10000")
public class Item {
	//...
}
  • 메시지 코드는 아래처럼 생성
	ScriptAssert.item
	ScriptAssert

but

  • 제약이 많고 복잡하다
  • 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우가 종종 있으며
    대응이 어려움

따라서 오브젝트 오류(글로벌 오류)의 경우
오브젝트 관련 부분만 직접 자바 코드로 작성하는 것을 권장


Bean Validation 한계

데이터를 등록할 때와 수정할 때의 요구사항이 다를 수 있다


해결 방법

  1. Bean Validation의 groups 기능 사용
HTML Form -> Item -> Controller -> Item -> Repository
  • 장점 : 도메인 객체를 컨트롤러, 리포지토리까지 직접 전달하여 간단
  • 단점 : 간단한 경우에만 적용 가능, 수정시 검증 중복 가능성

👉 등록시 폼에서 전달하는 데이터가 도메인 객체와 딱 맞지 않아 잘 사용 안 함

  1. Item을 직접 사용하지 않고 ItemSaveForm, ItemUpdateForm 같은
    폼 전송을 위한 별도의 모델 객체를 만들어 사용
HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository
  • 장점 : 폼 데이터가 복잡해도 딱 맞는 별도의 객체를 사용, 검증이 중복되지 않음
  • 단점 : 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정 추가

HTTP 메시지 컨버터

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

💡 참고
@ModelAttribute : HTTP 요청 파라미터(URL 쿼리 스트링, POST Form)를 다룰 때 사용
@RequestBody : HTTP Body의 데이터를 객체로 변환할 때 사용 (주로 API JSON 요청)

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

API의 경우 3가지 경우가 있다

  • 성공 요청 : 성공
  • 실패 요청 : JSON을 객체로 생성하는 것 자체가 실패 👉 컨트롤러 호출 X
  • 검증 오류 요청 : JSON을 객체로 생성하는 것은 성공, 검증에서 실패

@ModelAttribute vs @RequestBody

@ModelAttribute

  • 필드 단위로 정교하게 바인딩이 적용
  • 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩

@RequestBody

  • HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면
  • 이후 단계가 진행되지 않고 예외 발생 (전체 객체 단위로 바인딩 적용)

0개의 댓글