[Spring] DTO 유효성 검사(Validation) : @Valid로 처리하기

2025. 1. 5. 16:01·spring

1. DTO에서 왜 검증이 필요할까?

스프링 부트 애플리케이션에서 클라이언트로부터 입력받는 데이터(주로 @RequestBody로 전달되는 JSON 요청 등)는 유효성 검사가 필요합니다.
예를 들어, 회원가입 시 다음과 같은 상황이 발생할 수 있습니다.

  • 이메일 형식이 잘못됨
  • 비밀번호가 너무 짧거나 빈 값
  • 나이(age)가 음수

만약 입력값에 대한 검증을 제대로 하지 않는다면, 잘못된 데이터가 DB에 저장되거나, 내부 로직에서 오류를 발생시킬 수 있습니다.

이를 방지하기 위해 DTO(Data Transfer Object)에 검증 노테이션을 달아 유효성을 검사하고, 문제가 있다면 적절한 에러 응답을 반환하도록 하는 방식을 사용합니다.


2. 의존성 추가 (Validation Starter)

스프링 부트에서 Validation을 사용하려면, 보통 spring-boot-starter-validation 의존성을 추가해야 합니다.
Maven 기준으로 pom.xml에 다음과 같이 추가합니다.

<dependencies>
    <!-- 다른 의존성들... -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>
 
Gradle 기준이라면 build.gradle에 다음과 같이 추가합니다.
dependencies {
    // 다른 의존성들...
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}
 

3. DTO 클래스에 검증 어노테이션 적용하기

가장 많이 사용하는 검증 노테이션으로는 다음과 같은 것들이 있습니다.

  • @NotNull: null이 아니어야 함
  • @NotEmpty: null이 아니고, 빈 문자열도 아니어야 함(공백 문자열은 가능)
  • @NotBlank: null이 아니고, 빈 문자열, 공백 문자열도 아니어야 함
  • @Size(min=, max=): 문자열, 배열, 컬렉션 등 크기에 대한 제한
  • @Min(value): 숫자에 대한 최소값
  • @Max(value): 숫자에 대한 최대값
  • @Pattern(regexp=): 정규 표현식을 통한 검증
  • @Email: 이메일 형식 검증

예시로 회원가입을 위한 UserSignupRequest DTO를 정의해보겠습니다.

package com.example.demo.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class UserSignupRequest {

    @NotBlank(message = "이름(name)은 필수 입력 값입니다.")
    private String name;

    @Email(message = "이메일 형식이 올바르지 않습니다.")
    @NotBlank(message = "이메일(email)은 필수 입력 값입니다.")
    private String email;

    @NotBlank(message = "비밀번호(password)는 필수 입력 값입니다.")
    @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.")
    private String password;

    public UserSignupRequest() {
    }

    // Getter/Setter 생략 가능(또는 Lombok @Getter, @Setter 사용)
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

여기서 @NotBlank, @Email, @Size 등을 사용해, 요청으로 들어오는 각 필드를 검증합니다.

  • 예) @NotBlank가 붙은 필드에 null이나 ""(빈 문자열)를 보내면 검증 에러가 발생합니다.

Tip: 자카르타 패키지(jakarta.validation.constraints.*)와 기존의 javax.validation.constraints.* 패키지의 차이는 스프링 부트 버전과 관련이 있습니다. 새 버전에서는 jakarta 패키지를 사용합니다.


4. Controller에서 @Valid 사용하기

DTO에 설정한 검증 어노테이션이 적용되려면 Controller에서 @Valid를 사용해야 합니다. 간단한 예시를 들어보겠습니다.

package com.example.demo.controller;

import com.example.demo.dto.UserSignupRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;

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

    @PostMapping("/signup")
    public ResponseEntity<String> signup(@Valid @RequestBody UserSignupRequest request) {
        // 정상적으로 검증이 끝나면 이곳에 도달
        // TODO: UserService로 회원가입 처리 로직 수행 (가정)

        return ResponseEntity.ok("회원가입 성공");
    }
}
  • @RequestBody DTO의 데이터를 JSON → 자바 객체로 변환
  • @Valid 어노테이션이 붙어있으면, 스프링이 자동으로 DTO에 설정된 검증 노테이션을 확인
  • 만약 검증 에러가 발생하면, MethodArgumentNotValidException 등이 발생하여 예외 처리 로직으로 넘어감

5. 검증 에러 처리 (Exception Handling)

@Valid를 사용하면, 검증 실패 시 MethodArgumentNotValidException이 발생합니다.
스프링 부트에서는 기본적으로 400 Bad Request 에러와 함께 검증 실패 메시지를 반환해주지만, 사용자 정의 에러 형식을 만들기 위해 컨트롤러 레벨에서 예외를 처리할 수도 있습니다.

5.1 @ExceptionHandler로 처리하기

아래 예시는 Controller 내 혹은 전역 예외 처리(ControllerAdvice)에서 MethodArgumentNotValidException을 직접 처리하는 방법입니다.

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.validation.FieldError;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        
        // BindingResult에서 발생한 모든 필드 에러를 순회하며, 에러 메시지를 매핑
        for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
            errors.put(fieldError.getField(), fieldError.getDefaultMessage());
        }

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }
}
  • @RestControllerAdvice: 모든 @RestController에 대해 발생하는 예외를 처리할 수 있는 전역 컨트롤러 어드바이스
  • MethodArgumentNotValidException: DTO 검증 실패 시 발생하는 예외
  • FieldError: 어떤 필드에서 에러가 발생했는지, 그리고 어떤 메시지가 있는지를 확인할 수 있음

