Spring List 타입 객체 검증 방법 비교 @Valid vs @Validated

동재·2025년 3월 18일

개요

그동안 Spring에서 RequestBody 검증을 할 때 습관처럼 @Validated를 사용해왔습니다. 그런데 문득, RequestBody를 List로 받을 때 검증이 어떻게 동작하는지 궁금해졌습니다.

예를 들어, 아래와 같이 List 타입의 RequestBody를 받으면 어떻게 검증이 이루어질까요?

 @PostMapping("/test")
    public ResponseEntity<?> validatedTest(@Validated @RequestBody List<TestCreate> testCreates) {
        // List 내부의 각 요소가 개별적으로 검증됨
        return ResponseEntity.ok("success");
    }

저는 직관적으로 List 객체 검증 시, 내부 요소를 재귀적으로 검사하기보다는 List가 비었는지 여부만 확인할 것이라 생각했습니다. 그런데 실제 동작은 달랐습니다.

이번 포스트에서는 @Validated@Valid를 사용했을 때 List 객체 검증 방식이 어떻게 달라지는지 비교하고, 올바른 검증 방법에 대해 알아보겠습니다.


테스트✅

간단히 테스트 코드를 작성해 실제 동작을 확인해봤습니다.

1. DTO 정의

public record TestCreate(
    @NotNull(message = "id는 필수입니다.")
    Long id,
    String name
) {}

@NotNull 애노테이션이 붙은 id 필드는 null이 들어오면 검증에 실패합니다.
name은 별도의 제약조건이 없으므로 null이어도 검증에 통과합니다.

2. Controller 구현

List 내부 요소에 대해 재귀적으로 검증하는 두 가지 방법을 소개합니다.

2-1. @Valid를 사용하는 방식

@PostMapping("/test")
public ResponseEntity<?> validatedTest(@Valid @RequestBody List<TestCreate> testCreates) {
    return ResponseEntity.ok("success");
}

또는, 제네릭 요소에 직접 @Valid를 붙이는 방식도 있습니다.

@PostMapping("/test")
public ResponseEntity<?> validatedTest(@RequestBody List<@Valid TestCreate> testCreates) {
    return ResponseEntity.ok("success");
}

두 방식 모두 List 내부에 있는 모든 객체에 대해 검증을 수행합니다.

2-2. @Validated를 사용하는 방식

@PostMapping("/test")
public ResponseEntity<?> validatedTest(@Validated @RequestBody List<TestCreate> testCreates) {
    // 컬렉션(List) 자체만 검증 대상으로 삼아, 내부 요소에 대한 재귀적 검증은 수행되지 않습니다.
    return ResponseEntity.ok("success");
}

결과 확인

테스트를 통해 확인한 결과, @Valid를 사용하면 List 내부 요소까지 재귀적으로 검증되지만, @Validated를 사용하면 컬렉션 자체만 검증 대상으로 인식되어 내부 요소 검증은 이루어지지 않습니다.

List 객체 검증에 있어서는 @Validated 대신 @Valid를 반드시 사용해야 합니다.
왜냐하면, @Validated는 컬렉션 자체만 검증 대상으로 삼아 내부 요소에 대한 재귀적 검증을 수행하지 않기 때문입니다.

테스트 코드

아래는 다양한 케이스에 대해 List 내부 요소 검증을 확인할 수 있는 테스트 코드 예시입니다.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class TestControllerTest {

    @Autowired
    private MockMvc mockMvc;

    // 1. 빈 리스트일 때 (객체가 하나도 없음)
    @Test
    void whenEmptyList_thenSuccess() throws Exception {
        String json = "[]";
        mockMvc.perform(post("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isOk())
                .andExpect(content().string("success"));
    }

    // 2. TestCreate가 1개이고 id값만 있을 때
    @Test
    void whenOneObjectIdOnly_thenSuccess() throws Exception {
        String json = "[{\"id\": 1, \"name\": null}]";
        mockMvc.perform(post("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isOk())
                .andExpect(content().string("success"));
    }

    // 3. TestCreate가 1개이고 name값만 있을 때 (id 누락 → 검증 실패)
    @Test
    void whenOneObjectNameOnly_thenFail() throws Exception {
        String json = "[{\"name\": \"Name Only\"}]";
        mockMvc.perform(post("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isBadRequest());
    }

    // 4. TestCreate가 1개이고 모든 값이 정상일 때
    @Test
    void whenOneObjectAllFields_thenSuccess() throws Exception {
        String json = "[{\"id\": 1, \"name\": \"Test Name\"}]";
        mockMvc.perform(post("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isOk())
                .andExpect(content().string("success"));
    }

    // 5. TestCreate가 2개이고 모든 값이 정상일 때
    @Test
    void whenTwoObjectsAllFields_thenSuccess() throws Exception {
        String json = "[{\"id\": 1, \"name\": \"Name1\"}, {\"id\": 2, \"name\": \"Name2\"}]";
        mockMvc.perform(post("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isOk())
                .andExpect(content().string("success"));
    }

    // 6. TestCreate가 2개이고 id값만 있을 때
    @Test
    void whenTwoObjectsIdOnly_thenSuccess() throws Exception {
        String json = "[{\"id\": 1, \"name\": null}, {\"id\": 2, \"name\": null}]";
        mockMvc.perform(post("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isOk())
                .andExpect(content().string("success"));
    }

    // 7. TestCreate가 2개이고 name값만 있을 때 (id 누락 → 검증 실패)
    @Test
    void whenTwoObjectsNameOnly_thenFail() throws Exception {
        String json = "[{\"name\": \"Name1\"}, {\"name\": \"Name2\"}]";
        mockMvc.perform(post("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isBadRequest());
    }

    // 7. TestCreate가 2개이고 1개는 정상 1개는 name값만 있을 때 (id 누락 → 검증 실패)
    @Test
    void whenTwoObjectsOneValidOneInvalid_thenFail() throws Exception {
        // 첫 번째 객체는 정상: id와 name 모두 있음.
        // 두 번째 객체는 id가 누락되어 있음.
        String json = "[{\"id\": 1, \"name\": \"Name1\"}, {\"name\": \"Name2\"}]";

        mockMvc.perform(post("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isBadRequest());
    }
}

결론

이번 테스트를 통해 알 수 있는 점은 다음과 같습니다.

@Valid 사용 시

@Valid @RequestBody List<TestCreate> 또는 @RequestBody List<@Valid TestCreate> 방식 모두 List 내부 요소에 대해 재귀적으로 검증이 수행됩니다.

@Validated 사용 시

@Validated @RequestBody List<TestCreate>는 컬렉션 자체만 검증 대상으로 인식되며, 내부 요소에 대한 검증은 이루어지지 않습니다.

결국, List 내부 요소까지 검증을 원한다면 @Valid를 사용해야 합니다.
Spring에서 제공하는 어노테이션이라 @Validated도 내부 요소까지 검증을 해줄 것 같았는데, 직접 테스트해보니 아니란 걸 확인할 수 있었습니다.

테스트 코드를 통해 검증 메커니즘을 직접 확인해보고, 프로젝트에 맞는 올바른 방법을 선택하시기 바랍니다.

끝!

profile
Backend Developer

0개의 댓글