Spring/강의

[📙 숙련 Spring] 1-3. Validation과 Bean Validation

가지코딩 2025. 5. 15. 16:53

📙 목차

  1. Validation
  2. BindingResult
  3. Bean Validation
  4. Bean Validation 사용 예제 1 - 기본 흐름
  5. Bean Validation 사용 예제 2 - 글로벌 예외 처리
  6. Bean Validation 사용 예제 3 - 그룹별 검증 조건(groups)

1. Validation

Validation - 검증

  • 특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 절차
  • 잘못된 데이터의 유입을 방지하여 시스템의 신뢰성과 안정성을 확보하는 핵심 과정이다.
  • Controller의 주요한 역할 중 하나는 Validation 이다. HTTP 요청이 정상인지 검증한다.

 

Validation의 역할

  • 사용자에게 입력 오류에 대한 명확한 피드백을 제공한다.
  • 오류 발생 시에도 시스템이 정상적으로 동작하도록 보호한다.
  • 사용자가 입력한 데이터를 보존하여 재입력의 불편을 줄인다.

 

Validation의  종류

  • 프론트엔드 검증
    • 사용자 인터페이스에서 입력값을 즉시 검사하여 빠른 피드백을 제공하는 검증이다.
    • 사용자 경험 향상에 도움을 주지만, 클라이언트 조작이 가능하여 보안에 취약하다.
  • 서버 검증
    • 서버에서 모든 입력 데이터를 재검증하는 검증이다.
    • 보안상 반드시 수행해야 하며, API 명세서에 검증 규칙과 오류 응답을 명확히 정의하는 것이 중요하다.
  • 데이터베이스 검증
    • NOT NULL, UNIQUE, CHECK 제약조건 등 데이터 무결성을 보장하는 최종 방어선
    • 애플리케이션 차원의 검증을 우회하는 경우에도 데이터 일관성을 유지한다.

2. BindingResult

BindingResult

  • 스프링(Spring) MVC에서 주로 폼 데이터를 검증하고 바인딩할 때 사용하는 인터페이스
  • 클라이언트가 전송한 폼 데이터(요청 파라미터)를 컨트롤러 메서드의 객체에 바인딩(맵핑)할 때 발생하는 바인딩 오류검증 오류를 담는 객체이다.
  • @Valid 또는 @Validated 어노테이션과 함께 사용하여, 데이터 검증 결과를 담고 검증 실패 시 적절한 처리를 가능하게 한다.
  • 보통 @ModelAttribute나 @RequestBody로 바인딩된 객체 바로 다음 파라미터로 선언한다.
@PostMapping("/user")
public String createUser(@Valid @ModelAttribute UserForm form, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        // 검증 실패 시 다시 입력 폼으로 이동
        return "userForm";
    }
    // 검증 성공 시 다음 로직 실행
    userService.save(form);
    return "redirect:/success";
}

 

 

BindingResult 의 역할

  • 폼 데이터 바인딩 시 발생한 타입 변환 오류 저장
  • 검증기(Validator)가 실행한 검증 결과 저장
  • 오류가 있을 때 이를 확인하고 사용자에게 피드백 제공

3. Bean Validation

Bean Validation

  • Java 객체의 필드나 속성에 제약 조건을 선언하고, 이를 자동으로 검증할 수 있도록 도와주는 표준 검증 프레임워크
  • JSR-303, JSR-380 스펙에 기반한 Java의 표준 Validation API
  • 어노테이션 기반으로 객체의 필드에 제약 조건을 선언한다.
  • 런타임 시 자동으로 유효성 검사를 수행한다.
  • DTO 검증, 폼 입력값 검증 등에 자주 사용된다.
  • 대표적인 구현체: Hibernate Validator
// 사용 예시
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class UserDTO {

    @NotNull	// null이면 안 됨
    private String name;

    @Size(min = 6, max = 20)	// 문자열의 길이 제약
    private String password;
}
// 검증 수행 방법
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody @Valid UserDTO user) {
    // 유효하지 않으면 자동으로 400 Bad Request 반환
    return ResponseEntity.ok().build();
}

 

 

