|

Spring Boot Bean Validation은 어디까지 믿어도 될까: @Valid와 비즈니스 규칙의 경계

Spring Boot Bean Validation은 어디까지 믿어도 될까: @Valid와 비즈니스 규칙의 경계
Bean Validation은 입력 형식 검증에 강하지만 모든 비즈니스 규칙을 대신하지는 않습니다.

Spring Boot Bean Validation은 요청 DTO를 검증할 때 매우 편리하지만, 모든 검증을 `@Valid` 하나로 끝내려 하면 구조가 금방 애매해집니다.

핵심은 입력 형식 검증과 비즈니스 규칙 검증을 같은 층에 몰아넣지 않는 것입니다. 이 글은 Spring Framework와 Jakarta Bean Validation 문서를 기준으로 @Valid, DTO, 서비스 계층, 도메인 규칙의 경계를 정리합니다.

Spring Boot Bean Validation 책임 경계 요약 카드
DTO 검증과 비즈니스 규칙 검증은 실패 이유와 필요한 정보가 다릅니다.

Spring Boot Bean Validation이 잘하는 일

Bean Validation은 요청 값의 기본 모양을 확인하는 데 강합니다. null 여부, 문자열 길이, 숫자 범위, 이메일 형식처럼 입력 데이터 자체만 보고 판단할 수 있는 규칙에 잘 맞습니다.

public record SignupRequest(
    @NotBlank String email,
    @Size(min = 8, max = 64) String password,
    @NotBlank String nickname
) {}

@PostMapping("/signup")
public SignupResponse signup(@Valid @RequestBody SignupRequest request) {
    return signupService.signup(request);
}

이 단계의 목적은 컨트롤러에 들어온 요청이 최소한의 형식 조건을 만족하는지 빠르게 거르는 것입니다.


@Valid가 애매해지는 순간

@Valid는 요청 객체 하나만 보고 판단할 수 있는 규칙에는 좋습니다. 하지만 DB 조회, 현재 사용자 상태, 외부 시스템 결과, 여러 aggregate의 관계가 필요한 규칙은 DTO annotation만으로 표현하기 어렵습니다.

  • 이 이메일이 이미 가입되어 있는가
  • 현재 사용자가 이 쿠폰을 쓸 수 있는가
  • 주문 상태가 결제 가능 상태인가
  • 재고가 요청 수량보다 충분한가

이런 규칙은 단순 입력 형식이 아니라 업무 상태를 해석해야 합니다. 그래서 서비스 계층이나 도메인 모델에서 다루는 편이 자연스럽습니다.


DTO 검증과 비즈니스 규칙 검증을 나누는 기준

좋은 기준은 ‘이 규칙을 판단하는 데 요청 값만 있으면 충분한가’입니다. 요청 값만 있으면 DTO 검증 후보이고, 저장소나 현재 상태가 필요하면 서비스/도메인 규칙 후보입니다.

public void signup(SignupRequest request) {
    if (userRepository.existsByEmail(request.email())) {
        throw new DuplicateEmailException(request.email());
    }

    User user = User.create(request.email(), request.nickname());
    userRepository.save(user);
}

이메일 형식은 DTO에서 검증할 수 있지만, 이미 가입된 이메일인지는 repository를 봐야 하므로 서비스 흐름에서 판단합니다.


도메인 객체가 직접 지켜야 하는 규칙

도메인 객체 안에는 객체가 절대 깨지면 안 되는 불변식을 둡니다. 예를 들어 주문 총액은 음수가 될 수 없다거나, 취소된 주문은 다시 결제할 수 없다는 규칙은 요청 DTO보다 도메인 상태에 가깝습니다.

public class Order {
    private OrderStatus status;

    public void pay() {
        if (status != OrderStatus.READY) {
            throw new InvalidOrderStateException(status);
        }
        this.status = OrderStatus.PAID;
    }
}

이렇게 두면 API, batch, admin 기능 어디에서 호출하더라도 핵심 규칙이 같은 곳에서 지켜집니다.


검증 실패 응답은 어떻게 나눌까

DTO 검증 실패는 보통 field error 목록으로 표현하기 좋습니다. 반대로 비즈니스 규칙 실패는 오류 코드와 메시지로 표현하는 편이 API 사용자에게 더 명확합니다.

  • DTO 검증 실패: `email` 필드가 비어 있음, `password` 길이가 짧음
  • 비즈니스 규칙 실패: 이미 가입된 이메일, 결제할 수 없는 주문 상태
  • 도메인 불변식 실패: 객체 상태가 허용되지 않는 전이를 시도함

실무 체크리스트

  1. 요청 값만 보고 판단할 수 있으면 DTO annotation을 우선 검토한다
  2. DB 조회나 현재 상태가 필요하면 서비스 계층에서 판단한다
  3. 객체가 항상 지켜야 하는 규칙은 도메인 메서드 안에 둔다
  4. annotation으로 표현하기 어려운 규칙을 억지 custom validator로 숨기지 않는다
  5. 검증 실패 응답은 field error와 business error를 분리해 설계한다

정리

Spring Boot Bean Validation를 이해할 때는 용어부터 외우기보다 이 도구가 어떤 경계를 나누려는지 먼저 보는 편이 좋습니다. 오늘 글의 기준은 후보를 좁히는 흐름, 반복 상태의 이동, 입력 검증의 책임, 빌드 입력의 변경 빈도, 읽기와 쓰기의 복잡도 차이를 구분하는 것입니다.

함께 보면 좋은 내부 글은 Spring Boot 의존성 주입은 왜 필요할까, Spring Boot 예외 처리는 어디서 모아야 할까, Application Service와 Domain Service 차이입니다. 외부 기준은 Spring Framework – Bean Validation, Spring Framework – @ModelAttribute method arguments, Jakarta Bean Validation 3.0 Specification를 확인했습니다.

함께보면 좋은 글