개발/Spring

[Spring] @valid

함수형 인간 2025. 3. 27. 17:55

스프링 @Valid 애노테이션 

@Valid 애노테이션은 스프링 프레임워크, 특히 스프링 MVC에서 자바 빈 유효성 검사(Java Bean Validation) 표준(JSR-303, JSR-380 등)을 적용하기 위해 사용되는 핵심 애노테이션. 컨트롤러(Controller) 메소드의 파라미터에 이 애노테이션을 붙이면, 해당 파라미터 객체(주로 DTO 또는 Command 객체)에 정의된 유효성 검사 제약 조건들을 자동으로 검증해준다.

@Valid를 사용하는 이유

  1. 데이터 무결성 보장: 클라이언트로부터 들어오는 데이터(폼 입력, JSON 요청 본문 등)가 애플리케이션에서 요구하는 형식과 제약 조건을 만족하는지 검증하여 데이터의 정합성을 유지.
  2. 보안 강화: 유효하지 않거나 예상치 못한 형식의 입력 데이터를 사전에 차단하여 SQL 인젝션, 악의적인 스크립트 주입 등의 보안 취약점을 예방.
  3. 코드 분리 및 가독성 향상: 컨트롤러 메소드 내에서 직접 if문 등으로 복잡하게 유효성 검사 로직을 작성하는 대신, 검증 로직을 해당 데이터 객체(DTO)의 필드 애노테이션으로 분리하여 컨트롤러 코드를 깔끔하게 유지하고 가독성을 높임.
  4. 표준 기반: 자바 표준 기술(Bean Validation)을 사용하므로 특정 프레임워크에 종속되지 않고 일관된 방식으로 유효성 검사를 구현할 수 있다.

@Valid 사용 방법

1단계: 의존성 추가

@Valid를 사용하려면 자바 빈 유효성 검사 구현체가 필요. 가장 널리 사용되는 구현체는 Hibernate Validator. 스프링 부트 프로젝트의 경우, spring-boot-starter-validation 또는 spring-boot-starter-web 의존성을 추가하면 보통 Hibernate Validator가 자동으로 포함됨.

XML
 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

 

코드 스니펫
 
// Gradle (build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-validation'

 

2단계: DTO/Command 객체에 유효성 검사 애노테이션 추가

검증 대상이 될 객체(주로 DTO)의 필드에 표준 Bean Validation 애노테이션들을 사용하여 제약 조건을 정의.

Java
 
import javax.validation.constraints.*; // JSR-380 (Jakarta Bean Validation)
// 또는 import jakarta.validation.constraints.*; // Jakarta EE 9+

public class UserDto {

    @NotBlank(message = "사용자 이름은 필수 입력 항목입니다.") // null, "", " " 모두 허용 안 함
    @Size(min = 2, max = 10, message = "사용자 이름은 2자 이상 10자 이하로 입력해주세요.")
    private String username;

    @NotNull(message = "나이는 필수 입력 항목입니다.")
    @Min(value = 1, message = "나이는 1살 이상이어야 합니다.")
    private Integer age;

    @NotEmpty(message = "이메일은 필수 입력 항목입니다.") // null, "" 허용 안 함
    @Email(message = "유효한 이메일 형식이 아닙니다.")
    private String email;

    // 필요에 따라 @Pattern, @Max, @Past, @Future 등 다양한 애노테이션 사용 가능
}

 

3단계: 컨트롤러 메소드 파라미터에 @Valid 적용

유효성 검사를 수행할 DTO 파라미터 앞에 @Valid 애노테이션을 붙인다. @ModelAttribute (폼 데이터)와 @RequestBody (JSON/XML 데이터) 모두에 적용할 수 있다.

Java
 
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/api/users")
public class UserController {

    // JSON 요청 본문을 검증할 때
    @PostMapping
    public ResponseEntity<?> createUser(@Valid @RequestBody UserDto userDto, BindingResult bindingResult) {

        // 1. 유효성 검사 오류 확인
        if (bindingResult.hasErrors()) {
            // 오류 처리 로직 (예: 오류 메시지를 모아 응답)
            StringBuilder errorMsg = new StringBuilder();
            for (FieldError error : bindingResult.getFieldErrors()) {
                errorMsg.append(error.getField())
                        .append(": ")
                        .append(error.getDefaultMessage())
                        .append("\n");
            }
            // 400 Bad Request 와 함께 오류 메시지 반환
            return ResponseEntity.badRequest().body(errorMsg.toString());
        }

        // 2. 유효성 검사 통과 시 비즈니스 로직 수행
        User savedUser = userService.createUser(userDto);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
    }

    // 폼 데이터를 검증할 때 (일반적인 웹 MVC)
    @PostMapping("/register")
    public String registerUser(@Valid @ModelAttribute("user") UserDto userDto, BindingResult bindingResult, Model model) {

        // 1. 유효성 검사 오류 확인
        if (bindingResult.hasErrors()) {
            // 오류가 있으면 다시 회원가입 폼으로 이동 (오류 메시지 포함)
            return "user/registerForm"; // 폼 뷰 이름
        }

        // 2. 유효성 검사 통과 시 비즈니스 로직 수행
        userService.register(userDto);
        return "redirect:/login"; // 성공 시 로그인 페이지로 리다이렉트
    }
}

