검증 - Bean Validation

Single Ko·2023년 6월 3일
0

Spring 강의 정리

목록 보기
16/31

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;
   
	//...
}

이런 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이 바로 Bean Validation 이다.(애노테이션만 봐도 무슨 역할을 하는지 느낌이 올것이다.)
Bean Validation을 잘 활용하면, 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.

Bean Validation 이란?

  • 먼저 Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다.
  • 쉽게 이야기해서 검증 애노테이션과 여러 인터페이스의 모음이다. 마치 JPA가 표준 기술이고 그 구현체로 하이버네이트가 있는 것과 같다.
  • Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다.

하이버네이트 Validator 관련 링크
공식 사이트: http://hibernate.org/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'

이렇게 하면 jakarta.valitation이 들어오게 되고 이 구현체로 보통 하이버네이트의 Validator가 들어오게 된다.

객체로 사용되는 domain에 들어가서 애노테이션을 붙여주면 적용이 된다.
간단하게만 몇개 알아 보자.

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

지금까지 배웠던 스프링 MVC 검증 방법에 빈 검증기를 어떻게 적용하면 좋을지 여러가지 생각이 들 것이다. 하지만 우리가 고민할 필요는 없다. 스프링은 이미 개발자를 위해 빈 검증기를 스프링에 완전히 통합해두었다.

Bean Validation - Spring 적용

우리가 할 것은 없다. 그 전의 validator 코드에서 기존의 검증 로직을 빼도 별 문제없이 작동하는 것을 확인 할 수 있다. 도대체 어떻게 이게 가능한 것일까?

스프링 부트는 자동으로 글로벌 Validator로 등록한다.

  • 저번에 수동으로 직접 글로벌 적용을 해 봤는데, 이건 스프링 부트가 그냥 자동으로 등록해주는 것이다. 그렇기 때문에 @Valid , @Validated 만 붙어 있다면 검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담아 준다.

검증 순서

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

주의점
바인딩에 성공한 필드만 Bean Validation 적용이 된다
바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다. 생각해보면 당연한 일이다. (일단 모델 객체에 바인딩 받는 값이 정상으로 들어와야 검증도 의미가 있다. 실패해서 아무 값도 없는데 검증을 뭐하러 하는가)

예시

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

Bean Validation - 에러코드

에러 코드가 어떻게 들어오는가?

오류 코드가 애노테이션 이름으로 등록된다. 마치 typeMismatch와 유사하다.

@NotBlank라는 검증이 적용된 에러 코드는 다음과 같이 생성된다.

  • NotBlank.item.itemName
  • NotBlank.itemName
  • NotBlnak.java.lang.String
  • NotBlank

@Range의 경우

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

똑같다. 그냥 처음에 에러코드 이름이 애노테이션으로 바뀐것 말고는 Object의 이름과 Field의 이름, 타입이 온것은 달라질 것이 없다. 기존의 방식대로 정의를 한다면 내가 원하는 에러 메시지로 출력이 가능 하다.

아무것도 설정하지 않는다면 라이브러리의 Default 메시지가 출력이 된다. 또한 오브젝트의 애노테이션에 바로 @NotBlank(message = "공백x") 이런 식으로 정의도 가능 하다.

Bean Validation - 오브젝트 오류

Bean Validation은 필드에 들어가는 오류검증이었다. 그렇다면 오브젝트 오류에 대해서는 어떻게 적용이 가능할까??

바로 @ScriptAssert() 를 사용하면 된다.

@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
 //...
}

실행해보면 정상 수행되는 것을 확인할 수 있다. 메시지 코드도 다음과 같이 생성된다.

메시지 코드

  • ScriptAssert.item
  • ScriptAssert

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

Bean Validation - 한계

만약 데이터를 등록할때와 수정할 때의 검증 요구사항이 다르다면?
이것은 빈 애노테이션으로 어떻게 해결 할 수 있을까?

등록과 수정이 같은 domain을 사용하고 있는데, 애노테이션을 다는 방법으로는 서로 다른 요구사항을 충족시킬 수 없다.

이를 해결할 수 있는 두가지 방법이 있다.

  • Bean Validation의 groups 기능
  • Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용하면 된다.

다음 두가지 기능을 한번 알아 보도록 하겠다.

Bean Validation - groups

등록시에 검증할 기능과 수정할때 검정할 기능을 그룹으로 나눠서 적용할 수 있다.

저장용 groups 생성
public interface SaveCheck {
}

수정용 groups 생성
public interface UpdateCheck {
}


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

이렇게 체크용 인터페이스를 만들어 놓고, 각각의 그룹을 적용시킨뒤 실제 컨트롤러에서는 지정해서 사용한다.

@Validated(UpdateCheck.class) , @Validated(SaveCheck.Class) 를 적어놓으면 된다.

다만 이런 groups 방식은 사용하기에 복잡하고, 실무에서는 회원 등록 및 수정때의 데이터들도 다르기때문에 잘 사용되지 않고, 별도의 모델 객체를 사용하는 방법을 사용한다.

Form 전송 객체 분리

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

  • 장점 : 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있다. 보통 등록과, 수정용으로 별도의 폼 객체를 만들기 대문에 검증이 중복되지 않는다.

  • 단점 : 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 가정이 추가된다.

    이렇게 폼 데이터 전달을 위한 별도의 객체를 사용하고, 등록, 수정용 폼 객체를 나누면 등록, 수정이 완전히 분리되기에 groups를 적용할 일은 드물다.

Bean Validation - HTTP 메시지 컨버터


@RestController
public class ValidationItemApiController {

	@PostMapping("/add")
 	public Object addItem(@RequestBody @Validated ItemSaveForm form,
 						  BindingResult bindingResult) {
                          
 		if (bindingResult.hasErrors()) {
 			return bindingResult.getAllErrors();
 		}
        
 		return form;
 	}
}

API의 경우 3가지 경우를 나누어 생각해야 된다

  • 성공 요청: 성공
  • 실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
  • 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함
  1. price 의 값에 숫자가 아닌 문자를 전달해서 실패하게 만들어보자.

    • JSON 객체로 생성하는 것 자체가 실패함 -> Controller 자체가 호출이 안되고, 예외가 터져버린다.
  2. 검증 오류 요청

    • ObjectError 와 FieldError 를 반환한다. 스프링이 객체를 JSON으로 변환해서 클라이언트에 전달했다. 실제 개발할 때는 검증 오류 객체들을 그대로 사용하지 말고, 필요한 데이터만 뽑아서 별도의 API 스펙을 정의하고 그에 맞는 객체를 만들어서 반환해야 한다.

@ModelAttribute vs @RequestBody

  • HTTP 요청 파리미터를 처리하는 @ModelAttribute 는 각각의 필드 단위로 세밀하게 적용된다. 그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있었다.
  • HttpMessageConverter 는 @ModelAttribute 와 다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용된다. 따라서 메시지 컨버터의 작동이 성공해서 ItemSaveForm 객체를 만들어야 @Valid , @Validated 가 적용된다.

  • @ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.

  • @RequestBody 는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다. 이때는 예외 처리를 해야된다.

참고 : 본 글은 김영한님의 스프링 강의 공부를 위해 정리한 것이다.

profile
공부 정리 블로그

0개의 댓글