[MVC2] 5. 검증2 - Bean Validation

kiwonkim·2021년 10월 25일
0

이전 포스팅

스프링의 기본적인 Validation에 대해 알아보았다.
우선 Form 태그에 예기치 못한 입력이 들어올 수 있기 때문에 서버단 Validation은 꼭 필요하다. 그래서 단계적으로 Validation을 설계해보았다.

  • V1 - 직접구현
    Controller에 HashMap을 생성하고, if문으로 확인해가며 HashMap에 추가하였다. 하지만 타입에러를 잡을 수 없는 한계가 존재했다.
  • V2 - BindingResult 사용
    BindingResult 를 같이 파라미터로 넘겨주어, if문으로 예외가 생기면 FieldError나 ObjectError를 생성하여 BindingResult에 추가해주었다. 타입에러 발생시 스프링이 자동으로 FieldError를 추가해서 넣어준다. 타임리프가 BindingResult 를 쉽게 쓸 수 있도록 되어있어 뷰 출력도 편하다.
  • V3 - 메시지 활용
    FiledError 생성자에 메시지 코드 배열이 들어가는 부분이 있다. 이를 바탕으로 basename이 등록된 properties 메시지를 찾아 넣어준다. FieldError를 더욱 편리하게 사용하도록 해주는 rejectValue도 있다. rejectValue는 오류코드를 넣는데, MessageCodesResolve로 오류코드를 바탕으로 메시지코드들을 생성하여 FieldError의 파라미터로 넣어 BindingResult에 넣어준다.
  • V4 - Validator 분리
    컨트롤러에 검증코드가 모두 다 들어있으니 너무 지저분하다. 검증객체와 BindingResult를 파라미터로 하며 Validator를 상속하는 클래스를 만들고, validate 메서드에 검증로직을 때려박자. 그 후 검증이 필요한 컨트롤러에서 WebDataBinder 에 추가하고, 필요한 핸들러의 파라미터에 @Validated 를 추가하면 자동으로 스프링이 검증을 해준다. 모든 컨트롤러에 적용할 수 있도록 글로벌 설정을 해주는 방법도 있다.

위처럼 검증 과정을 진행하였다. 현재로서는 Validator를 상속하도록 클래스를 만들고. 해당 클래스의 validate 메서드에서 reject와 rejectValue를 이용하여 검증로직을 처리하는 것이 최선이다. 더 편리하게 할 수는 없을까?


생각해보면 null 검증, 길이 검증 등 검증 처리내용이 거의 비슷하다. 스프링이 제공하는 Bean Validation을 이용해 보다 편리하게 검증을 수행해보자.



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에 담아준다.

검증 순서는 다음과 같다.

  1. 파라미터의 @ModelAttribute 에서 각각의 필드로 타입 변환 시도
    1. 성공시 다음으로
    2. 실패시 오류코드를 typeMismatch로 하여 reject 호출. FieldError가 추가됨.
  2. Validator 적용

즉 타입에러가 발생한 경우 typeMismatch 를 오류코드로 FieldError를 추가해버리고, 검증을 수행하지 않는다. 제대로 바인딩 된 객체의 필드들만 검증을 수행하게된다.


검증 어노테이션

자주 활용되는 어노테이션들이다.


오류코드

@NotBlank 어노테이션을 추가한 필드에 빈 문자열이 들어와서 예외가 발생했다고 가정해보자. 이때 rejectValue 를 자동호출하여 FieldError가 생성된다고 하였는데 오류코드는 어떤 값이 사용될까?
Bean Validation 에서 오류코드는 어노테이션 이름과 동일하다. 즉 NotBlank를 오류코드로 rejectValue가 호출되며 'NotBlank.객체명.필드명', 'NotBlank.필드명', 'NotBlank.타입명', 'NotBlank'. 4개의 메시지코드가 파라미터로 넘겨져 FieldError가 생성되어 BindingResult에 삽입된다. 따라서 이 메시지코드들을 properties에서 설정하면 메시지를 지정할 수 있다.

BeanValidation의 메시지 찾는 순서

  1. 메시지 코드 순서대로 properties에서 찾기 -> NotBlank="공백 금지"
  2. 어노테이션의 messsage 속성 사용하기 -> @NotBlank(message = "공백 금지")
  3. 라이브러리가 제공해주는 기본 값 사용하기 -> "공백일 수 없습니다."

글로벌 오류 처리

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라는 기능을 통해 이를 해결할 수 있다.

  1. 저장용과 수정용 텅 빈 interface 생성 -> SaveCheck, UpdateCheck 정의
  2. 객체의 검증 어노테이션마다 저장용과 수정용 언제 수행할지 groups 설정 -> @NotBlank(groups={SaveCheck.class, UpdateCheck.class}
  3. @Validated 에 interface 넘겨줌. -> @Validated(SavedCheck.class)

하지만 코드가 너무 지저분해 진다. 따라서 보통 Form 마다 각각 객체를 새로 정의해서 사용한다. 즉 Entity가 되는 Item과 수정용 UpdateDto와 등록용 AddDto 를 따로 선언해서 사용한다. Entity를 따로 정의하는 이유는 Form 에서 모든 필드를 입력받지 않을 수 있기 때문이다.


Http 메시지 컨버터

@Valid 와 @Validated 는 @RequestBody에도 사용할 수 있다. 여기서 @RequestBody를 짚고 넘어가자.

@RestController : 뷰를 넘겨주는 것이 아닌 데이터를 직접 넘겨주는 Controller. 객체를 반환하면 메시지 컨버터가 JSON으로 변환하여 클라이언트에게 전달한다.
@ModelAttribute : Request 파라미터를 객체로 변환할 때 사용
@RequestBody : Request Body의 데이터를 객체로 변환할 때 사용. 주로 JSON 요청을 객체로 변환한다.

메시지 컨버터가 JSON 요청을 객체로 변환해준다고 하였다. 따라서 JSON을 @RequestBody의 객체에 담은 후 Validation을 수행한다. 그런데 타입에러가 발생하면 자동으로 typeMismatch로 FieldError를 생성하고 나머지 필드의 검증을 수행하는 @ModelAttribute 의 검증과 달리. @RequestBody는 JSON을 객체화 할때 타입에러가 발생하면 예외가 발생하고 꺼진다. 따라서 이는 후에 예외처리를 통해 별도로 처리가 필요하다.

0개의 댓글