[이펙티브 자바] 제네릭 Item30 - 이왕이면 제네릭 메서드로 만들어라

이성훈·2022년 5월 21일
0

이펙티브 자바

목록 보기
15/17
post-thumbnail
post-custom-banner

자바5 전까지는 컬렉션에서 객체를 꺼낼 때마다 형변환(Casting)을 해줘야만 했다.

이는 런타임때 빈번하게 형변환 오류를 발생시키고는 했다.

자바5부터는 제네릭(Generic)을 지원한다.

제네릭을 통해, 컬렉션은 자신이 담을 수 있는 타입을 컴파일러에게 알려준다.

컴파일러는 알아서 형변환 코드를 추가할 수 있고,
엉뚱한 타입의 객체를 넣으려는 시도 또한 차단해준다.


제네릭을 통해, 코드가 간결하고 명확하며 안전한 프로그램을 만들 수 있다.


"5장 - 제네릭" 에서는,
위와 같은 혁신 기능을 제공하는 제네릭에 대해 서술한다.


  • Item26. 로 타입을 사용하지 말라.
  • Item27. 비검사 경고를 제거하라.
  • Item28. 배열보다는 리스트를 사용하라.
  • Item29. 이왕이면 제네릭 타입으로 만들라.
  • Item30. 이왕이면 제네릭 메서드로 만들어라.
  • Item31. 한정적 와일드카드를 사용해 API 유연성을 높이라.
  • Item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라.
  • Item33. 타입 안전 이종 컨테이너를 고려하라.




<"이왕이면 제네릭 메서드로 만들어라">


이번 아이템은 바로 직전 아이템과 문맥을 같이하고 있다.

클래스와 마찬가지로, 메서드 또한 일반 형태에서 제네릭 형태로 만들 수가 있다.

먼저 일반 메서드를 제네릭으로 만드는 법에 대해 알아본다.





#   제네릭 메서드로 만드는 방법


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

그러니까 형태로 입력을 받아서 코드에서 쓰고,
런타임 때 소거하는 형태의 메서드는 모두 제네릭이다.

대표적인 예시로 Collections의 '알고리즘' 메서드 (binarySearch, sort) 등이 있다.


제네릭 메서드로 작성하는 방법은 앞선 아이템의 제네릭 타입으로 작성하는 것과 유사하다.

일단 다음 예시를 살펴보자.

public class Union {

    // 코드 30-2 제네릭 메서드 (177쪽)
    public static Set union(Set s1, Set s2) {
        Set result = new HashSet(s1);
        result.addAll(s2);
        return result;
    }
}

위 코드는 두 집합을 입력으로 받아 합집합을 반환하는 코드이다.

컴파일 자체는 문제가 없지만, 타입 안전하지 않다는 문제가 있어 다음 두 줄에서 경고를 반환한다.

Set result = new HashSet(s1);
result.addAll(s2);

위의 HashSet과 addAll은 제네릭 형태의 메서드인데,
입력값이 타입 매개변수가 아닌 그냥 raw type 형태로 호출하고 있어 문제가 발생하는 것이다.


위의 예시를 통해 일반 메서드를 제네릭 메서드로 반드는 방법을 알아보자.

  • 일단 입력과 반환의 원소 타입을 타입 매개변수로 지정한다.
  • 메서드 안에서도 이 타입 매개변수만 사용하게 수정한다.

이제 변형된 코드를 보자.

public class Union {

    // 코드 30-2 제네릭 메서드 (177쪽)
    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        Set<E> result = new HashSet<>(s1);
        result.addAll(s2);
        return result;
    }
}
    public static void main(String[] args) {
        Set<String> guys = Set.of("톰", "딕", "해리");
        Set<String> stooges = Set.of("래리", "모에", "컬리");
        Set<String> aflCio = union(guys, stooges);
        System.out.println(aflCio);
    }

이 메서드는 경고 없이 컴파일 되고, 타입 안전하며 쓰기도 쉽다.

클라이언트 코드에서 또한 직접 형변환 하지 않아도 오류나 경고가 없다.

프로그램을 실행하면 "[모에, 톰, 해리, 래리, 컬ㄹ, 딕]"이 출력된다.
(문제가 없다는 말이다.)





#   제네릭 싱글턴 팩토리 패턴


때때로 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있다.

<Comment.>

  • 말하자면 이런거다.
  • 불변 객체라는건 인스턴스화 되면 더이상 바뀌지 않기에 로직도 일정하다.
  • 근데 이런 로직을 사용할 때 여러가지 타입에서 사용할 수 있도록 하고 싶을 때가 있다는 거다.

보통 제네릭은 컴파일 단계가 아닌 런타임 단계에 타입 정보가 소거된다.

<Comment.>

  • 소거란, 원소 타입을 컴파일 타입에만 검사하고 런타임에는 해당 타입 정보를 알 수 없다는 것이다.
  • 한마디로, 컴파일 타임에만 타입 제약 조건을 정의하고, 런타임에는 타입을 제거한다는 뜻입니다.

그렇기에 하나의 객체를 어떤 타입으로든 매개변수화 할 수 있는 것이다.


하지만 이렇게 하려면 요청으로 들어온 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩토리를 만들어 줘야 한다.

이 패턴을 제네릭 싱글턴 팩터리라고 한다.


다음의 예시를 보자.

