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 이란?
하이버네이트 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
사용하기 위해서는 의존관계를 추가해줘야 된다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
이렇게 하면 jakarta.valitation이 들어오게 되고 이 구현체로 보통 하이버네이트의 Validator가 들어오게 된다.
객체로 사용되는 domain에 들어가서 애노테이션을 붙여주면 적용이 된다.
간단하게만 몇개 알아 보자.
@NotBlank
: 빈값 + 공백만 있는 경우를 허용하지 않는다.@NotNull
: null 을 허용하지 않는다.@Range(min = 1000, max = 1000000)
: 범위 안의 값이어야 한다.@Max(9999)
: 최대 9999까지만 허용한다.지금까지 배웠던 스프링 MVC 검증 방법에 빈 검증기를 어떻게 적용하면 좋을지 여러가지 생각이 들 것이다. 하지만 우리가 고민할 필요는 없다. 스프링은 이미 개발자를 위해 빈 검증기를 스프링에 완전히 통합해두었다.
우리가 할 것은 없다. 그 전의 validator 코드에서 기존의 검증 로직을 빼도 별 문제없이 작동하는 것을 확인 할 수 있다. 도대체 어떻게 이게 가능한 것일까?
스프링 부트는 자동으로 글로벌 Validator로 등록한다.
@Valid
, @Validated
만 붙어 있다면 검증 오류가 발생하면 FieldError
, ObjectError
를 생성해서 BindingResult
에 담아 준다.@ModelAttribute
각각의 필드에 타입 변환 시도. typeMistach
로 FieldError
추가주의점
바인딩에 성공한 필드만 Bean Validation 적용이 된다
바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다. 생각해보면 당연한 일이다. (일단 모델 객체에 바인딩 받는 값이 정상으로 들어와야 검증도 의미가 있다. 실패해서 아무 값도 없는데 검증을 뭐하러 하는가)
예시
에러 코드가 어떻게 들어오는가?
오류 코드가 애노테이션 이름으로 등록된다. 마치 typeMismatch
와 유사하다.
@NotBlank
라는 검증이 적용된 에러 코드는 다음과 같이 생성된다.
@Range
의 경우
똑같다. 그냥 처음에 에러코드 이름이 애노테이션으로 바뀐것 말고는 Object의 이름과 Field의 이름, 타입이 온것은 달라질 것이 없다. 기존의 방식대로 정의를 한다면 내가 원하는 에러 메시지로 출력이 가능 하다.
아무것도 설정하지 않는다면 라이브러리의 Default 메시지가 출력이 된다. 또한 오브젝트의 애노테이션에 바로 @NotBlank(message = "공백x")
이런 식으로 정의도 가능 하다.
Bean Validation은 필드에 들어가는 오류검증이었다. 그렇다면 오브젝트 오류에 대해서는 어떻게 적용이 가능할까??
바로 @ScriptAssert() 를 사용하면 된다.
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
//...
}
실행해보면 정상 수행되는 것을 확인할 수 있다. 메시지 코드도 다음과 같이 생성된다.
메시지 코드
그런데 실제 사용해보면 제약이 많고 복잡하다. 그리고 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우들도 종종 등장하는데, 그런 경우 대응이 어렵다.
따라서 오브젝트 오류(글로벌 오류)의 경우 @ScriptAssert 을 억지로 사용하는 것 보다는 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장한다.
만약 데이터를 등록할때와 수정할 때의 검증 요구사항이 다르다면?
이것은 빈 애노테이션으로 어떻게 해결 할 수 있을까?
등록과 수정이 같은 domain을 사용하고 있는데, 애노테이션을 다는 방법으로는 서로 다른 요구사항을 충족시킬 수 없다.
이를 해결할 수 있는 두가지 방법이 있다.
다음 두가지 기능을 한번 알아 보도록 하겠다.
등록시에 검증할 기능과 수정할때 검정할 기능을 그룹으로 나눠서 적용할 수 있다.
저장용 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 방식은 사용하기에 복잡하고, 실무에서는 회원 등록 및 수정때의 데이터들도 다르기때문에 잘 사용되지 않고, 별도의 모델 객체를 사용하는 방법을 사용한다.
HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository
장점 : 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있다. 보통 등록과, 수정용으로 별도의 폼 객체를 만들기 대문에 검증이 중복되지 않는다.
단점 : 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 가정이 추가된다.
이렇게 폼 데이터 전달을 위한 별도의 객체를 사용하고, 등록, 수정용 폼 객체를 나누면 등록, 수정이 완전히 분리되기에 groups
를 적용할 일은 드물다.
@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가지 경우를 나누어 생각해야 된다
price 의 값에 숫자가 아닌 문자를 전달해서 실패하게 만들어보자.
검증 오류 요청
HttpMessageConverter 는 @ModelAttribute 와 다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용된다. 따라서 메시지 컨버터의 작동이 성공해서 ItemSaveForm 객체를 만들어야 @Valid , @Validated 가 적용된다.
@ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.
@RequestBody 는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다. 이때는 예외 처리를 해야된다.
참고 : 본 글은 김영한님의 스프링 강의 공부를 위해 정리한 것이다.