이렇게 하면 다음과 같은 JSON 형태로 에러 응답을 보낼 수 있습니다(예시):

{
  "name": "이름(name)은 필수 입력 값입니다.",
  "email": "이메일(email)은 필수 입력 값입니다."
}

5.2 BindingResult로 처리하기(Controller에)

또 다른 방식으로, 컨트롤러 메서드 파라미터에 BindingResult를 추가해 에러를 직접 확인하고 처리할 수도 있습니다.

@PostMapping("/signup")
public ResponseEntity<?> signup(
        @Valid @RequestBody UserSignupRequest request,
        BindingResult bindingResult) {
    
    if (bindingResult.hasErrors()) {
        // 에러가 있다면, 에러를 모아서 반환
        Map<String, String> errors = new HashMap<>();
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            errors.put(fieldError.getField(), fieldError.getDefaultMessage());
        }
        return ResponseEntity.badRequest().body(errors);
    }

    // 에러가 없으면 다음 로직 실행
    return ResponseEntity.ok("회원가입 성공");
}
  • 파라미터에 BindingResult를 선언하면, 검증 이후 에러 정보를 직접 확인하고 원하는 로직을 적용할 수 있음
  • 전역 예외 처리(@RestControllerAdvice)와는 달리, 메서드별로 에러 처리를 다르게 하고 싶을 때 유용

6. 커스텀 검증 어노테이션

기본 어노테이션으로 처리가 어려운 경우, 커스텀 검증 어노테이션을 만드는 방법도 있습니다.
예를 들어, 한글 이름만 허용한다거나, 특정 규칙(예: 비밀번호 복잡도) 등을 검사할 때 활용합니다.

6.1 커스텀 노테이션 정의

package com.example.demo.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = {OnlyKoreanValidator.class})
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface OnlyKorean {
    String message() default "한글만 입력 가능합니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

6.2 실제 검증 로직 구현

package com.example.demo.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class OnlyKoreanValidator implements ConstraintValidator<OnlyKorean, String> {

    @Override
    public void initialize(OnlyKorean constraintAnnotation) {
        // 초기화 코드(필요하다면)
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // null이면 다른 곳(@NotBlank 등)에서 처리가 가능하다고 가정
        if (value == null) {
            return true; 
        }
        // 한글 정규식 체크
        return value.matches("^[가-힣]+$");
    }
}

6.3 DTO에서 사용

public class UserSignupRequest {

    @OnlyKorean
    @NotBlank(message = "이름(name)은 필수 입력 값입니다.")
    private String name;

    // ... 생략
}

이렇게 하면, UserSignupRequest의 name 필드에 한글만 입력될 수 있도록 검증할 수 있습니다.


7. 정리

  1. 의존성 추가: spring-boot-starter-validation(필요하다면)
  2. DTO에 검증 어노테이션 붙이기: @NotBlank, @Email, @Size 등
  3. Controller에서 @Valid 사용: (@Valid @RequestBody DTO)
  4. 에러 처리: 전역 예외 처리(@RestControllerAdvice) 또는 메서드 단위 BindingResult 활용
  5. 커스텀 검증: 필요 시 직접 노테이션과 ConstraintValidator 구현

스프링 부트에서 DTO 검증을 잘 활용하면, 비즈니스 로직에 앞서 잘못된 요청을 걸러낼 수 있고, 코드 품질과 안정성을 크게 높일 수 있습니다.
더 나아가 커스텀 검증 로직을 활용하면 서비스 요구사항에 맞는 정교한 입력 데이터 검증이 가능해집니다.

'spring' 카테고리의 다른 글
  • [Spring] 이벤트 시스템으로 느슨한 결합 구현하기
  • [Spring] 날짜/시간 처리하기 (@DateTimeFormat vs @JsonFormat)
  • [Spring] profile 환경 분리하기
  • [Spring/React] WebSocket, STOMP를 활용한 실시간 채팅 구현
당훈이
당훈이
당훈이 님의 블로그 입니다.
  • 당훈이
    당훈IT
    당훈이
  • 전체
    오늘
    어제
    • 분류 전체보기 (40)
      • spring (7)
      • vue.js (8)
      • docker (1)
      • javascript (1)
      • aws (21)
      • database (1)
        • oracle (1)
      • nuxt (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ec2 nodejs
    route53
    AWS
    스프링부트
    EC2
    aws domain
    aws dns
    스프링 배포
    nuxt cache
    nuxt dedupe
    AWS ELB
    aws spring
    nuxt fetch
    spring boot
    nodejs 배포
    ec2 spring 배포
    vue3
    Spring
    nuxt vue
    스프링
    aws 스프링
    aws route53
    nuxt usefetch
    ec2 domain
    중복요청
    elb
    Vue
    ec2 route53
    배포
    AWS EC2
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
당훈이
[Spring] DTO 유효성 검사(Validation) : @Valid로 처리하기
상단으로

티스토리툴바