입력값 검증, 어디서 해야 할까?
항상 개발하다 보면 고민에 빠지게 됩니다.
"이 데이터가 올바른지 확인하는 코드는 어디에 넣어야 할까?"
모든 곳에서 검증하자니 코드가 중복되고, 또 한 곳에서만 하자니 불안합니다.
해서 자료들을 찾아보며 각 계층이 어떤 책임을 가지고, 어느 곳에 검증 로직을
넣어야 중복을 피하면서 안정성을 확보할 수 있는지 정리해보았습니다.
검증의 3단계
1. 표현 계층 (Controller): "형식이 맞는가?"

사용자의 요청을 처음 받는 입구
핵심 책임: 시스템이 이해할 수 있는 입력인가?
검증 항목 (데이터의 껍데기)
왜 여기서 하나?
예시 코드
("요청이 비어 있지는 않나?", "이메일 형식은 맞는가?")
public record CreateUserRequest(
@Email
@NotBlank
String email,
@NotBlank
String name
) {}
@PostMapping("/users")
public void create(@Valid @RequestBody CreateUserRequest request) {
service.create(request);
}
2. 비즈니스 계층 (Service): "맥락상 가능한가?"

데이터의 형식은 맞지만, "현재 시스템의 상태에서 허용되는 행동인가?"
핵심 책임: 비즈니스 규칙을 만족하는가?
검증 항목
왜 여기서 하나?
예시 코드
public void create(CreateUserRequest request) {
if (userRepository.existsByEmail(request.email())) {
throw new IllegalStateException("이미 존재하는 이메일입니다");
}
}
3. 도메인 계층 (Service): "존재 자체가 정당한가?"

존재 자체가 말이 되는 객체인가?
핵심 책임: 도메인은 스스로를 지킨다.
검증 항목
왜 여기서 하나?
예시 코드
@Entity
public class Product {
private int price;
public Product(int price) {
if (price < 0) {
throw new IllegalArgumentException("가격은 음수일 수 없습니다");
}
this.price = price;
}
}
| 계층 | 핵심 철학 | 검증 대상 | 책임 |
|---|---|---|---|
| Controller | 형식이 맞나? | 문법 / 구조 | 요청 필터 |
| Service | 이 상태가 맞나? | 시스템 상태 | 흐름 제어 |
| Domain | 존재해도 되나? | 객체 본질 | 무결성 보장 |
중복을 피하고, 안정성을 높이는 전략
1. 형식 검증은 ‘입구’에서 한 번만
@NotBlank, @Size 같은 형식 검증은 DTO(Controller)에서 한 번만 수행2. 도메인은 “태어날 때부터 스스로를 검증한다”
객체가 생성될 때(생성자) 본질적인 규칙을 검증하게 만들어서
잘못된 객체는 아예 생성되지 못하게 막는 것이 중요합니다.
도메인은 생성자, 상태 변경 메서드에서 규칙을 직접 검증합니다.
서비스가 실수해도 테스트가 누락되어도
잘못된 데이터가 DB에 저장되는 것을 최종 차단합니다.
public class Product {
private final int price;
public Product(int price) {
if (price < 0) {
throw new IllegalArgumentException("가격은 음수일 수 없습니다");
}
this.price = price;
}
}
3. 공통 규칙은 한 곳에만 둔다 (Single Source of Truth)
비밀번호 정규식, 아이디 형식, 전화번호 패턴 같은
규칙은 한 군데에서만 관리합니다.
규칙이 여러 군데 흩어지면 만약 수정해야 할때
여러 부분을 수정해야 하니 누락 가능성이 있고,
논리적 오류가 발생하기에 한 곳에 두고,
수정이 필요할 때 한 곳만 고치면 되므로
논리적 오류를 방지합니다.
public final class PhoneNumberPolicy {
// 대한민국 휴대폰 번호 (하이픈 없음)
// 예: 01012345678, 0112345678
public static final String REGEX = "^01[0-9]{1}[0-9]{3,4}[0-9]{4}$";
private PhoneNumberPolicy() {
// 인스턴스 생성 방지
}
}
트레이드오프 (Trade-off): 무엇을 얻고, 무엇을 포기하나?
설계에는 정답이 없고, 항상 기회비용이 존재합니다.
1. 철저한 검증 vs 개발 속도
2. 실시간 검증 vs 성능
중복 체크, 권한 검증 -> DB 조회 필요
DB 조회가 필요한 검증(중복 체크 등)을 자주 하면
안정성은 높지만 시스템 성능에 부하를 줄 수 있습니다.
그래서 서비스에서만 정말 필요한 타이밍에 수행합니다.