본문 바로가기
Java Study

Type Erasure(타입 소거)

by xogns93 2024. 7. 18.

Type Erasure(타입 소거)

Java의 제네릭이 런타임 시 타입 정보를 유지하지 않고 컴파일 타임에만 사용되는 메커니즘을 말합니다. 이는 제네릭의 타입 정보가 컴파일 시점에서 제거되고, 런타임에는 원래의 비제네릭 타입으로 변환되는 것을 의미합니다. 타입 소거 덕분에 제네릭이 자바 1.5 이전의 코드와 호환성을 유지할 수 있습니다.

타입 소거(Type Erasure)는 Java에서 제너릭을 도입할 때, 기존의 코드와의 하위 호환성(Backward Compatibility)을 유지하기 위해 채택된 메커니즘입니다. 자바 5 때 도입 됐으므로 그전 자바 버전에서는 제너릭을 인식 못하기 때문에 타입 소거

 

타입 소거의 작동 방식

  1. 제네릭 타입 정보 제거: 컴파일러는 제네릭 타입 정보를 제거하고 원래의 비제네릭 타입으로 변환합니다. 예를 들어, List<String>은 컴파일 후에 List로 변환됩니다.
  2. 타입 변환: 컴파일러는 필요한 경우 적절한 타입 변환을 추가합니다.
  3. 바운드 타입으로 대체: 타입 파라미터가 상한 경계를 가지고 있으면, 컴파일러는 이를 상한 경계 타입으로 대체합니다.

 

타입 소거의 예

예제 1: 기본적인 타입 소거

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

public class TypeErasureExample {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        stringList.add("World");

        // 컴파일 후 타입 소거
        // 실제로는 List stringList = new ArrayList();로 변환됨
        for (String s : stringList) {
            System.out.println(s);
        }
    }
}

위 코드에서 List<String>은 컴파일 후에 List로 변환됩니다. 따라서 런타임 시에는 타입 정보가 존재하지 않습니다.

 

예제 2: 바운드 타입 소거

public class TypeErasureExample2<T extends Number> {
    private T value;

    public TypeErasureExample2(T value) {
        this.value = value;
    }

    public void display() {
        System.out.println("Value: " + value);
    }

    public static void main(String[] args) {
        TypeErasureExample2<Integer> example = new TypeErasureExample2<>(123);
        example.display();
    }
}

위 코드에서 T는 Number 타입으로 상한 경계를 가지고 있습니다. 컴파일 후에는 다음과 같이 변환됩니다.

 

public class TypeErasureExample2 {
    private Number value;

    public TypeErasureExample2(Number value) {
        this.value = value;
    }

    public void display() {
        System.out.println("Value: " + value);
    }

    public static void main(String[] args) {
        TypeErasureExample2 example = new TypeErasureExample2(123);
        example.display();
    }
}

 

 

타입 소거의 결과

  1. 타입 안전성: 제네릭은 컴파일 타임에 타입 안전성을 제공합니다. 컴파일러는 제네릭 타입을 검사하여 타입 불일치를 방지합니다.
  2. 런타임 타입 정보 소실: 타입 소거로 인해 런타임에는 제네릭 타입 정보가 소실됩니다. 따라서 리플렉션을 사용하여 제네릭 타입을 얻을 수 없습니다.
  3. 호환성 유지: 타입 소거는 제네릭이 자바 1.5 이전의 코드와 호환되도록 합니다.

타입 소거의 한계

  1. 리플렉션: 제네릭 타입 정보는 런타임에 소실되므로 리플렉션을 사용할 때 타입 정보를 얻을 수 없습니다.
  2. 캐스팅: 타입 소거로 인해 컴파일러는 적절한 타입 캐스팅을 추가해야 하므로, 불필요한 캐스팅이 발생할 수 있습니다.
  3. 타입 검사: 런타임에 제네릭 타입을 검사할 수 없으므로, 컴파일러는 타입 안전성을 보장하기 위해 일부 경우에 경고를 생성합니다.

와일드카드와 타입 소거

와일드카드도 타입 소거의 영향을 받습니다. 예를 들어, List<?>는 컴파일 후에 List로 변환됩니다.


Bridge Methods(브리지 메소드)

Java 컴파일러가 제네릭과 상속을 처리할 때 타입 안전성을 유지하기 위해 자동으로 생성하는 메소드입니다. 이러한 메소드는 타입 소거(Type Erasure)로 인해 발생하는 타입 불일치를 해결하기 위해 필요합니다. 브리지 메소드는 주로 제네릭 타입을 상속하는 경우와 관련이 있습니다.

 

브리지 메소드의 필요성

브리지 메소드의 필요성을 이해하기 위해, 제네릭과 상속의 예를 들어 설명하겠습니다.

class Node<T> {
    private T data;

