파란하늘의 지식창고
반응형

이전에 RestTemplate을 사용하면서 List 같은 Collection 형태로 반환받기 위한 방법에 대해 소개하는 글을 썼다.

2018/11/16 - [Study/Java] - RestTemplate list 반환하기

 

RestTemplate list 반환하기

요청을 반환받는 클래스가 다음과 같다고 가정한다. @Data public class ResultClass { private long idx; private String name; } List로 리턴 받는 방법 - 문제가 있음. 비추천 List list = restTe..

luvstudy.tistory.com

해당 방법은 ParameterizedTypeReference를 사용해서 리턴 값을 설정하는 방법인데 대략 다음과 같이 사용한다.

 

List<SomeDomain> someDomainList = restTemplate.exchange("url", HttpMethod.GET, null, new ParameterizedTypeReference<List<SomeDomain>> {});

ParameterizedTypeReference의 Interface는 다음과 같다.

public abstract class ParameterizedTypeReference<T> {

    private final Type type;


    protected ParameterizedTypeReference() {
        Class<?> parameterizedTypeReferenceSubclass = findParameterizedTypeReferenceSubclass(getClass());
        Type type = parameterizedTypeReferenceSubclass.getGenericSuperclass();
        Assert.isInstanceOf(ParameterizedType.class, type, "Type must be a parameterized type");
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        Assert.isTrue(actualTypeArguments.length == 1, "Number of type arguments must be 1");
        this.type = actualTypeArguments[0];
    }

    private ParameterizedTypeReference(Type type) {
        this.type = type;
    }


    public Type getType() {
        return this.type;
    }

    @Override
    public boolean equals(@Nullable Object other) {
        return (this == other || (other instanceof ParameterizedTypeReference &&
                this.type.equals(((ParameterizedTypeReference<?>) other).type)));
    }

    @Override
    public int hashCode() {
        return this.type.hashCode();
    }

    @Override
    public String toString() {
        return "ParameterizedTypeReference<" + this.type + ">";
    }

    public static <T> ParameterizedTypeReference<T> forType(Type type) {
        return new ParameterizedTypeReference<T>(type) {
        };
    }

    private static Class<?> findParameterizedTypeReferenceSubclass(Class<?> child) {
        Class<?> parent = child.getSuperclass();
        if (Object.class == parent) {
            throw new IllegalStateException("Expected ParameterizedTypeReference superclass");
        }
        else if (ParameterizedTypeReference.class == parent) {
            return child;
        }
        else {
            return findParameterizedTypeReferenceSubclass(parent);
        }
    }

}

ParameterizedTypeReference는 Generic <T>를 통해 전달받은 ParameterizedType으로 restTemplate의 리턴 타입을 정의하는 역할을 한다.

ParameterizedType은 java.lang.reflect 패키지에 있으며 Collection 형태의 Class Type을 의미한다.

ParameterizedTypeReference를 사용할 때는 Collection 형태로 전달해야 하며 처음 소개한 예제처럼 사용하게 된다.

그런데 이 ParameterizedTypeReference를 다음과 같이 경우에 따라 다른 값을 쓰고 싶은 경우는 어떻게 해야 할까?

// 도메인 구조
public abstract class SuperDomain {
}

public class Sub1Domain extends SuperDomain {
}

public class Sub2Domain extends SuperDomain {
}

// 호출 method
public <T extends SuperDomain> List<T> getList() {
    return restTemplate.exchange("url", HttpMethod.GET, null, new ParameterizedTypeReference<List<T>> {});
}

// 호출 예
List<Sub1Domain> sub1DomainList = getList();
List<Sub2Domain> sub2DomainList = getList();

해당 예처럼 코드를 작성해도 compile 오류는 발생하지 않는다.

하지만 실행하면 runtime 시점에 class cast exception이 발생한다.

반환 값은 문제없이 캐스팅되는 선언이지만 ParameterizedTypeReference로 넘겨받은 T는 수행 시 T로 인식되어 생성자가 요구하는 ParameterizedType인 Type이 아니고 실제 Class Type을 알 수 없기 때문이다.

즉 ParameterizedTypeReference에 사용할 type은 컴파일 시점에 형 선언이 명확하게 되어있지 않으면 ParameterizedTypeReference을 통한 리턴 값 변환도 이루어지지 않는다.

Generic<T>를 외부에 위임해서 사용하려면 다음처럼 getType method를 override 하면 된다.

