개발/Spring Boot

Spring Boot 시리즈 8편 – 입력값 검증 전략: @Valid부터 도메인 중심 유효성 설계까지

B컷개발자 2025. 4. 24. 08:00

Spring Boot에서 @Valid를 활용한 입력값 검증부터 커스텀 Validator, 도메인 기반 유효성 설계까지 실무 중심으로 정리했습니다. 예외 처리 통합 전략도 포함됩니다.


Spring Boot 시리즈 8편 – 입력값 검증 전략: @Valid부터 도메인 중심 유효성 설계까지

REST API에서 입력값 유효성 검증은 단순히 올바른 형식을 검사하는 것을 넘어,
비즈니스 로직을 보호하고, 클라이언트의 예측 가능한 에러 응답을 보장하는 중요한 첫 관문입니다.

이번 글에서는 @Valid와 Bean Validation 기본 사용법부터,
복잡한 검증 로직을 도메인 내부로 이동시키고,
필요한 경우 Validator 클래스를 직접 커스터마이징하는 전략까지 단계별로 설명합니다.


🧱 1. 기본 검증 구조 – DTO + @Valid

1️⃣ DTO에 검증 애노테이션 적용

public class RegisterUserRequest {

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

    @Email(message = "이메일 형식이 아닙니다.")
    @NotBlank(message = "이메일은 필수입니다.")
    private String email;

    @Size(min = 4, max = 20, message = "비밀번호는 4~20자 사이여야 합니다.")
    private String password;
}
  • Bean Validation(Jakarta Validation) 기반으로 제공
  • @NotBlank, @Email, @Size, @Pattern 등 기본 제약조건을 조합하여 사용

2️⃣ Controller에서 @Valid 적용

@PostMapping("/api/users")
public ResponseEntity<ApiResponse<Void>> register(@Valid @RequestBody RegisterUserRequest request) {
    userService.register(request);
    return ResponseEntity.ok(ApiResponse.success());
}
  • @Valid 사용 시 자동으로 유효성 검사가 수행되며, 실패 시 MethodArgumentNotValidException 발생

❌ 2. 검증 실패 시 예외 처리 통합

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<List<FieldErrorResponse>>> handleValidationException(MethodArgumentNotValidException ex) {
    List<FieldErrorResponse> errors = ex.getBindingResult().getFieldErrors().stream()
        .map(err -> new FieldErrorResponse(err.getField(), err.getDefaultMessage()))
        .toList();
    return ResponseEntity.badRequest().body(ApiResponse.fail("C001", "유효성 검사 실패", errors));
}
  • FieldErrorResponse 클래스는 각 필드별 오류를 클라이언트에게 명확하게 전달합니다.
  • 응답 형식은 7편에서 다룬 ApiResponse<T> 구조와 일관성 있게 유지합니다.

🔁 3. 커스텀 유효성 검사 – 직접 Validator 만들기

예시: 중복 이메일 방지 @EmailNotRegistered


1️⃣ 애노테이션 정의

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailNotRegisteredValidator.class)
public @interface EmailNotRegistered {
    String message() default "이미 등록된 이메일입니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2️⃣ Validator 구현

@Component
public class EmailNotRegisteredValidator implements ConstraintValidator<EmailNotRegistered, String> {

    private final UserRepository userRepository;

    public EmailNotRegisteredValidator(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && !userRepository.existsByEmail(value);
    }
}

3️⃣ DTO에 적용

public class RegisterUserRequest {

    @EmailNotRegistered
    private String email;
}
  • Repository 의존성이 필요한 검증도 깔끔하게 구성할 수 있습니다.
  • 코드 재사용성과 명확성이 모두 높아집니다.

🧠 4. 도메인 내부 유효성 검증 전략

복잡한 비즈니스 조건은 DTO가 아니라 도메인 객체 내부에서 검증하는 것이 바람직합니다.

예시: User 엔티티 내부에서 검증

@Entity
public class User {

    public void changePassword(String current, String newPassword) {
        if (!this.password.equals(current)) {
            throw new BusinessException(ErrorCode.INVALID_PASSWORD);
        }
        this.password = newPassword;
    }
}
  • 비밀번호 일치 여부, 가입 자격 조건, 정책 기반 유효성 등은 도메인 책임
  • Entity가 스스로 자신의 유효한 상태를 유지하도록 설계

✅ 5. 실무 적용 전략 정리

구분 적용 위치 설명

형식 검증 DTO, @Valid JSON 역직렬화 전후 기본 조건 확인
외부 의존 검증 커스텀 Validator DB 조회, 외부 API 의존 검증
정책 기반 검증 Domain 객체 비즈니스 규칙, 상태 전이 조건 등
공통 오류 처리 GlobalExceptionHandler MethodArgumentNotValidException, BindException 등 처리

🧪 테스트 전략

검증 대상 테스트 방식

기본 검증 (@Valid) 컨트롤러 통합 테스트 (MockMvc or RestAssured)
커스텀 Validator 단위 테스트 (스프링 컨텍스트 없이 가능)
도메인 유효성 도메인 단위 테스트, 상태 전이 테스트

✅ 마무리 요약

항목 핵심 내용

검증 도구 @Valid, @NotBlank, @Size, 커스텀 애노테이션
실패 처리 @ExceptionHandler(MethodArgumentNotValidException)
고급 설계 DB 기반 검증은 Validator로, 정책 검증은 도메인 내부에
응답 통일 ApiResponse<T> 구조로 에러 응답 포함
테스트 책임 분리된 계층마다 명확한 테스트 전략 수립

📌 다음 편 예고

Spring Boot 시리즈 9편: 로그인 및 인증 처리 전략 – JWT 기반 인증 흐름 구현과 보안 고려사항

 

LIST