JaeWon's Devlog
article thumbnail
반응형

SpringBoot 로 개발을 하다 보면 클라이언트와 통신 중 데이터에 대해서 Validation(검증)이 필요한 경우가 있습니다.

검증 클래스를 별도로 생성하여 검증을 진행할 수 있지만 간단하게 어노테이션을 통해서 JSR 표준을 이용하여 검증도 가능합니다.

이번 글에서는 이 JSR 표준을 이용한 @Valid 어노테이션에 대해서 정리해보고자 합니다.


0. Dependency 추가

  • Gradle(build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-validation'
  • Maven(pom.xml)
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.6.6</version>
</dependency>

1. @Valid

- @Valid 는 JSR-303 표준 스펙(자바 진영)으로써 빈 검증기(Bean Validator)를 이용해 객체의 제약 조건을 검증하도록 하는 어노테이션입니다.

- Spring 에서는 일종의 어댑터인 LocalValidatorFactoryBean 이 제약 조건 검증을 처리합니다.

- 이를 사용하기 위해서는 빈으로 등록해야 하는데 위 Dependency 를 추가하면 사용할 수 있습니다.

1-1. 사용방법

- 간단하게 API 를 만들어보겠습니다.

@RestController
@Slf4j
public class TestController {

    @PostMapping("/todos/save")
    public ResponseEntity<String> createJsonTodo(@RequestBody @Valid TodoRequest request){
        log.info("Post : Todo Save -: {}", request.toString);

        return ResponseEntity.ok().body("Todo 객체 검증 성공");
    }
}

- 클라이언트로부터 전달받는 파라미터에 @RequestBody 어노테이션을 붙이고 그 앞에 @Valid 어노테이션을 작성하면, RequestBody 로 들어오는 객체에 대해서 Validation(검증)을 수행합니다.

- @Valid 어노테이션의 검증의 세부적인 사항은 객체 안에 정의하여야 합니다.(여기서는 TodoRequest 에 정의)

@Getter
@Setter
@ToString
public class TodoRequest {

    @NotNull(message = "제목은 필수입니다.")
    private String title;

    @NotEmpty(message = "내용은 필수입니다.")
    private String content;

    private boolean completed;
    
    @Min(1)
    @Max(7)
    private int dDay;
}

- 예를 들어 @NotNull 어노테이션은 필드의 값이 null 이 아님을 확인하고, @Min 어노테이션은 해당 값의 최솟값을 지정할 수 있습니다.

- PostMan 으로 간단하게 @Valid 를 테스트하기 위해 content 값을 빈 값으로 요청하면 자동으로 일정 규격에 맞게 SpringBoot 에서 만든 에러를 응답합니다.

@Valid 와 검증 어노테이션을 같이 잘 사용하면, 객체 단에서 충분히 에러를 잡을 수 있다.

1-2. 동작원리

- SpringBoot 에서 모든 요청은 프론트 컨트롤러인 DispatcherServlet 을 통해 Controller 로 전달됩니다.

- 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver 가 동작하는데, @Valid 어노테이션도 이 ArgumentResolver 에 의해 처리가 됩니다.

- 검증 도중 오류가 있다면 MethodArgumentNotValidException 예외가 발생하고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver 에 의해 400 BadRequest 가 발생하게 됩니다.

Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.String> com.todo.blog.controller.TestController.createJsonTodo(com.todo.blog.controller.TodoRequest): [Field error in object 'todoRequest' on field 'content': rejected value []; codes [NotEmpty.todoRequest.content,NotEmpty.content,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [todoRequest.content,content]; arguments []; default message [content]]; default message [내용은 필수입니다.]] ]

- 이러한 내용으로 @Valid 는 기본적으로 Controller(컨트롤러)에서만 동작을 합니다.

- 다른 계층(ex: service)에서 사용하기 위해서는 @Validated 어노테이션과 결합하여 사용해야만 합니다.

2. @Validated

- 요청 파라미터의 유효성 검증은 Controller(컨트롤러)에서 최대한 처리하는 것이 좋습니다.