대표 어노테이션

어노테이션 설명
@NotNull null이 아니어야 함
@NotEmpty null 또는 빈 문자열은 안 됨
@NotBlank null, 빈 문자열, 공백만 있는 문자열 안 됨
@Size(min, max) 크기(문자열, 컬렉션 등) 제약
@Email 이메일 형식 검증
@Pattern 정규표현식 패턴 매칭
@Min, @Max 숫자의 최소/최대 값 지정
@Positive, @Negative 양수 / 음수 검증

 

 

 

@Valid, @Validated

  • Bean Validation을 적용할 때 사용하는 어노테이션
구분 @Valid (javax.validation) @Validated (Spring Framework)
소속 패키지 javax.validation.Valid 또는 jakarta.validation.Valid org.springframework.validation.annotation.Validated
기본 기능 단순히 Bean Validation 수행 Bean Validation 수행 + 그룹(Group) 지정 가능
그룹 지정 불가능 (기본 그룹만 검증) 가능 (@Validated(Group.class))
스프링 지원 스프링 MVC, 스프링 부트에서 기본 지원 스프링 전용 기능
사용 위치 주로 @RequestBody, 메서드 파라미터 등에서 검증 시 사용 컨트롤러, 서비스 등에서 그룹별 검증 시 사용
추가 기능 없음 AOP 기반으로 클래스 레벨 검증 등도 가능

 

 

 

BindingResult 한계와 Bean Validation의 주요 특징

BindingResult 한계 Bean Validation의 주요 특징
컨트롤러에만 사용 가능 도메인 객체에 선언적 제약 → 어디서든 재사용 가능
검증 코드와 비즈니스 로직 혼재 선언형 어노테이션으로 관심사 분리
응답 포맷 직접 구성 필요 예외 기반 전역 처리로 일관된 오류 응답 제공
스프링에 종속적 (표준 아님) JSR 303/380 표준 → 다양한 프레임워크와 호환 가능

 

 

@Valid + BindingResult vs @Valid

항목 @Valid + BindingResult @Valid
검증 실패 시 동작 BindingResult에 오류 저장 (예외 발생 X) MethodArgumentNotValidException 발생
예외 발생 여부 ❌ 예외 발생하지 않음 ✅ 예외 발생 (스프링 예외 처리로 감)
오류 처리 위치 컨트롤러 메서드 내부에서 직접 처리 가능 전역 예외 처리기 또는 기본 에러 응답
사용 추천 케이스 HTML 폼, 유효성 실패 시 다시 폼 보여줄 때 REST API에서 일괄적으로 오류 처리할 때

4. Bean Validation 사용 예제 1 - 기본 흐름

DTO 정의 → Controller 검증 → 오류 처리

 

1) DTO 클래스

  • @NotBlank, @Email, @Size 등 Bean Validation 어노테이션을 붙여 검증 조건 선언
public class UserDTO {

    @NotBlank(message = "이름은 필수입니다.")
    private String name;

    @Email(message = "이메일 형식이 올바르지 않습니다.")
    private String email;

    @Size(min = 6, message = "비밀번호는 최소 6자 이상이어야 합니다.")
    private String password;
}

 

 

2) Controller

  • @RequestBody @Valid를 사용해 DTO 검증 수행
  • 검증 실패 시 Spring Boot가 자동으로 400 에러와 기본 JSON 오류 메시지를 응답
@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public ResponseEntity<String> register(@RequestBody @Valid UserDTO userDTO) {
        // 검증 실패 시 Spring이 자동으로 400 에러를 발생시키고 메시지 반환
        return ResponseEntity.status(HttpStatus.CREATED).body("회원가입 성공");
    }
}

 

 

3) 검증 실패 시 응답

{
  "timestamp": "2025-05-15Txx:xx:xx.xxx+00:00",
  "status": 400,
  "errors": [
    "name: 이름은 필수입니다.",
    "email: 이메일 형식이 올바르지 않습니다.",
    "password: 비밀번호는 최소 6자 이상이어야 합니다."
  ],
  "path": "/users"
}

