Spring에서는 spring-data-jpa를 통해 공식적으로 Page 데이터 클래스를 제공한다. 하지만 sort 부분의 각 Entity 별 필드 유효성 검증이 이루어 지지 않아 내부에 따로 유효성 검증을 두어야 한다. 이에 대한 처리 방법에 대해 기록해본다.
선택지
ConstraintValidator를 통한 어노테이션 검증 처리
JSR 380
표준에 따른 검증 처리 방법을 사용한다.
AOP를 통한 유효하지 않은 필드 무효화 처리
Spring에서 제공하는
AOP
기능을 사용한다.
커스텀 DTOSpring 기본 제공 범위에서 사용 가능한 Pageable을 포기하므로 의사 결정에서 제외한다.
커스텀 ArgumentResolverSpring 기본 제공 범위 외 추가 설정으로 인한 리소스 낭비로 의사 결정에서 제외한다.
선택지 별 장단점
ConstraintValidator를 통한 어노테이션 검증 처리
메서드 파라미터 단 어노테이션 추가로 sort내 유효하지 않은 필드 Exception 발생
클래스단에
@Validated
를 붙여야 함
AOP를 통한 유효하지 않은 필드 무효화 처리
메서드 단 어노테이션 추가로 sort 내 유효하지 않은 필드 제거 후 요청 처리
제약 설정이 간단함
두 방식의 차이점
[1] 방법은 메서드 파라미터에 어노테이션을 추가하지만
[2] 방법은 메서드 상단에 어노테이션을 추가한다.
[1] 방법은 유효하지 않은 필드를 Exception 처리하지만
[2] 방법은 유효하지 않은 필드를 제거한 뒤 정상 처리한다.
[1] : /page?sort=idd,asc → ConstraintViolationException 발생!!
[2] : /page?sort=idd,asc → idd 항목 sort 제외 후 정상 처리
[1] 방법은 사용하는 클래스 상단에 @Validated 어노테이션을 붙인다.
[2] 방법은 붙이지 않는다.
구현 방법
ConstraintValidator를 통한 어노테이션 검증 처리
Controller.java
@RestController @RequestMapping("/api") @Validated public class Controller { @GetMapping public Page<Response> findList( @PageableConstraint(DomainEntity.class) Pageable pageable) { ... } }
PageableConstraint.java
@Documented @Constraint(validatedBy = PageableValidator.class) @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PageableConstraint { Class<?>[] value() default {}; Class<?>[] groups() default {}; Class<?>[] payload() default {}; }
PageableValidator.java
public class PageableValidator implements ConstraintValidator<PageableConstraint, Pageable> { private Class<?>[] entityClasses; public void initialize(PageableConstraint constraintAnnotation) { this.entityClasses = constraintAnnotation.value(); } @Override public boolean isValid(Pageable pageable, ConstraintValidatorContext context) { Sort sort = pageable.getSort(); for (Order order : sort.toList()) { String field = order.getProperty(); for (Class<?> entityClass : entityClasses) { if (isInvalidClass(entityClass, field)) { return false; } } } return true; } private boolean isInvalidClass(Class<?> entityClass, String field) { try { Class<?> clazz = entityClass.getDeclaredField(field).getType(); if (clazz.getAnnotation(Entity.class) != null) { return true; } return false; } catch (Exception e) { return true; } } }
AOP를 통한 유효하지 않은 필드 무효화 처리
Controller.java
@RestController @RequestMapping("/api") public class Controller { @GetMapping @PageableConstraint(DesignBlock.class) public Page<Response> findList(Pageable pageable) { ... } }
PageableConstraint.java
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface PageableConstraint { Class<?>[] value() default {}; }
PageableAspect.java
@Aspect @Component public class PageableAspect { private Class<?>[] entityClasses; @Around("@annotation(com.example.validation.pageable.PageableConstraint)") public void pageableValidation(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); PageableConstraint pageableConstraint = methodSignature.getMethod().getAnnotation(PageableConstraint.class); if (pageableConstraint != null) { entityClasses = pageableConstraint.value(); for (int i = 0; i < args.length; i++) { if (args[i] instanceof Pageable) { Pageable pageable = (Pageable)args[i]; Sort sort = pageable.getSort(); List<Order> list = sort.filter(this::isValid).toList(); args[i] = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(list)); } } } joinPoint.proceed(args); } private boolean isValid(Order order) { String field = order.getProperty(); for (Class<?> entityClass : entityClasses) { try { Class<?> clazz = entityClass.getDeclaredField(field).getType(); if (clazz.getAnnotation(Entity.class) != null) { return false; } } catch (Exception e) { return false; } } return true; } }