Etc

WebFlux.fn 에서 전역범위 Validator를 만들어보자.

Jungsoomin :) 2021. 4. 20. 19:04

목차

  1. 구상
  2. 제네릭 선언만으로 해결해보기
  3. 결과

 

1. 구상

WebFlux.fn 작성 중, WebFluxConfigurer 에 Validator 를 등록해보니 안먹는 것 같아 직접 머리를 굴려보았다.

=> 기존 Valiadtor 의 Scope 를 Global 단위로 올리고 싶다.

=> 모든 Handler 에 Validator 추가 코드를 넣기 싫다.

=> Validator 를 넣더라도 추상화 단위가 있음 좋겠다.

=> 추상화 단위에서 Handler 마다 각각 다른 Validator 를 지정해주고 싶다.

 

 

이렇게 생각한 이유를 보면..

 

=> 기존에 ReactiveChain 을 쓰다가 중복되는 코드가 싫어서 Builder 패턴을 생각해서 이렇게 만들었었다.

public interface HandlerHelper {
    Logger LOGGER = LoggerFactory.getLogger(HandlerHelper.class);

    default Mono<Long> castingPathVariable(ServerRequest request) {
        return Mono.just(request.pathVariable("id"))
                .map(Long::new)
                .filter(aLong -> aLong > 0);
    }

    default Mono<ServerResponse> checkParameter(Mono<ServerResponse> mono) {
        return mono.switchIfEmpty(Mono.error(new InvalidParameterException("음수 파라미터")))
                .doOnError(throwable -> LOGGER.warn("에러 발생, 메시지 : {}",throwable.getMessage()))
                .onErrorMap(throwable -> throwable instanceof NumberFormatException, e -> new InvalidParameterException("캐스팅 불가능한 파라미터"))
                .onErrorResume(throwable -> ServerResponse.status(HttpStatus.BAD_REQUEST).bodyValue(throwable.getMessage()));
    }
    default Mono<ServerResponse> checkBody(Mono<ServerResponse> mono) {
       return mono.doOnError(throwable -> LOGGER.warn("에러 발생, 메시지 : {}",throwable.getMessage()))
                .onErrorMap(throwable -> throwable instanceof ServerWebInputException, throwable -> new UnProcessableEntityException(throwable.getMessage()))
                .onErrorMap(throwable -> new UnProcessableEntityException(throwable.getMessage()))
                .onErrorResume(throwable -> ServerResponse.status(HttpStatus.UNPROCESSABLE_ENTITY).bodyValue(throwable.getMessage()));
    }
}

 

 

매핑 같은 경우는 ServerWebExceptionHandler..? 를 아직 구현못해서.. 이렇게 쓰고 있었다.

 

여기서 Validator 를 생각해보니.. 결국 Controller 단위 Validator 구현체는 "Domain 이랑 Validator 구현체만 다르구나" 싶더라.

Errors errors = new BeanPropertyBindingResult(domain, domainName);
            validator.validate(domain, errors);
            if (errors.hasErrors()) {
                String collect = errors.getAllErrors().get(0).getCodes()[0];
                throw new ServerWebInputException(collect);
            }

 

그래서 시작했다.


2. 제네릭 선언만으로 해결해보기

 

의존성 전파도란..말이 거창하지만, 변경사항이 크리티컬하게 Handler 들에게 전파되지 않기를 바랬다. 결과적으로 말하자면 Validator 바꾸면 바뀌기는 하는데..  무엇보다도 Handler 마다 소스코드에 Validator 넣기가 너무 싫었고..

  • 컨트롤러에 제네릭 선언 => <Validator 구현체, Domain>
  • Reflection 으로 상속받은 추상 클래스에서 자식클래스 제네릭에 맞는 Validator 를 생성하여 저장
  • 추상 클래스에서 validating() 추상화 
  • 상속 관계로 인해 Handler 클래스가 만들어지면, 추상클래스의 super() 생성자도 도니, 그것을 써보자고 생각

 

자식클래스에 달린 제네릭을 이용한 Reflection 코드

  • 자식클래스 => 제네릭에 사용할 Domain & Validator 선언
@Controller
@Slf4j
public class RoleHandler extends ValidatorHelper<RoleValidator, Role>  {

    private final RoleService roleService;

    public RoleHandler(RoleService roleService) {
        this.roleService = roleService;
    }

    public Mono<ServerResponse> provideAllRoles(ServerRequest request) {
        return roleService.processReadAll().flatMap(maps -> ServerResponse.status(HttpStatus.OK).bodyValue(maps))
                .onErrorResume(throwable -> ServerResponse.status(HttpStatus.BAD_REQUEST).bodyValue(throwable.getMessage()));
    }
...
}

 

  • 제네릭을 이용해 인스턴스를 만들어내는 ValidatorHelper 추상클래스 => 노력해보았으나 모자람.. ServerRequest#bodyToMono(Class) 를 구동시키기위해 Domain 제네릭도 클래스로 끌고 왔음.. 
