Effective Java : 아이템30. 이왕이면 제네릭 메서드로 만들라

    들어가기 전

    이 글은 인프런 백기선님의 이펙티브 자바 강의를 복습하며 작성한 글입니다. 


    이 글의 요약

    • 유틸리티 클래스에서 매개변수화 타입을 받는 정적 유틸리티 메서드는 제네릭 타입을 사용하면 좋다. 
    • 유틸리티 메서드는 주로 Collection 타입이 오는데, 이 때는 제네릭 타입으로 만들자. Collection을 Raw 타입으로 받을 경우 타입 안정성 / 표현력이 떨어져서 런타임 에러가 발생할 수 있는데 이 부분을 해결해준다.
    • 싱글턴 팩토리에서 다양한 타입의 인스턴스로 반환해야 하는 경우, 제네릭 싱글톤 팩토리로 만들면 코드의 양을 줄일 수 있다. 주로 동일한 인스턴스에서 타입에 따라 다른 역할을 할 수 있을 때 제네릭 싱글톤 팩토리를 사용한다. 
    • 재귀적 한정 타입은 주로 Comparable과 관련된 기능을 지원할 때 주로 사용한다. 주된 예시는 <E extends Comparable<E>>같은 것이고, String이 대표적인 예가 된다. 

    아이템 30. 이왕이면 제네릭 메서드로 만들라

    • 매개변수화 타입을 받는 정적 유틸리티 메서드
      • 한정적 와일드카드 타입(아이템 31)을 사용하면 더 유연하게 개선할 수 있다. 
    • 제네릭 싱글턴 팩터리
      • (소거 방식이기 때문에) 불변 객체 하나를 어떤 타입으로든 매개변수화 할 수 있다.
    • 재귀적 타입 한정
      • 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정한다. 

    이 글에서는 제네릭 메서드를 이용하면 좋은 경우를 살펴보고자 한다. 제네릭 메서드를 사용했을 때 좋은 경우는 두 가지가 있다. 이 내용에 대해서 아래에서 계속 살펴본다. 

    1. 매개변수화 타입을 매개변수로 받는 메서드를 구현할 때 사용 
    2. 제네릭 싱글턴 팩토리에서 사용 

    매개변수화 타입을 매개변수로 받는 정적 유틸리티 메서드 

    유틸리티 클래스에서 유틸리티 메서드를 만들 때, Collection들을 매개변수로 받는 경우라면 일반적으로 제네릭 유틸리티 메서드로 만드는 경우가 많다. 특히 Collections 클래스들은 모두 유틸리티성 메서드가 전부 제네릭 메서드로 되어있다. 이처럼 제네릭 메서드로 만드는 이유는 '컴파일 시점에 타입 안정성을 보장할 수 있기 때문'이다. 

     

    아래 코드를 살펴보자.

    • union() 메서드는 인자로 Raw 타입을 받는다. 
    • 사용하는 쪽에서도 Raw 타입으로 작성한다. 

    이렇게 작성된 코드는 컴파일 단계에서는 문제가 없다. 왜냐하면 Raw 타입은 Object 타입으로 동작하고, Object 타입에는 String / Integer가 모두 들어갈 수 있기 때문이다. 여기서 문제가 되는 부분은 o를 String으로 캐스팅 할 때 발생한다. 여기서 발생하는 에러는 Runtime Exception이다. 따라서 프로그램의 안정성에 나쁜 영향을 준다. 

    // Generic Union 메서드의 테스트 프로그램 (177쪽)
    // 유틸리티 클래스 Union
    public class Union {
    
        // 유틸리티 메서드 union()
        // 코드 30-2 제네릭 메서드 (177쪽)
        public static Set union(Set s1, Set s2) {
            Set result = new HashSet(s1);
            result.addAll(s2);
            return result;
        }
    
        // 코드 30-3 제네릭 메서드를 활용하는 간단한 프로그램 (177쪽)
        public static void main(String[] args) {
            Set guys = Set.of("톰", "딕", "해리");
            // Set<String> stooges = Set.of("래리", "모에", "컬리");
            Set stooges = Set.of(1, 2, 3);
            Set all = union(guys, stooges);
    
            for (Object o : all) {
                System.out.println((String)o); // 런타임, classCastException 발생.
            }
        }
    }

    이런 문제를 컴파일 시점에 방지하기 위해서는 제네릭 타입을 사용한 메서드를 정의하면 된다. 아래 코드처럼 사용하면 된다. 다음과 같이 코드를 작성하면, 제네릭을 사용하면서 얻는 이점과 동일하다.

    1. 컴파일 시점에 캐스팅 관련 문제를 빨리 알 수 있음. 
    2. 컴파일러가 자동으로 타입 추론을 해서 캐스팅 해줌. 따라서 타입 변환 코드를 사용하지 않아도 됨. 
    // Generic Union 메서드의 테스트 프로그램 (177쪽)
    // 유틸리티 클래스 Union
    public class Union {
    
        // 유틸리티 메서드 union()
        // 코드 30-2 제네릭 메서드 (177쪽)
        public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
            Set<E> result = new HashSet<E>(s1);
            result.addAll(s2);
            return result;
        }
    
        // 코드 30-3 제네릭 메서드를 활용하는 간단한 프로그램 (177쪽)
        public static void main(String[] args) {
            Set<String> guys = Set.of("톰", "딕", "해리");
            Set<String> stooges = Set.of("래리", "모에", "컬리");
            Set<String> all = union(guys, stooges);
    
            // 이렇게 할 경우 컴파일 에러 발생
            // Set<Integer> stooges = Set.of(1, 2, 3);
            // Set<String> all = union(guys, stooges);
    
            for (String o : all) {
                System.out.println(o); // 캐스팅 하지 않아도 됨.
            }
        }
    }

    제네릭 싱글턴 팩터리 

    싱글톤 팩터리는 싱글톤 인스턴스를 리턴하는 메서드다. 제네릭을 사용한 싱글톤 팩토리를 제네릭 싱글톤 팩토리라고 한다. 여러 타입의 인스턴스를 반환해야 하는 경우, 제네릭 싱글톤 팩토리를 이용하면 만들어야 하는 코드가 적어진다는 장점이 있다.

    아래 코드는 일반적인 싱글턴 팩토리 패턴이다. 

    • stringIdentityFunction() / integerIdentityFunction()을 메서드를 사용해서 각각의 Function 인스턴스를 반환한다. 

    실제로 봤을 때 함수가 하는 일은 동일하다. 값 t가 들어오면 그대로 반환하는 동작을 한다. 가장 중요한 점은 제네릭은 소거 방식이기 때문에 Function<String, String>은 컴파일 이후 Function으로 되어 기존의 제네릭 타입 정보가 없어진다. 하는 일이 동일하고 타입이 동일하기 때문에 같은 객체로 볼 수 있다. 이런 경우에는 제네릭을 사용해서 코드 양을 줄일 수 있다.

    // 제네릭 싱글턴 팩토리 패턴 (178쪽)
    public class GenericSingletonFactory {
    
        public static Function<String, String> stringIdentityFunction() {
            return (t) -> t;
        }
    
        public static Function<Number, Number> integerIdentityFunction() {
            return (t) -> t;
        }
    
        // 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
        public static void main(String[] args) {
            String[] strings = {"삼베", "대마", "나일론"};
            Function<String, String> sameString = stringIdentityFunction();
            for (String string : strings) {
                System.out.println(sameString.apply(string));
            }
    
            Number[] numbers = {1, 2.0, 3L};
            Function<Number, Number> sameNumber = integerIdentityFunction();
            for (Number n : numbers) {
                System.out.println(sameNumber.apply(n));
            }
        }
    }

    제네릭을 사용한 제네릭 싱글턴 팩토리 패턴은 아래 코드에 있다.

    • UnaryOperator<Object> 타입의 싱글턴 인스턴스를 생성한다. 여기서 Object를 사용한 이유는 제네릭 <T>는 컴파일 이후 바이트 코드 상에서는 Object로 표현되기 때문이다. 만약 <T extends Number> 같은 한정적 타입 매개변수를 사용할 것이라면 UnaryOperator<Number>로 선언해야한다.
    • identityFuction()은 팩토리 메서드다. 여기서 제네릭을 사용하도록 하고, 반환하기 전 형변환을 해서 반환해준다. 클라이언트에서 반환 받을 타입을 UnaryOperator<String>으로 하면 <T>는 <String>이 된다. 

    사실상 제네릭 <T>는 컴파일 이후 소거가 되어 바이트 코드 상에서는 Object가 된다. <T>라는 것은 컴파일 이후에 컴파일러가 바이트코드에 우리가 원하는 타입이 어떤 건지 알고 있고, 타입 캐스팅하는 코드를 추가해주는 역할을 한다. 

    // 제네릭 싱글턴 팩토리 패턴 (178쪽)
    public class GenericSingletonFactory {
    
        // 코드 30-4 제네릭 싱글턴 팩터리 패턴 (178쪽)
        public static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
    
        @SuppressWarnings("unchecked")
        public static <T> UnaryOperator<T> identityFunction() {
            return (UnaryOperator<T>) IDENTITY_FN;
        }
    
        // 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
        public static void main(String[] args) {
            String[] strings = {"삼베", "대마", "나일론"};
            Function<String, String> sameString = identityFunction();
            for (String string : strings) {
                System.out.println(sameString.apply(string));
            }
    
            Number[] numbers = {1, 2.0, 3L};
            Function<Number, Number> sameNumber = identityFunction();
            for (Number n : numbers) {
                System.out.println(sameNumber.apply(n));
            }
        }
    }

     


    재귀적 타입 한정

    재귀적 타입한정은 제네릭에 자기 자신이 한번 더 언급된다. 아래 코드에서 볼 수 있다. 

    • 가장 자주 사용되는 것은 <E extends Comparable<E>> 같은 것이다. 주로 객체끼리 비교해야 하는 제네릭 메서드에서 사용한다. 따라서 자주 사용되지는 않는다.
    • <E extends Comparable<E>>는 Comparable<E> 타입을 구현한 E 클래스를 받는다. 예를 들면 String이다. 
    // 재귀적 타입 한정을 이용해 상호 비교할 수 있음을 표현 (179쪽)
    public class RecursiveTypeBound {
    
        public static <E extends Comparable<E>> E max(Collection<E> c) {
            if (c.isEmpty())
                throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
    
            E result = null;
            for (E e : c)
                if (result == null || e.compareTo(result) > 0 )
                    result = Objects.requireNonNull(e);
    
            return result;
        }
    
        public static void main(String[] args) {
            List<String> argList = Arrays.asList(args);
            System.out.println(max(argList));
        }
    }

    댓글

    Designed by JB FACTORY