이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Controller 파라미터 검증과 Service 메서드 검증은 어떤 상황에서 각각 필요한가?
- Method Validation은 왜 프록시, 내부 호출, Bean 등록 여부의 영향을 받는가?
- 검증을 DTO, Service, Domain, Database에 나누어 배치하는 기준은 무엇인가?
개요
Method Validation은 메서드 파라미터와 반환값에 Bean Validation 제약 조건을 적용하는 방식이다. Controller의 @RequestParam, @PathVariable, Service 메서드 파라미터, 반환 DTO 등에 제약을 걸 수 있다. @Valid는 중첩 객체 검증을 연결하지만 그 자체가 constraint는 아니므로, 파라미터 자체의 null 여부나 범위는 @NotNull, @Positive 같은 실제 제약 조건으로 표현해야 한다.
이 기능은 계층 간 계약을 명확히 하는 데 유용하다. 하지만 모든 메서드에 제약 애너테이션을 붙이는 것이 좋은 설계는 아니다. 각 계층의 책임을 기준으로 어떤 검증을 어디에 둘지 먼저 정해야 한다.
Controller Method Validation
Controller에서는 HTTP 요청의 세부 파라미터를 검증할 때 Method Validation이 빛난다. 요청 body DTO가 아니라 쿼리 파라미터, 경로 변수, 헤더 값 자체가 규칙을 가져야 할 때다. Spring Framework 6.1 이후 MVC는 controller method validation을 자체적으로 지원하므로, class-level @Validated를 습관적으로 붙여 AOP 기반 검증 흐름으로 바꾸지 않는지도 확인해야 한다.
@GetMapping("/orders/{orderId}")
public OrderResponse find(
@PathVariable @Positive Long orderId,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
) {
return orderService.find(orderId, size);
}이 검증은 HTTP 계약에 가깝다. orderId가 양수여야 하고 페이지 크기가 1에서 100 사이여야 한다는 것은 API 요청 형식의 일부다.
Service Method Validation
Service Method Validation은 Controller를 우회하는 진입점이 있을 때 의미가 커진다. 배치 작업, 메시지 컨슈머, 다른 서비스 Bean, 테스트 코드가 같은 Service를 호출한다면 Service의 입력 계약을 별도로 검증할 수 있다.
@Service
@Validated
public class PaymentService {
public PaymentReceipt pay(@Valid PaymentCommand command, @Positive long amount) {
return executePayment(command, amount);
}
}서비스 검증은 유스케이스 진입 조건을 표현한다. 다만 서비스 안의 모든 도메인 규칙을 애너테이션으로 표현하려 하면 금방 한계가 온다. 결제 가능 여부, 재시도 정책, 계정 상태 확인은 명시적 코드와 도메인 정책으로 두는 편이 좋다.
프록시와 내부 호출
Service 계층의 선언적 Method Validation은 AOP 방식으로 적용될 수 있다. 그래서 Spring Bean으로 등록되어 있고 외부에서 프록시를 통해 호출되는 흐름인지가 중요하다. 같은 클래스 내부에서 검증 대상 메서드를 직접 호출하면 프록시를 통과하지 않아 검증이 실행되지 않을 수 있다.
이 특성은 @Transactional, AOP, Security 메서드 보안에서도 반복해서 나타나는 패턴이다. 애너테이션이 메서드에 붙어 있더라도 실제 호출 경로가 Spring 인프라를 지나야 한다는 점을 기억해야 한다.
계층별 배치 기준
입력 DTO는 외부 계약을 지킨다. 예를 들어 JSON 필드가 비어 있지 않은지, 숫자 범위가 요청 스펙에 맞는지 확인한다.
Service는 유스케이스 진입 조건을 지킨다. 예를 들어 외부 진입점이 여러 개일 때 공통으로 만족해야 하는 파라미터 조건을 둔다.
Domain은 불변식을 지킨다. 예를 들어 주문 총액은 음수가 될 수 없고, 완료된 주문은 다시 취소 요청을 받을 수 없다는 규칙은 도메인 객체가 스스로 지켜야 한다.
Database는 최후의 무결성을 지킨다. unique 제약, not null, foreign key는 애플리케이션 검증을 뚫고 들어온 잘못된 상태를 막는 마지막 방어선이다.
실전 팁
- Method Validation은 계층 간 계약을 드러내는 도구로 사용한다.
- Controller에서 이미 검증한 DTO를 Service에서 그대로 반복 검증하기보다 Service 전용 Command 모델을 둘지 검토한다.
- Controller method validation에서는
MethodArgumentNotValidException과HandlerMethodValidationException중 어느 흐름을 타는지 메서드 서명별로 테스트한다. - 내부 호출에 검증을 기대하지 않는다. 검증이 꼭 필요하면 책임을 별도 Bean으로 분리한다.
- 반환값 검증은 외부로 내보낼 계약을 보장하는 데 유용하지만 실패 시 운영 장애로 이어질 수 있으므로 테스트를 충분히 둔다.
- DB 제약과 애플리케이션 검증을 경쟁 관계로 보지 말고 서로 다른 방어선으로 본다.
위험 신호!
- 같은 검증 규칙이 DTO, Service, Entity, DB에 이유 없이 복사되어 있다.
- Method Validation을 붙였는데 테스트에서는 직접
new로 만든 객체를 호출한다. - 내부 호출에 검증이 적용된다고 기대한다.
@Valid만 붙이고 실제 constraint 없이 파라미터 자체가 검증될 것이라고 기대한다.- Controller class-level
@Validated와 MVC 내장 method validation의 차이를 확인하지 않는다. - 검증 그룹으로 계층별 책임을 모두 해결하려 한다.
- 도메인 불변식이 DTO 애너테이션에만 존재한다.
확인 질문
확인 질문
- Controller Method Validation이 잘 맞는 대상은 무엇인가?
- 쿼리 파라미터, 경로 변수, 헤더처럼 HTTP 요청 파라미터 자체의 제약이다.
- Service Method Validation이 필요한 대표 상황은 무엇인가?
- Controller 외에 배치, 메시지 리스너, 다른 서비스 등 여러 진입점에서 Service가 호출되는 상황이다.
- Method Validation이 내부 호출에서 동작하지 않을 수 있는 이유는 무엇인가?
- 선언적 검증이 Spring 프록시를 통과하는 외부 호출에 적용되는 방식일 수 있기 때문이다.