티스토리 뷰

728x90
반응형

제네릭과 가변인수를 함께 쓸 때는 신중하라


  • 가변인수 메서드는 메서드에 넘기는 인수의 갯수를 클라이언트가 조절할 수 있게해줍니다. 하지만 가변인수를 받는 메서드측에서는 가변인수를 담기위한 배열이 자동적으로 만들어지는데 이러한 가변인수 매개변수에 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생하게 됩니다. 이는 제네릭이나 매개변수화 타입은 실체화가 불가능한 타입이기 때문입니다. 또한 매개변수화 타입의 변수가 다른 타입의 객체를 참조하게 되면 힙 오염이 발생하게 됩니다.
  • 이는 대부분 제네렉과 매개변수화 타입은 실체화가 불가능한 타입이기 때문에 생기는 문제로 메서드를 선언할 때 실체화 불가 타입으로 가변인수 매개변수를 선언할 경우 컴파일러가 경고를 내보내는데, 경고 내용은 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다는 내용입니다.

 

💡가변인수를 담기위해 자동적으로 만들어지는 배열

public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        printVarargs("a", "b", "c", "d");
        printVarargsGeneric(List.of("a", "b", "c", "d"));
    }

    public static void printVarargs(String... strings) {
        System.out.println(strings.getClass().getSimpleName());

        System.out.println("-----------------");
        for(String s : strings) {
            System.out.println(s);
        }

    }

    public static void printVarargsGeneric(List<String>... strings) {
        System.out.println(strings.getClass().getSimpleName());

        System.out.println("-----------------");
        Arrays.stream(strings).forEach(System.out::println);

    }
}

💡힙 오염이 발생하는 코드

  • dangerous 메서든 List<String> 이라는 실체화 불가 타입을 가변인수로 사용하는 메서드인데, 내부를 보면 명시적으로 형변환을 하는 부분은 없지만 stringLists의 원소는 매개변수화 타입이기 때문에 컴파일시 컴파일러가 형변환을 자동적으로 해줍니다. 그렇기 때문에 intLists의 숫자를 String으로 컴파일러가 형변환을 시도하다가 ClassCastException을 발생시킵니다. 즉 타입 안전성이 깨지기 때문에 제네릭 가변인수 배열 매개변수에 값을 저장하는것은 불안합니다.
public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        dangerous(List.of("a", "b", "c"));
    }

    static void dangerous(List<String>... stringLists) {
        List<Integer> intLists = List.of(42);

        Object[] objects = stringLists;
        objects[0] = intLists;            // 힙 오염
        String s = stringLists[0].get(0); // ClassCastException
    }
}

 

💡제네릭 가변인수 매개변수를 허용해 주는 이유는?

  • 실무에서는 유용하다고 하기 때문입니다.
  • 아래 유틸리티 클래스에는 제네릭 가변인수 매개변수를 받는 메서드들이 있습니다. 물론 해당 메서드는 타입이 안전합니다.
public class Arrays {

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
}

public class Collections {

    @SafeVarargs
    public static <T> boolean addAll(Collection<? super T> c, T... elements) {
        boolean result = false;
        for (T element : elements)
            result |= c.add(element);
        return result;
    }
}

 

💡@SafeVarags

  • 자바 7에서 추가된 @SafeVarags 어노테이션은 제네릭 가변인수 메서드 작성자가 그 클라이언트 측에서 발생하는 경고를 숨길 수 있도록 해줍니다. 이 어노테이션은 그 메서드가 타입 안전하다는 것을 보장한다는 의미로 컴파일러는 이러한 약속을 믿고 경고를 내보내지 않습니다.

💡제네릭 가변인수 메서드가 안전한지 확인하는 방법

  • 가변인수 메서드를 호출할 때 위에서 매개변수 담을 제네릭 배열이 만들어진다는 사실을 알게되었습니다. 이렇게 생성된 배열에 아무것도 저장하지 않고(그 매개변수를 덮어쓰지 않고), 그 배열의 참조가 밖으로 노출되지 않는다면 타입이 안전합니다.

 

