스프링의 기본적인 Validation에 대해 알아보았다.
우선 Form 태그에 예기치 못한 입력이 들어올 수 있기 때문에 서버단 Validation은 꼭 필요하다. 그래서 단계적으로 Validation을 설계해보았다.
위처럼 검증 과정을 진행하였다. 현재로서는 Validator를 상속하도록 클래스를 만들고. 해당 클래스의 validate 메서드에서 reject와 rejectValue를 이용하여 검증로직을 처리하는 것이 최선이다. 더 편리하게 할 수는 없을까?
생각해보면 null 검증, 길이 검증 등 검증 처리내용이 거의 비슷하다. 스프링이 제공하는 Bean Validation을 이용해 보다 편리하게 검증을 수행해보자.
public class Item {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
}
위는 Bean Validation의 예시이다. Null 검사, 길이 검사 등 자주 사용되는 검증 로직을 어노테이션으로 적용하도록한 인터페이스가 Bean Validation이다. 구현체로는 주로 하이버네이트 Validator를 사용한다.
implementation 'org.springframework.boot:spring-boot-start-validation'
Bean Validation 을 사용하려면 build.gralde에 위의 의존관계를 추가해야한다.
스프링부트는 라이브러리가 추가되면 자동으로 Bean Validator 를 스프링에 통합한다. 이 때 LocalValidatorFactoryBean 을 글로벌 Validator 로 등록시킨다. 이 Validator 는 필드의 @Notnull 같은 어노테이션을 보고 검증을 수행한다. 이처럼 글로벌 Validator 가 적용되어 있기에 우리는 검증할 파라미터에 @Valid나 @Validated만 추가하면 된다. @Valid는 자바의 @Validated는 스프링의 어노테이션인데 검증기를 실행하라는 뜻이다. 검증오류가 발생하면 자동으로 FieldError를 생성해 BindingResult에 담아준다.
검증 순서는 다음과 같다.
즉 타입에러가 발생한 경우 typeMismatch 를 오류코드로 FieldError를 추가해버리고, 검증을 수행하지 않는다. 제대로 바인딩 된 객체의 필드들만 검증을 수행하게된다.
자주 활용되는 어노테이션들이다.
@NotBlank 어노테이션을 추가한 필드에 빈 문자열이 들어와서 예외가 발생했다고 가정해보자. 이때 rejectValue 를 자동호출하여 FieldError가 생성된다고 하였는데 오류코드는 어떤 값이 사용될까?
Bean Validation 에서 오류코드는 어노테이션 이름과 동일하다. 즉 NotBlank를 오류코드로 rejectValue가 호출되며 'NotBlank.객체명.필드명', 'NotBlank.필드명', 'NotBlank.타입명', 'NotBlank'. 4개의 메시지코드가 파라미터로 넘겨져 FieldError가 생성되어 BindingResult에 삽입된다. 따라서 이 메시지코드들을 properties에서 설정하면 메시지를 지정할 수 있다.
BeanValidation의 메시지 찾는 순서
if(item.getPrice() != null && item.getQuantity() != null) {
if(item.getPrice * item.getQuantity < 10000) {
bindingResult.reject("오류코드", 메시지 파라미터 Object 배열, 기본 메시지);
}
}
Bean Validation 은 필드에 어노테이션으로 검증 방식을 결정하는데, 필드에러가 아닌 오브젝트 에러(글로벌 오류)는 어떻게 처리할까?
클래스명 상단에 @ScriptAssert() 문법을 사용해도 되지만 기능에 제약이 있다. 따라서 글로벌 오류의 경우 컨트롤러에서 직접 if문과 reject()를 통해 처리하자.
상품 등록과 상품 수정에서 Form 을 사용한다. 이때 Form 값을 받기위해 같은 Item 클래스 객체를 사용한다고 가정해보자. 수정 시에는 상품 수량 제한이 없다던가.. 이 둘의 검증 요구조건이 다를 수 있다. Bean Validation 객체의 필드에 제약조건을 걸어버리기 때문에 이런 상황에 유연하게 대처할 수 없다. Bean Validation의 groups라는 기능을 통해 이를 해결할 수 있다.
하지만 코드가 너무 지저분해 진다. 따라서 보통 Form 마다 각각 객체를 새로 정의해서 사용한다. 즉 Entity가 되는 Item과 수정용 UpdateDto와 등록용 AddDto 를 따로 선언해서 사용한다. Entity를 따로 정의하는 이유는 Form 에서 모든 필드를 입력받지 않을 수 있기 때문이다.
@Valid 와 @Validated 는 @RequestBody에도 사용할 수 있다. 여기서 @RequestBody를 짚고 넘어가자.
@RestController : 뷰를 넘겨주는 것이 아닌 데이터를 직접 넘겨주는 Controller. 객체를 반환하면 메시지 컨버터가 JSON으로 변환하여 클라이언트에게 전달한다.
@ModelAttribute : Request 파라미터를 객체로 변환할 때 사용
@RequestBody : Request Body의 데이터를 객체로 변환할 때 사용. 주로 JSON 요청을 객체로 변환한다.
메시지 컨버터가 JSON 요청을 객체로 변환해준다고 하였다. 따라서 JSON을 @RequestBody의 객체에 담은 후 Validation을 수행한다. 그런데 타입에러가 발생하면 자동으로 typeMismatch로 FieldError를 생성하고 나머지 필드의 검증을 수행하는 @ModelAttribute 의 검증과 달리. @RequestBody는 JSON을 객체화 할때 타입에러가 발생하면 예외가 발생하고 꺼진다. 따라서 이는 후에 예외처리를 통해 별도로 처리가 필요하다.