5. Bean Validation 사용 예제 2 - 글로벌 예외 처리

Spring Boot는 기본적으로 @Valid 검증 실패 시  400 에러와 기본 오류 메시지를 반환하지만,

 

사용자 정의 형식으로 에러 메시지를 통일하거나 추가 정보를 담고 싶을 때 글로벌 예외 처리기를 구현할 수 있다.

 

 

1) 글로벌 예외 처리기 클래스 생성

  • @RestControllerAdvice가 모든 컨트롤러의 예외를 감지한다.
  • @ExceptionHandler(MethodArgumentNotValidException.class)로 Bean Validation 실패 예외를 잡는다.
  • 필드명과 메시지를 Map으로 만들어 클라이언트에게 JSON 형태로 반환한다.
  • 원하는 포맷으로 메시지를 통일하거나 추가 정보를 담기 쉽다.
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {

        Map<String, String> errors = new HashMap<>();

        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String message = error.getDefaultMessage();
            errors.put(fieldName, message);
        });

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }
}

 

 

2) DTO, Controller 는 기본 흐름과 동일하게 작성

 

3) 검증 실패 시 응답

{
  "name": "이름은 필수입니다.",
  "email": "이메일 형식이 올바르지 않습니다.",
  "password": "비밀번호는 최소 6자 이상이어야 합니다."
}

6. Bean Validation 사용 예제 3 - 그룹별 검증 조건(groups)

 

1) 그룹 인터페이스 선언

public interface CreateGroup {}
public interface UpdateGroup {}

 

 

2) DTO 클래스에 그룹별 검증 조건 지정

  • groups 속성에 그룹 인터페이스를 지정하여 특정 상황에 맞는 검증 조건을 분리한다.
  • 공통 검증은 그룹 지정 없이 @Size처럼 기본 그룹으로 설정 가능하다.
public class UserDTO {

    // 회원가입 시 필수, 수정 시 무시
    @NotBlank(message = "이름은 필수입니다.", groups = CreateGroup.class)
    private String name;

    // 회원가입 시 필수, 수정 시 무시
    @Email(message = "이메일 형식이 올바르지 않습니다.", groups = CreateGroup.class)
    private String email;

    // 수정 시 필수, 회원가입 시 무시
    @NotBlank(message = "아이디는 필수입니다.", groups = UpdateGroup.class)
    private String id;

    // 회원가입, 수정 공통 (groups 지정 없으면 기본 그룹)
    @Size(min = 6, message = "비밀번호는 최소 6자 이상이어야 합니다.")
    private String password;
}

 

 

3) Controller에서 그룹 지정하여 검증 수행

  • @Validated(그룹.class)로 검증 대상 그룹을 지정한다. (@Valid는 그룹 지정 불가)
@RestController
@RequestMapping("/users")
public class UserController {

    // 회원가입: CreateGroup 검증
    @PostMapping
    public String createUser(@RequestBody @Validated(CreateGroup.class) UserDTO userDTO) {
        return "회원가입 성공";
    }

    // 수정: UpdateGroup 검증
    @PutMapping("/{id}")
    public String updateUser(@PathVariable String id, @RequestBody @Validated(UpdateGroup.class) UserDTO userDTO) {
        return "회원 수정 성공";
    }
}

 

 

* groups 방식 vs DTO 분리 방식

구분 groups 방식 DTO 분리 방식
검증 관리 방식 하나 DTO에 그룹별 검증 어노테이션 분리 검증 시나리오별 DTO 클래스 분리
코드량 적음 많음
유지보수성 그룹이 많아질수록 복잡해질 수 있음 명확하고 직관적
사용 용도 비슷한 검증 대상에서 세밀한 검증 제어 필요 완전히 다른 검증 규칙 및 역할 구분 필요

 

실무에서는 등록, 수정 등 상황별로 DTO 클래스를 따로 만들어 관리하는 것이 권장된다.