아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

weekbelt·2022년 11월 25일
0

가변인수란 클라이언트의 입장에서 파라미터를 몇개 보낼지 선택할 수 있는 변수를 말합니다. 아래에 dangerous메서드의 인자에 ...이렇게 점이 세개가 있는데 이것을 가변인수를 받을수 있다는것을 의미합니다.

제네릭타입의 가변인수를 받는 dangerous메서드

package me.whiteship.chapter05.item32;

import java.util.ArrayList;
import java.util.List;

// 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다. (191-192쪽)
public class Dangerous {
    // 코드 32-1 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다! (191-192쪽)
    static void dangerous(List<String>... stringLists) {
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList; // 힙 오염 발생
        String s = stringLists[0].get(0); // ClassCastException
    }

    public static void main(String[] args) {
        dangerous(List.of("There be dragons!"));
    }
}

문제는 제네릭타입과 같이쓰면 문제가 생깁니다. 제네릭은 성격상 배열과 맞지 않고 배열보다는 리스트를 사용하도록 아이템 28에서 다루었습니다. 배열은 공변이고 실체화가 되지만 제네릭은 불공변이고 실체화가 되지 않기때문에 같이 쓸때 문제가 생긴다고 했습니다.

List<String>[] myList = new ArrayList<String>[10];

컴파일러는 이러한 선언을 허용하지 않습니다. 하지만 내부적으로는 제네릭의 배열이 만들어지는 경우가 있습니다.

	static void dangerous(List<String>... stringLists) {
		// 코드생략
    }

가변인자와 제네릭타입을 같이 쓰면 내부적으로 배열을 생성합니다. 그래서 직접 선언해서 제네릭 타입의 배열을 만들 순 없지만 가변인자를 사용하면 자바 내부적으로는 배열을 만드는것이 가능합니다. 위의 dangerous메서드의 가변인자는 컴파일에러는 아니지만 아래와 같은 경고가 뜹니다.

Possible heap pollution from parameterized vararg type

parameterezed라는게 제네릭을 사용했다는 뜻이고 가변인자때문에 heap안에 들어있는 메모리가 오염될 수 있다라고 경고메시지가 뜨는데 위와 같이 제네릭타입의 가변인자를 받아서 내부적으로 배열을 생성하는것은 위험한 코드입니다. 먼저 왜 힙 오염이 발생하는지 알아보겠습니다.

    static void dangerous(List<String>... stringLists) {
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList; // 힙 오염 발생
        String s = stringLists[0].get(0); // ClassCastException
    }

dangerous메서드의 첫줄에 리스틀 선언하는 부분을 살펴보면 Integer타입의 숫자가 42인 하나의 요소를 가지고 있는 리스트를 선언했습니다. 가변인자는 String타입의 리스트의 배열을 Object배열인 objects에 할당합니다. stringLists는 objects에 할당하면서 String의 상위타입인 Object로 사용할 수 있습니다. 그리고 그 다음줄에 object[0]에 처음 선언한 Integer타입의 intList를 참조하게 되면 기존에 Object는 Integer에도 상위 타입이기 때문에 object의 첫번째 요소로 받을 수 있게 됩니다. 이부분에서 힙 오염이 일어납니다. 마지막줄에 문자를 꺼내려고할때 컴파일시 stringLists[0].get(0)하는 부분에서 문자열로 Casting하는 코드를 넣어줍니다. 그 이유는 가변인자에서 String타입의 리스트라고 알고 있기 때문입니다. 이렇게되면 런타임에 stringLists[0].get(0)을 할경우 ClassCastException이 일어나게 됩니다. 그래서 제네릭타입의 가변인자를 받는것을 권장하지 않습니다. 결국은 배열을 리스트로 변경하는것이 안전한 방법인데 배열을 쓰면서 안전하게 사용하는 방법을 먼저 살펴보겠습니다.

코드 32-3 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 (195쪽)

package me.whiteship.chapter05.item32;

import java.util.ArrayList;
import java.util.List;

public class FlattenWithVarargs {

