Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item 32 & 33 가변인수와 타입 안전 이종 컨테이너 #17

Open
Yg-Hong opened this issue Dec 12, 2023 · 0 comments
Open

Item 32 & 33 가변인수와 타입 안전 이종 컨테이너 #17

Yg-Hong opened this issue Dec 12, 2023 · 0 comments

Comments

@Yg-Hong
Copy link
Contributor

Yg-Hong commented Dec 12, 2023

section: 5장 item32 제네릭과 가변인수를 함께 쓸 때는 신중하라


🍵 서론

가변인수 메서드란 메서드의 매개변수로 동적으로 개수가 변할 수 있는 인수를 받는 메서드를 말한다.

// 가변인수 메서드
public class VarargsPractice {
		
// 가변인수 메서드 선언
    public static void printNumbers(int... numbers) {
        System.out.println("인수의 개수: " + numbers.length);
        System.out.print("인수들: ");
        for (int num : numbers) {
            System.out.print(num + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        printNumbers(1, 2, 3, 4, 5);
        printNumbers(10, 20, 30);
    }
}

즉, 가변인수 메서드와 제너릭은 성격에서 공통점을 어느정도 공유하고 있다고 볼 수 있다. 혹시 이 두가지를 같이 쓰면 자바는 무적이 되지 않을까?
Untitled
하지만 상황은 그렇게 낙관적이지 않다.

🌒 본론

가변인수 메서드에서 메서드를 호출하면 내부적으로 배열이 생성되어 해당 배열에 가변인수가 저장된다. 이러한 구현 방식이 골때린다.

public class HeapPollutionExample {
    // 가변인수 메서드 내에서 배열을 클라이언트에 노출
    public static void modifyArray(Object... values) {
        // 배열에 새로운 요소 추가
        Object[] arr = values;
        arr[0] = 0;

        // 가변인수 메서드 외부에서는 예상치 못한 동작이 발생할 수 있음
        for (Object value : values) {
            System.out.print(value + " ");
        }
    }

    public static void main(String[] args) {
        printValues("Hello", 1, 2, 3); // 컴파일러 경고 발생
    }
}

이 배열은 클라이언트에 노출되지 않아야 하는데, 만약 노출된다면 클라이언트 코드에서 배열을 수정할 수 있고, 이로 인해 예상치 못한 동작이 발생할 수 있다.

public class NonReifiableExample {
    // 실체화 불가 타입을 사용한 가변인수 메서드 선언
    public static <T> void printValues(T... values) {
        for (T value : values) {
            System.out.print(value + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        printValues("Hello", 1, 2, 3); // 컴파일러 경고 발생
    }
}

가변인수 메서드가 내부적으로 생성하는 배열이 타입 안전성을 해칠 때 발생하는 것을 "heap pollution(힙 오염)"이라고 한다.

이미 우린 실체화 불가 타입(item 28)은 런타임에서 타입 관련 정보가 소거되는 것을 알고 있다.
대부분의 제너릭과 매개변수화 타입은 실체화 불가 타입이므로 이를 가변인수 메서드의 매개변수로 선언하면 컴파일러는 자연스럽게 런타임에서 발생할 수 있는 힙 오염에 대해 경고를 보내게 된다.

"Type safety: Potential heap pollution via varargs parameter"

그렇게 강한 타입을 지향하고 싶으면 컴파일 시에 제너릭 가변인자 메서드를 오류로 막으면 되지 왜 경고만 보내는거지?

실무에서는 이런 제너릭 가변인자 메서드가 유용하다.

  • Arrays.asList(T… a)
  • Collections.addAll(Collection<? super T> c, T… elements)

결국 어쩔 수 없는 필요성을 인정하고 자바 7부터는 메서드를 만드는 개발자가 타입 안전성을 완벽하게 보장할 수 있는 경우에는 @SafeVarargs 애너테이션을 달아 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다.

💡 가변인수 메서드를 호출할 때 varargs 매개변수를 담는 제너릭 배열이 만들어진다는 것을 기억하자. 그 1)매개변수들을 덮어쓰거나 변형하지 않고 2)신뢰할 수 없는 코드가 배열에 접근할 수 없다면 타입 안전성은 보장되는 것이다.

즉, varargs의 목적대로만 쓰인다면 그 메서드는 안전하다.

🍃 결론

문제를 하나 내보자. 맞춘다면 당신은 제너릭과 타입 안전성의 신이다.
아래는 왜 타입 안전성을 해칠 위험성이 존재하는 코드인가?

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 1: return toArray(b, c);
    }
    throw new AssertionError(); // 도달할 수 없다.
}
public static void main(String[] args) {
    String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}

어떻게 고칠까?

정답은...더보기


section: 5장 item33 타입 안전 이종 컨테이너를 고려하라


🍵 서론

