Chef.Yeon
Code Cook
Chef.Yeon
전체 방문자
오늘
어제
  • 분류 전체보기 (230)
    • 게임 개발 (1)
      • Unity (1)
    • Android (27)
      • Kotlin (19)
      • 우아한테크코스 5기 (4)
    • Language (11)
      • 파이썬 (3)
      • Java (7)
    • DB (2)
      • SQL (16)
    • Spring (25)
    • 코딩테스트 (56)
    • Git (1)
    • TIL (85)
    • DevOps (6)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • til
  • 코틀린 인 액션
  • 프리코스
  • 내림차순
  • 레포지토리
  • SQL
  • grafana
  • 프로그래머스
  • 다이나믹 프로그래밍
  • Android
  • webflux
  • 에라토스테네스의 체
  • 파이썬
  • ec2
  • MariaDB
  • kotlin
  • spring
  • 코딩테스트
  • 백준
  • rsocket
  • java
  • kibana
  • enum
  • Wil
  • 안드로이드
  • elasticsearch
  • 우아한테크코스
  • 문자열
  • 코틀린
  • Docker

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Chef.Yeon

Code Cook

[TIL-231219] 사용자 정의 애노테이션을 사용한 List 요소 검증
TIL

[TIL-231219] 사용자 정의 애노테이션을 사용한 List 요소 검증

2024. 3. 20. 23:31

 

💻문제점

게시글 등록 API 요청 시에는 요청 바디에 title, content, imageUrls, tags 필드를 포함해야 한다.

public record PostAddRequest(
        @NotBlank @Length(min = 1, max = 20) String title,
        @NotBlank @Length(min = 1, max = 1000) String content,
        @Size(max = 5) List<String> imageUrls,
        @Size(max = 5) List<String> tags) {}

 

이러한 데이터를 받을 때, 각 항목에 대한 유효성 검증을 해야한다.

특히 tags와 같은 리스트 형태로 전달되는 여러 문자열 각각에 대한 길이 검증이 필요했다.

기존의 애노테이션을 사용한 검증 방식으로는 리스트의 각 요소에 대한 길이 검증을 수행하기 어려웠다.


📃고민

인터셉터 사용

처음에는 요청 전에 로직을 수행하기 위해 인터셉터 사용을 고려했다. 인터셉터는 AOP를 사용하여 검증 로직을 모듈화할 수 있다. 그러나, 기존 검증 메커니즘의 일관성을 유지하기 위해 다른 방법을 모색했다.

 

커스텀 검증 애노테이션

이미 `@Valid` 애노테이션을 통한 요청 객체 검증 방식과 예외 처리가 프로젝트에 정립되어 있었다.

커스텀 애노테이션을 사용하면 기존 검증 메커니즘과 일관성을 유지할 수 있고, `@RestControllerAdvice`를 통한 예외 처리가 구현되어 있다.


🔍해결

커스텀 애노테이션을 만들고 이를 처리한 Validator를 구현했다.

 

커스텀 애노테이션 `@StringElementLength` 

리스트 내 각 문자열 요소의 길이를 검증할 수 있는 애노테이션을 정의했다.

이 애노테이션은 `min`과 `max` 속성을 가지고, 이를 통해 각 문자열의 최소 및 최대 길이를 명시할 수 있다.

 

StringElementLength

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = ListStringValidator.class)
public @interface StringElementLength {
    int min() default 0;

    int max() default Integer.MAX_VALUE;

    String message() default "각 문자열은 길이가 {min}에서 {max} 사이여야 합니다.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

 

Validator 구현

`@StringElementLength` 애노테이션에 대한 실제 검증 로직은 `ListStringValidator` 클래스에서 구현했다.

이 클래스는 `ConstraintValidator` 인터페이스를 구현하여, `isValid` 메서드를 통해 각 문자열이 지정된 길이 조건을 충족하는지 검증한다.

 

ListStringValidator

public class ListStringValidator implements ConstraintValidator<StringElementLength, List<String>> {
    private int min;
    private int max;

    @Override
    public void initialize(StringElementLength constraintAnnotation) {
        this.min = constraintAnnotation.min();
        this.max = constraintAnnotation.max();
    }

    @Override
    public boolean isValid(List<String> values, ConstraintValidatorContext context) {
        if (values == null) {
            return false;
        }
        return values.stream().allMatch(value -> min <= value.length() && value.length() <= max);
    }
}

 

이 방식을 사용하여 리스트 내 각 문자열 요소의 길이를 검증하는 요구사항을 만족시켰다. 해당 커스텀 애노테이션을 통해 유사한 검증이 필요한 다른 상황에서도 간편하게 검증 로직을 적용할 수 있었다.

public record PostFormRequest(
        @NotBlank @Length(min = 1, max = 20) String title,
        @NotBlank @Length(min = 1, max = 1000) String content,
        @Size(max = 5) List<String> imageUrls,
        @Size(max = 5) @StringElementLength(min = 2, max = 10) List<String> tags) {}

💡알게 된 점

@Valid 동작 원리

클라이언트로부터 들어오는 요청은 디스패처 서블릿을 통해 Controller로 전달된다. 아때 컨트롤러에서 `@ResponseBody` 애노테이션을 사용하는 경우 `ReqeustResponseBodyMethodProcessor` 클래스가 해당 요청을 처리한다.

 

`ReqeustResponseBodyMethodProcessor` 클래스의 `resolveArgument()` 메서드 호출

 

해당 클래스의 `resolveArgument()` 메서드 내부에서 유효성 검증이 진행된다.

 

`AbstractMessageConverterMethodArgumentResolver`클래스의 `validateIfApplicable` 메서드를 호출

`ReqeustResponseBodyMethodProcessor` 클래스는 `AbstractMessageConverterMethodArgumentResolver` 추상 클래스를 상속받는다.

 

`ValidationAnnotationUtils`클래스의 `determineValidationHints` 메서드 호출

해당 메서드에서는 애노테이션이 jakarta.validation.Valid인지 Validated 인스턴스인지 Valid로 시작하는지 확인한 후 적용할 검증 힌드들을 Object Array로 반환한다.

 

`validateIfApplicable` 메서드에서 `DataBinder` 클래스의 `validate` 메서드 호출

 

해당 메서드는 데이터 바인딩 과정에서 객체 상태가 유효한지 확인하기 위해 Validator를 순회하고, 발견된 문제들은 `BindingResult` 객체에 수집된다.

 

이후 `BindingResult`에 Error가 있다면 `MethodArgumentNotValidException` 발생한다.

 

728x90

'TIL' 카테고리의 다른 글

[TIL - 20240612] Swagger HTTPS 설정  (0) 2024.06.12
[TIL - 20240612] Swagger Failed to load remote configuration 해결  (0) 2024.06.12
[TIL-20240307] 주간/월간/연간 섭취량 조회 API 성능 개선  (1) 2024.03.07
[TIL - 20240110] 컨트롤러 나누기?!  (0) 2024.01.10
[TIL - 20240103] Spring Data JPA @Modifying 문제  (0) 2024.01.03
    'TIL' 카테고리의 다른 글
    • [TIL - 20240612] Swagger HTTPS 설정
    • [TIL - 20240612] Swagger Failed to load remote configuration 해결
    • [TIL-20240307] 주간/월간/연간 섭취량 조회 API 성능 개선
    • [TIL - 20240110] 컨트롤러 나누기?!
    Chef.Yeon
    Chef.Yeon
    보기 좋고 깔끔한 코드를 요리하기 위해 노력하고 있습니다.

    티스토리툴바