Validation은 외부 입력이 애플리케이션의 규칙 안에 들어오는지 확인하는 방어선이다.
Jakarta Bean Validation은 @NotBlank, @Email, @Size 같은 제약 애너테이션으로 객체의 필드와 메서드 파라미터를 검증하는 표준이다.
Spring은 Validator, DataBinder, LocalValidatorFactoryBean을 통해 표준 검증을 MVC 요청 처리와 서비스 계층 검증에 연결한다.
핵심 정리
검증은 단순히 null을 막는 작업이 아니라 시스템 경계에서 잘못된 상태가 도메인 내부로 들어오지 못하게 하는 설계다.
Bean Validation은 객체에 선언된 제약 조건을 기준으로 검증한다. @NotNull, @Min, @Pattern, @Past 같은 제약 애너테이션은 코드 가까이에 규칙을 남긴다.
@Valid는 그 자체가 값의 제약 조건이라기보다 검증 실행과 중첩 객체 검증을 연결하는 표준 애너테이션이다. 내부 객체나 컬렉션 요소의 제약까지 보려면 cascade 검증 여부를 의식해야 한다.
Spring MVC는 요청 값을 객체로 바인딩한 뒤 검증을 수행하고, 실패 결과를 BindingResult나 예외로 전달한다.
LocalValidatorFactoryBean은 Jakarta Bean Validation 구현체를 Spring의 Validator로 노출해 MVC, 서비스, 직접 호출 검증에서 같은 검증 인프라를 쓰게 한다.
검증 규칙은 입력 DTO, 도메인 불변식, 데이터베이스 제약 중 어디에 둘지 나누어야 한다. 모든 규칙을 DTO 애너테이션으로 밀어 넣으면 도메인 모델이 빈약해진다.
헷갈리는 지점
Validation을 Controller의 부가 작업 정도로 생각하기 쉽다. 요청 DTO에서 자주 보이기 때문이다.
핵심은 검증이 시스템 경계마다 필요한 계약 확인이라는 점이다.
Controller는 HTTP 입력 검증을 맡고, 도메인 객체는 자기 불변식을 스스로 지켜야 한다.
Bean Validation 애너테이션이 있으면 모든 비즈니스 규칙이 해결된다고 오해하기 쉽다. 애너테이션이 선언적이고 편해 보이기 때문이다.
핵심은 Bean Validation은 주로 형태와 단일 객체 기준의 제약에 강하다는 점이다.
재고, 권한, 중복 여부처럼 외부 상태가 필요한 규칙은 서비스나 도메인 정책으로 분리해야 한다.
@NotNull과 @NotBlank를 같은 것으로 보기 쉽다. 둘 다 값이 비어 있는 상황을 막기 때문이다.
핵심은 @NotNull은 null만 막고, @NotBlank는 문자열의 공백까지 막는다는 점이다.
요청 문자열 검증에서는 의미 있는 값이 필요한지, 단순 존재만 필요한지 먼저 정한다.
@Valid를 @NotNull 같은 제약 조건으로 오해하기 쉽다. 이름상 유효성을 보장하는 애너테이션처럼 보이기 때문이다.
핵심은 @Valid 자체는 “이 값이 null이면 안 된다” 같은 규칙을 만들지 않는다는 점이다.
null 금지, 문자열 길이, 숫자 범위 같은 실제 조건은 별도의 constraint로 선언해야 한다.
확인 질문
검증이 해결하려는 가장 기본적인 문제는 무엇인가?
외부 입력이나 계층 간 전달 값이 애플리케이션의 규칙을 깨뜨린 상태로 내부에 들어오는 문제다.
Bean Validation은 어떤 방식으로 검증 규칙을 표현하는가?
필드, 메서드 파라미터, 반환값, 객체 그래프에 제약 애너테이션을 붙여 선언적으로 표현한다.
모든 비즈니스 규칙을 Bean Validation으로 처리해도 되는가?
아니다. 입력 형태 검증에는 적합하지만 외부 상태나 도메인 의사결정이 필요한 규칙은 별도 정책으로 분리해야 한다.
@Valid와 @Validated
3줄 요약
@Valid는 Jakarta Bean Validation 표준 애너테이션이고, @Validated는 Spring이 제공하는 검증 트리거이자 그룹 지정 수단이다.
Controller에서 요청 객체 앞에 @Valid 또는 @Validated를 붙이면 바인딩 이후 제약 조건 검증이 실행된다.
검증 실패를 직접 다루려면 검증 대상 바로 뒤에 BindingResult를 두고, 그렇지 않으면 예외 처리 흐름으로 넘어간다.
핵심 정리
@Valid는 표준이므로 객체 그래프 검증과 Jakarta Bean Validation 생태계에서 가장 기본적인 선택이다.
@Validated는 Spring의 애너테이션이며 검증 그룹을 지정할 수 있다. 같은 DTO를 생성과 수정에서 다르게 검증해야 할 때 쓰지만, 남용하면 DTO 책임이 흐려진다.
MVC 요청 처리에서는 먼저 HTTP 요청 값이 객체에 바인딩되고, 그 다음 검증이 실행된다. 타입 변환 실패와 제약 조건 실패는 서로 다른 단계의 실패다.
BindingResult는 반드시 검증 대상 파라미터 바로 뒤에 있어야 한다. 위치가 떨어지면 Spring이 해당 검증 결과를 전달할 수 없다.
@RequestBody 검증 실패, 메서드 파라미터 검증 실패, 바인딩 실패는 예외 타입과 처리 지점이 다를 수 있으므로 한 가지 실패로 단순화하지 않는다.
Spring MVC에서는 개별 argument 검증 실패가 MethodArgumentNotValidException으로, 메서드 파라미터 제약을 포함한 method validation 실패가 HandlerMethodValidationException으로 나타날 수 있다.
헷갈리는 지점
@Valid와 @Validated를 항상 같은 것으로 보기 쉽다. Controller 예제에서 둘 다 검증을 실행하기 때문이다.
핵심은 @Valid는 표준 검증 트리거이고 @Validated는 Spring의 그룹 지정 기능까지 포함한다는 점이다.
그룹이 필요 없다면 @Valid로 충분한 경우가 많다.
BindingResult가 아무 위치에 있어도 된다고 생각하기 쉽다. 단순히 에러 객체처럼 보이기 때문이다.
핵심은 BindingResult가 바로 앞 검증 대상의 결과를 받는다는 점이다.
파라미터 순서가 틀리면 예외 처리 흐름으로 넘어가거나 원하는 결과를 받지 못한다.
타입 변환 실패와 검증 실패를 같은 문제로 보기 쉽다. 둘 다 요청 값이 잘못되었을 때 발생하기 때문이다.
핵심은 변환 실패는 문자열을 목표 타입으로 만들지 못한 것이고, 검증 실패는 만들어진 값이 규칙을 어긴 것이다.
에러 응답을 설계할 때 두 실패를 모두 사용자가 이해할 수 있게 표현해야 한다.
@Valid만 붙이면 모든 메서드 파라미터 검증이 된다고 생각하기 쉽다. Controller 예제에서 자주 보이기 때문이다.
핵심은 @Valid는 중첩 제약 검증을 연결하지만 그 자체는 constraint가 아니라는 점이다.
메서드 파라미터 자체를 검증하려면 @NotNull, @Min 같은 실제 제약 조건이 필요하다.
확인 질문
@Valid와 @Validated의 가장 중요한 차이는 무엇인가?
@Valid는 Jakarta 표준이고, @Validated는 Spring이 제공하며 검증 그룹을 지정할 수 있다.
BindingResult는 어디에 두어야 하는가?
검증 대상 파라미터 바로 뒤에 두어야 한다.
MVC 검증 흐름에서 바인딩과 검증은 같은 단계인가?
아니다. 요청 값을 객체로 바인딩한 뒤, 만들어진 객체의 제약 조건을 검증한다.
BindingResult와 에러 처리
3줄 요약
BindingResult는 바인딩 오류와 검증 오류를 Controller에서 직접 다룰 수 있게 해 주는 결과 객체다.
폼 화면에서는 BindingResult를 이용해 같은 화면으로 돌아가 필드별 메시지를 보여주는 방식이 자연스럽다.
API에서는 BindingResult를 직접 쓰기보다 공통 예외 처리에서 일관된 에러 응답 형식으로 변환하는 경우가 많다.
핵심 정리
BindingResult에는 필드 오류, 글로벌 오류, 거절된 값, 오류 코드, 메시지 인자 등이 담긴다.
HTML 폼에서는 사용자가 입력한 값과 오류 메시지를 함께 유지해야 하므로 BindingResult를 Controller에서 직접 확인하는 흐름이 자주 쓰인다.
REST API에서는 각 Controller가 에러 응답을 직접 만들면 응답 형식이 흔들리기 쉽다. @RestControllerAdvice에서 MethodArgumentNotValidException, HandlerMethodValidationException, BindException 등을 공통 형식으로 바꾸는 편이 낫다.
에러 메시지는 개발자용 예외 메시지를 그대로 노출하지 말고 사용자에게 필요한 필드, 코드, 설명으로 정리한다.
검증 오류 응답은 HTTP 상태 코드만으로 충분하지 않다. 어떤 필드가 왜 실패했는지 클라이언트가 자동 처리할 수 있는 구조가 필요하다.
헷갈리는 지점
BindingResult를 쓰면 예외 처리가 필요 없다고 생각하기 쉽다. 검증 실패를 직접 받을 수 있기 때문이다.
핵심은 BindingResult가 있는 요청에만 직접 처리 흐름이 생긴다는 점이다.
API 전체의 일관성을 위해 예외 처리 계층도 함께 설계해야 한다.
검증 메시지를 예외 메시지 그대로 내려도 된다고 생각하기 쉽다. 빠르게 구현하면 바로 화면에 표시할 수 있기 때문이다.
핵심은 내부 예외 메시지와 클라이언트 계약은 다르다는 점이다.
응답에는 안정적인 오류 코드와 필요한 설명만 담는 편이 유지보수에 유리하다.
필드 오류만 처리하면 충분하다고 보기 쉽다. 대부분의 검증 예제가 필드 단위이기 때문이다.
핵심은 객체 전체의 조합 규칙도 실패할 수 있다는 점이다.
비밀번호 확인, 시작일과 종료일 관계처럼 글로벌 오류가 필요한 경우를 분리해 생각한다.
확인 질문
BindingResult에는 어떤 오류가 담기는가?
타입 변환 같은 바인딩 오류와 제약 조건 검증 오류가 담길 수 있다.
API에서 검증 오류를 공통 예외 처리로 다루는 이유는 무엇인가?
Controller마다 응답 형식이 달라지는 것을 막고 클라이언트가 일관되게 오류를 처리하게 하기 위해서다.
검증 오류 응답에 HTTP 상태 코드 외에 무엇이 필요한가?
실패한 필드, 오류 코드, 사용자에게 전달할 메시지, 필요하면 거절된 값이나 추적 정보를 담아야 한다.
Method Validation과 계층별 검증
3줄 요약
Method Validation은 메서드 파라미터와 반환값에 Bean Validation 제약을 적용하는 방식이다.
Controller, Service, Repository 경계마다 검증의 목적이 다르므로 같은 규칙을 무작정 반복하면 안 된다.
서비스 계층 검증은 프록시와 AOP 흐름의 영향을 받을 수 있으므로 내부 호출과 Bean 등록 여부를 함께 확인해야 한다.
핵심 정리
MVC Controller의 Method Validation은 요청 파라미터, 경로 변수, 헤더, 메서드 파라미터 단위 제약을 검증할 때 유용하다.
Service 계층의 Method Validation은 외부 Controller를 거치지 않는 호출, 배치, 메시지 리스너, 다른 서비스 호출에서도 계약을 지키고 싶을 때 의미가 있다.
입력 DTO 검증은 HTTP 계약을 다루고, 도메인 객체 검증은 불변식을 다루며, 서비스 검증은 유스케이스 진입 조건을 다룬다.
Method Validation은 선언적이지만 모든 계층의 책임을 대체하지 않는다. 중요한 도메인 규칙은 객체 메서드와 도메인 서비스에서 명시적으로 드러나야 한다.
프록시 기반 Method Validation을 사용할 때 같은 클래스 내부 호출은 검증 프록시를 거치지 않을 수 있다.
헷갈리는 지점
Controller에서 검증했으니 Service는 아무 검증도 필요 없다고 생각하기 쉽다. 웹 요청만 고려하면 그렇게 보여서다.
핵심은 Service가 HTTP 이외의 진입점에서도 호출될 수 있다는 점이다.
여러 진입점이 있는 규칙은 Service 계약이나 도메인 불변식으로 올려야 한다.
검증 애너테이션을 여러 계층에 반복하면 더 안전하다고 생각하기 쉽다. 같은 조건을 많이 확인할수록 방어가 강해 보이기 때문이다.
핵심은 반복 검증이 책임 중복과 메시지 불일치를 만들 수 있다는 점이다.
각 계층이 어떤 실패를 책임질지 먼저 나누어야 한다.
Method Validation이 일반 메서드 호출에도 항상 적용된다고 오해하기 쉽다. 애너테이션이 메서드에 붙어 있기 때문이다.
핵심은 Spring이 관리하는 Bean의 외부 호출 흐름을 확인하는 것이다.
내부 호출이나 직접 생성 객체는 검증 프록시를 지나지 않을 수 있다.
확인 질문
Method Validation은 무엇을 검증하는가?
메서드 파라미터와 반환값에 선언된 Bean Validation 제약 조건을 검증한다.
Controller 검증과 Service 검증은 왜 목적이 다른가?
Controller는 HTTP 입력 계약을 확인하고, Service는 유스케이스 진입 조건과 여러 진입점에서의 계약을 확인하기 때문이다.
Method Validation에서 프록시 흐름을 조심해야 하는 이유는 무엇인가?
프록시를 통과하지 않는 내부 호출이나 직접 생성 객체에는 검증이 적용되지 않을 수 있기 때문이다.