Effective Java : 아이템28. 완벽공략 42. @SafeVarargs

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다. 


    이 글의 요약

    • 가변인자는 메서드에 ... 형태로 전달되는 녀석들이다. 가변인자는 배열 ([])로 전달된다.
    • 배열을 사용하는 것은 Heap Polution의 가능성이 있다. 
    • Heap Polution은 들어오지 말아야 할 타입의 값이 들어오고, 이 값이 런타임에서 처리될 때 ClassCastException 같은 런타임 에러를 낼 수 있음을 암시한다. 
    • 제네릭 가변인수도 마찬가지다. 제네릭 가변인수는 제네릭 []을 생성하고, 제네릭 [] 역시 Heap Polution 가능성이 존재한다.
    • Heap Polution 경고가 뜨지만, Heap Polution이 발생하지 않을 상황에서만 @SafeVarargs를 이용해서 컴파일 경고를 무시한다. 

    완벽 공략 42. @SafeVarargs

    • @SafeVarargs는 생성자 / 메서드의 제네릭 가변인자에 사용할 수 있는 어노테이션
      • 가변 인자는 ...으로 표현되는 녀석들이다.
    • 제네릭 가변인자는 근본적으로 타입 안전하지 않음.
      • 가변 인자 ...은 []로 받아진다. 제네릭 []은 컴파일 이후에 Object []이 된다.
    • 가변 인자(배열)의 내부 데이터가 오염될 가능성이 있다. (Heap Polution)
    • @SafeVarargs는 가변 인자에 대한 Heap Polution 컴파일 경고를 숨기는데 사용된다. 

    가변 인자란 무엇을 말하는 것일까? 

    가변 인자는 메서드 / 생성자에서 '...'으로 받는 인자를 말한다. 만약 가변 인자에 제네릭을 사용했을 때, 복잡한 문제가 생길 수 있다. 이번 아이템에서 제네릭과 배열은 근본적으로 어울리지 않는다고 이야기를 했었다. 또한, 제네릭 배열을 생성할 수 없다고 했었다. 하지만 제네릭 배열이 생성되는 유일한 곳이 '제네릭 가변인자'다 

     

    가변 인자는 배열이다. 그래서 Heap Polution이 발생할 수 있음. → unsafe() 메서드

    사실 가변 인자(String...)은 배열이다. 받은 타입을 반환해보면 바로 알 수 있는데 String[]로 변환된다. 가변 인자는 배열이고, 배열은 오염이 될 수 있다. 배열이 오염된다는 것은 배열 안에 들어갈 수 없는 타입이 들어갈 수 있다는 것이다.

    static void notSafe(String...   stringLists) {
        String[] stringLists1 = stringLists;

    아래 코드를 대상으로 살펴보자. 먼저 notSafe()에서는 HeapPolution 경고가 발생한다. 이것은 List<String>...이 실제로는 List[]이기 때문에 발생한다. 아래 코드에서는 Heap Polution 에러의 구체적인 예시를 보여준다. 

    1. List<String>... 은 List[]로 바꿀 수 있다.
    2. 배열은 공변이기 때문에 List[] → Object[]로 바꿀 수 있다.
    3. Object[]에는 아무 타입의 인스턴스나 들어갈 수 있다. 여기서 List<Integer> 타입을 Object[]의 첫번째에 넣었다. 여기서 Object[]의 첫번째는 List[]의 첫번째와 동일하다. 이 때 들어가는 값이 List<String> 이어야 하는데 잘못된 타입인 List<Integer>가 들어갔다. 여기서 Heap Polution이 발생한다.
    4. stringList에는 항상 String만 들어가야하는데, Integer가 들어가있다. 따라서 런타임에서 classCastException이 발생한다. 

    이처럼 가변인자를 사용하는 것은 Heap Polution 에러를 잠재적으로 내포하고 있다. 따라서 사용할 때 굉장히 신중해야 한다. 

    public class SafeVarargsExample {
    
    
        // Not Actually safe!
        static void notSafe(List<String>... stringLists) {
            Object[] array = stringLists; // List<String>... => List[]. 그리고 배열은 공ㄴ병이니까
            List<Integer> tmpList = List.of(42);
            array[0] = tmpList; // Semantically invalid, but compiles without warnings.
            String s = stringLists[0].get(0); // Oh no, ClassCastException at runtime.
        }
    
        // @SafeVarargs
        static <T> void safe(T... values) {
            for (T value : values) {
                System.out.println(value);
            }
        }
    
        public static void main(String[] args) {
            SafeVarargsExample.safe("a", "b", "c");
            SafeVarargsExample.notSafe(List.of("a","b","c"));
        }
    }

     


    가변 인자가 안전한 경우

    T...를 사용하면, T...는 T[]로 바뀌게 된다. 제네릭을 사용한 가변인자는 제네릭을 사용하더라도 배열이기 때문에 Heap Polution이 발생할 가능성은 존재한다. 그렇지만 가변 인자에 제네릭을 써도 안전할 때가 있는데, 이럴 때는 Heap Polution 컴파일 경로를 @SafeVaragrs를 이용해서 무시해주면 된다. 

    아래 내용이 대표적인 예시가 될 수 있다. 

    @SafeVarargs
    static <T> void safe(T... values) {
        for (T value : values) {
            System.out.println(value);
        }
    }
    • 위의 safe() 메서드를 살펴보자. 가변인자로 T...를 받기 때문에 실제로는 T[]이 된다. 
    • T[] 이기 때문에 Heap Polution 경고를 준다. 

    그런데 중요한 것은 이 메서드 내부에서 T[]이 하는 일이 단순히 출력을 하는 것이다. 위의 코드에서는 다음을 보장할 수 있을 것으로 기대된다. 

    • 단순 출력을 하기 때문에 타입 캐스팅이 없다. → ClassCastException이 발생하지 않을 것이다. 
    • 출력만 한다 → 다른 사이드 이펙트가 없고, 멱등성도 존재한다. 

    이런 이유 때문에 Heap Polution을 무시해도 될 것으로 판단되기 때문에 @SafeVarargs를 이용해서 Heap Polution 에러를 무시할 수 있게 된다. 


    정리

    Possible Heap Polution 컴파일 경고가 뜬다면, @SafeVarargs로 컴파일 경고를 무시하는 것은 가장 최후의 수단이다. 일반적으로는 Heap Polution 컴파일 경고가 뜨지 않도록 제네릭 타입을 사용하도록 한다. 만약 어쩔 수 없이 사용해야 한다고 하면, 반드시 Heap Polution이 안 일어날 것 같은 상황에서만 @SafeVarargs를 붙이고 컴파일 경고를 무시하도록 한다. 

    댓글

    Designed by JB FACTORY