[Spring/Java] 계층별 검증 전략 총정리

Jun_k·2026년 2월 1일

Spring

목록 보기
3/9
post-thumbnail

입력값 검증, 어디서 해야 할까?

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

검증의 3단계

1. 표현 계층 (Controller): "형식이 맞는가?"

  • 사용자의 요청을 처음 받는 입구

  • 핵심 책임: 시스템이 이해할 수 있는 입력인가?

  • 검증 항목 (데이터의 껍데기)

    • 필수값 누락
    • 타입이 맞는지 (숫자/문자열)
    • 문자열 길이
    • 이메일 형식
    • null 여부
  • 왜 여기서 하나?

    • 잘못된 요청을 가장 싸게 거르기 위해서
    • 잘못된 데이터가 비즈니스 로직, DB까지 가지 않게 Fail-Fast
  • 예시 코드
    ("요청이 비어 있지는 않나?", "이메일 형식은 맞는가?")

public record CreateUserRequest(
    @Email
    @NotBlank
    String email,
    
    @NotBlank
    String name
) {}
@PostMapping("/users")
public void create(@Valid @RequestBody CreateUserRequest request) {
    service.create(request);
}

2. 비즈니스 계층 (Service): "맥락상 가능한가?"

  • 데이터의 형식은 맞지만, "현재 시스템의 상태에서 허용되는 행동인가?"

  • 핵심 책임: 비즈니스 규칙을 만족하는가?

  • 검증 항목

    • 아이디 / 이메일 중복
    • 권한 체크
    • 잔액 부족 여부
    • 이미 처리된 주문인지 여부
  • 왜 여기서 하나?

    • 이 검증은 데이터베이스(DB)를 조회해야만 알 수 있는
      동적인 상태에 의존하기 때문입니다.
  • 예시 코드

public void create(CreateUserRequest request) {
    if (userRepository.existsByEmail(request.email())) {
        throw new IllegalStateException("이미 존재하는 이메일입니다");
    }
}

3. 도메인 계층 (Service): "존재 자체가 정당한가?"

  • 존재 자체가 말이 되는 객체인가?

  • 핵심 책임: 도메인은 스스로를 지킨다.

  • 검증 항목

    • 절대 깨지면 안 되는 규칙 (불변 조건)
      • 가격은 음수 불가
      • 수량은 1 이상
      • 주문에는 상품이 반드시 존재
  • 왜 여기서 하나?

    • Service가 실수해도 잘못된 객체가 DB에 저장되는 것을 막는 최후의 방어선
  • 예시 코드

@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)에서 한 번만 수행
    내부 계층(Service, Domain)은 "이미 형식은 검증된 데이터가 들어왔다"
    가정하고, 로직에만 집중합니다.

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 개발 속도

  • 모든 계층에서 꼼꼼히 검증하면 안정성은 극대화되지만,
    코드 양이 늘어나고(Boilerplate) 유지보수 비용이 발생합니다.

2. 실시간 검증 vs 성능

  • 중복 체크, 권한 검증 -> DB 조회 필요

  • DB 조회가 필요한 검증(중복 체크 등)을 자주 하면
    안정성은 높지만 시스템 성능에 부하를 줄 수 있습니다.

  • 그래서 서비스에서만 정말 필요한 타이밍에 수행합니다.

profile
개발을 즐겨보자.

0개의 댓글