public abstract class ValidatorHelper<Valid extends Validator, Domain> implements HandlerHelper {
    private Class<Domain> domainClass;
    private Valid validator;

    protected ValidatorHelper() {
        setValidator();
    }

    private void setValidator() {
        try {
            System.out.println("================================헬퍼로직 시작================================");
            // noinspection unchecked
            Class<Valid> clazz = (Class<Valid>) ClassUtils.getReclusiveGenericClass(this.getClass(), 0);
            System.out.println(clazz);
            domainClass = (Class<Domain>) ClassUtils.getReclusiveGenericClass(this.getClass(),1);
            if (domainClass != null) {
                this.validator = clazz.newInstance();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("체크 >>" + domainClass );
            System.out.println("체크 >> "+validator);
            System.out.println("================================헬퍼로직 종료================================");
        }
    }

    protected Mono<Domain> validating(ServerRequest request, String domainName) {
        return request.bodyToMono(domainClass).doOnSuccess(domain -> {
            Errors errors = new BeanPropertyBindingResult(domain, domainName);
            validator.validate(domain, errors);
            if (errors.hasErrors()) {
                String collect = errors.getAllErrors().get(0).getCodes()[0];
                throw new ServerWebInputException(collect);
            }
        });
    }

}

 

  • 리플렉션 구동시키는 유틸
public class ClassUtils {
    private static final String TYPE_NAME_PREFIX = "class ";

    public static Class<?> getReclusiveGenericClass(Class<?> clazz, int index) {
        Class<?> targetClass = clazz;
        while (targetClass != null) {
            Class<?> genericClass = getGenericClass(targetClass, index);
            if (genericClass != null) {
                return genericClass;
            }
            targetClass = targetClass.getSuperclass();
        }
        return null;
    }

    public static Class<?> getGenericClass(Class<?> clazz, int index) {
        Type types[] = getParameterizedTypes(clazz);
        if ((types != null) && (types.length >= index)) { // 타입이 안나오거나, 인덱스보다 길이가 크다면
            try {
                return getClass(types[index]); //타입의 인덱스 넘김
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    static public Type[] getParameterizedTypes(Class<?> target) {
        Type[] types = getGenericType(target); // 제네릭 타입 추출
        if (types.length > 0 && types[0] != null) { //
            return ((ParameterizedType) types[0]).getActualTypeArguments();
        }
        return null;
    }

    static public Class<?> getClass(Type type) throws ClassNotFoundException {
        if (type instanceof Class) {
            return (Class) type;
        } else if (type instanceof ParameterizedType) {
            return getClass(((ParameterizedType) type).getRawType());
        } else if (type instanceof GenericArrayType) {
            Type componentType = ((GenericArrayType) type).getGenericComponentType();
            Class<?> componentClass = getClass(componentType);
            if (componentClass != null) {
                return Array.newInstance(componentClass, 0).getClass();
            }
        }
        String className = getClassName(type);
        if (className == null || className.isEmpty()) {
            return null;
        }
        return Class.forName(className);
    }

    static public String getClassName(Type type) {
        if (type == null) {
            return "";
        }
        String className = type.toString();
        if (className.startsWith(TYPE_NAME_PREFIX)) {
            className = className.substring(TYPE_NAME_PREFIX.length());
        }
        return className;
    }

    static public Type[] getGenericType(Class<?> target) {
        if (target == null) {
            return new Type[0];
        }
        Type[] types = target.getGenericInterfaces();
        if (types.length > 0) {
            return types;
        }
        Type type = target.getGenericSuperclass();
        if (type != null) {
            if (type instanceof ParameterizedType) {
                return new Type[]{type};
            }
        }
        return new Type[0];
    }
}

 

 

 

3. 결과

목적대로 제네릭을 가지고 스프링빈 초기화 과정 중 Handler Bean 생성 시에 상속 받은 추상클래스의 생성자에서 ValidatorClass 를 생성해서 저장하고, 요청을 보낼경우 Validator 구현체가 정상적으로 동작하게 됨.

 

=> 대표님께 평가를 받아보니, 사내에서 사용하기는 위험하다고 판단받았음, 스스로도 충분히 그렇다고 생각 중..

=> 그래도 기발하시다고 하신게 참 감사했음.

추상화를 해보겠다고 노력을 해봤는데... 참 재밌는 과정이라서 기록 :)

public Mono<ServerResponse> provideCreateOneRole(ServerRequest request) {
        Mono<ServerResponse> handlerMono =
                validating(request,"role")
                .flatMap(role -> ServerResponse.status(HttpStatus.OK).body(roleService.processCreateOne(role), Role.class));

        return checkBody(handlerMono);
    }