    public Node(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

class MyNode extends Node<Integer> {
    public MyNode(Integer data) {
        super(data);
    }

    @Override
    public Integer getData() {
        return super.getData();
    }
}

위 코드에서 Node 클래스는 제네릭 클래스로, 타입 파라미터 T를 받습니다. MyNode 클래스는 Node<Integer>를 상속하며, getData 메소드를 오버라이드합니다.

브리지 메소드 생성

컴파일러는 Node<T>와 Node<Integer>의 타입 소거 후에 다음과 같은 문제가 발생할 수 있음을 인식합니다. Node의 getData 메소드는 T를 반환하지만, MyNode의 getData 메소드는 Integer를 반환합니다. 타입 소거 후, 두 메소드의 시그니처는 동일하지 않게 됩니다.

 

타입 소거 후의 코드:

class Node {
    private Object data;

    public Node(Object data) {
        this.data = data;
    }

    public Object getData() {
        return data;
    }
}

class MyNode extends Node {
    public MyNode(Integer data) {
        super(data);
    }

    @Override
    public Integer getData() {
        return (Integer) super.getData();
    }
}

이 시점에서 Node의 getData 메소드는 Object를 반환하고, MyNode의 getData 메소드는 Integer를 반환합니다. 이는 오버라이딩 규칙을 위반합니다. 이를 해결하기 위해 컴파일러는 브리지 메소드를 생성합니다.

 

브리지 메소드의 역할

브리지 메소드는 컴파일러가 자동으로 생성하여 메소드 시그니처의 일관성을 유지합니다. 브리지 메소드가 생성된 후의 코드:

class MyNode extends Node {
    public MyNode(Integer data) {
        super(data);
    }

    @Override
    public Integer getData() {
        return (Integer) super.getData();
    }

    // 컴파일러가 생성한 브리지 메소드
    @Override
    public Object getData() {
        return getData(); // 실제로는 this.getData()
    }
}

이 브리지 메소드는 Object를 반환하도록 정의되며, 실제로 Integer를 반환하는 getData 메소드를 호출합니다. 이렇게 하면 타입 안전성을 유지하면서도 오버라이딩 규칙을 준수할 수 있습니다.

 

요약

  • 브리지 메소드제네릭과 상속을 처리할 때 타입 불일치를 해결하기 위해 컴파일러가 자동으로 생성하는 메소드입니다.
  • 타입 소거로 인해 제네릭 타입이 제거되면, 오버라이딩된 메소드의 시그니처가 달라질 수 있습니다.
  • 브리지 메소드는 이러한 문제를 해결하고, 런타임 시 타입 안전성을 유지합니다.
  • 컴파일러는 브리지 메소드를 자동으로 생성하므로, 개발자는 이를 명시적으로 작성할 필요가 없습니다.

 


Non-Reifiable Types (비구체화 타입)

 

Non-Reifiable Types(비구체화 타입)은 Java에서 런타임에 완전한 타입 정보를 유지하지 않는 제네릭 타입을 의미합니다. 즉, 컴파일 타임에만 타입 정보가 사용되고, 런타임에는 타입 정보가 소거되는 타입입니다. 이로 인해 일부 제네릭 타입은 런타임에 타입 검사를 할 수 없으며, 이는 타입 안전성 문제를 일으킬 수 있습니다.

 

 

Reifiable Types Vs Non-Reifiable Types

 

Reifiable Types

Reifiable Types(구체화 타입)은 컴파일 타임과 런타임 모두에서 완전한 타입 정보를 유지하는 타입입니다. 이러한 타입들은 다음과 같습니다:

  • 기본 데이터 타입 (예: int, double)
  • 비제네릭 타입 (예: String, Object)
  • 원시 타입(raw type) (예: List, Map)
  • 제네릭 타입에서 소거된 타입 파라미터를 포함하는 타입 (예: List<?>, Map<?, ?>)

Non-Reifiable Types

Non-Reifiable Types(비구체화 타입)은 런타임에 완전한 타입 정보를 유지하지 않는 제네릭 타입입니다. 이러한 타입들은 다음과 같습니다:

  • 매개변수화된 타입 (예: List<String>, Map<String, Integer>)
  • 제네릭 배열 타입 (예: List<String>[], T[])

Non-Reifiable Type 사용 예

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

public class NonReifiableTypeExample {
    public static void main(String[] args) {
        List<String>[] stringLists = new ArrayList[10]; // 경고 발생
        Object[] objArray = stringLists;
        List<Integer> intList = new ArrayList<>();
        intList.add(42);
        objArray[0] = intList; // ClassCastException 발생 가능

        String s = stringLists[0].get(0); // ClassCastException 발생
    }
}

위 코드에서 List<String>[]는 비구체화 타입입니다. 이로 인해 런타임에 타입 안전성을 보장할 수 없습니다.

 

 

요약

  • Reifiable Types(구체화 타입): 런타임에 완전한 타입 정보를 유지하는 타입.
  • Non-Reifiable Types(비구체화 타입): 런타임에 완전한 타입 정보를 유지하지 않는 제네릭 타입. 타입 소거로 인해 발생.
  • 주의 사항: Non-Reifiable Types는 런타임에 타입 안전성을 보장할 수 없으므로 사용에 주의해야 함. 특히, 제네릭 배열 사용 시 주의가 필요.
  • 경고 억제: @SuppressWarnings("unchecked")를 사용하여 경고를 억제할 수 있지만, 타입 안전성을 보장하지 않으므로 신중히 사용해야 함.

Heap Pollution(힙 오염)

 

Java에서 제네릭 타입의 안전성을 위반하여 잘못된 타입의 객체가 힙에 저장되는 상황을 말합니다. 이는 주로 제네릭의 타입 안전성이 보장되지 않는 방법으로 객체를 다룰 때 발생합니다. 힙 오염이 발생하면 런타임 시 ClassCastException과 같은 예외가 발생할 가능성이 높아집니다.

 

Heap Pollution의 원인

Heap Pollution은 주로 다음과 같은 상황에서 발생합니다:

  1. 제네릭 타입의 가변 인자(varargs) 사용:
    • 가변 인자 메소드를 사용할 때 제네릭 타입을 전달하면, 이로 인해 타입 안전성이 손상될 수 있습니다.
  2. 비제네릭 코드와 제네릭 코드의 혼합:
    • 제네릭 타입을 사용하는 코드와 원시 타입(raw type)을 사용하는 코드를 혼합할 때 발생할 수 있습니다.
  3. 잘못된 캐스팅:
    • 제네릭 타입을 잘못 캐스팅하면 힙 오염이 발생할 수 있습니다.

 

다음은 힙 오염이 발생할 수 있는 예제입니다.

import java.util.Arrays;
import java.util.List;

public class HeapPollutionExample {
    public static void main(String[] args) {
        List<String> stringList1 = Arrays.asList("one", "two");
        List<String> stringList2 = Arrays.asList("three", "four");
        
        // 경고: 제네릭 타입의 가변 인자 사용
        heapPollutionMethod(stringList1, stringList2);
    }

    @SafeVarargs
    private static void heapPollutionMethod(List<String>... lists) {
        Object[] array = lists;
        array[0] = Arrays.asList(42); // 힙 오염 발생
        String s = lists[0].get(0); // ClassCastException 발생 가능
    }
}

위 예제에서 heapPollutionMethod는 제네릭 타입의 가변 인자를 사용합니다. 이로 인해 Object 배열로 변환할 수 있으며, 잘못된 타입의 객체(Arrays.asList(42))를 배열에 저장할 수 있습니다. 이는 런타임에 ClassCastException을 일으킬 수 있습니다.

 

 

Heap Pollution 방지 방법

  1. 비제네릭 코드와 제네릭 코드의 혼합을 피함:
    • 가능한 한 원시 타입(raw type)을 사용하지 않도록 합니다.
  2. 제네릭 타입의 가변 인자 사용 피함:
    • 제네릭 타입의 가변 인자를 사용하는 메소드를 피하고, 대신 리스트나 컬렉션을 사용합니다.
  3. 타입 안전성 유지:
    • 타입 캐스팅을 최소화하고, 항상 타입 안전성을 유지하도록 합니다.
  4. @SafeVarargs 어노테이션 사용:
    • 제네릭 타입의 가변 인자를 사용하는 메소드가 타입 안전하다고 확신할 수 있는 경우, @SafeVarargs 어노테이션을 사용하여 경고를 억제할 수 있습니다.

 

 

예제: 타입 안전한 코드 작성

import java.util.Arrays;
import java.util.List;

public class TypeSafeExample {
    public static void main(String[] args) {
        List<String> stringList1 = Arrays.asList("one", "two");
        List<String> stringList2 = Arrays.asList("three", "four");
        
        // 타입 안전한 메소드 사용
        typeSafeMethod(Arrays.asList(stringList1, stringList2));
    }

    private static void typeSafeMethod(List<List<String>> lists) {
        for (List<String> list : lists) {
            for (String s : list) {
                System.out.println(s);
            }
        }
    }
}

위 예제는 제네릭 타입의 가변 인자를 사용하지 않고, 대신 리스트를 사용하여 타입 안전성을 유지합니다.

 

 

요약

  • **Heap Pollution(힙 오염)**은 잘못된 타입의 객체가 힙에 저장되는 상황을 말합니다.
  • 힙 오염은 주로 제네릭 타입의 가변 인자 사용, 비제네릭 코드와 제네릭 코드의 혼합, 잘못된 캐스팅으로 인해 발생합니다.
  • 힙 오염을 방지하려면 비제네릭 코드와 제네릭 코드를 혼합하지 않고, 제네릭 타입의 가변 인자를 피하며, 항상 타입 안전성을 유지해야 합니다.
  • @SafeVarargs 어노테이션을 사용하여 타입 안전한 가변 인자 메소드의 경고를 억제할 수 있습니다.