웹 애플리케이션에서 입력값 검증(validation)은 단순히 예외를 막는 수준이 아니라, 시스템 전체의 안정성과 무결성을 지키는 핵심 기초이다.
사용자의 실수, 악의적인 요청, 외부 API의 예기치 못한 반환값까지 고려해, 각 계층은 자신이 감당할 수 있는 수준에서 데이터를 검증해야 한다.
클라이언트가 전송한 데이터의 형식, 범위, null 여부 등을 검증
@Valid, @RequestBody, @RequestParam 등 사용
DTO 단에서 주로 처리
public record UserCreateRequest(
@NotBlank(message = "사용자명은 필수입니다.")
@Size(min = 2, max = 10, message = "사용자명은 2자 이상 10자 이하여야 합니다.")
String username,
@Email(message = "유효한 이메일 형식이어야 합니다.")
String email
) {}
@PostMapping("/users")
public ResponseEntity<UserDto> createUser(@Valid @RequestBody UserCreateRequest request) {
return ResponseEntity.ok(userService.create(request));
}
빠른 피드백 제공
클라이언트 측 실수 빠르게 차단 가능
메시지를 통해 사용자 친화적 에러 제공
필드 단위 검증에만 적합
다중 필드 간의 관계 검증, DB 조회 기반 검증, 도메인 상태 기반 검증은 어려움
DTO의 단일 필드로 검증할 수 없는 다중 필드 간 관계
DB 기반 중복 체크
현재 도메인 상태 기반으로 유효 여부 판단
사용자 등록 시 사용자명 중복 확인
public UserDto create(UserCreateRequest request) {
if (userRepository.existsByUsername(request.username())) {
throw new UsernameAlreadyExistsException(request.username());
}
if (userRepository.existsByEmail(request.email())) {
throw new EmailAlreadyExistsException(request.email());
}
User user = new User(request.username(), request.email());
return userMapper.toDto(userRepository.save(user));
}
사용자 상태가 "비활성"이면 수정 불가
public void updateUser(UUID userId, UserUpdateRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
if (user.getStatus() == UserStatus.INACTIVE) {
throw new InvalidUserStateException("비활성화된 사용자는 수정할 수 없습니다.");
}
user.update(request);
}
비즈니스 로직과 함께 유기적으로 검증
엔티티 상태, DB 정보 등 복합적인 조건 활용 가능
로직이 길어지면 가독성 저하
서비스 코드 내에서 유효성 검증 책임이 명확하지 않으면 유지보수가 어려움
⇒ 별도의 Validator 클래스로 위임 가능
비즈니스 검증 로직이 많아지면 다음처럼 Validator 클래스로 추출
@Component
@RequiredArgsConstructor
public class UserValidator {
private final UserRepository userRepository;
public void validateDuplicateUsername(String username) {
if (userRepository.existsByUsername(username)) {
throw new UsernameAlreadyExistsException(username);
}
}
public void validateUpdatable(User user) {
if (user.getStatus() == UserStatus.INACTIVE) {
throw new InvalidUserStateException("수정 불가 상태입니다.");
}
}
}
→ 서비스는 검증을 위임받고, 핵심 도메인 로직에 집중할 수 있게 된다.
기본적으로 검증 책임 없음
단, UNIQUE 제약조건, NOT NULL, FOREIGN KEY 등으로 마지막 방어선 역할
CREATE TABLE users (
id UUID PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
트랜잭션 충돌 또는 동시성 이슈 대비
DB 제약은 Service 검증의 보완 수단이지 대체 수단이 아님
“중복 검증”이란 같은 검증 로직이 여러 계층, 또는 여러 위치에 반복해서 존재하는 것을 말한다.
예를 들어, 사용자 이메일 중복 여부를
→ 이것이 바로 중복 검증이다.
1. 유지보수의 어려움
하나의 검증 로직이 여러 군데 흩어져 있으면, 정책이 변경될 때 모두 수정해야 함
2. 불일치 위험
로직이 중복된 만큼 한쪽은 수정됐지만, 다른 쪽은 안 되는 일이 발생할 수 있음
3. 코드랑 증가 및 가독성 저하
같은 검증 코드가 곳곳에 있으면 로직이 중첩돼 읽기 어려움
4. 버그 유발 가능성
서로 다른 계층에서 서로 다르게 판단하여 모순된 행동을 유발 (Controller에선 통과, Service에선 실패 등
❌ 잘못된 방식 (중복 검증)
// Controller
if (userRepository.existsByEmail(request.email())) {
throw new EmailAlreadyExistsException(request.email());
}
// Service
if (userRepository.existsByEmail(request.email())) {
throw new EmailAlreadyExistsException(request.email());
}
✅ 바람직한 방식 (책임 분리)
// Controller: @Valid로 형식만 검증
@PostMapping
public ResponseEntity<?> createUser(@Valid @RequestBody UserCreateRequest request) {
userService.create(request);
}
// Service: 비즈니스 로직 기반 중복 여부 확인
public void create(UserCreateRequest request) {
if (userRepository.existsByEmail(request.email())) {
throw new EmailAlreadyExistsException(request.email());
}
}
🔹 단순 형식 검증: DTO + @Valid (Controller 레이어)
🔹 복잡한 비즈니스 규칙: Service 레이어에서만 처리
🔹 Validator 클래스로 공통화: 중복 방지 + 책임 분리
🔹 DB 제약 조건은 마지막 보루로 사용 (예: UNIQUE)
Entity에도 어노테이션을 붙여 직접 검증할 수 있다.
@Entity
public class User {
@Id
private UUID id;
@NotBlank
@Size(min = 2, max = 10)
private String username;
@Email
@Column(unique = true)
private String email;
// ...
}
✅ 장점
도메인 객체 스스로 유효한 상태만 유지하려는 의도 표현
도메인 주도 설계(DDD)에 가까운 방식
❗️단점
Presentation과 Domain의 경계를 흐림
유효성 메시지 관리 불편
API 요청이 아닌 내부 로직에서 생성된 객체에도 검증 제약이 걸릴 수 있음
🟡 결론
일반적으로는 DTO에만 검증 어노테이션을 부여하고, Entity는 비즈니스 상태 중심의 불변성만 유지하는 게 명확한 책임 분리에 유리하다.
| 항목 | DTO 검증 (@Valid) | Entity 검증 |
|---|---|---|
| 주 목적 | API 요청의 유효성 검증 | 도메인 객체의 무결성 표현 |
| 사용 위치 | Controller, 일부 Service | Entity 클래스 내부 |
| 검증 타이밍 | 요청 수신 시점 | Entity 생성 및 영속화 시점 |
| 장점 | 에러 메시지 명확, 클라이언트 피드백 쉬움 | 도메인 객체의 자기 방어적 설계 |
| 단점 | 비즈니스 로직 적용 불가, 상태 판단 불가 | 메시지 관리 불편, 요청 검증 부적합 |
| 일반적 권장 | ✅ 요청 DTO에만 어노테이션 사용 | ❌ Entity에는 최소한만 (또는 사용 X) |
정리가 잘 되어 있어 잘 읽고 갑니다!👍