💻문제점
게시글 등록 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` 발생한다.
'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 |