Bean Validation과 입력 검증

3줄 요약

  • 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에서 프록시 흐름을 조심해야 하는 이유는 무엇인가?
    • 프록시를 통과하지 않는 내부 호출이나 직접 생성 객체에는 검증이 적용되지 않을 수 있기 때문이다.