Technology/Java

@SafeVarargs 언제 사용할까?

ikjo 2022. 10. 9. 16:41

Java 5, 제네릭(generic)와 가변인자(varargs)의 등장

Java 5부터는 제네릭(Generic)과 함께 가변인자(Variable arguments, varargs라고도 함)가 등장했다. 기존 메서드의 매개변수 개수는 고정적이었지만, 가변인자 덕에 동적으로 지정할 수 있게 되었는데, 가변인자는 내부적으로 배열을 이용하므로, 가변인자가 선언된 메서드가 호출될 때마다 새로운 배열이 생성된다. 이때 매개변수로 가변인자가 선언된 메서드는 매개변수로 배열이 선언된 메서드와 달리 인자를 생략할 수 있다는 차이가 있다.

 

 

제네릭과 가변인자 혼용 사용 시 발생할 수 있는 문제점

그렇다면 제네릭과 가변인자를 함께 활용하여 아래와 같이 List<String>이라는 매개변수화 타입(parameterized type)의 가변인자를 가지는 test 메서드를 선언해볼 수 있을 것이다.

 

import java.util.List;

public class Test {

    private static void test(List<String>... elements) {
        // ...
    }

    public static void main(String[] args) {
        List<String> dishes = List.of("pizza", "burger", "rice");
        List<String> drinks = List.of("coffee", "juice", "coke");
        List<String> deserts = List.of("cake", "juice", "coke");
        test(dishes, drinks, deserts);
    }
}

 

메서드 선언부에서 발생하는 컴파일러 경고 메시지

하지만 컴파일 결과, 컴파일러는 해당 메서드의 선언부에 대해 어떤 경고 메시지를 띄우는데, 해당 내용을 살펴보니 매개변수화 가변인자 타입으로 인해 '힙 오염(Heap Pollution)'이 발생할 수 있다고 한다.

 

제네릭으로 가변인자 매개변수 선언 시 발생한 경고 메시지

 

이때 '힙 오염'이란 매개변수화 타입의 변수가 '타입이 다른 객체'를 참조하면 발생하는 것이다. 이러한 힙 오염이 발생할 경우 생길 수 있는 문제점은 무엇일까? 바로 다른 타입의 객체를 참조하는 상황에서 컴파일러가 자동 생성한 형변환이 실패할 수 있기 때문이다. 아래 예시를 살펴보자.

 

import java.util.List;

public class Test {

    private static void test(List<String>... elements) {
        Object[] objects = elements;
        objects[0] = List.of(5000); // 힙 오염 발생
        String dish = elements[0].get(0); // ClassCastException 발생!!
    }
    
    // (생략)
}

 

앞서 언급했듯이 가변 인자가 선언된 메서드가 호출되면 새로운 배열이 생성된다. 즉, 위 코드에서 elements의 실제 타입은 List<String> 타입의 배열일 것이다. (List<String>[] e = elements;) 이때 배열은 공변(convariant)이므로, 또한 Object는 최상위 클래스이므로 elements를 Object 배열에 할당할 수 있을 것이다. (Object[] objects = elements;)

 

이후 Object 배열의 특정 요소에 정수형 List를 할당하게 되면 List<String> 타입의 해당 요소가 자신과 다른 List<Integer> 타입의 객체를 참조하는 형국 즉, 힙 오염이 발생하게 된다. 사실 이는 있어선 안 될 일이지만 문제는 Object 배열의 요소는 Object 타입이므로 어느 타입의 객체든 할당이 가능하다는 것이다.

 

이때 이를 List<String>인 것으로 간주하고 해당 리스트의 특정 요소를 꺼내 String 타입의 변수에 할당하게 되면 런타임 단계에서 ClassCastException이라는 런타임 에러가 발생하는데, 이는 컴파일 단계에서 컴파일러가 자동으로 생성했었던 형변환 코드(Integer → String)로 인한 것이다.

 

매개변수화 타입이 아닌 다음과 같이 제네릭 타입의 가변인자로 선언할 경우에도 동일한 컴파일 경고 메시지가 발생한다.

 

public class Test {

    private static <T> T[] toArray(T... elements) {
        return elements; // Object[] 반환
    }

    private static <T> T[] test(T a, T b) {
        return toArray(a, b); // Object[] 반환
    }

    public static void main(String[] args) {
        test("pizza", "burger");
        // String[] test = test("pizza", "burger"); // ClassCastException 발생!!
    }
}

 

이 경우엔 메서드에 선언된 타입 T가 컴파일 과정에서 Object로 바뀌면서 Object 배열이 생성되기 때문이다. 마찬가지로 Object 배열에는 모든 타입의 객체를 저장할 수 있으므로 앞서 다루었던 문제점이 발생할 여지가 있다. 뿐만 아니라 이를 형변환할 수 없는 타입(예를 들어, 위 예시처럼 String[])에 할당하면 ClassCastException 예외가 런타임 중 발생한다.

 

사실, 이러한 ClassCastException 등의 잠재적인 문제는 가변인자의 타입이 매개변수화 타입이나 제네릭이 아니더라도 발생할 수 있다. 예를 들어 이전과 동일한 상황에서 List<String>이라는 매개변수화 타입 대신 String 타입을 적용해보자.

 

import java.util.List;

public class Test {

    private static void test(String... elements) {
        Object[] o = elements;
        o[0] = 1; // 컴파일러의 경고 메시지(~ will produce 'ArrayStoreException') 발생!
        String s = (String) o[0]; // 컴파일러의 경고 메시지(~ will produce 'ClassCastException') 발생!
        // String s = (String) o[1]; // 컴파일 정상 처리!
    }
    
    // (생략)
}

 

