클라이언트로부터 받은 데이터가 사전에 정의된 규칙에 맞는지 확인하는 과정
검증 과정 없이 잘못된 데이터가 시스템으로 들어오면, 예상치 못한 오류가 발생하거나 데이터베이스에 원치 않는 값이 저장될 수 있다. 따라서 입력값 검증 과정을 통해 어플리케이션의 안정성과 데이터의 신뢰성을 확보할 수 있다.
✏️ Maven의 경우, 프로젝트 루트에 있는
pom.xml에 아래 의존성을 추가로 설정해야 한다.<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
@Valid vs @Validated@ValidJava 표준 어노테이션으로 간단한 객체 검증에 사용한다. 그룹 지정이 불가능하며, 기본 그룹만 사용한다. 대부분의 일반적인 검증용으로 충분하다.
@ValidatedSpring에서 제공하는 확장 검증 어노테이션이다. 그룹을 지정하여 상황별로 다르게 검증하거나, 그룹을 구분하여 메서드나 클래스에 적용할 수 있다. Spring AOP 기반으로 컨트롤러/서비스 계층에서도 사용 가능하다.
✏️ AOP (Aspect Oriented Programming)?
관점 지향 프로그래밍은 로깅, 보안, 트랜잭션 관리와 같은 횡단 관심사를 분리하여 모듈화함으로써 코드의 재사용성을 높이고 유지보수성을 개선하는 프로그래밍 패러다임이다.
@Valid와 @Validated 모두 동일하다.
| 어노테이션 적용 위치 | 발생 예외 | 설명 |
|---|---|---|
@RequestBody | MethodArgumentNotValidException | JSON → 객체 변환은 성공했지만 Bean Validation에서 위반이 발생한 경우 |
@ModelAttribute | BindException | 요청 파라미터를 객체 필드에 매핑하거나 검증 과정에서 바인딩/검증 에러가 생긴 경우 |
@RequestParam | ConstraintViolationException | 단일 파라미터를 대상으로 메서드 검증을 수행할 때 제약 조건 위반이 생긴 경우 |
개별 데이터에 설정하고, @Valid나 @Validated를 통해 검증한다.
| 어노테이션 | 설명 | 예시 |
|---|---|---|
@NotNull | null 이 아니어야 함 | 필수 입력값 |
@NotEmpty | null 또는 빈 문자열 ("") 이 아니어야 함 | 공백 불가(문자열, 컬렉션) |
@NotBlank | null, 빈문자, 공백문자만 있는 값 불가 | 공백 불가(문자열) |
@Size | 문자열, 컬렉션, 배열의 길이/크기 제한 | @Size(min=2, max=20) |
@Min | 최소값 (숫자) | @Min(18) |
@Max | 최대값 (숫자) | @Max(100) |
@Positive | 양수여야 함 | 1, 2, ... |
@PositiveOrZero | 0 또는 양수여야 함 | 0, 1, 2, ... |
@Negative | 음수여야 함 | -1, -2, ... |
@NegativeOrZero | 0 또는 음수여야 함 | 0, -1, -2, ... |
@Email | 이메일 형식 | @Email |
@Pattern | 정규표현식 패턴 일치 | @Pattern(regexp="^[0-9]+$") |
@Past | 과거 날짜여야 함 | 생년월일, etc. |
@PastOrPresent | 과거나 오늘 날짜 | |
@Future | 미래 날짜여야 함 | 예약일, etc. |
@FutureOrPresent | 오늘 또는 미래 날짜여야 함 | |
@Digits | 자릿수 및 소수점 자리 제한 | @Digits(integer=5, fraction=2) |
@AssertTrue | 반드시 true | 체크박스, etc. |
@AssertFalse | 반드시 false | |
@Null | null 이어야 함 |
@Valid일반적인 단일 검증 방식으로, 유효성 검증은 Service가 아닌 Controller에서 처리하는 것이 표준 아키텍처 패턴이다. 검증 실패시 400 Bad Request로 예외가 발생한다.
// DTO
public class UserRequest {
@NotBlank
private String name;
@Email
private String email;
@Min(18)
private int age;
}
// Controller에서 @Valid 사용 검증
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody UserRequest request) {
// 유효성 검증 통과 시 로직 진행
return ResponseEntity.ok("가입 완료");
}
@Validated// 그룹 인터페이스 선언
public interface CreateGroup {
}
public interface UpdateGroup {
}
// DTO에 그룹 지정
public class UserRequest {
@NotBlank(groups = CreateGroup.class)
private String name;
@Email(groups = { CreateGroup.class, UpdateGroup.class })
private String email;
@Min(value = 18, groups = CreateGroup.class)
private int age;
}
// Controller에서 그룹 지정하여 검증
// 생성(Create) 요청: 모든 값 검증
@PostMapping("/users")
public ResponseEntity<?> createUser(
@Validated(CreateGroup.class) @RequestBody UserRequest request) {
// name, email, age 모두 검증됨
return ResponseEntity.ok("회원 등록");
}
// 수정(Update) 요청: 이메일만 검증
@PutMapping("/users/{id}")
public ResponseEntity<?> updateUser(
@Validated(UpdateGroup.class) @RequestBody UserRequest request) {
// email만 검증됨
return ResponseEntity.ok("회원 정보 수정");
}
✏️ Controller에서 검증하는 이유
역할과 책임의 분리 (관심사의 분리)
각 계층은 자신만의 명확한 역할과 책임이 있다.
- Controller 계층: HTTP 요청을 받아들이는 어플리케이션의 관문 역할로, 요청으로 들어온 데이터 (Payload, Parameter) 형식, 구문, 값의 유효성을 검증하고 인증/인가 여부를 확인한 후 유효한 데이터를 Service 계층으로 전달한다.
- Service 계층: Controller로부터 받은 데이터를 가지고 핵심 비즈니스 로직을 수행하면서 데이터의 형식이 아닌, 비즈니스 규칙에 맞는지 (ex. 이미 존재하는 이메일인가?, 재고가 충분한가?) 검증하고 트랜잭션을 처리한다.
빠른 실패 (Fail-Fast)의 원칙
잘못된 데이터는 가능한 한 시스템의 가장 바깥쪽에서, 가장 빨리 차단하는 것이 효율적이다.
예외 처리의 일관성
컨트롤러에서 검증이 실패하면 전역적 예외 처리 (
MethodArgumentNotValidException발생)를 통해 일관된 형식의 응답 (어떤 필드가 왜 잘못되었는지)을 클라이언트에게 보내주기 용이하다. 만약 서비스 계층에서 검증한다면ConstraintViolationException이 발생하는데, 이는 비즈니스 로직 예외와 입력값 검증 예외가 섞여 예외 처리 로직이 더 복잡해질 수 있다.