@Valid @Validated 차이

Lee Seung Jae·2022년 3월 15일
1

@Valid @Validated 차이

@Valid

@Valid는 JSR-303표준 스펙이다.

org.hibernate.validator.internal.constraintvalidators 안에 구현된

여러 Validator 구현체들로 인해 값을 검증해준다.

이의 핵심은 LocalValidatorFactoryBean 이며, 나는 스프링 부트를

사용하였기 때문에 자동으로 구성이 된다.

동작 원리

기본적으로 컨트롤러에서 @Valid가 없더라도

유효성 검증을 처리하는 로직을 지나간다.

이유는??

InvocableHandlerMethod는 적절한 파라미터 처리기를 찾으려고

HandlerMethodArgumentResolverComposite로 보낸다.

HandlerMethodArgumentResolverComposite

얘가 처리해줄 resolver를 찾는데 getArgumentResolver();

private final Map<MethodParameter, HandlerMethodArgumentResolver>argumentResolverCache = new ConcurrentHashMap<>(256);

이 인자에서 들어있는 RequestResponseBodyMethodProcessor를 통해

RequestResponseBodyMethodProcessor

validation을 진행한다.

RequestResponseBodyMethodProcessor

AbstractMessageConverterMethodArgumentResolver 를 상속받고 있는데

상속받는 이 클래스의 validateIfApplicable에서 어노테이션 for 루프를 돌면서

AbstractMessageConverterMethodArgumentResolver

@Valid가 있는지 검색한다.

있으면 DataBinder객체에 넘겨서 validate를 수행한다.

여기서 검증에 오류가 있으면 MethodArgumentNotValidException이 발생하고,

이는 스프링 ExceptionResolverDefaultHandlerExceptionResolver덕분에

400 에러를 뱉게된다.

@Validated


@Validated (전역 컨트롤러에 붙임)

위의 @Valid와 다르게 cglib 그러니까 AOP기반으로 메소드 요청을

MethodValidationInterceptor가 받아서 처리해준다.

왜 cglib이냐면 SampleController는 일반 클래스이므로

인터페이스처럼 JDK 동적 프록시가 아닌 Cglib proxy를 사용한다.

그리고선 이 프록시가 요청을 가로채서 유효성 검증을 진행해준다.

검증을 수행하고서는 Set<ConstraintViolation<Object>>result;

가 비어있는 값이 아니라면 ConstraintViolationException을 던져주는데

에러 메시지의 기본값은 javax.validation.constraints.XXX.message properties에 정의되어있다.

이는 위처럼 DefaultHandlerExceptionResolver에 등록되어 있는 객체가 아니기에

500에러와 함께 밖으로 뱉어주게 된다. 별도의 ExceptionHandler를 같이 구현해주어야 할것이다.

아래는 내가 구현한 예제 소스이다.

SampleController.java

import javax.validation.Valid;
import javax.validation.constraints.Min;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@Validated
public class SampleController {

    @PostMapping("/hello")
    public String hello(@Valid @RequestBody MessageRequest messageRequest) {
        log.info(messageRequest.getMessage());
        return "hello";
    }

    @GetMapping("/hi")
    public String hi(@Min(value = 1) int value) {
        log.info(String.valueOf(value));
        return "hi";
    }

}

MessageRequest.java

import javax.validation.constraints.NotNull;
import lombok.Getter;

@Getter
public class MessageRequest {

    @NotNull(message = "message는 null일 수 없습니다.")
    private String message;

}

SampleControllerTest.java

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.lsj8367.web.request.MessageRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@WebMvcTest(SampleController.class)
class SampleControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext ctx;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(ctx)
            .alwaysDo(print())
            .build();
    }

    @Test
    @DisplayName("Post @Valid 테스트")
    void test() throws Exception {
        final String obj = objectMapper.writeValueAsString(new MessageRequest());

        mockMvc.perform(post("/hello")
                .content(obj)
                .contentType(MediaType.APPLICATION_JSON_VALUE))
            .andExpect(status().isBadRequest());
    }

    @Test
    @DisplayName("Get @Validated 테스트")
    void hiTest() throws Exception {
        mockMvc.perform(get("/hi")
                .param("value", "0")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
            )
            .andExpect(status().isInternalServerError());
    }
profile
💻 많이 짜보고 많이 경험해보자 https://lsj8367.tistory.com/ 블로그 주소 옮김

0개의 댓글