public static <T> Comparator<T> reverseOrder() {
// T 타입으로 캐스팅된 Singleton ReverseComparator 반환, 타입정보는 소거됨으로 그때그때 캐스팅만 되는 것
        return (Comparator<T>) ReverseComparator.REVERSE_ORDER; 
}
public static final <T> Set<T> emptySet() { 
  		return (Set<T>) EMPTY_SET; 
}

그냥 팩터리 메서드가 제네릭으로 만들어졌고, 반환할 때도 캐스팅 되서 반환되는거 정도가 차이이다.

위와 같은 방식으로 불변 객체를 타입에 맞게 여러번 재사용 할 수가 있다.



이번에는 항등함수 (identity function)를 담은 클래스를 고려해보자.

항등함수 객체는 상태가 없으므로 요청할 때마다 새로 생성하는 것은 낭비다.
(상태가 없을 수밖에 없다. 입력으로 들어온 것을 그대로 다시 내보내줘야 하니.)


자바의 제네릭이 실체화된다면 항등함수를 타입별로 하나씩 만들어야 했겠지만,
소거 방식을 사용한 덕에 제네릭 싱글턴 하나면 충분하다.


public class GenericSingletonFactory {
    // 코드 30-4 제네릭 싱글턴 팩터리 패턴 (178쪽)
    private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

    @SuppressWarnings("unchecked")
    public static <T> UnaryOperator<T> identityFunction() {
        return (UnaryOperator<T>) IDENTITY_FN;
    }

위 코드에서 T가 어떤 타입이든 UnaryOperator가 UnaryOperator가 아니기 때문에 경고가 발생한다.

하지만 항등함수란 입력 값을 수정 없이 그대로 반환하는 특별한 함수.

그러므로 T가 어떤 타입으로 들어오던 UnaryOperator를 사용해도 타입 안전하다는 것을 개발자는 안다.

그렇기에 SuppressWarinings 어노테이션을 추가해 경고를 지워줌으로써 깔끔하게 컴파일 할 수 있다.


위 코드에 대한 클라이언트 코드는 다음과 같다.

    // 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
    public static void main(String[] args) {
        String[] strings = { "삼베", "대마", "나일론" };
        UnaryOperator<String> sameString = identityFunction();
        for (String s : strings)
            System.out.println(sameString.apply(s));

        Number[] numbers = { 1, 2.0, 3L };
        UnaryOperator<Number> sameNumber = identityFunction();
        for (Number n : numbers)
            System.out.println(sameNumber.apply(n));
    }

위와 같은 방식으로,





#   재귀적 타입 한정


드문 경우긴 하지만, 자기 자신이 들어간 표현식을 사용해 타입 매개변수의 허용 범위를 한정할 수 있다.

이를 재귀적 타입 한정 (Recursive type bound) 이라고 부른다.


재귀적 타입 한정은 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰인다.

다음은 그 예이다.

public interface Comparable <T>	{
  	int compare(T o);
}

위에서 타입 매개변수 T는,
Comparable를 구현한 타입이 비교할 수 있는 원소의 타입을 정의한다.

<Comment.>

  • 이게 뭔 소릴까 ㅎ.. 번역의 한계다.
  • 보다 이해하기 쉬운 내용은 다음 문장에서 등장한다.
  • 예를들어, Comparable을 구현한 것은 String이다.
  • 그랬을 때 String은 String과만 비교할 수 있기 때문에, 타입 매개변수 T가 한정되는 것이다.
  • 실제로 거의 모든 타입은 자신과 같은 타입의 원소와만 비교가 가능하다.

Comparable을 구현한 원소의 컬렉션을 입력받는 메서드들은,
(Comparable이라고 한다면 String을 입력받는 메서드들은..)

주로 그 원소들을 정렬, 혹은 검색, 최대/최소 구하기 등에 사용된다.

이 기능을 수행하려면 컬렉션에 담긴 모든 원소가 상호 비교될 수 있어야 한다.


쉽게 말해, 애초에 원소들을 서로 비교하려면 그 제약조건이 내재되어 있으니 코드에서 명시하자는게 하고자 하는 말이다.

다음 예시를 보자.

public static <E extends Comparable<E>> E max(Collection<E> c);

타입 한정인 <E extends Comparable> 는 다시 말해 Comparable를 구현한 E만 받을 수 있다는 뜻이다.

타입 제한을 아주 정확하게 표현했다고 할 수 있다.

좀 더 전체적인 예시를 보자.

public class RecursiveTypeBound {
    // 코드 30-7 컬렉션에서 최댓값을 반환한다. - 재귀적 타입 한정 사용 (179쪽)
    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));
    }
}

위 코드는 컬렉션에 담긴 원소에 순서를 기준으로 최댓값을 계산한다.

컴파일 오류나 경고는 발생하지 않는다.


재귀적 타입 한정은 프로그램을 복잡하게 할 가능성도 있지만, 그런 일은 잘 일어나지 않는다.




지금까지 메서드를 제네릭으로 만드는 방법에 대해 알아보았다.

필자의 코멘트로 마무리를 한다.

<Item30 정리>

  • 제네릭 타입과 마찬가지로, 제네릭 메서드가 더 안전하며 사용하기 쉽다.
  • 타입과 마찬가지로 메서드도 형변환 없이 사용할 수 있는 편이 좋다.
  • 형변환을 해줘야 하는 기존 메서드는 제네릭으로 변경하자.

profile
IT 지식 공간
post-custom-banner

0개의 댓글