이전 글처럼 BindingResult를 사용해 비즈니스 로직 전에 매번 검증 코드를 작성하는 것은 번거로운 작업이다.
이런 반복적인 검증 로직을 표준화하여 간편하게 사용할 수 있도록 해주는 것이 바로 Bean Validation이다.
Bean Validation을 활용하면 단순한 어노테이션만으로도 손쉽게 검증 로직을 구현할 수 있다.
📌 Bean Validation 이란?
먼저 Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다.
쉽게 이야기해서 검증 어노테이션과 여러 인터페이스의 모음이라는 의미이다.
JPA가 표준 기술이고 그 구현체로 하이버네이트가 있는 것처럼, Bean Validation도 구현체가 있다.
일반적으로 사용하는 Bean Validation의 구현체는 하이버네이트 Validator이다.
(여기서 말하는 하이버네이트는 ORM과는 관련이 없다.)
♼ Bean Validation의 동작 방식
1️⃣ 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
2️⃣ 스프링 부트의 자동 설정
의존성 추가만으로 `LocalValidatorFactoryBean`이 글로벌 Validator로 자동 등록된다.
`@Valid` 또는 `@Validated`가 붙은 객체를 자동으로 검증한다.
3️⃣ 검증 수행 순서
HTTP 요청 파라미터의 각 필드 타입 변환을 시도한다.
타입 변환에 성공한 필드만 Bean Validation 적용한다.
검증 오류 발생 시 FieldError, ObjectError를 생성해서 BindingResult에 저장한다.
즉, BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다.
📌 Bean Validation Annotation 종류
1. 검증 실행 어노테이션
Bean Validation을 사용할 때는 `@Valid`와 `@Validated` 두가지 검증 어노테이션을 사용할 수 있는데
`@Valid`는 자바 표준 스펙인 `javax.validation`의 검증 어노테이션이고,
`@Validated`는 스프링 전용 검증 어노테이션이다.
둘 중 아무거나 사용해도 동일하게 작동하지만, `@Validated`는 내부에 groups라는 기능을 포함하고 있다는 차이점이 있다.
2. 검증 기능 어노테이션
어노테이션 | 설명 | 예시 |
@NotNull | Null을 허용하지 않음 | @NotNull(message = "값이 null일 수 없습니다") |
@NotEmpty | Null, 빈 문자열("") 허용하지 않음 | @NotEmpty(message = "값이 비어있을 수 없습니다") |
@NotBlank | Null, 빈 문자열(""), 공백(" ") 허용하지 않음 | @NotBlank(message = "값이 공백일 수 없습니다") |
@Size | 문자열, 컬렉션의 크기 검증 | @Size(min = 2, max = 10) |
@Min | 숫자의 최솟값 검증 | @Min(value = 1000) |
@Max | 숫자의 최댓값 검증 | @Max(value = 9999) |
@Range | 숫자의 범위 검증 | @Range(min = 1000, max = 9999) |
이메일 형식 검증 | @Email(message = "이메일 형식이 올바르지 않습니다") | |
@Pattern | 정규식을 통한 문자열 패턴 검증 | @Pattern(regexp = "^[A-Za-z0-9]{3,}$") |
3. 패키지별 검증 기능 구분
- javax.validation.constraints: 자바 표준 검증 기능
- @NotNull, @Size, @Min, @Max 등
- org.hibernate.validator.constraints: 하이버네이트 전용 검증 기능
- @Range, @NotEmpty 등
💡 스프링은 기본적으로 하이버네이트 validator를 구현체로 사용하기 때문에, 위 표의 모든 기능을 자유롭게 사용할 수 있다.
👩🏻💻 Bean Validation 검증 예시
회원 가입 기능에서 데이터 검증이 필요한 경우라고 가정해보자.
@Data
@NoArgsConstructor
public class Member {
@NotBlank(message = "이름은 필수입니다.")
@Size(min = 2, max = 10, message = "이름은 2~10자 사이여야 합니다.")
private String name;
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;
@NotBlank(message = "비밀번호는 필수입니다.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,16}$",
message = "비밀번호는 8~16자리수여야 합니다. 영문, 숫자, 특수문자를 1개 이상 포함해야 합니다.")
private String password;
@Min(value = 14, message = "14세 이상만 가입 가능합니다.")
@Max(value = 99, message = "99세 이하만 가입 가능합니다.")
private int age;
}
회원가입시 요청으로 오는 MemberRequest DTO를 컨트롤러에서는
다음과 같이 파라미터에 @Valid나 @Validated를 붙여주면 유효성 검증이 진행된다.
@RestController
@RequestMapping("/members")
public class MemberController {
@PostMapping("/signup")
public ResponseEntity<Void> signup(@Valid @RequestBody Member member) {
memberService.signup(member);
return ResponseEntity.ok().build();
}
@PatchMapping("/{id}")
public ResponseEntity<Void> update(@PathVariable Long id,
@Valid @RequestBody Member member) {
memberService.update(id, member);
return ResponseEntity.ok().build();
}
}
Resolved [org.springframework.web.bind.MethodArgumentNotValidException:
Validation failed for argument [0] in public org.springframework.http.ResponseEntit
<hello.springmvc.example.Member>
hello.springmvc.example.MemberController.signup
(hello.springmvc.example.Member) with 2 errors:
[Field error in object 'member' on field 'age': rejected value [4];
codes [Min.member.age,Min.age,Min.int,Min];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [member.age,age]; arguments []; default message [age],14];
default message [14세 이상만 가입 가능합니다.]]
[Field error in object 'member' on field 'email': rejected value [asj];
codes [Email.member.email,Email.email,Email.java.lang.String,Email];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [member.email,email]; arguments []; default message
[email],[Ljavax.validation.constraints.Pattern$Flag;@4a65e921,.*];
default message [이메일 형식이 올바르지 않습니다.]] ]
이렇게 검증에 실패하게 되면 404에러와 함께
필드 검증 어노테이션에 기본으로 지정해둔 default message가 콘솔에 출력된다.
검증 오류를 로그로 관리하고 싶다면?
1. 컨트롤러 레벨에서 BindingResult를 파라미터로 추가하여 각 메소드에서 직접 검증 에러를 처리할 수 있고
2. `@ControllerAdvice`를 사용하여 전역적으로 validation 예외를 처리할 수 있다.
회원가입 시에는 모든 필드가 필수지만, 회원 정보 수정 시에는 변경하고 싶은 필드만 입력받고 싶을 수 있다.
현재 구조에서는 정보 수정 시에도 모든 필드에 대한 검증이 수행되어 불필요한 제약이 발생한다.
이러한 경우에 @Validated의 groups 기능을 사용하면 상황에 따라 다른 검증 전략을 적용할 수 있다.
동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법을 알아보자
- Bean Validation의 groups 기능 사용하기
- Member를 직접 사용하지 않고 MemberSignupDto, MemberUpdateDto 와 같은 기능에 맞는 별도의 모델 객체를 만들어서 사용하기
📌 @Validated의 또 다른 기능 - groups
회원가입과 정보 수정에 동일한 DTO를 사용하면서도 다른 검증 규칙을 적용하고 싶다면 groups 기능을 활용할 수 있다.
1. 검증 그룹 정의
public interface CreateMember {} // 회원 가입 시 검증
public interface UpdateMember {} // 회원 정보 수정 시 검증
@Data
@NoArgsConstructor
public class Member {
@NotNull(groups = UpdateMember.class) //수정시에만 적용
private Long id;
@NotBlank(message = "이름은 필수입니다.", groups = CreateMember.class)
@Size(min = 2, max = 10, groups = {CreateMember.class, UpdateMember.class})
private String name;
@NotBlank(message = "이메일은 필수입니다.", groups = CreateMember.class)
@Email(groups = {CreateMember.class, UpdateMember.class})
private String email;
@NotBlank(groups = CreateMember.class, UpdateMember.class)
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,16}$",
groups = CreateMember.class, UpdateMember.class)
private String password;
@Min(value = 14, groups = CreateMember.class)
@Max(value = 99, groups = CreateMember.class)
private int age;
}
@RestController
@RequestMapping("/members")
public class MemberController {
@PostMapping("/signup")
public ResponseEntity<Void> signup(
@Validated(CreateMember.class) @RequestBody Member member) {
memberService.signup(member);
return ResponseEntity.ok().build();
}
@PatchMapping("/{id}")
public ResponseEntity<Void> update(
@PathVariable Long id,
@Validated(UpdateMember.class) @RequestBody Member member) {
memberService.update(id, member);
return ResponseEntity.ok().build();
}
}
회원가입 시에는 CreateMember.class로 id를 제외한 모든 필드가 필수이며 지정된 검증을 수행한다.
정보 수정 시에는 UpdateMember.class로 회원가입과 달리 id가 필수이며, 나이를 제외한 나머지 필드에 대해 검증을 수행하게 된다.
groups는 하나의 DTO로 여러 검증 상황을 처리 가능하다는 장점이 있지만
설정이 복잡할 수 있고, 검증 로직이 한 클래스에 모이면서 복잡해질 수 있다는 단점이 있다.
따라서 실무에서는 groups를 잘 사용하지 않기 때문에
각 기능의 요구사항에 맞는 별도의 DTO 클래스를 만들어서 사용하는 것을 더 권장한다.
ex) MemberSignupDto, MemberUpdateDto
🗂️ References
[Spring] @Valid와 @Validated를 이용한 유효성 검증의 동작 원리 및 사용법 예시
'Study > Spring' 카테고리의 다른 글
[Spring] BindingResult 활용한 Validation 검증 동작 방식 (0) | 2025.01.07 |
---|---|
[Spring] @Controller와 @RestController의 차이점 (0) | 2024.12.29 |