본문 바로가기

[Java] Spring Boot Validation 완벽 가이드: API 데이터 유효성 검증 마스터하기

IT by Sense. 2025. 6. 4.

목차

    안녕하세요! 이번 포스팅에서는 Spring Boot 애플리케이션에서 클라이언트로부터 전달받는 데이터의 유효성을 효과적으로 검증할 수 있는 Spring Boot Validation에 대해 알아보겠습니다. 견고하고 안정적인 API를 만들기 위해 데이터 유효성 검증은 필수적인 과정입니다.

    1. Spring Validation이란?

    Spring Validation은 Bean Validation API (JSR-380 등의 표준 명세)의 구현체를 사용하여 객체의 제약 조건을 검증하는 기능을 제공합니다. 주로 Hibernate Validator가 기본 구현체로 사용됩니다. 이를 통해 개발자는 어노테이션 기반으로 간편하게 유효성 검증 규칙을 정의하고, 데이터가 비즈니스 로직에 도달하기 전에 문제를 미리 파악하여 처리할 수 있습니다.

    spring-boot-starter-validation 의존성을 추가하면 별도의 복잡한 설정 없이 바로 사용할 수 있다는 장점이 있습니다.

    2. 개발 환경

    본 포스팅에서 사용된 주요 개발 환경은 다음과 같습니다.

    • Java: 11 (또는 1.8 이상)
    • Spring Boot: 2.7.x (또는 최신 안정 버전)
    • 빌드 관리 도구: Gradle (또는 Maven)
    • 개발 툴: IntelliJ IDEA (또는 Eclipse)

    3. 데이터 유효성 검증 전체 흐름

    Spring Validation을 사용한 데이터 유효성 검증은 일반적으로 다음과 같은 흐름으로 진행됩니다.

    1. 클라이언트 요청: 클라이언트가 데이터를 HTTP 요청 (주로 JSON 형태의 Body, Query Parameter, Path Variable 등)에 담아 API 서버로 전송합니다.
    2. 컨트롤러 수신 및 검증:
      • Spring MVC의 컨트롤러는 @RequestBody, @RequestParam, @PathVariable 등을 통해 데이터를 수신합니다.
      • 수신하는 데이터 객체 또는 파라미터 앞에 @Valid 또는 @Validated 어노테이션을 명시하여 유효성 검증을 트리거합니다.
    3. 검증 결과 처리:
      • 유효성 검증 성공: 데이터가 모든 제약 조건을 만족하면, 비즈니스 로직이 정상적으로 수행되고 클라이언트에게 성공 응답을 반환합니다.
      • 유효성 검증 실패: 하나 이상의 제약 조건 위반 시, 기본적으로 MethodArgumentNotValidException (주로 @RequestBody 사용 시) 또는 ConstraintViolationException (주로 @RequestParam, @PathVariable 사용 시)이 발생합니다.
    4. 예외 처리 (Global Exception Handling):
      • 발생한 예외는 @ControllerAdvice@ExceptionHandler를 사용하여 구성된 전역 예외 처리기에서 캐치됩니다.
      • 전역 예외 처리기는 표준화된 오류 응답 형식(예: 에러 코드, 메시지)으로 클라이언트에게 전달하여 일관된 사용자 경험을 제공합니다.

    4. 핵심 어노테이션

    Spring Validation을 이해하기 위해 알아야 할 주요 어노테이션들입니다.

    4.1. 데이터 수신 관련 어노테이션

    • @RequestBody: HTTP 요청의 본문(body)에 담겨 오는 JSON, XML 등의 데이터를 Java 객체로 변환(역직렬화)할 때 사용합니다.
    • @RequestParam: URL의 쿼리 파라미터(e.g., ?name=john&age=30) 값을 메소드 파라미터로 바인딩할 때 사용합니다.
    • @PathVariable: URL 경로의 일부(e.g., /users/{userId})를 메소드 파라미터로 바인딩할 때 사용합니다.

    4.2. 유효성 검증 트리거 어노테이션

    • @Valid (javax.validation.Valid): JSR-303/JSR-380 표준 Bean Validation 어노테이션입니다. 객체 내부의 필드에 정의된 제약 조건들을 검증하도록 지시합니다.
    • @Validated (org.springframework.validation.annotation.Validated): Spring 프레임워크에서 제공하는 어노테이션으로, @Valid의 기능을 포함하며 추가적으로 유효성 검증 그룹(groups)을 지정할 수 있는 기능을 제공합니다. 이를 통해 특정 상황에 따라 다른 유효성 검증 규칙을 적용할 수 있습니다.

    @Valid vs @Validated 간단 비교

    구분 @Valid @Validated
    제공처 Java Bean Validation (JSR) Spring Framework
    기본 기능 객체 유효성 검증 객체 유효성 검증
    그룹(Group) 기능 없음 있음 (특정 그룹에 속한 제약조건만 검증 가능)
    사용 위치 주로 Controller 메소드의 파라미터, 필드 등 Controller 클래스 레벨 (메소드 파라미터 유효성 검증), 파라미터

    @Validated를 클래스 레벨에 사용하면 해당 클래스의 @RequestParam이나 @PathVariable로 받는 파라미터에 대한 유효성 검증이 가능해집니다. (이 경우 ConstraintViolationException 발생)

    5. 주요 제약 조건 어노테이션 (Bean Validation Annotations)

    DTO (Data Transfer Object)나 VO (Value Object)의 필드에 적용하여 데이터의 유효성을 정의하는 어노테이션들입니다. javax.validation.constraints 패키지에 주로 위치합니다.

    어노테이션 설명 예시
    @NotNull null 값만 허용하지 않음. (""이나 " "는 허용) @NotNull String name;
    @NotEmpty null과 빈 문자열 ("")을 허용하지 않음. (문자열, 컬렉션, 맵, 배열에 사용) @NotEmpty String username;
    @NotBlank null, 빈 문자열, 공백 문자열 (" ")을 허용하지 않음. (문자열에 사용) @NotBlank String title;
    @Size(min=, max=) 문자열 길이, 배열/컬렉션 크기가 지정된 범위 내여야 함. @Size(min=2, max=10) String nickname;
    @Min(value) 지정된 값 이상이어야 함. (숫자 타입) @Min(1) int quantity;
    @Max(value) 지정된 값 이하여야 함. (숫자 타입) @Max(100) int age;
    @Email 유효한 이메일 형식이어야 함. @Email String userEmail;
    @Pattern(regexp=) 지정된 정규 표현식과 일치해야 함. @Pattern(regexp="^\\d{2,3}-\\d{3,4}-\\d{4}$") String phoneNumber;
    @Positive 양수여야 함 (0 불포함). @Positive int stock;
    @PositiveOrZero 0 또는 양수여야 함. @PositiveOrZero int count;
    @Future 현재보다 미래의 날짜/시간이어야 함. (Date, Calendar, Temporal 타입) @Future LocalDate eventDate;
    @Past 현재보다 과거의 날짜/시간이어야 함. (Date, Calendar, Temporal 타입) @Past LocalDate birthDate;

    이 외에도 다양한 제약 조건 어노테이션이 존재하며, 필요에 따라 커스텀 어노테이션을 만들어 사용할 수도 있습니다.

    6. 실전 예제: 사용자 등록 API

    간단한 사용자 등록 API를 통해 Spring Validation을 적용하는 방법을 살펴보겠습니다.

    6.1. 의존성 추가 (build.gradle)

    dependencies {
        // Spring Web Starter
        implementation 'org.springframework.boot:spring-boot-starter-web'
        // Spring Validation Starter
        implementation 'org.springframework.boot:spring-boot-starter-validation'
        // Lombok (Optional, for boilerplate code reduction)
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
    }
    

    6.2. UserCreateRequestDto.java (DTO 작성)

    클라이언트로부터 받을 사용자 생성 요청 데이터를 담을 DTO입니다.

    package com.example.validationdemo.dto;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import javax.validation.constraints.*;
    
    @Getter
    @Setter
    @ToString
    public class UserCreateRequestDto {
    
        @NotBlank(message = "사용자 아이디는 필수 입력 항목입니다.")
        @Size(min = 4, max = 20, message = "사용자 아이디는 4자 이상 20자 이하로 입력해주세요.")
        private String userId;
    
        @NotBlank(message = "비밀번호는 필수 입력 항목입니다.")
        @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$",
                 message = "비밀번호는 8자 이상이며, 영문, 숫자, 특수문자를 최소 하나씩 포함해야 합니다.")
        private String password;
    
        @NotBlank(message = "이름은 필수 입력 항목입니다.")
        private String name;
    
        @Email(message = "유효한 이메일 주소를 입력해주세요.")
        @NotBlank(message = "이메일은 필수 입력 항목입니다.")
        private String email;
    
        @Min(value = 19, message = "만 19세 이상만 가입 가능합니다.")
        private Integer age;
    }
    

    6.3. UserController.java (Controller 작성)

    package com.example.validationdemo.controller;
    
    import com.example.validationdemo.dto.UserCreateRequestDto;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.validation.annotation.Validated; // @RequestParam, @PathVariable 등에 사용
    import org.springframework.web.bind.annotation.*;
    
    import javax.validation.Valid; // @RequestBody 에 사용
    import javax.validation.constraints.Min; // @RequestParam, @PathVariable 에 사용
    import javax.validation.constraints.Size;
    
    @RestController
    @RequestMapping("/api/users")
    @Validated // 클래스 레벨에 선언하여 해당 컨트롤러 내의 @RequestParam, @PathVariable 검증 활성화
    public class UserController {
    
        private static final Logger log = LoggerFactory.getLogger(UserController.class);
    
        @PostMapping
        public ResponseEntity<String> createUser(@Valid @RequestBody UserCreateRequestDto userDto) {
            log.info("사용자 등록 요청: {}", userDto);
            // TODO: 실제 사용자 등록 로직 (Service 호출 등)
            return ResponseEntity.status(HttpStatus.CREATED).body("사용자 등록 성공: " + userDto.getUserId());
        }
    
        @GetMapping("/{userId}")
        public ResponseEntity<String> getUser(
                @PathVariable @Size(min=3, message="사용자 ID는 3자 이상이어야 합니다.") String userId,
                @RequestParam(required = false, defaultValue = "1") @Min(value=1, message="페이지 번호는 1 이상이어야 합니다.") int page
        ) {
            log.info("사용자 조회 요청 - userId: {}, page: {}", userId, page);
            // TODO: 실제 사용자 조회 로직
            return ResponseEntity.ok("사용자 조회 성공: " + userId + ", 페이지: " + page);
        }
    }
    

    6.4. GlobalExceptionHandler.java (간단한 전역 예외 처리)

    유효성 검증 실패 시 발생하는 예외를 처리하여 클라이언트에게 일관된 오류 메시지를 전달합니다.

    package com.example.validationdemo.exception;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.validation.FieldError;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    import javax.validation.ConstraintViolation;
    import javax.validation.ConstraintViolationException;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
            Map<String, String> errors = new HashMap<>();
            ex.getBindingResult().getAllErrors().forEach((error) -> {
                String fieldName = ((FieldError) error).getField();
                String errorMessage = error.getDefaultMessage();
                errors.put(fieldName, errorMessage);
            });
            return ResponseEntity.badRequest().body(errors);
        }
    
        @ExceptionHandler(ConstraintViolationException.class)
        public ResponseEntity<Map<String, Object>> handleConstraintViolationException(ConstraintViolationException ex) {
            Map<String, Object> errors = new HashMap<>();
            errors.put("message", "입력 값 유효성 검증에 실패했습니다.");
            errors.put("details", ex.getConstraintViolations()
                    .stream()
                    .map(violation -> Map.of(
                            "field", getFieldName(violation),
                            "rejectedValue", violation.getInvalidValue(),
                            "reason", violation.getMessage()
                    ))
                    .collect(Collectors.toList()));
            return ResponseEntity.badRequest().body(errors);
        }
    
        private String getFieldName(ConstraintViolation<?> violation) {
            String propertyPath = violation.getPropertyPath().toString();
            // "methodName.parameterName" -> "parameterName"
            return propertyPath.substring(propertyPath.lastIndexOf('.') + 1);
        }
    }
    

    6.5. 테스트 (Postman 등 사용)

    성공 케이스 (POST /api/users)

    Request Body:

    {
        "userId": "testuser1",
        "password": "Password123!",
        "name": "테스트유저",
        "email": "test@example.com",
        "age": 25
    }
    

    Response (201 Created):

    사용자 등록 성공: testuser1
    

    실패 케이스 1 (POST /api/users - @RequestBody 유효성 검증)

    Request Body:

    {
        "userId": "tu",
        "password": "pwd",
        "name": "",
        "email": "invalid-email",
        "age": 15
    }
    

    Response (400 Bad Request) (GlobalExceptionHandler에 의해 포맷됨):

    {
        "name": "이름은 필수 입력 항목입니다.",
        "userId": "사용자 아이디는 4자 이상 20자 이하로 입력해주세요.",
        "age": "만 19세 이상만 가입 가능합니다.",
        "password": "비밀번호는 8자 이상이며, 영문, 숫자, 특수문자를 최소 하나씩 포함해야 합니다.",
        "email": "유효한 이메일 주소를 입력해주세요."
    }
    

    실패 케이스 2 (GET /api/users/ab?page=0 - @PathVariable, @RequestParam 유효성 검증)

    Response (400 Bad Request):

    {
        "message": "입력 값 유효성 검증에 실패했습니다.",
        "details": [
            {
                "field": "userId",
                "rejectedValue": "ab",
                "reason": "사용자 ID는 3자 이상이어야 합니다."
            },
            {
                "field": "page",
                "rejectedValue": 0,
                "reason": "페이지 번호는 1 이상이어야 합니다."
            }
        ]
    }
    

    7. 결론

    Spring Boot Validation을 사용하면 어노테이션 기반으로 간편하게 데이터 유효성 검증 로직을 추가할 수 있습니다. 이를 통해 코드의 가독성을 높이고, 비즈니스 로직과 유효성 검증 로직을 분리하여 더욱 깨끗하고 유지보수하기 좋은 코드를 작성할 수 있습니다. 또한, 전역 예외 처리와 결합하여 일관된 오류 응답을 제공함으로써 API의 안정성과 사용자 경험을 향상시킬 수 있습니다.

    애플리케이션의 요구사항에 맞춰 다양한 제약 조건 어노테이션을 활용하고, 필요하다면 커스텀 Validator나 Validation Group을 사용하여 더욱 정교한 유효성 검증을 구현해보시길 바랍니다.

    ☝️ 데이터 유효성 검증은 안전하고 신뢰할 수 있는 애플리케이션을 구축하는 첫걸음입니다!

    • 트위터 공유하기
    • 페이스북 공유하기
    • 카카오톡 공유하기