ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ParameterizedTypeReference (feat. Super Type Token)
    Java/Spring Framework 2020. 11. 26. 18:09

      서론

      스프링MVC에서 보통 API 통신은 RestTemplate을 활용해서 사용한다. restTemplate의 메소드 중 exchange를 많이 쓰고 아래 형식처럼 사용하고 있다.

    final List<Member> response = restTemplate.exchange(
            "http://localhost:18080/members",
            HttpMethod.GET,
            null,
            new ParameterizedTypeReference<List<Member>>() {},
            kycRequestId.toString()).getBody();

      5개의 argument 중에 1개가 눈에 걸린다. 네 번째 argument인 new ParameterizedTypeReference<List<Member>>() {} 부분이다. 기존 코드들 복붙 해서 잘 돌고, 잘 쓰고 있긴 한데 왜 이렇게 쓰는지 모른 채 쓰고 있었다. 그러던 중 토비님의 유튜브에서 슈퍼 타입 토큰에 대한 걸 보았고 비로소 저걸 왜 저렇게 쓰는지 이해하게 되었다. 그래서 해당 내용을 바탕으로 정리해 보았다.

     

     


     

      일반적인 자식 클래스 생성 방법

      보통 부모 클래스를 상속받는 자식 클래스의 경우 따로 클래스를 선언해서 사용한다.

    package token.s0;
    
    import org.junit.Test;
    
    public class SubClass {
        static class Book {
            String name;
        }
    
        // Novel은 Book을 상속
        static class Novel extends Book {
        }
    
        @Test
        public void test1() {
        	// 위에서 만든 Novel 클래스를 가지고 인스턴스 생성!
            Novel novel = new Novel();
        }
    }
    

      근데 만약 사용하는 곳이 한 군데뿐이라면 굳이 따로 클래스를 만들고 쓰고 할 필요가 없다. 익명 자식 클래스를 선언하면서 만들 수 있다. (마치 함수형 인터페이스를 일일이 구현하지 않고 람다로 코드 조각을 넘기는 것처럼 느껴진다.)

     

      익명 자식 클래스 생성 방법

      먼저 아래 코드를 살펴보자. getClass().getSuperClass()를 이용해서 부모 클래스를 출력해 보았다.

    package token.s1;
    
    import org.junit.Test;
    
    public class SuperClassTest {
    
        static class Book {}
    
        static class Novel extends Book {
            int price;
    
            Novel(int price) {
                this.price = price;
            }
    
            public int getPrice() {
                return price;
            }
        }
    
        @Test
        public void test1() {
            Book book = new Book();
            System.out.println(book.getClass().getSuperclass());
            // 출력: class java.lang.Object
        }
    
        @Test
        public void test2() {
            Book book = new Book() {};
            System.out.println(book.getClass().getSuperclass());
            // 출력: class token.s1.SuperClassTest$Book
        }
    
        @Test
        public void test3() {
            Book book = new Novel(5000);
            System.out.println(book.getClass().getSuperclass());
            // 출력: class token.s1.SuperClassTest$Book
        }
    
        @Test
        public void test4() {
            Book book = new Novel(6000) {
                @Override
                public int getPrice() {
                    return super.getPrice() + 1000;
                }
            };
            System.out.println(book.getClass().getSuperclass());
            // 출력: class token.s1.SuperClassTest$Novel
        }
    }
    

    test1()

      Book은 따로 상속하는 클래스가 없으니, new Book()의 super 클래스는 모든 클래스의 최상위 클래스인 Object가 나왔다.

    test2()

      클래스 생성 부분에 살짝 이상한 게 있다. new Book() 뒤에 중괄호가 들어가 있다. 이는 자바에서 익명 자식 클래스를 만드는 문법이다. (필자도 이번에 처음 알았다) 아무튼 그러면 Book의 자식 클래스를 만들었고 그것의 super 클래스인 Book이 나왔다.

    test3()

      Book을 상속한 Novel을 생성하였다. 이의 super 클래스는 Book이다.

    test4()

        익명 자식 클래스 생성에 {}만 있기보다는 오버라이딩하는 예제가 있으면 좋겠다 싶어서 넣었다. 저런 식으로 getPrice()를 재정의 가능하다. 이번엔 Novel의 익명 하위 클래스를 만들었으니 이의 super 클래스는 Novel이다.

     

      이상으로 익명 자식 클래스를 만드는 방법들을 살펴보았다.

     

     

    Java Generic Type Erasure (자바 제네릭 타입 소거)

      제네릭은 자바 5에 도입되었다. 그리고 자바는 하위 호환성을 매우 중요하게 여긴다. 이 둘을 만족시키기 위해 제네릭 정보는 컴파일되면 사라진다. 아래 코드는 선언된 필드의 타입 값을 출력한다.

    package token.s2;
    
    import org.junit.Test;
    
    import java.util.List;
    
    /**
     * Type erasure
     */
    public class GenericTypeTest {
    
        static class TClass<T> {
            T value;
        }
    
        @Test
        public void test1() throws NoSuchFieldException {
            TClass<String> obj = new TClass<>();
            System.out.println(obj.getClass().getDeclaredField("value").getType());
            // 출력: class java.lang.Object
        }
    
        @Test
        public void test2() throws NoSuchFieldException {
            TClass<List<String>> obj = new TClass<>();
            System.out.println(obj.getClass().getDeclaredField("value").getType());
            // 출력: class java.lang.Object
        }
    }
    

      TClass에 정의된 "value"라는 이름의 필드 타입을 출력해 보았다. test1과 test2에서 객체 생성 시에 각각 String, List<String>로 넣었는데 결과는 Object이다! 객체 생성 시에 제네릭 값으로 준 String, List<String>을 뽑을 수는 없는 걸까? (왜 뽑아야 하는지는 뒤에 나옴)

     

     

    Super Type Token

      위의 문제를 해결하기 위해 나온 기법이 Super Type Token이라고 한다. (닐 개프터라는 분이 만든 거라고 하고, 스프링 소스에 주석으로 이분의 블로그 글이 링크되있다.)

      그럼 Type Token은 뭘까? 쉽게 말해서 String.class, List.class 같은 것들을 클래스 리터럴이라 하고 Class<String>, Class<List>들이 타입 토큰이다. 메소드 호출 시 String.class를 넘기면 받는 쪽에서는 Class<String>의 형태로 받을 수 있다.

      타입이 다 소거(erasure)되어 버리는 문제를 어떻게 해결할 수 있을까? 상속 구조에서 제네릭 정보를 넘기면 해결할 수 있다. 아래 코드를 살펴보자.

    package token.s3;
    
    import org.junit.Test;
    
    import java.util.List;
    import java.util.Set;
    
    public class GenericSuperTypeTest {
        static class Book<T> {
            T t;
        }
    
        static class Novel extends Book<List<String>> {}
    
        @Test
        public void test1() throws NoSuchFieldException {
            Novel novel = new Novel();
            System.out.println(novel.getClass().getGenericSuperclass());
            // 출력: token.s3.GenericSuperTypeTest$Book<java.util.List<java.lang.String>>
        }
    
        @Test
        public void test2() throws NoSuchFieldException {
            Book book = new Book<Set<String>>() {};
            System.out.println(book.getClass().getGenericSuperclass());
            // 출력: token.s3.GenericSuperTypeTest$Book<java.util.Set<java.lang.String>>
        }
    }
    

    test1()

      Book은 클래스 레벨에서 T 타입을 정의하고 변수도 하나 가지고 있다. Novel은 Book을 List<String> 타입을 주는 것으로 선언되어 있다. Novel을 생성하고 novel.getClass().getGenericSuperclass()를 호출하면 Novel의 부모 클래스의 제네릭 정보를 가져올 수 있다. 결과는 Object가 아닌 java.util.List<java.lang.String>가 나온다!

    test2()

      익명 자식 클래스 생성 기법으로 Book에 Set<String>타입을 주는 자식 클래스를 생성하였다. 결과는 역시 원하는 대로 java.util.Set<java.lang.String>가 나왔다.

     

      ParameterizedTypeReference

      위에서 한 것처럼 따로 직접 구현할 필요는 없고 ParameterizedTypeReference를 이용하면 편리하다. 아래 코드를 살펴보자.

    package token.s4;
    
    import org.junit.Test;
    import org.springframework.core.ParameterizedTypeReference;
    
    import java.util.List;
    
    public class SuperTypeTest {
        private ParameterizedTypeReference<List<String>> p1 = new ParameterizedTypeReference<List<String>>() {};
    
        @Test
        public void test1() {
            System.out.println(p1.getClass().getGenericSuperclass());
    	// 출력: org.springframework.core.ParameterizedTypeReference<java.util.List<java.lang.String>>
        }
    
        @Test
        public void test2() {
            System.out.println(p1.getType());
            // 출력: java.util.List<java.lang.String>
        }
    }
    

    List<String>타입을 넣어서 ParameterizedTypeReference의 자식 클래스를 생성했다.

    test1()

      p1의 super 클래스의 제네릭 정보는 java.util.List<java.lang.String>가 잘 나온다.

    test2()

      ParameterizedTypeReference가 제공하는 getType()을 호출해 봐도 똑같은 결과가 나온다.

     

     


    어디에 쓰이나?

      돌고 돌아 정확한 제네릭 정보를 가져오는 기법을 알아냈다. 토비님 유튜브에서는 직접 type safe한 맵을 구현 하면서 풀어 나갔다. 일반적인 상황에서 제네릭 정보를 정확하게 알아야 할 필요가 있을까? 토비님은 딱 한 군데 있다고 하셨고 그건 바로 RestTemplate의 응답 타입을 지정할 때라고 했다. 이제 많은 것을 알았으니 다시 처음의 그 코드를 보자.

    final List<Member> response = restTemplate.exchange(
            "http://localhost:18080/members",
            HttpMethod.GET,
            null,
            new ParameterizedTypeReference<List<Member>>() {},
            kycRequestId.toString()).getBody();

      오 이제 정확히 읽을 수 있게 되었다! ParameterizedTypeReference에 List<Member>타입을 줘서 익명 자식 클래스를 만들고 있었던 것이다. (참고로 ParameterizedTypeReference는 abstract 클래스라 직접 생성이 불가능하다) 이 정보를 바탕으로 API의 응답값을 List<Member> 타입으로 변환해서 받을 수 있는 것이다. 이제는 왜 이렇게 쓰고 어떻게 돌아가는지도 알게 되었다!

     

     

    참고

    댓글

Designed by Tistory.