    @SafeVarargs
    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;
    }
}

@SafeVarargs를 이용해서 가변인자의 제네릭타입이 안전하게 쓰이고 있다고 선언하면 경고표시를 띄우지 않게 됩니다. 그렇다면 어떻게 쓰는것이 안전한 것일까요? 가변인자의 lists에 아무것도 넣지 않으면 됩니다. lists를 가져와서 메서드를 호출하거나 처리를하기만 하면 안전하게 쓸 수 있습니다. 한가지 더 주의해야할 사항은 가변인자의 lists를 컴파일러가 내부적으로 만들어주는 제네릭배열을 절대로 외부에 노출시키면 안됩니다. lists라는 파라미터를 리턴하면 안됩니다.

코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다! (193쪽)

package me.whiteship.chapter05.item32;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

// 미묘한 힙 오염 발생 (193-194쪽)
public class PickTwo {
    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(a, c);
            case 2: return toArray(b, c);
        }
        throw new AssertionError(); // 도달할 수 없다.
    }

    public static void main(String[] args) { // (194쪽)
        String[] attributes = pickTwo("좋은", "빠른", "저렴한");
        System.out.println(Arrays.toString(attributes));
    }
}

toArray를 보면 제네릭 배열을 그대로 리턴했고 pickTwo를 호출하면 toArray가 호출되면서 리턴하는값이 그대로 pickTwo에서도 리턴이 됩니다. toArray를 컴파일할때 리턴타입을 Object로 선언합니다. 결과적으로 리턴하는것은 Object의 배열이 됩니다. main메소드에서 pickTwo의 3개의 string타입의 인자값을 넣는데 컴파일러가 제네릭 때문에 컴파일을 하면서 문자열로 타입캐스팅하는 코드를 넣어줍니다. 이렇게되면 추상적인 Object타입의 배열을 하위타입인 String으로 casting하려고 하기 때문에 런타임시 ClassCastException예외가 생깁니다. 이 문제가 생기는 근본적인 원인은 toArray메소드에서 제네릭 배열을 그대로 리턴해 줬기 때문입니다.

결국 안전하게 사용하는 방법은 배열대신 리스트를 사용해야합니다.

배열 대신 List를 이용해 안전하게 바꿘 PickTwo (196쪽)

package me.whiteship.chapter05.item32;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class SafePickTwo {
    static <T> List<T> pickTwo(T a, T b, T c) {
        switch(ThreadLocalRandom.current().nextInt(3)) {
            case 0: return List.of(a, b);
            case 1: return List.of(a, c);
            case 2: return List.of(b, c);
        }
        throw new AssertionError();
    }

    public static void main(String[] args) {
        List<String> attributes = pickTwo("좋은", "빠른", "저렴한");
        System.out.println(attributes);
    }
}

가변인수 대신에 List.of를 사용합니다.

List of메서드

    static <E> List<E> of(E e1, E e2) {
        return new ImmutableCollections.List12<>(e1, e2);
    }

List.of같은경우 2개의 element를 받게되어 있는데 여기서 내부적으로 그대로 가변인수로 받아서 리턴하는게 아니라 List를 만들어서 리턴하고 있습니다. 따라서 타입안정성이 컴파일, 런타임시에 보장이 됩니다.

코드 32-4 제네릭 varargs 매개변수를 List로 대체한 예 - 타입 안전하다. (195-196쪽)

package me.whiteship.chapter05.item32;

import java.util.ArrayList;
import java.util.List;

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

    public static void main(String[] args) {
        List<Integer> flatList = flatten(List.of(
                List.of(1, 2), List.of(3, 4, 5), List.of(6,7)));
        System.out.println(flatList);
    }
}

flatten메서드를 보면 리스트의 배열이 아니라 리스트의 리스트를 받아서 처리하고 있습니다. 결론적으로 제네릭과 가변인자를 같이 사용하지말고 제네릭을 사용할 경우 리스트로 사용하는것이 안전합니다.

profile
백엔드 개발자 입니다

0개의 댓글