- 하지만, 개발 도중 불가피하게 다른 곳에서 파라미터를 검증해야 할 수 있는데, Spring 에서 이를 위해 AOP 기반으로 메소드의 요청을 가로채어 유효성 검증을 진행해주는 @Validated 어노테이션을 제공하고 있습니다.

- @Validated 어노테이션은 @Valid 어노테이션의 JSR 표준 기술이 아니며, 다르게 Spring Framework 에서 제공하는 어노테이션 및 기능입니다.

2-1. 사용방법

- Service 계층에서 주로 사용하고 @Validated 어노테이션을 붙여 사용하시면 됩니다.

@Service
@Validated
public class TestTodoService {

    public ResponseEntity todoSave(@Valid TodoRequest request){

        return ResponseEntity.ok().body("Todo 객체 검증 성공");
    }
}

- PostMan 으로 간단하게 테스트하기 위해 content 값을 빈 값으로 요청하면 자동으로 일정 규격에 맞게 SpringBoot 에서 만든 에러를 응답합니다.

2-2. 동작 원리

- @Valid 는 특정 ArgumentResolver 에 의해 유효성 검사가 진행되었다면, @Validated 는 AOP 기반으로 메소드 요청을 Interceptor(인터셉터)하여 처리합니다.

- @Validated 어노테이션을 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 인터셉터(MethodValidationInterceptor)가 등록됩니다.

- 클래스에 선언하게 되면 메소드들이 해당 클래스를 호출할 때 AOP 의 PointCut(포인트컷)으로써 요청을 가로채어 유효성 검증을 진행합니다.

- 이러한 이유로 @Validated 를 사용하면 계층과 무관하게 Controller, Service, Repository 등 스프링 빈에 등록되면 유효성 검증을 진행할 수 있습니다.

대신 @Validated 어노테이션은 클래스에 유효성 검증 인터셉터를 등록하도록 작성이 필요하고, @Valid 어노테이션은 검증을 진행할 메소드에 작성이 필요합니다.

- @Valid 어노테이션과 다르게 예외는 ConstaintViolationException 으로 발생합니다.

javax.validation.ConstraintViolationException: todoSave.request.content: 내용은 필수입니다. at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.23.jar:5.3.23] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.23.jar:5.3.23] at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.23.jar:5.3.23] at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.23.jar:5.3.23] at com.todo.blog.service.TestTodoService$$EnhancerBySpringCGLIB$$9ae86a0a.todoSave(<generated>) ~[main/:na] at com.todo.blog.controller.TestController.createJsonTodo(TestController.java:25) ~[main/:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at
        ...

3.@Valid 제약조건 어노테이션 정리

- JSR 표준 스펙은 다양한 제약 조건 어노테이션을 제공하고 있습니다.

Anotation 제약조건
@NotNull    Null 검증
@Null    Null 만 입력 가능
@NotEmpty    Null 이 아니고, 빈 스트링("") 이 아닌지 검증(" " 은 허용)
@NotBlank    Null 이 아니고, 공백(""과 " " 모두 포함)이 아닌지 검증
@Size(min=,max=)    해당 값이 주어진 값 사이에 해당하는지 검증(String, Collection, Map, Array 에도 가능)
@Pattern(regex=)    주어진 패턴과 일치하는지 검증
@Max(숫자)    지정 값 이하인지 검증
@Min(숫자)    지정 값 이상인지 검증
@Future    현재 보다 미래인지 검증
@Past    현재 보다 과거인지 검증
@Positive    양수만 가능
@PositiveOrZero    양수와 0만 가능
@Negative    음수만 가능
@NegativeOrZero    음수와 0만 가능
@Email    이메일 형식인지 검증
@Digits(integer=, fraction = )    대상 수가 지정된 정수와 소수 자리 수 보다 작은지 검증
@DecimalMax(value=)     지정된 값(실수) 이하 인지 검증
@DecimalMin(value=)    지정된 값(실수) 이상 인지 검증
@AssertFalse    false 여부 검증
@AssertTrue    true 여부 검증

참고

- https://jyami.tistory.com/55

- https://mangkyu.tistory.com/174

반응형
profile

JaeWon's Devlog

@Wonol

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!