여기서 중요한 차이점이 있다면 이 경우에는 컴파일러가 '어떤 예외 상황이 발생할지' 구체적으로 예측해줄 수 있다는 것이다. 개발자는 이 메시지를 보며 String 타입의 요소에 Integer 타입이 할당되고 있는 문제점(ArrayStoreException 발생)과 아울러 이 요소를 다시 String으로 변환 시 ClassCastException이 발생할 수 있다는 문제점을 인지할 수 있는 것이다.

 

메서드를 호출하는 부분에서 발생하는 컴파일러 경고 메시지

또한 이 메서드를 호출하는 부분에 대해서도 어떤 경고 메시지가 보이는데, 해당 내용에선 가변인자에 대해 '검사되지 않은(Unchecked) 제네릭' 배열이 생성되었다고 한다.

 

제네릭 가변인자가 매개변수로 선언된 메서드를 호출 시 발생한 경고 메시지

 

이는 가변인자의 타입이 현재 non-reifiable 타입이기 때문이다. non-reifiable 타입이라는 것은 컴파일 후에는 타입 정보가 유지되지 않는다는 것인데, 대표적으로 제네릭 타입이 있다. 제네릭 타입들의 경우 컴파일 시 타입 정보가 제거(Type erasure)되므로 전형적인 non-reifiable 타입이다.

 

이와 반대로 reifiable 타입이라는 것은 컴파일 후에도 타입 정보가 유지된다는 것인데, 대표적으로 primitive type(int, char 등), non-generic type, raw type(List, Set 등) 등이 있다.

 

이러한 컴파일 경고 메시지는 앞서 다루었던 문제점이 발생할 수 있다는 것을 암시한다.

 

 

타입 안전을 위해 지켜야 할 일

지금까지 제네릭과 가변인자를 혼용하여 사용 시 발생할 수 있는 문제점에 대한 예시를 들기 위해 다소 극단적인 상황을 연출하긴 했다. 사실 이러한 문제점(ClassCastException)이 발생할 것을 대비해 자바에서는 애초에 new 연산자로 제네릭 배열을 만드는 것이 불가능하다.

 

하지만, 실무에선 제네릭이나 매개변수화 타입의 가변인자를 가지는 메서드가 매우 유용하게 사용되기 때문에 다소 모순적이긴 하지만 이를 허용했으며, 이를 사용하는 입장에서는 이로 인한 문제점이 발생하지 않도록 해당 메서드가 안전한지 점검해야한다.

 

우선 해당 메서드를 작성하는 개발자 입장에서는 앞서 다루었던 문제점이 발생하지 않도록 제네릭이나 매개변수화 타입의 가변인자로 생성된 배열에는 어떤 값도 저장되지 않도록 해야한다. 즉, 해당 메서드를 호출할 경우 가변인자의 매개변수로 전달되는 여러 인자들은 순수하게 전달되는 용도로 사용되도록(중간에 변경되지 않도록) 해야하는 것이다.

 

또한, 가변인자로 생성된 배열의 참조가 밖으로 노출되지 않도록 해야한다. 이를 호출하는 측에서 해당 배열을 수정할 수 있고 더욱이 Object 배열을 형변환하는 과정에서 ClassCastException 예외가 발생할수도 있기 때문이다.

 

 

이제 @SafeVarargs을 사용해야할 때!

앞서 제네릭이나 매개변수화 타입의 가변인자를 가진 메서드를 사용 시 타입 안전을 위한 처리를 다해주었다 하더라도, 컴파일러는 이를 인지하지 못하고 동일한 경고 메시지를 보여줄 것이다.

 

이 경우 @SuprpessWarnings를 해당 메서드에 붙여주면 해당 메서드 선언부에서 발생하는 경고 메시지는 사라지겠지만, 해당 메서드를 호출하는 부분에서 발생하는 경고 메시지는 사라지지 않는다. 따라서 다음과 같이 호출하는 부분에도 @SuprpessWarnings을 붙여줘야하는 불편함이 있다.

 

import java.util.List;

public class Test {

    @SuppressWarnings("unchecked")
    private static void test(List<String>... elements) {
        // ... 타입 안전 처리 
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        test(List.of("pizza"), List.of("coffee"), List.of("cake"));
    }
}

 

※ 참고로 (앞서 다루었던) 제네릭이나 매개변수화 타입의 가변인자를 가지는 메서드 사용 시 메서드 선언부와 호출하는 부분에서 발생하는 경고 메시지의 종류는 unchecked이다.

 

하지만, 다음과 같이 @SafeVarargs를 메서드 선언부에 붙여주면 호출하는 부분에 별다른 애노테이션을 붙여주지 않아도 된다.

 

import java.util.List;

public class Test {

    @SafeVarargs
    @SuppressWarnings("varargs")
    private static void test(List<String>... elements) {
        // ... 타입 안전 처리
    }

    public static void main(String[] args) {
        test(List.of("pizza"), List.of("coffee"), List.of("cake"));
    }
}

 

다만, -Xlint 옵션을 붙여서 컴파일을 해보면 메서드 선언부에 varargs 경고 메시지가 발생하는데, 이는 가변인자의 타입이 지네릭 타입일 때 발생하는 경고로서, 이 경고 메시지 역시 억제하기 위해선 @SafeVarargs와 함께 @SuppressWarnings("varargs")를 같이 붙여주면 된다.

 

 

참고자료

  • 도우출판 "자바의 정석"
  • 프로그래밍 인사이트 "이펙티브 자바"
  • https://www.baeldung.com/java-safevarargs
  • https://www.geeksforgeeks.org/safevarargs-annotation-in-java-9-with-example/