Java

[Java] 바이트코드로 분석하는 new T()가 안되는 이유

OptimizerStart 2025. 4. 27. 22:24

Intro

제네릭 타입 변수가 new T()로 인스턴스를 생성할 수 없는 이유를 자바의 바이트 코드 관점에서 분석해보자. 

 

제네릭 클래스가 아닌 클래스의 new 키워드 적용

new 키워드가 어떻게 생성할 클래스를 알고 있는지 확인할 코드 예제이다. 아래 코드를 컴파일 한 후 NewKeywordTest.class 클래스 파일을 만든다. 

java
닫기
package CH12_Generics_Enums_Annotation; public class NewKeywordTest { ​​​​public static void main(String[] args) { ​​​​​​​​String str; ​​​​​​​​str = new String("HELLO TEST"); ​​​​} }

 

 

 

'javap -v 클래스파일명' 명령어를 통해 (*.class) 파일을 사람이 읽기 쉬운 형태로 보여주게 하자.

[그림2]에서 new 명령어의 피연산자인 #7이 constant pool에 기록된 'java/lang/String'을 가리키는 걸 확인할 수 있다. 

즉, 바이트 코드 단계에서 "어떤 클래스를 생성할지" constant pool에 명시해야한다를 알 수 있다.

[그림 1] javap -v 명령어 결과 Constant Pool 내용 확인

[그림1] #7의 'Class #8 ' 부분이 new 키워드에서 참조할 클래스를 의미한다.

[그림1] #8에는 Class가 java/lang/String인것을 확인할 수 있다. 즉, 바이트 코드 레벨에서는 '어떤 클래스를 생성'할지 constant pool (상수 풀)에서 명시해야한다. 

 

[그림2] 클래스 파일(*.class) 파일을 디스어셈블한 결과

일반적인 경우, new 클래스(); 는 0: new   #7을 통해 상수 풀에 있는 클래스 정보를 참조해 객체를 생성함을 확인할 수 있다. 

 

정상적인 지네릭 타입 제거 과정

지네릭 클래스의 코드 예시이다. 

java
닫기
package CH12_Generics_Enums_Annotation; class Box<T> {} // 내용 생략 class Fruit{} // 내용 생략 public class GenericNewTest { ​​​​public static void main(String[] args) { ​​​​​​​​Box<Fruit> box = new Box<Fruit>(); ​​​​} }

 

[그림4] 지네릭 클래스에서도 동일하게 상수 풀의 Class에 참조형태로 클래스 명(Box)를 기록해두었다. 단, 대입된 타입 Fruit은 기록해두지 않았다. 이는 타입이 컴파일 시점에 제거를 하기 때문이다. 

[그림4] 지네릭 클래스가 있는 클래스 파일의 상수 풀

[그림5] 제네릭 클래스의 클래스 파일을 디스어셈블한 결과도, 제네릭 적용안한 클래스 파일과 동일한 형태로 표현됐다. 

new #7 로 생성할 클래스를 상수풀에서 참조를 하고 있다. 

[그림5] 제네릭 호함된 클래스의 클래스 파일을 디스어셈블한 결과

 

 

 

[그림6]은 개발자가 고급언어인 자바로 소스코드를 작성하는 시점부터 main메서드를 실행하기까지 일련의 과정을 정리한 것이다. 

기존에 갖고 있던 생각은
1. 지네릭 타입이 제거가 된다는데, 그럼 new T();에서 T는 대입된 타입(Fruit) 혹은 Object로 대체되는 것이 아닌가? 
2. 그렇다면 컴파일러는 생성할 타입을 알고 있는데, 왜 잘못된 문법이라고 에러를 표시할까? 

[그림6] 코드 작성 후 main 메서드 실행까지 일련의 과정

 

1번 답 & 2번 답

'new T()' 에러 4단계 속성 부여(의미 분석)단계에서 발생한다.

즉, 6단계 설탕 문법 해소를 통해 타입 대체 및 제네릭 타입 제거를 하기 전에 4단계 속성 부여에서 에러가 발생하여 타입 대체가 발생하지 않는다. 

 

4단계 속성 부여(의미 분석)에서 에러 발생하는 이유

new 뒤에 오는 타입은 reifiable(재구성)이 가능해야한다.

하지만 타입 변수는 타입을 제거하므로, 런타임에 확인할 수 없어 재구성이 불가능해 에러를 발생시킨다. 

 

타입을 대체하는 것은 이전 단계를 정상적으로 통과한 '유효한 코드'만 넘어 온 후에 수행된다. 4단계에서 이미 걸러졌기 때문에 타입 대체가 적용되지 않는다. 

재구성(reifiable) 가능
런타임에도 모든 타입 정보가 남아있어 JVM이 확인할 수 있는 타입
ex. 원시 타입(int, boolean, char, double,  등), 비제네릭 클래스/인터페이스 타입(String, Integer 등), 모든 타입 인자가 와일드 카드인 제네릭 ( List<?>, Map<?> 등)

재구성 불가능 (non-reifiable) 
런타임에 정보가 지워지는 타입 . 컴파일 때 타입이 소거됨. 
ex. 타입변수(T, E), 매개변수화된 제네릭 타입 (List<String>), 제한된 와일드 카드 (List < ? extends Fruit> )

 

 

Conclusion

 

new 연산은 바이트코드 단계에서 constant pool 에 있는 구체 클래스 이름이 필요

설탕 문법 제거 및 타입 제거 과정에서는 이미 유효성 검사를 통과한 코드만 변환 → new T()는 해당되지 않음