☕️ Java/기본

[Java] Varargs(가변인수)와 Heap Pollution(힙 오염)

말 랑 2023. 3. 21. 15:51
728x90

 

 

 

🧐 Varargs가 없었을 때

Varargs(이하 가변인수)가 없었을 때에는, 임의의 수의 값을 파라미터로 사용하는 메서드를 사용하기 위해서는 배열을 사용하여야 했습니다.
예를 들어 다음과 같습니다.

public static void main(String[] args) {
    final Object[] objects = {
            new Date(),
            "안녕하세요",
            24,
            "살 말랑이입니다."
    };
    printSequence(objects);
}

public static void printSequence(Object[] args) {
    for (int i = 0; i < args.length; i++) {
        System.out.println("[" + i + "] : " + args[i]);
    }
}

 

 

그러나 매번 배열을 만들고 이를 전달해 주는 것은 귀찮은 작업입니다.

 

 

 

 

 

 

 

 

 

🧐 Varargs를 사용하면?

이를 해결하기 위해 가변인수가 등장했는데요, 가변인수를 사용하면 다음과 같이 코드가 바뀝니다.

public static void main(String[] args) {
    printSequence(
            new Date(),
            "안녕하세요",
            24,
            "살 말랑이입니다."
    );
}

public static void printSequence(Object... args) {
    for (int i = 0; i < args.length; i++) {
        System.out.println("[" + i + "] : " + args[i]);
    }
}

 

가변인수를 사용하더라도 결국 내부적으로는 배열에 여러 인수를 전달해야 하지만, 가변인수는 이를 배열로 만들어주는 과정을 자동화하고 숨겨줍니다.

가변인수는 결국 배열로 바뀝니다.

 

 

 

또한 가변인수는 기존 배열을 사용하는 API를 가변인수를 사용하도록 변경하더라도 완벽하게 호환됩니다.

 

 

 

 

 

 

 

 

 

 

 

 

🧐 가변인수 사용 방법

가변인수는 마침표(.) 3개를 통해 사용할 수 있습니다.

public static void sample(Object... others);

 

 

가변인수는 파라미터의 마지막에만 사용할 수 있습니다. 그렇지 않으면 컴파일되지 않습니다.

즉 다음과 같이 사용할 수 없습니다.

public static void sample(String first, Object... others, Integer second);

 

 

 

 

 

 

 

 

 

 

 

🧐 가변인수의 장점

임의의 수의 파라미터를 넘길 때 배열을 만들어서 넘길 필요가 없기 때문에 코드의 가독성이 좋아지며,

무엇보다 사용하는 입장에서 편하게 사용할 수 있다는 장점이 있습니다.

 

 

 

 

 

 

 

 

 

 

🧐 가변인수의 단점

가변인수를 사용하는 메서드를 호출하는 경우, 내부적으로 배열을 생성하고 초기화해야 하기 때문에 고정된 매개변수를 사용하는 메서드에 비해 성능 저하가 발생할 수 있습니다.

(그러나 그렇게 큰 차이는 발생하지 않습니다.)

 

 

또한 오버로딩 시 문제가 발생할 수 있는데요, 예를 들어 다음과 같이 오버로딩된 메서드가 있는 경우, 무엇을 호출해야 할 지 모르기 때문에 문제가 발생할 수 있습니다.

public static void main(String[] args) {
    printSequence(
            new Date() /* 컴파일 에러! 무엇을 호출해야 할 지 모름! */
    );
}

public static void printSequence(Object... args) {
    for (int i = 0; i < args.length; i++) {
        System.out.println("[" + i + "] : " + args[i]);
    }
}

public static void printSequence(Object a, Object... args) {
    for (int i = 0; i < args.length; i++) {
        System.out.println("[" + i + "] : " + args[i]);
    }
}

 

 

 

 

 

성능 저하 문제를 해결하기 위해서는 다음과 같이 구현할 수 있습니다.

public static void main(String[] args) {
    printSequence(
            new Date()
    );  // (1) 호출

    printSequence(
            new Date(),
            new Date()
    );  // (2) 호출

    printSequence(
            new Date(),
            new Date(),
            new Date()
    );  // (3) 호출
}

public static void printSequence(Object first) {} // (1)

public static void printSequence(Object first, Object second) {} // (2)

public static void printSequence(Object... args) {} // (3)

 

특정 개수의 파라미터는 미리 메서드로 정의해둔 후, 이후 임의의 개수의 파라미터에 대해서만 가변 인자를 받도록 사용하면,

오버로딩 문제를 회피하면서 성능 문제를 해결할 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

🧐 가변인수와 Heap Pollution

Heap Pollution에 대한 설명을 살펴보면 Non-Reifiable type 이라는 용어가 등장합니다.

 

 

 

 

 

 

 

📕 Non-Reifiable type

Reifiable Type은 런타임에 타입 정보를 완전히 사용할 수 있는 타입을 의미합니다.

예를 들어 int, double 등의 primitive type과, String, Integer등의 비 제네릭 유형 등이 있습니다.

 

