개발/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