[Java Spring Data JPA] Pageable의 sort와 Entity 필드 유효성 검증 추가하기

작성일 : 2023년 08월 18일
  • #Java
  • #Spring Boot
  • #jpa

Spring에서는 spring-data-jpa를 통해 공식적으로 Page 데이터 클래스를 제공한다. 하지만 sort 부분의 각 Entity 별 필드 유효성 검증이 이루어 지지 않아 내부에 따로 유효성 검증을 두어야 한다. 이에 대한 처리 방법에 대해 기록해본다.

선택지

  1. ConstraintValidator를 통한 어노테이션 검증 처리

    • JSR 380 표준에 따른 검증 처리 방법을 사용한다.

  2. AOP를 통한 유효하지 않은 필드 무효화 처리

    • Spring에서 제공하는 AOP 기능을 사용한다.

  3. 커스텀 DTO

    • Spring 기본 제공 범위에서 사용 가능한 Pageable을 포기하므로 의사 결정에서 제외한다.

  4. 커스텀 ArgumentResolver

    • Spring 기본 제공 범위 외 추가 설정으로 인한 리소스 낭비로 의사 결정에서 제외한다.

선택지 별 장단점

  1. ConstraintValidator를 통한 어노테이션 검증 처리

    1. 메서드 파라미터 단 어노테이션 추가로 sort내 유효하지 않은 필드 Exception 발생

    2. 클래스단에 @Validated를 붙여야 함

  2. AOP를 통한 유효하지 않은 필드 무효화 처리

    1. 메서드 단 어노테이션 추가로 sort 내 유효하지 않은 필드 제거 후 요청 처리

    2. 제약 설정이 간단함

두 방식의 차이점

  • [1] 방법은 메서드 파라미터에 어노테이션을 추가하지만

    [2] 방법은 메서드 상단에 어노테이션을 추가한다.

  • [1] 방법은 유효하지 않은 필드를 Exception 처리하지만

    [2] 방법은 유효하지 않은 필드를 제거한 뒤 정상 처리한다.

    • [1] : /page?sort=idd,asc → ConstraintViolationException 발생!!

    • [2] : /page?sort=idd,asc → idd 항목 sort 제외 후 정상 처리

  • [1] 방법은 사용하는 클래스 상단에 @Validated 어노테이션을 붙인다.

    [2] 방법은 붙이지 않는다.

구현 방법

  1. 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;
      		}
      	}
      }


  1. 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;
      	}
      }