목차
- 구상
- 제네릭 선언만으로 해결해보기
- 결과
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 생성 시에 상속 받은 추상클래스의 생성자에서 Validator 와 Class 를 생성해서 저장하고, 요청을 보낼경우 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);
}
'Etc' 카테고리의 다른 글
WebFlux 사양의 슬라이스 테스트 (0) | 2021.04.15 |
---|---|
Gradle 테스트 사이클에서 원하는 테스트 클래스를 실행하기 (0) | 2021.01.08 |
Jq ( JSON Parser ) 설치 (0) | 2021.01.02 |
수강하며 드는 개인적 생각을 모음 (0) | 2020.09.18 |