반대로 Non-Reifiable Type은 컴파일 타임에 타입 이레이저에 의해 정보가 제거된 타입을 의미하는데요,

제네릭을 사용하는 경우가 해당됩니다.

예를 들어 List<String>, List<Integer> 등이 Non-Reifiable Type 에 속합니다.

 

 

 

 

 

 

 

 

 

📙 Heap Pollution

매개변수화된 타입의 변수가 해당 매개변수화된 타입이 아닌 객체를 참조하는 경우 힙 오염이 발생합니다.

 

예를 들어 아래와 같은 상황이 있습니다.

List l = new ArrayList<Number>();
List<String> ls = l;       // (1) unchecked warning
l.add(0, 42);              // (2) another unchecked warning
String s = ls.get(0);      // (3) ClassCastException is thrown

 

 

타입이 List<Number>인 변수 l 이, 타입이 List<String>에 할당되는 경우 힙 오염이 발생합니다.

이러한 것이 가능한 이유는 Non-Reifiable Type인 List<Number>와 List<String>은 모두 타입 이레이저에 의하여 컴파일 이후 List가 되기 때문입니다.

 

또한 l.add()를 호출한 경우에도 힙 오염이 발생합니다.

add 메서드의 정적 타입 파라미터는 String입니다.

그러나 이 메서드는 String이 아닌 Integer 타입의 파라미터로 호출됩니다.

 

 

 

 

 

제네릭 타입(혹은 매개변수화된 타입)을 가변인수로 사용하는 경우에도 힙 오염이 발생할 수 있습니다.

 

예시는 다음과 같습니다.

public static void main(String[] args) {
    String[] a = new String[12];
    a[0] = "123";
    a[1] = "123";
    some(a);
}

public static <T> void some(T... p) {
    p[0] = (T) Integer.valueOf(123);  // Heap Pollution
}
public static void some(List<String>... l) {
    Object[] objectArray = l;
    objectArray[0] = Arrays.asList(Integer.valueOf(100));  // Heap Pollution
    String s = l[0].get(0);    // ClassCastException thrown here
}

 

 

 

 

 

 

 

 

 

 

📒 Heap Pollution을 발생시키지 않고 제네릭과 가변인수를 함께 쓰는 방법

다음 규칙을 지킨다면, 힙 오염을 발생시키지 않고 타입에 안전하게 제네릭 가변인수를 사용했음을 확신할 수 있습니다.

 

  • 제네릭 가변인수를 통해 만들어진 제네릭 배열에 아무것도 저장하지 않아야 합니다.
  • 해당 배열의 참조가 밖으로 노출되지 않아야 합니다.

 

제네릭 가변인수를 통해 만들어진 제네릭 배열에 무언가를 저장하는 예시는 바로 위에서 오류가 발생했던 두 예시를 확있했으므로, 이번에는 해당 배열의 참조가 외부로 노출되는 경우를 살펴보겠습니다.

public static void main(String[] args) {
    final String[] strings = toArray("123", "123");  // ClassCastException
    System.out.println();
}

static <T> T[] toArray(T a, T b) {
    final T[] tarr = toArray2(a, b);
    return tarr; // tarr은 Object[] 타입이 된다.
}

// 자신의 제네릭 매개변수 배열의 참조를 노출하는 메서드
static <T> T[] toArray2(T... args) {
    return args;
}

 

위 코드는 컴파일 시점에는 문제가 없다가 실행 시 오류가 발생하는 코드입니다.

 

오류가 발생하는 이유는 아래와 같습니다.

 

toArray() 내부에서 toArray2()를 호출합니다.

이때  toArray2()에서 제네릭 타입의 가변인수로 생성되는 배열은 항상 Object[]가 되는데, 이는 toArray()에 어떠한 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이기 때문입니다.

그러나 이를 strings에 할당하는 과정에서 String[] 로 형변환이 자동으로 발생하고, Object[] 는 String[]의 하위 타입이 아니므로 캐스팅 할 수 없기에 위와 같은 문제가 발생합니다.

 

 

 

 

 

 

 

 

 

📗 @SafeVarargs

위에서 살펴본 규칙을 잘 지켜서 타입에 안전하게 제네릭 가변인수를 사용한 경우라면 @SafeVarags를 통해 컴파일러가 발생시켜주는 경고를 숨길 수 있습니다.

 

@SafeVarargs를 사용할 때를 정하는 규칙은 간단합니다.

 

제네릭이나 매개변수화된 타입을 가변인수로 쓰는 모든 메서드에 @SafeVarargs를 달아야 하는데요, 이는 결국 안전하지 않은 가변인수 메서드를 절대 만들어서는 안된다는 뜻이기도 합니다.

 

만약 작성한 메서드 중에서 제네릭 가변인수를 사용하여 힙 오염 경고가 뜨는 메서드가 있다면, 해당 메서드가 정말 안전한지 점건한 후, @SafeVarargs를 달아주어야 합니다.

 

 

 

 

 

 

 

 

728x90