유효성 검사 오류 처리 (BindingResult / Errors)

  • @Valid 애노테이션이 붙은 파라미터 바로 뒤에 BindingResult 또는 Errors 타입의 파라미터를 선언하면, 유효성 검사 결과(오류 정보)가 해당 파라미터에 자동으로 담긴다.
  • 중요: BindingResult 파라미터가 @Valid 파라미터 바로 뒤에 오지 않으면, 검증 실패 시 BindException(@ModelAttribute의 경우) 또는 MethodArgumentNotValidException(@RequestBody의 경우) 예외가 발생하고 컨트롤러 메소드가 실행되지 않는다. BindingResult를 사용하면 예외 발생 없이 컨트롤러 내에서 오류를 직접 처리할 수 있다.
  • bindingResult.hasErrors(): 오류가 하나라도 있는지 확인합니다.
  • bindingResult.getFieldErrors(): 모든 필드 오류 정보를 List<FieldError> 형태로 반환합니다.
  • FieldError 객체 주요 메소드:
    • getField(): 오류가 발생한 필드명
    • getRejectedValue(): 검증에 실패한 실제 입력 값
    • getDefaultMessage(): DTO 애노테이션에 정의된 message 속성 값 (오류 메시지)

주요 유효성 검사 애노테이션 (javax.validation.constraints / jakarta.validation.constraints)

  • @NotNull: null 값만 허용하지 않음 ("" 이나 " "는 허용)
  • @NotEmpty: null과 빈 문자열("")을 허용하지 않음 (" "는 허용). 주로 Collection 이나 Map 에도 사용됨 (비어있는지 검사).
  • @NotBlank: null, 빈 문자열(""), 공백 문자열(" ") 모두 허용하지 않음. 주로 String 타입에 사용.
  • @Size(min=, max=): 문자열 길이, 배열 크기, 컬렉션 크기가 지정된 범위 내에 있는지 검사.
  • @Min(value=): 숫자 값이 지정된 최소값 이상인지 검사.
  • @Max(value=): 숫자 값이 지정된 최대값 이하인지 검사.
  • @Email: 유효한 이메일 형식인지 검사.
  • @Pattern(regexp=): 값이 지정된 정규 표현식과 일치하는지 검사.
  • @Positive, @PositiveOrZero: 양수 또는 0 이상인지 검사.
  • @Negative, @NegativeOrZero: 음수 또는 0 이하인지 검사.
  • @Future, @Past, @FutureOrPresent, @PastOrPresent: 날짜/시간 값이 미래, 과거, 현재 또는 미래/과거인지 검사.

@Valid vs @Validated

  • @Valid: JSR 표준 애노테이션. 기본적으로 유효성 검사를 트리거합니다. 그룹(Group) 기능을 직접 지원하지 않는다.
  • @Validated: 스프링 프레임워크에서 제공하는 애노테이션. @Valid의 기능을 포함하며, 추가적으로 유효성 검사 그룹을 지정할 수 있는 기능을 제공. 특정 상황(예: 객체 생성 시 검증 규칙과 수정 시 검증 규칙을 다르게 적용)에 유용하게 사용될 수 있다. 서비스 계층 등 컨트롤러가 아닌 다른 계층의 메소드 파라미터 유효성 검사에도 사용할 수 있다.
Java
 
// 그룹 인터페이스 정의
public interface CreationGroup {}
public interface UpdateGroup {}

public class ItemDto {
    @NotNull(groups = UpdateGroup.class) // 수정 시에만 NotNull 검증
    private Long id;

    @NotBlank(groups = {CreationGroup.class, UpdateGroup.class}) // 생성, 수정 모두 NotBlank 검증
    private String itemName;
}

@RestController
public class ItemController {
    @PostMapping("/items") // 생성
    public void createItem(@Validated(CreationGroup.class) @RequestBody ItemDto itemDto, BindingResult bindingResult) { ... }

    @PutMapping("/items/{id}") // 수정
    public void updateItem(@PathVariable Long id, @Validated(UpdateGroup.class) @RequestBody ItemDto itemDto, BindingResult bindingResult) { ... }
}

중첩된 유효성 검사 (Nested Validation)

DTO 안에 다른 객체가 포함되어 있고, 그 내부 객체까지 검증하고 싶을 때는 해당 필드에도 @Valid 애노테이션을 붙여주면 된다.

Java
 
public class OrderDto {
    @NotNull
    private Long orderId;

    @Valid // Address 객체 내부의 필드들도 검증하도록 지정
    @NotNull
    private Address address;
}

public class Address {
    @NotBlank
    private String street;
    @NotBlank
    private String zipCode;
}