🧨 힙 오염 발생

  • 가변인수 배열에 아무것도 저장하지 않고도 타입 안정성이 깨질 수 있는데 주의해야 합니다.
  • 다음 코드는 가변인수로 넘어온 매개변수들을 배열에 담아 반환하는 제네릭 메서드입니다. 얼핏 보면 편리한 유틸리티로 보이지만 위험이 도사리고 있습니다.
  • 이 메서드가 반환하는 배열의 타입은 이 메서드에 인수를 넘기는 컴파일 타임에 결정이 되는데, 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있습니다. 따라서 자신의 가변인수 매개변수 배열을 그대로 반환하면 힙 오염을 이 메서드를 호출한 쪽의 콜스택까지 전이하는 결과를 낳을 수 있습니다.
public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        String[] strings = pickTwo("one", "two", "three");
        System.out.println("strings = " + strings);
    }

	// 힙 오염이 발생하는 코드
    static <T> T[] toArray(T... args) {
        return args;
    }

    static <T> T[] pickTwo(T a, T b, T c) {
        switch(ThreadLocalRandom.current().nextInt(3)) {
            case 0: return toArray(a, b);
            case 1: return toArray(b, c);
            case 2: return toArray(a, c);
        }
        throw new AssertionError(); //도달할 수 없다.
    }
}

  • 우리가 기대하기로는 one, tow, three라는 문자열 중 랜덤으로 2개가 선택되어 반환되어야할거 같지만 실행해보면 ClassCastException이 발생하게 됩니다.
  • 우선 pickTwo 메서드는 T 인스턴스 2개를 담은 매개변수 배열을 만들어 반환하는 코드입니다. 이 메서드의 결과는 항상 Object[] 일수밖에 없는데 pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이기 때문입니다. 그래서 pickTwo() -> toArray()로 진행되는 호출의 결과는 항상 Object[] 배열 타입을 반환합니다.
  • 그런데 위 main 로직에서는 pickTwo("one", "two", "three")는 String 타입을 인수타입으로 전달하며, 반환 값을 String[] 배열에 저장할려고 합니다. 그렇기 때문에 컴파일러는 String[]으로 형변환할려는 코드를 자동으로 생성해주는데, 실제로 반환되는 값은 Object[] 타입이기 때문에 ClassCastException을 발생시키는 것입니다.
  • 이 예제를 통해 제네릭 가변인수 매개변수 배열에 다른 메서드가 접근하도록 허용한다면 안전하지 않다라는 것을 알 수 있게 되었습니다. 물론 다음과 같은 2경우는 제외 대상입니다.
    • @SafeVarargs 어노테이션이 붙어있는 안전한 메서드
    • 가변인수 매개변수 배열을 넘기지 않아도 되는 일반 함수

 

💡올바른 사용법

  • 다음은 리스트를 가변인수 매개변수로 받아 내부의 값을 꺼내 하나의 리스트로 만들어 반환하는 flatten 메서드입니다.
public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        List<Integer> list1 = new ArrayList<>(Arrays.asList(1, 2, 3));
        List<Integer> list2 = new ArrayList<>(Arrays.asList(4, 5, 6));
        flatten(list1, list2).forEach(System.out::println);
    }

    static <T> List<T> flatten(List<? extends T>... lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists) {
            result.addAll(list);
        }
        return result;
    }
}

 

💡정리

  • 가변인수와 제네릭은 둘 다 자바 5버전에서 나왔지만 궁합이 좋지 않습니다.
  • 가변인수 기능은 배열을 노출하기에 추상화가 완벽하지 않습니다.
  • 하지만 유용한 경우에는 제네릭 가변인수 메서드 타입은 허용됩니다.
  • 메서드에 제네릭 가변인수 매개변수를 사용할려면 타입이 안전한지 확인하고 @SafeVarargs 어노테이션을 달아 경고가 나오지 않게 합니다.

 

 

 

 

728x90
반응형