✅ Type Erasure
- 타입 소거
제네릭은 컴파일 타임에 더 엄격한 타입 체크를 제공하고 제네릭 프로그래밍을 지원하기 위해 Java 언어에 도입되었습니다. 제네릭을 구현하기 위해 Java 컴파일러는 타입 소거(Type Erasure)를 다음에 적용합니다:
- 제네릭 타입의 모든 타입 파라미터를 Bounds로 바꾸거나 타입 파라미터가 제한되지 않은 경우 Object로 바꿉니다. 따라서 생성된 바이트코드에는 일반 클래스, 인터페이스 및 메서드만 포함됩니다.
- 타입 안전을 유지하기 위해 필요한 경우 타입 캐스트를 삽입하십시오.
- 확장된 제네릭 타입에서 다형성을 유지하기 위해 브리지 메서드를 생성합니다.
타입 소거는 파라미터화된 타입에 대해 새 클래스가 생성되지 않도록 합니다. 결과적으로 제네릭은 런타임 오버헤드를 발생시키지 않습니다.
✅ Erasure of Generic Types
- 제네릭 타입 소거
타입 소거 프로세스 중에 Java 컴파일러는 모든 타입 파라미터를 지우고 타입 파라미터가 제한되어 있으면 각각을 첫 번째 바인딩으로, 타입 파라미터가 제한되지 않으면 Object로 바꿉니다.
- 다음 클래스를 살펴봅시다.
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; } // ... }
타입 파라미터 T는 제한(bounds)이 없기 때문에 Java 컴파일러는 이를 Object로 바꿉니다.
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; } // ... }
👉 내부적으로 타입 파라미터 T가 Object로 변환됩니다.
다음 예제에서 일반 Node 클래스는 제한된 타입 파라미터를 사용합니다.
public class Node<T extends Comparable<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; } // ... }
Java 컴파일러는 경계 타입 파라미터 T를 첫 번째 경계 클래스인 Comparable로 바꿉니다.
public class Node { private Comparable data; private Node next; public Node(Comparable data, Node next) { this.data = data; this.next = next; } public Comparable getData() { return data; } // ... }
- 다음 클래스를 살펴봅시다.
✅ Erasure of Generic Methods
- 제네릭 메소드의 타입 소거
Java 컴파일러는 제너릭 메소드 또한 아규먼트에서 타입 파라미터를 지웁니다.
// 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; }
타입 파라미터 T는 제한(bounds)이 없기 때문에 Java 컴파일러는 이를 Object로 바꿉니다.
public static int count(Object[] anArray, Object elem) { int cnt = 0; for (Object e : anArray) if (e.equals(elem)) ++cnt; return cnt; }
다음 클래스가 정의되어 있다고 가정합니다.
class Shape { /* ... */ } class Circle extends Shape { /* ... */ } class Rectangle extends Shape { /* ... */ }
다른 Shape을 그리는 제너릭 메서드를 작성할 수 있습니다.
public static <T extends Shape> void draw(T shape) { /* ... */ }
Java 컴파일러는 타입 파라미터 T를 아래와 같이 Shape으로 변경합니다.
public static void draw(Shape shape) { /* ... */ }
✅ Effects of Type Erasure and Bridge Methods
- 타입 소거의 효과와 브릿지 메소드
때때로 타입 소거로 인해 예상하지 못한 상황이 발생할 수 있습니다. 다음 예에서는 이것이 어떻게 발생하는지 보여줍니다. 다음 예제는 컴파일러가 때때로 타입 소거 프로세스의 일부로 브리지 메서드라고 하는 합성 메서드를 생성하는 방법을 보여줍니다.
➡️ 다음 두 클래스가 주어집니다.
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); } }
➡️ 두 클래스의 객체를 생성하고 메소드를 호출합니다.
MyNode mn = new MyNode(5); Node n = mn; // A raw type - compiler throws an unchecked warning n.setData("Hello"); // Causes a ClassCastException to be thrown. Integer x = mn.data;
➡️ 타입 소거 후 위 코드는 다음과 같습니다.
MyNode mn = new MyNode(5); Node n = mn; // A raw type - compiler throws an unchecked warning // Note: This statement could instead be the following: // Node n = (Node)mn; // However, the compiler doesn't generate a cast because // it isn't required. n.setData("Hello"); // Causes a ClassCastException to be thrown. Integer x = (Integer)mn.data;
왜
n.setData(”Hello”);
에서 ClassCastException이 발생하는지 다음 섹션에서 설명하겠습니다.
🔹Bridge Methods
- 브릿지 메소드
파라미터된 클래스를 확장하거나 파라미터된 인터페이스를 구현하는 클래스 또는 인터페이스를 컴파일할 때 컴파일러는 타입 소거 프로세스의 일부로 브리지 메서드라고 하는 합성 메서드를 생성해야 할 수 있습니다. 일반적으로 브리지 메서드에 대해 걱정할 필요는 없지만 스택 추적에 브리지 메서드가 나타나면 당황할 수 있습니다.
➡️ 타입 소거 후에, Node와 MyNode 클래스는 다음과 같이 나타납니다.
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로 처리되기 때문에 T 에 해당하는 타입은 모두 Objcet로 변경되었습니다. 타입 소거 후 메서드 시그니처가 일치하지 않습니다. Node.setData(T) 메서드는 Node.setData(Object)가 됩니다. 결과적으로 MyNode.setData(Integer) 메서드는 Node.setData(Object) 메서드를 재정의하지 않습니다.
이 문제를 해결하고 타입 소거 후 제너릭 타입의 polymorphism을 유지하기 위해 Java 컴파일러는 하위 타입 지정이 예상대로 작동하는지 확인하는 브리지 메서드를 생성합니다.
➡️ 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)는 원래 MyNode.setData(Integer) 메서드에 위임합니다. 결과적으로 n.setData("Hello"); 명령문은 MyNode.setData(Object) 메서드를 호출하고 "Hello"를 Integer로 캐스팅할 수 없기 때문에 ClassCastException이 발생합니다.
👉 실제로 위 사진에서 우리가 정의하지 않은 MyNode.setData(Object)가 호출된다는 것을 확인할 수 있습니다. 이는 타입 소거의 결과이며 브릿지 메소드가 생성되어 MyNode.setData(Integer) 메소드에게 위임하는 과정입니다. 위임한다는 말은, 기존의 setData(Object data) 메서드가 setData(Integer data) 메서드를 호출한다는 말과 같습니다.
우리가 정의하지 않는 브릿지 메소드가 호출되기 때문에 당황스러울 수 있다고 이야기한 것입니다.
✅ Non-Reifiable Types
- 검증 가능한 타입, 비검증 가능한 타입
검증 가능한 타입은 런타임에도 제네릭 타입의 실제 타입 정보가 유지되어 타입 검사를 수행할 수 있는 타입을 의미합니다. 여기에는 프리미티브, 비제네릭 타입, 원시 타입(raw Type) 및 바인딩되지 않은 와일드카드(extends, super을 사용하지 않은, “?”만 사용) 호출이 포함됩니다.
비검증 가능한 타입은 런타임에 제네릭 타입의 실제 타입 정보가 소거되어 타입 검사를 수행할 수 없는 타입을 의미합니다. 제한되지 않은(unbounded) 와일드카드로 정의되지 않은 제너릭 타입의 호출인 타입 소거에 의해 컴파일 타임에 정보가 제거된 타입입니다. 수정 불가능 타입은 런타임에 모든 정보를 사용할 수 없습니다. 수정 불가능한 타입의 예로는 List<String> 및 List<Number>가 있습니다. JVM은 런타임 시 이러한 타입 간의 차이를 구분할 수 없습니다. 제네릭에 대한 제한에서 볼 수 있듯이 수정 불가능한 타입을 사용할 수 없는 특정 상황이 있습니다. 예를 들어 instanceof 표현식에서 또는 배열의 요소로 사용할 수 있습니다.
➡️ 비검증 가능한(non-reifiable) 타입이 instanceof 연산자와 배열의 요소로 사용할 수 없음을 보여주는 예시 코드:
- instanceof 연산자의 피연산자로 비검증 가능한 타입 사용 불가
public class Example { public static void main(String[] args) { List<String> stringList = new ArrayList<>(); boolean isList = stringList instanceof List<String>; // 컴파일 에러! // instanceof 연산자의 피연산자로 비검증 가능한 타입 사용 불가 } }
위의 예시에서
stringList
는List<String>
타입의 객체입니다. 그러나stringList instanceof List<String>
표현식은 컴파일 에러가 발생합니다. 비검증 가능한(non-reifiable) 타입인List<String>
은 instanceof 연산자의 피연산자로 사용할 수 없습니다.👉
instanceof
연산자는 런타임 시에 객체의 타입을 확인하는 데 사용됩니다. 그러나 Java에서는 제네릭 타입에 대한 타입 정보가 런타임에는 소거되는 타입 소거(type erasure)라는 개념을 적용합니다. 타입 소거는 제네릭 타입의 타입 매개변수 정보를 컴파일 시에만 유지하고, 런타임에는 해당 정보를 제거하여 일반적인 타입으로 처리하는 과정입니다.예를 들어,
List<String>
은 제네릭 타입인데, 컴파일된 코드에서는List
로 타입이 소거됩니다. 타입 소거에 의해List<String>
는 런타임에는 모두 동일한 원시 타입인List
로 취급됩니다.따라서
stringList instanceof List<String>
와 같은 코드는 컴파일 시에는 허용되지 않습니다. 이는instanceof
연산자가 런타임에 객체의 타입을 확인하는데 사용되는데, 제네릭 타입의 타입 매개변수 정보가 소거되므로List<String>
이 런타임에서는 단순히List
로 취급되기 때문입니다.타입 소거로 인해 제네릭 타입의 타입 매개변수 정보가 소거되고, 모든 제네릭 타입은 컴파일된 코드에서는 원시 타입으로 취급됩니다. 따라서
instanceof
연산자를 사용할 때에는 원시 타입으로 검사해야 하며, 제네릭 타입의 구체적인 타입 정보는 사용할 수 없습니다.
- instanceof 연산자의 피연산자로 비검증 가능한 타입 사용 불가
✅ Heap Pollution
파라미터된 타입의 변수가 파라미터된 타입이 아닌 객체를 참조할 때 힙 오염이 발생합니다. 이 상황은 프로그램이 컴파일 타임에 확인되지 않은 경고를 발생시키는 일부 작업을 수행한 경우에 발생합니다. 확인되지 않은 경고는 컴파일 시간(컴파일 시간 유형 검사 규칙의 제한 내) 또는 런타임에 파라미터된 타입(예: 캐스트 또는 메서드 호출)과 관련된 작업의 정확성을 확인할 수 없는 경우 생성됩니다. 확인. 예를 들어 원시 타입과 파라미터된 타입을 혼합하거나 확인되지 않은 캐스트를 수행할 때 힙 오염이 발생합니다.
👉 아래는 기본적인 힙 오염이 발생하는 예시입니다.
- 힙 오염이 발생하는 예시(1)
import java.util.ArrayList; import java.util.List; public class HeapPollutionExample { public static void main(String[] args) { List<String> stringList = new ArrayList<>(); List<Object> objectList = (List<Object>) stringList; // 비검증 가능한 타입의 형변환 objectList.add(10); // 잘못된 유형의 객체를 추가, Integer을 추가함 String value = stringList.get(0); // ClassCastException 발생: 잘못된 유형의 객체를 가져옴 } }
위의 코드에서, stringList는 List<String> 타입의 리스트입니다. 그러나 objectList에 형변환을 통해 할당할 때 비검증 가능한(non-reifiable) 타입이 됩니다. 이후 objectList에 add 메서드를 사용하여 잘못된 유형의 객체인 정수를 추가합니다. 이는 힙 오염을 발생시키는 예시입니다. 마지막으로 stringList에서 첫 번째 요소를 가져올 때 ClassCastException이 발생하게 됩니다. 이는 힙 오염으로 인해 잘못된 유형의 객체가 가져와졌음을 나타냅니다.
🔹Potential Vulnerabilities of Varargs Methods with Non-Reifiable Formal Parameters
- 비검증 가능한 파라미터가 있는 가변 인자 메소드의 취약성
varargs 입력 파라미터를 포함하는 제네릭 메서드는 힙 오염을 일으킬 수 있습니다.
- Consider the following ArrayBuilder class
public class ArrayBuilder { public static <T> void addToList (List<T> listArg, T... elements) { for (T x : elements) { listArg.add(x); } } public static void faultyMethod(List<String>... l) { Object[] objectArray = l; // Valid objectArray[0] = Arrays.asList(42); String s = l[0].get(0); // ClassCastException thrown here } }
- The following example, HeapPollutionExample uses the ArrayBuiler class
public class HeapPollutionExample { public static void main(String[] args) { List<String> stringListA = new ArrayList<String>(); List<String> stringListB = new ArrayList<String>(); ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine"); ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve"); List<List<String>> listOfStringLists = new ArrayList<List<String>>(); ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB); ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!")); } }
컴파일할 때 ArrayBuilder.addToList 메서드의 정의에 의해 다음 경고가 생성됩니다.
warning: [varargs] Possible heap pollution from parameterized vararg type T
파일러는 varargs 메서드를 만나면 varargs formal 파라미터를 배열로 변환합니다. 그러나 Java 프로그래밍 언어는 파라미터된 타입의 배열 작성을 허용하지 않습니다. 메서드 ArrayBuilder.addToList에서 컴파일러는 varargs formal 파라미터 T... 요소를 formal 파라미터 T[] 요소인 배열로 변환합니다. 그러나 타입 소거로 인해 컴파일러는 varargs formal 파라미터를 Object[] 요소로 변환합니다. 결과적으로 더미 오염의 가능성이 있습니다.
- Consider the following ArrayBuilder class
Uploaded by N2T