Bean Validation이란,

JSR-380에 등재된 Bean Validation 2.0 이라는 표준 기술이다.

즉, 검증을 위한 애노테이션과 여러 인터페이스를 어떻게 구현해야하는지 가이드가 기술되어 있다.

Bean Validation을 구현한 구현체중 일반적으로 Hibernate Validator를 사용한다.

@Valid, @Validated

@Valid

자바 표준 스팩인 JSR-303에 등장한 검증용 애노테이션이다.

javax.validation 패키지에 속해 있다.

ValidationAutoConfiguration의 LocalValidatorFactoryBean 메서드를 통해 Bean으로 등록된다.

ArgumentResolver 단계에서 Controller에 넘어가기전 검증을 진행한다.

ArgumentResolver 단계에서 동작하기 때문에, 기본적으로 컨트롤러에서만 동작하며, Service, Repository 같이 다른 계층에서 검증을 할 수 없다.

별도의 Validator가 없고 검증에 오류가 발생하면, MethodArgumentNotValidException가 발생한다.

@Validated

Spring에서 제공하는 검증용 애노테이션이다.

org.springframework.validation.annotation 패키지에 속해 있다.

@Valid의 기능을 포함하며, 유효성 검증에 대한 그룹을 지정할 수 있다.

ValidationAutoConfiguration의 MethodValidationPostProcessor 메서드를 통해 Bean으로 등록된다.

AOP 기반으로 동작하며, 각 메서드의 요청을 가로채서 검증을 진행한다.

AOP 기반으로 동작하기 때문에 계층에 무관하게 Spring Bean이라면 검증을 진행할 수 있다. 하지만, 검증과정은 Controller에서 끝내는 것이 좋다.

별도의 Validator가 없고 검증에 오류가 발생하면, ConstraintViolationException가 발생한다.

정리

@Valid@Validated
제공자바 표준 스팩 JSR-303Spring Framework
동작 방식ArgumentResolverAOP 기반
사용 가능 계층ControllerSpring Bean으로 등록된 모든 계층
예외MethodArgumentNotValidExceptionConstraintViolationException

애노테이션

Java Validator 애노테이션 모음

JSR 표준 스펙은 다양한 검증 애노테이션을 제공하고 있다.

그 중에서 많이 사용되어지는 애노테이션은 다음과 같다.

@NotNull해당 값이 null이 아닌지 검증
@NotEmpty해당 값이 null, "" 이 아닌지 검증
@NotBlank해당 값이 null, "", " " 이 아닌지 검증
@AssertTrue해당 값이 true 인지 검증
@AssertFalse해당 값이 false 인지 검증
@Size(int min, int max)해당 값의 크기가 min ~ max 인지 검증
String, Collection, Map, Array에 적용 가능
@Min(long value)해당 값이 value보다 작지 않은지 검증
@Max(long value)해당 값이 value보다 크지 않은지 검증
@Pattern(String regexp)해당 값이 해당 정규식을 지키고 있는지 검증
@Email해당 값이 Email 형식을 지키고 있는지 검증

이 외에도 Hibernate에서 제공하는 추가 애노테이션들이 있으니, 필요하면 공식 Doc를 읽어보자.

참고
Hibernate에서 제공하는 애노테이션을 사용하면, 나중에 프로젝트에서 Validator에 대한 구현체를 변경하게 되는 경우 사용할 수 없게 된다.

DTO 분리 후, 적용

저번 시간에서 사용했던 Item 도메인이 아닌, 클라이언트에서 값을 보내줄 때에만 사용할 DTO를 생성해보자.

@Getter
@RequiredArgsConstructor
public class ItemDto {
    @NotBlank
    private final String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private final Integer price;

    @NotNull
    @Max(value = 9999)
    private final Integer quantity;
}

사실 지난 번과의 차이는 이름 밖에 없다.

하지만, 나중에 Item 도메인에 대한 요구사항이 추가되면 그에 따른 field가 증가되는데, 이는 현재 컨트롤러에서도 사용하고 있기 때문에, 클라이언트가 보내야하는 데이터가 덩달아 바뀌게 된다.

해당 도메인이 바뀔 때마다 클라이언트가 보내야되는 데이터의 형식이 바뀌게 된다면, 그것은 협업하는 사람의 입장에서는 정말 짜증나는 일이 될 것이다.

그것을 최소화하기위해 클라이언트에서 보내는 값은 ItemDto 객체를 이용해 전달받고, 컨트롤러는 이를 도메인에 맞는 형식으로 변환하는 것이 좋다. (당연한 말이겠지만, 엔드 포인트 별로 각각 다른 DTO를 사용하도록 분리해야 한다.)

또한, @Validated 애노테이션을 사용하여 각 Field에 조건을 걸게 되면, AOP 방식을 사용하는 @Validated 애노테이션 특성상 계층을 이동할 때마다 불필요한 검증 과정을 거치게 될 것이다.

그렇기 때문에 가장 좋은 것은 각 계층을 넘어다닐때마다 특수화된 DTO를 생성하여 전달하는 것이 좋다….하지만 현실적으로 그렇게 많은 DTO를 관리하기도 어려울 뿐더러, 아예 똑같은 형식의 이름만 다른 객체가 나올 가능성도 존재한다. 그러니 다음의 경우에는 분리(ISP 원칙 준수!!)하도록 노력하자.

  • 클라이언트 To 컨트롤러 ( 엔드 포인트 별로 )
  • 컨트롤러 To 서비스, 레포지토리
  • 컨트롤러 To 클라이언트
  • 레포지토리를 통해 저장되는 도메인

Controller

@RestController
public class ValidationController {
    @PostMapping("/valid")
    public String valid(
            @Validated @RequestBody ItemDto item,
            BindingResult bindingResult
    ) throws Exception {
        if (bindingResult.hasErrors()) {
            throw new ValidationException("validation failed");
        }

        return "validation ok";
    }
}

저번 시간에 작성했던 Validator를 주입하고 등록하는 부분이 제거되고, 클라이언트에서 오는 데이터는 ItemDto 객체로 저장되도록 변경되었다.

이는 Spring에서 기본적으로 ValidationAutoConfiguration을 통해 Valid와 Validated를 등록하기 때문이다.

이에 대한 자세한 내용은 아래 블로그를 참고하자…

ModelAttribute과 RequestBody의 동작 차이

ModelAttribute

클라이언트에서 요청한 쿼리값과 HTTP Form 데이터를 객체로 변환할 때 사용

검증을 진행하는 경우, 각각의 필드 단위로 세밀하게 적용한다.

즉, 여러 필드 중에서 하나의 필드가 바인딩되지 못했어도(타입이 달라도) 그에 대한 에러를 BindingResult에 추가하기만 하고 다른 정상적인 필드에 대해서는 검증 단계를 거치게 된다.

그렇게 검증 단계를 거치게 되면, 그 결과를 가지고 컨트롤러를 호출하게 된다.

RequestBody

POST 요청의 JSON 데이터를 객체로 변환할 때 사용

검증을 진행하기 전에 HttpMessageConverter가 먼저 작동되어 JSON을 객체로 변환하는 작업이 진행된다.

이 때문에, 특정 필드가 바인딩되지 못하면(타입이 다르면) HttpMessageNotReadableException을 발생시켜 바로 클라이언트에게 전달하기 때문에, 이후의 컨트롤러 호출은 물론, 검증 단계마저도 진행되지 않는다.

profile
백엔드 개발자 지망생

0개의 댓글