"타입 안전 이종 컨테이너"는 서로 다른 데이터 타입을 안전하게 포함하는 컨테이너를 의미한다. 제너릭을 사용하여 다양한 데이터 타입을 지원하면서도 타입 안전성을 보장하는 구조를 가리킨다.

보통의 경우는 컨테이너의 일반적인 용도에 맞게 설계되어 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다. Set, Map<K, V>처럼 말이다. 하지만 더 유연한 수단이 필요한 경우가 있다.

🌒 본론

  • 다양한 데이터 타입의 혼합
    하나의 컨테이너 안에 서로 다른 데이터 타입의 원소들을 저장해야 하는 경우(다양한 형태의 구조체나 객체)

  • 동적인 타입 결정
    런타임에 데이터 타입이 결정되는 상황에서 컨테이너를 사용하고 싶을 때(사용자 입력으로부터 동적으로 데이터를 수집하고 각 입력마다 다른 타입의 데이터를 저장해야 하는 경우)

  • 다양한 데이터 소스와의 상호 작용
    여러 데이터 소스에서 가져온 데이터를 하나의 컨테이너에 통합하고 싶을 때

  • 제너릭 타입의 활용이 어려운 상황
    제너릭 타입으로 충분히 표현하기 어려운 특별한 상황에 컨테이너를 사용하고 싶을 때

책의 예시와 유사하게 아래 예시는 컨테이너 대신 키를 매개변수화 하여 컨테이너의 데이터를 조회하거나 추가하고 있다. 키가 매개변수화 되었다는 점만 빼면 일반 맵처럼 보일 정도로 단순하다.

public class TypeSafeHeterogeneousContainer { 
    // 1. 저장할 자료구조 선언
    private Map<Class<?>, Object> container = new HashMap<>();

    // 2. 데이터 추가
    public <T> void addValue(Class<T> type, T value) {
        container.put(type, value);
    }

    // 3. 데이터 조회
    public <T> T getValue(Class<T> type) {
        return type.cast(container.get(type));
    }

    public static void main(String[] args) {
        TypeSafeHeterogeneousContainer container = new TypeSafeHeterogeneousContainer();

        // 다양한 타입의 데이터 추가
        container.addValue(String.class, "Hello, World!");
        container.addValue(Integer.class, 42);
        container.addValue(Double.class, 3.14);

        // 데이터 조회
        String stringValue = container.getValue(String.class);
        Integer intValue = container.getValue(Integer.class);
        Double doubleValue = container.getValue(Double.class);

        // 출력
        System.out.println("String Value: " + stringValue);
        System.out.println("Integer Value: " + intValue);
        System.out.println("Double Value: " + doubleValue);
    }
}

데이터를 저장할 자료구조는 Map<Class<?>, Object>로 선언한다.
맵의 키를 와일드카드 타입으로 선언했으므로 모든 키가 서로 다른 매개변수화 타입일 수 있다.
값은 단순히 Obejct 타입이므로 키와 값 사이에 동일한 타입 관계를 보장하지 않는다. 데이터를 추가할 때 키와 값 사이에 ‘타입 링크(type linkage)’ 정보가 지워진다. 즉, 모든 값이 키로 명시한 타입임이 보장되지 않으므로 값 조회 시에 캐스팅이 필요하다.

위와 같은 구현 방식으로 사용에 고려해야 할 점은 두가지이다.

  1. 악의적인 클라이언트가 Class 객체를 제네릭이 아닌 raw type으로 넘기면 타입 안전성이 깨진다.
public static void main(String[] args) {
    UnsafeHeterogeneousContainer container = new UnsafeHeterogeneousContainer();

    // 악의적인 클라이언트가 로(raw) 타입 사용
    container.addValue((Class) String.class, "Hello, World!");
    container.addValue((Class) Integer.class, 42);

    // 로(raw) 타입을 사용하여 데이터 가져오기
    String stringValue = container.getValue((Class) String.class); // 컴파일러 경고 발생
    Integer intValue = container.getValue((Class) Integer.class); // 컴파일러 경고 발생

    // 출력
    System.out.println("String Value: " + stringValue);
    System.out.println("Integer Value: " + intValue);
}

로 타입은 제네릭의 안전성을 제공하지 않기 때문에, 이렇게 사용할 경우 프로그램이 런타임에 예기치 못한 동작을 할 수 있다.

container.addValue((Class) Integer.class, "Integer의 인스턴스가 아닙니다.");
int intValue = container.getValue(Integer.class); // ClassCastException!!!
  1. 실체화 불가 타입에는 사용할 수 없다.

즉, 즐겨찾는 String이나 String[]은 저장할 수 있어도 List은 저장할 수 없다. 저장을 시도하는 코드는 컴파일되지 않는다.

List과 List는 List.class라는 Class 객체를 공유하고 이 두개를 List.class와 List.class로 우겨서 저장하면 둘 다 똑같은 타입의 객체 참조를 만들게 된다.

🍃 결론

아니 이거 너무 어려운거 아니야?


reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant