1. 배경

중고판매 웹 사이트를 레퍼런스로 한 프로젝트의 상품 수정 api를 만들던 도중 생성 api의 dto의 유효성 검증 규칙이 많이 겹치는 부분에서 custom Valid를 적용할지에 대한 정보를 찾으며 경험한 과정을 작성합니다. 

 

2. Java Bean Validation 정의 

custom valid 적용 의사결정에 앞서 Java Bean Validation이 무엇인지 먼저 알아봅니다. 

출처: https://docs.spring.io/spring-framework/reference/core/validation/beanvalidation.html

 

Java Bean Validation :: Spring Framework

Bean Validation provides a common way of validation through constraint declaration and metadata for Java applications. To use it, you annotate domain model properties with declarative validation constraints which are then enforced by the runtime. There are

docs.spring.io

 

Bean Validation은 객체의 속성이나 메서드 매개변수에 제약 조건을 선언하고 런타임에서 자동으로 검증해 주는 기능입니다. 

valid 동작 흐름은 다음과 같습니다. 

1. Spring이 @Valid 발견
2. Bean Validation 실행
3. 하나라도 실패 → MethodArgumentNotValidException 발생
4. 컨트롤러 메서드는 실행 ❌

 

이러한 구조 덕분에 컨트롤러 실행 이전에 에러를 감지할 수 있게 됩니다. 

따라서 요청이 잘못된 것은 컨트롤러 도착 이전에 처리,

비즈니스 실패는 서비스에서 담당하도록 분리된 구조를 만들 수 있습니다. 

또한 컨트롤러는 “이미 정상적인 입력만 온다” 라는 전제를 가지므로 잘못된 요청은 아예 내부 로직에 접근할 수 없어 아키텍처 안정성을 보장할 수 있게 됩니다. 

 

Bean Validation의 사용 목적 

Bean Validation는 null 체크, 범위 등과 같은 요청 값 자체가 잘못된 경우에 사용합니다. 

@Min(1000)와 같은 상품 가격 정책을 제한하는 행위 즉 도메인 규칙을 Validation으로 사용하는 것은 좋지 않은데 

Bean Validation은 Spring이나 JPA가 실행하는 시점에만 적용되기 때문입니다. 

 

3. Custom Validation 정의

@Min, @NotNull 과 같이 이미 정의되어있는 것 이외에 직접 규칙을 정의하여 validation을 만들 수 있는데 이것을 Custom Validation이라고 합니다.  사용 방법을 간단히 요약하면 다음과 같습니다. 

 

1. 우선 검증에 필요한 설정들을 선언합니다. 

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UsernameValidator.class)
public @interface Username {
    String message() default "유효하지 않은 이름";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

 

2. 검증 로직이 작성된 Validator 클래스를 생성합니다. 

public class UsernameValidator
        implements ConstraintValidator<Username, String> { 
        // 어떤 어노테이션을 처리하는지, 어떤 타입의 값을 검증하는지 설정 
	
    @Override
    // 실제 검증이 실행되는 메서드 
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true;
        return value.length() >= 3;
    }
}

 

3. DTO 적용 예시입니다. 

public class SignupRequest {
    @Username // 직접 정의한 custom validation을 사용합니다
    private String username;
}

 

위의 코드에서도 알 수 있듯 긴 검증 로직을 @Username이라는 어노테이션 하나만으로 해석할 수 있게 됩니다.

또한 동일한 규칙이 적용되는 필드에 재사용할 수 있습니다. 

 

Custom Valid 사용 주의점 

Bean Validation의 본래 목적이 흐려질 수 있습니다.

값이 유효한지를 검증하는 것이나, 위의 형태로는 도메인의 정책을 검증하게 됩니다. 

뿐만아니라 실패추적이 어려워질 수 있습니다. 

@NotNull 검증이 실패했을 경우 에러메세지와 함께 전달되므로 그 즉시 무엇이 잘못되었는지 파악할 수 있으나 

@Username의 로직에 길이 제한 뿐아니라 문자열 제한 등 여러 조건을 섞어두었다면 어느 부분 때문에 에러를 발생했는지 추적하기 어렵습니다. 

 

4. valid vs Custom Valid 

custom valid 도입을 고려하게된 코드를 가져왔습니다. 

@NotBlank(message = "상품명은 필수입니다.")
@Size(max = 100, message = "상품명은 100자를 초과할 수 없습니다.")
@Pattern(
    regexp = "^[a-zA-Z0-9가-힣\\s()\\-]+$",
    message = "상품명에 허용되지 않은 문자가 포함되어 있습니다."
)
private String name;

총 3개의 조건이 설정되어 있고 상품 등록과 수정 모두 같은 규칙이 적용되어야 했습니다.

 

  • @NotBlank, @Size, @Pattern를 하나의 어노테이션으로 만들면 어떨까? 

도입 전 각각의 장단점을 생각했습니다. 

한줄씩 해석하지 않아도 되는 가독성이 좋아진다는 점, 같은 규칙이 사용되는 곳에 쉽게 재사용할 수 있다는 장점이 있습니다. 

그러나 위에서 정리된 valid의 사용 목적에서는 조금 어긋난다는 생각이 들었습니다. 

Validator 열어서 코드를 따라가야 실제 조건을 알 수 있게되고 어느 조건에서 실패했는지 파악하기가 어려워집니다. 

  • 내가 생각한 가독성이 보기 좋은 것을 기준으로 생각한 것은 아닐까? 

@NotBlank,@Size는 이미 정의된 어노테이션으로 다른 개발자들이 추가 학습 없이도 바로 유추가 가능한 반면, 

직접 어노테이션을 만들경우 의도가 바로 보이지 않고 설명이 추가되어야 합니다. 

즉 가독성이란 보기 편한것이라는 것 이외에 의미를 바로 해석할 수 있는 코드를 의미한다는 것을 놓치고 있었습니다. 

  • API가 늘어나도 동일 규칙이 유지될까?

지금은 허용되지 않은 문자열 확인 규칙만 있지만, 금칙어 목록을 관리한다면 반드시 custom valid가 필요하고, 한번 만들어 두면 쉽게 수정되지 않으며 에러메세지 또한 '금칙어 위반'처럼 하나로 표현하기에 적합합니다. 

 

5. 결론

단순히 코드가 길어진다고해서 가독성을 해치는 것은 아니라는 것을 알게되었습니다.

재사용성을 고려할때도 얼마나 자주 사용되는가 뿐만 아니라 얼마나 자주 수정되는가를 함께 고려해야한다는 것 또한 알게되었습니다. 

따라서 입력값 검증을 위한 어노테이션은 유지, 여러 조건이 하나의 규칙으로 묶을 수 있는 금칙어 또는 이름 규칙을 검증한다면 custom valid를 사용합니다. 

+ Recent posts