ResponseEntity<List<T>> response = getRestTemplate().exchange(
    "url"
    HttpMethod.GET, 
    null, 
    new ParameterizedTypeReference<List<T>>() {
        @Override
        public Type getType() {
            return parameterizedType;
        }
    }
);

위와 같이 사용하면 어디선가 가져온 type이 해당 호출의 리턴 값의 type이 된다.

아래는 class에 선언된 Generic Type을 가져와서 restTemplate method에서 사용하는 예제이다.

public abstract class SuperService<T extends superDomain> {
    public T getList() {
        Type type = ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        ParameterizedType parameterizedType = new ParameterizedType() {

            @Override
            public Type[] getActualTypeArguments() {
                return new Type[]{ type };
            }

            @Override
            public Type getRawType() {
                return List.class;
            }

            @Override
            public Type getOwnerType() {
                return null;
            }
			
        };
        ResponseEntity<List<T>> response = getRestTemplate().exchange(
                "url",
                HttpMethod.GET,
                null, 
                new ParameterizedTypeReference<List<T>>() {
                    @Override
                    public Type getType() {
                        return parameterizedType;
                    }
                }
            );
        return response != null ? response.getBody() : Collections.emptyList();
    }
}

public class Sub1Service extends SuperService<Sub1Domain> {
}

public class Sub2Service extends SuperService<Sub2Domain> {
}

위 예제에서는 ParameterizedTypeReference로 넘겨받던 ParameterizedType의 generic <T>를 service의 <T>를 통해 획득하고 해당 T를 가진 Collection type인 ParameterizedType을 만들어 getType method를 override 하여 리턴 값의 타입을 명시하였다.

위와 같은 경우 각각 상속받은 Sub1Service, Sub2Service에서는 반환 값이 List<Sub1Domain>, List<Sub2Domain>이 된다.

이런 식으로 동일 restTemplate을 통해 확장된 Domain을 사용할 수 있다.

위의 예제는 Collection 반환 값의 경우인데 Domain 반환 값인 경우는 좀 더 단순하다.

@SuppressWarnings("unchecked")
public T save(T tDomain) {
    Type type = ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    return getRestTemplate().postForObject("url", tDomain, (Class<T>) type);
}

generic type을 획득하는 부분은 Spring에서 제공하는 GenericTypeResolver를 사용하면 좀 더 유연하게 쓸 수 있다.

private Type getType() {
    return Arrays.asList(GenericTypeResolver.resolveTypeArguments(this.getClass(), SuperClass.class))
        .stream()
        .filter(x -> TypeUtils.isAssignable(SuperDomain.class, x))
        .findAny()
        .get();
}

참고 : 2018/11/14 - [Study/Java] - 자주 쓰는 spring util 기록

 

자주 쓰는 spring util 기록

자주 쓰는 유틸 목록 기록 계속 업데이트 할 예정 유틸은 알파벳 순서로 정렬 org.springframework.beans.BeanUtils T instantiateClass(Class clazz) 해당 class 로 Object 생성 A a = BeanUtils.instanti..

luvstudy.tistory.com

위의 예제는 확장한 서비스에서 공통 restTemplate method를 사용한 경우인데 Class<T> 또는 Type을 매개변수로 넘겨 사용할 수도 있다.

public class SampleService {
    public <T extends SuperDomain> T getList(Class<T> clazz) {
        Type type = (Class<T>) clazz;
        ParameterizedType parameterizedType = new ParameterizedType() {

            @Override
            public Type[] getActualTypeArguments() {
                return new Type[]{ type };
            }

            @Override
            public Type getRawType() {
                return List.class;
            }

            @Override
            public Type getOwnerType() {
                return null;
            }
        };

        ResponseEntity<List<T>> response = getRestTemplate().exchange(
                "url",
                HttpMethod.GET,
                null, 
                new ParameterizedTypeReference<List<T>>() {
                    @Override
                    public Type getType() {
                        return parameterizedType;
                    }
                }
            );
        return response != null ? response.getBody() : Collections.emptyList();
    }
}

List<Sub1Domain> sub1DomainList = getList(Sub1Domain.class);
List<Sub2Domain> sub2DomainList = getList(Sub2Domain.class);
반응형
profile

파란하늘의 지식창고

@Bluesky_

도움이 되었다면 광고를 클릭해주세요