본문 바로가기
학습/Java

Generics의 Type Erasure(타입 소거)

코동이 2024. 3. 8.

*개요

지네릭의 타입소거를 알아봅니다.

 

 

타입 소거란?


 타입 소거는 지네릭을 사용할 때 '컴파일 타임'에 엄격한 타입 체크를 위해 자바에서 제공하는 전략입니다. 왜 '컴파일 타임'일까요? 지네릭 타입은 컴파일 타임에만 존재하고 런타임에서는 사라지도록 설계되었기 때문입니다. JDK 1.5 이전 버전에서는 지네릭이 존재하지 않았기 때문에 과거 레거시 코드와의 호환성을 위해 컴파일에서만 엄격하게 타입 체크를 하고 런타임은 동일하게 유지합니다. class 파일로 변환된 코드를 확인해보면 지네릭 코드는 하나도 없고 모두 적절하게 형 변환되어 레거시 코드와 호환을 보장할 수 있습니다. 개발자는 지네릭을 통해 컴파일 타임에 미리 문제를 파악할 수 있어 안전한 코딩을 할 수 있습니다.

 

"지네릭 타입은 컴파일 타임에만 존재하고 런타임에서는 사라진다" 를 구체적으로 알아보겠습니다.

 

타입 소거 특징은 다음과 같습니다.

 

  • 타입 매개변수가 비한정적이면 Object로, 한정적이면 지네릭 타입으로 모든 타입 매개변수를 교체합니다.

비한정적 : Node<T>, T data, Map<?,?>

한정적 : Node<T extends Number>, <T extends Number> data, Map<String, String>

 

  • 타입 안정성 유지를 위해 필요시 형 변환을 수행합니다.

 

  • 확장된 지네릭 타입에서 필요시, 다형성을 유지하기 위해 브릿지(bridge) 메서드를 생성합니다.

 

 

간단한 예시를 통해 지네릭 타입 소거를 확인해보겠습니다.

 

//컴파일 타임
public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

//런타임
public class Node {

    private Object data;
    private Node next;

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

    public Object getData() { return data; }
    // ...
}

 

 컴파일 타임과 런타임의 차이점 2개는 Node<T> -> Node, T-> Object로 변경입니다. 런타임에는 지네릭이 소거되기 때문에 Node<T>는 Node로 교체되었습니다. 타입 T는 비한정적이므로 Object로 교체됩니다. extends, super 혹은 구체 타입이 없기 때문에 비한정적입니다.

 

 

 간단한 예시를 통해 지네릭 메서드 소거를 확인해보겠습니다.

 

// Counts the number of occurrences of elem in anArray.

//컴파일 타임
public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

//런타임
public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

 

  타입 T는 역시 비한정적이므로 Object로 교체됩니다. 지네릭 메서드의 지네릭 타입 <T> 도 런타임에서는 Object 타입으로 교체되면서 소거됩니다.

 

 

간단한 예시를 통해 한정적 지네릭 타입 소거를 확인해보겠습니다.

 

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

//컴파일 타임
public static <T extends Shape> void draw(T shape) { /* ... */ }


//런타임
public static void draw(Shape shape) { /* ... */ }

 

<T extends Shape> 에서 타입 T는 Shape를 상속하며 범위가 한정되므로, 한정적 지네릭 타입 소거가 적용됩니다. 따라서 타입 T는 Object가 아닌 Shape로 교체됩니다.

 

 

 

브릿지 메서드


 컴파일러는 타입 소거 시 특정 경우 브릿지 메서드라는 합성 메서드를 생성합니다. 개발자가 의도하지 않게 예외가 발생할 수 있어 주의해야 합니다.

 

간단한 예시를 통해 브릿지 메서드 생성 확인해보겠습니다.

 

public class Node<T> {

    public T data;

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

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

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

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

 

 지네릭 클래스 Node<T>와 이를 상속하는 MyNode가 있습니다. 

 

//원본 main 코드:

MyNode mn = new MyNode(5);
Node n = mn;            // 로타입 - 컴파일러는 비검사 경고를 알린다.
n.setData("Hello");     // ClassCastException 발생 구간
Integer x = mn.data; 


//타입 소거 이후 main 코드 상태:
MyNode mn = new MyNode(5);
Node n = mn;            // 로타입 - 컴파일러는 비검사 경고를 알린다.
                        // 참고: 이 문은 다음과 같을 수도 있습니다
                        //     Node n = (Node)mn;
                        // 그러나 컴파일러는 클래스 변환하지 않습니다.
                        // 왜냐하면 자동 형변환하기 때문입니다.
n.setData("Hello");     // ClassCastException 발생 구간
Integer x = (Integer)mn.data;

 

 다형성을 활용해 Node 타입의 참조변수가 자신을 상속하는 자손의 MyNode 타입을 참조합니다. 다형성을 활용하기 때문에 Node n = mn은 아무런 문제가 없습니다. 그리서 문자열이 필요해 n.setData("Hello") 합니다. 컴파일러는 클래스 형 변환이 가능한지 관계만 체크하고 내부에 어떤 타입을 가지고 있는지 확인하지 않습니다. 따라서 컴파일 타임에는 예외를 인식하지 못하고 런타임에서 비로소 예외를 확인합니다.

 

 

위의 예시 코드가 타입 소거 후 어떤 코드인지 확인해보겠습니다.

 

public class Node {

    public Object data;

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

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

 

 비한정적 와일드카드이기 때문에 T -> Object로 교체됩니다. 이 과정에서 Node.setData(Object data)MyNode.setData(Integer data) 의 매개변수 타입이 달라집니다. MyNode는 자손임에도 불구하고 setData 메서드를 오버라이드 하지 못합니다. 따라서 타입 소거 후에 다형성을 유지하여 서브 타입에서도 정상 작동하기 위해서 브릿지 메서드가 등장합니다.

 

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

 

 브릿지 메서드 MyNode.setData(Object data) 는 부모인 Node 클래스가 가진 메서드를 오버라이드 합니다. 명시적으로 Integer로 형변환하여 MyNode.setData(Integer data)에 위임합니다. 브릿지 메서드는 자신이 로직을 수행하지 않고 이름 그대로 다른 메서드에 연결하는  다리 역할입니다. 따라서 n.setData("Hello") 가 처음에는 MyNode.setData(Object data) 로 진입하지만 결국 MyNode.setData(Integer data) 에 위임되고 String 형을 Integer 형으로 변환할 수 없어 런타임에 ClassCastException 예외를 확인합니다.

 

  결국 개발자가 사용한 지네릭은 컴파일러가 자동으로 타입 소거 및 형 변환, 때때로 브릿지 메서드 생성으로 코드가 잘 동작하도록 도와줍니다. 따라서, 개발자는 지네릭의 쓰임새에 맞게 잘 사용만 하면 훨씬 견고한 코드를 작성할 수 있습니다.

반응형

'학습 > Java' 카테고리의 다른 글

싱글톤(Singleton)  (0) 2024.03.18
OOD, OOP(객체 지향 개발)의 원칙 - SOLID  (1) 2024.03.15
Generics의 Wildcards(와일드카드)  (0) 2024.03.07
Value Object 패턴  (0) 2023.07.07
Entity vs Value Object 차이점  (0) 2023.06.30