같은 DTO인데 생성과 수정에서 다른 검증 규칙이 필요할 때 — Validation Groups
문제 발생
사용자 생성 API에서는 password가 필수인데, 같은 DTO를 재사용하는 수정 API에서는 비밀번호를 바꾸지 않는 경우가 대부분이라 password가 항상 있어야 한다는 검증이 오히려 방해가 됐습니다.
public class UserDto {
@NotBlank
private String email;
@NotBlank // 생성엔 필요하지만 수정엔 불필요
private String password;
}원인 분석
Bean Validation(jakarta.validation)의 애너테이션은 기본적으로 클래스 하나에 고정된 규칙을 정의합니다. 생성/수정처럼 같은 데이터 구조를 쓰지만 요구되는 필드가 다른 상황을 표현할 방법이 기본으로는 없어서, DTO를 두 개로 나누거나(중복 발생), 검증 애너테이션을 아예 빼고 서비스 로직에서 수동 검증을 하게 되는(선언적 검증의 이점을 잃음) 타협을 하게 됩니다.
해결 방안
Bean Validation은 Validation Groups라는 기능으로 이 상황을 위한 공식적인 해법을 제공합니다. 검증 규칙마다 "이 규칙이 어느 그룹에 속하는지"를 표시하고, 검증을 실행할 때 어느 그룹의 규칙만 적용할지 선택합니다.
public interface OnCreate {}
public interface OnUpdate {}
public class UserDto {
@NotBlank(groups = {OnCreate.class, OnUpdate.class})
private String email;
@NotBlank(groups = OnCreate.class) // 생성 그룹에서만 필수
private String password;
}@PostMapping("/users")
public void create(@Validated(OnCreate.class) @RequestBody UserDto dto) { ... }
@PutMapping("/users/{id}")
public void update(@Validated(OnUpdate.class) @RequestBody UserDto dto) { ... }@Valid 대신 @Validated(그룹.class)를 쓰면, 지정한 그룹에 속한 제약만 검사됩니다.
- Validation Groups는 편리하지만 남용하면 하나의 DTO가 여러 상황의 규칙을 동시에 짊어지며 점점 읽기 어려워질 수 있습니다 — 생성/수정의 필드 구성 자체가 크게 다르다면, 애초에 DTO를 분리하는 것이 더 명확한 설계일 수 있습니다.
- 그룹 간 상속(
interface OnUpdate extends Default)으로 공통 규칙과 개별 규칙을 조합할 수도 있습니다 — 기본 그룹(Default)에 공통 규칙을 두고 특수 상황만 별도 그룹으로 추가하는 식입니다. - 클래스 레벨 커스텀 검증(
@AssertTrue가 붙은 메서드 등) 안에서 특정 그룹에서만 유효한 로직을 넣고 싶다면, 그 검증 로직 자체도 그룹을 인지하도록 신경 써서 설계해야 합니다 — 그룹 지정이 필드 단위 애너테이션에만 자동으로 적용되는 게 아니라는 점을 유의합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.