2주차 Unit 3.2 — 상수 풀(Constant Pool)의 생성과 구조

Psj·2026년 5월 15일

F-lab

목록 보기
61/238

Unit 3.2 — 상수 풀(Constant Pool)의 생성과 구조

F-LAB JAVA · 2주차 · Phase 3 · 바이트코드와 상수 풀


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • Constant Pool (.class 파일 안) 과 String Pool (런타임 Heap) 의 차이는?
  • 상수 풀에 들어가는 14가지 항목 타입의 카테고리는?
  • 같은 "hello" 리터럴이 코드에 5번 등장하면 상수 풀에 몇 번 들어가는가?
  • Methodref 항목 1개가 실제로 몇 개의 다른 항목을 참조하는가?
  • #10 = Class #11 같은 항목의 의미는?
  • 컴파일러가 상수 풀을 만들면서 어떤 최적화를 하는가?
  • 운영에서 상수 풀 크기를 줄여야 할 순간은 언제인가?

🎯 핵심 한 문장

Constant Pool은 ".class 파일 안에 들어있는 정적 상수 표"다.
컴파일 타임에 만들어지고, 바이트코드의 모든 #N 인덱스가 여기를 가리킨다.
클래스가 사용하는 모든 외부 참조 (다른 클래스, 메서드, 필드, 문자열) 가 이곳에 1번씩만 저장되며,
JVM이 클래스 로딩 시 이 표를 보고 실제 메모리 주소로 해소(resolve) 한다.

⚠️ 두 가지 다른 "풀"의 구분

위치시점내용
Constant Pool.class 파일 안컴파일 타임 생성, 클래스 로딩 시 메모리 적재메타 정보 (Class, Method, Field 참조 + 리터럴)
String PoolHeap의 일부 (Java 7+)런타임실제 String 객체 인스턴스

→ 둘 다 "풀" 이라 헷갈리지만 다른 개념.
→ Unit 1.6에서 본 것은 String Pool.
→ 이번 Unit은 Constant Pool.

비유 — 도서관 카드 카탈로그

카탈로그 시스템JVM
책 (실제 자료)다른 클래스/메서드/필드의 실제 정의
카드 카탈로그 (.class 안의 카드 목록)Constant Pool
카드에 적힌 정보책 제목, 저자, 청구기호 등
카드 번호 (#15)상수 풀 인덱스
사서가 카드 보고 책 찾기JVM의 Resolution (다음 Unit 3.3)

도서관 입장에서:

  • 모든 책이 동시에 책장에 있는 게 아님
  • 일단 카드 목록만 정리해두고
  • 필요할 때 카드 번호로 책 찾으러 감

→ 컴파일 타임에는 다른 클래스의 실제 주소를 모름. 카드(=심볼) 로 적어둠.
→ 클래스 로딩 시 카드를 따라가 실제 위치 찾음.


🧭 9개 섹션 로드맵

1. 두 가지 풀의 결정적 구분
2. Constant Pool이 왜 필요한가
3. 14가지 항목 타입 — 전체 카테고리
4. 항목 간 참조 — 단순 값 vs 복합 참조
5. javap -v로 상수 풀 직접 보기
6. 컴파일러가 상수 풀 만드는 과정
7. ILIC 실무 — 상수 풀 분석 사례
8. 흔한 실수 + 디버깅
9. 면접 질문 + 자기 점검

1️⃣ 두 가지 풀의 결정적 구분

1.1 같은 단어 "풀", 다른 개념

박승제씨가 1주차에서 본 String Pool과 이번 Unit의 Constant Pool은 완전히 다른 풀이다.

String Pool (Unit 1.6):
  위치: Heap (런타임)
  내용: String 객체의 실제 인스턴스
  목적: 같은 값의 String 객체 재사용
  생성: 런타임에 동적

Constant Pool (이번 Unit):
  위치: .class 파일 + Method Area
  내용: 클래스의 모든 외부 참조 (메타 정보)
  목적: 바이트코드의 #N 인덱스 해소
  생성: 컴파일 타임에 정적

1.2 어떻게 연결되는가

.java 소스
   ↓ 컴파일
.class 파일
   └── Constant Pool 섹션  ← (1) 여기 만들어짐
   
   ↓ JVM 클래스 로딩
   
Method Area
   └── 클래스 Klass
        └── 런타임 상수 풀  ← (2) 여기로 적재됨 (Constant Pool의 메모리 버전)
        
   ↓ 첫 String 리터럴 사용 시
   
Heap
   └── String Pool   ← (3) 여기 String 객체 생성됨

Constant Pool과 String Pool의 관계:

  • Constant Pool의 String 항목(#10 = String "hello") 이 있음
  • 클래스 로딩 + 첫 사용 시
  • JVM이 그 값을 보고 String Pool에 객체 등록
  • 이후 String Pool의 객체 참조를 반환

Constant Pool은 "재료", String Pool은 "완제품".

1.3 코드로 확인

public class App {
    public static void main(String[] args) {
        String a = "hello";    // String literal
    }
}

.class 파일 안:

Constant Pool:
  #10 = String      #16          // "hello"
  #16 = Utf8        hello        ← 실제 바이트

바이트코드:

0: ldc #10        // "hello"
2: astore_1

JVM의 동작:
1. ldc #10 만나면
2. Constant Pool의 #10 = String 타입, 가리키는 곳은 #16 (Utf8 "hello")
3. String Pool에 "hello" 있나? 없으면 등록
4. String Pool의 객체 참조를 Stack에 push

Constant Pool의 #10 항목String Pool의 String 객체는 다른 메모리에 있음.
→ 연결고리는 클래스 로딩 시 JVM이 만든다.


2️⃣ Constant Pool이 왜 필요한가

2.1 문제 — 컴파일 시점엔 외부 주소를 모름

public class ShipmentService {
    public Shipment findById(Long id) {
        return repository.findById(id).orElseThrow();
    }
}

이 코드를 컴파일하려면:

  • Shipment 클래스 정의 위치 알아야 함
  • repository.findById 메서드 위치 알아야 함
  • Optional.orElseThrow 메서드 위치 알아야 함

그런데 컴파일 시점엔:

  • 다른 클래스가 메모리에 로딩되지 않았음
  • 실제 메모리 주소가 존재하지 않음
  • 운영 환경마다 메모리 주소가 다를 수도 있음

2.2 해결 — 심볼 참조

컴파일러: "실제 주소를 모르겠는데, 이름은 알겠어"
        ↓
.class 파일에 "이름"으로 적어둠 (= 심볼 참조)
        ↓
런타임에 JVM이 그 이름을 찾아 실제 주소로 해소

이게 Constant Pool의 역할:

  • 클래스가 참조하는 모든 외부 자원의 이름과 시그니처 보관
  • 바이트코드는 인덱스(#15)로 이 항목을 가리킴
  • 런타임에 JVM이 실제 주소로 변환 (Resolution, Unit 3.3)

2.3 효율성 — 중복 제거

같은 참조가 코드에 100번 나와도:

public class Calc {
    public void method() {
        String a = "hello";
        String b = "hello";
        String c = "hello";
        // ... 100번
    }
}

Constant Pool에는 단 1번 저장:

Constant Pool:
  #5 = String  #6
  #6 = Utf8    hello

바이트코드에서 100번 모두 #5 참조:

ldc #5
astore_1
ldc #5
astore_2
ldc #5
...

.class 파일 크기 절약.
→ 런타임 메모리도 절약.

2.4 메타 정보의 단일화

String s = "hello";
String t = "hello";
boolean b = "hello".equals(s);

세 곳에서 "hello" 등장.
컴파일러가 정리:

Constant Pool:
  #5 = Utf8       hello             ← 실제 데이터 1번
  #6 = String     #5                ← String 항목 1번

세 곳의 ldc 명령 모두 #6 사용.
→ 일관성 + 효율성.

2.5 자기 점검 답변

같은 문자열 리터럴이 여러 곳에 쓰이면 상수 풀에 몇 번 저장되는가?

: 단 1번.

  • 컴파일러가 자동으로 중복 제거
  • 모든 사용처가 같은 인덱스를 참조
  • .class 파일 크기 + Method Area 사용량 최소화

3️⃣ 14가지 항목 타입 — 전체 카테고리

3.1 항목 타입 목록

Java 8 기준 17가지, 핵심 14가지:

Constant Pool 항목 타입:

  📝 텍스트/리터럴:
    1.  Utf8                      (실제 바이트 문자열)
    2.  String                    ("hello" 같은 자바 String)
    3.  Integer                   (int 상수)
    4.  Float                     (float 상수)
    5.  Long                      (long 상수)
    6.  Double                    (double 상수)
  
  🏷️ 클래스/타입 참조:
    7.  Class                     (클래스 또는 인터페이스)
    8.  NameAndType               (이름 + 시그니처 쌍)
  
  🔗 멤버 참조:
    9.  Fieldref                  (필드 참조)
    10. Methodref                 (메서드 참조)
    11. InterfaceMethodref        (인터페이스 메서드 참조)
  
  ⚡ 동적 호출 (Java 7+):
    12. MethodHandle              (메서드 핸들)
    13. MethodType                (메서드 시그니처)
    14. InvokeDynamic             (invokedynamic용)

3.2 각 항목의 역할

Utf8 — 가장 기본

#5 = Utf8    hello
  • 모든 텍스트의 진짜 저장소
  • 클래스명, 메서드명, 시그니처, String 값 모두 Utf8
  • 다른 항목들이 참조하는 벽돌

String — 자바 String 리터럴

#6 = String  #5    // 가리키는 곳: #5 = Utf8 "hello"
  • Utf8을 한 단계 감싼 자바 String 개념
  • 직접 데이터는 없고 Utf8을 참조
  • 왜? "hello"는 클래스명도 메서드명도 아닌 자바 String 값

Class — 클래스/인터페이스 참조

#10 = Class    #11
#11 = Utf8     com/ilic/Shipment
  • 어떤 클래스에 대한 참조
  • 내부 표현은 /로 구분 (Shipment.class 가 아닌 com/ilic/Shipment)
  • 자기 클래스, 부모 클래스, 사용하는 모든 클래스가 등록됨

NameAndType — 멤버 시그니처

#15 = NameAndType  #16:#17
#16 = Utf8         calculate
#17 = Utf8         (I)Ljava/math/BigDecimal;
  • 이름 + 디스크립터 쌍
  • 메서드: "이름 + 매개변수/반환 타입"
  • 필드: "이름 + 타입"
  • 멤버 참조의 부품

Methodref / Fieldref / InterfaceMethodref — 멤버 참조

#20 = Methodref  #10.#15
#10 = Class      #11     // com/ilic/Shipment
#15 = NameAndType #16:#17 // calculate:(I)Ljava/math/BigDecimal;

해독:

  • #20 = Method 참조
  • 어디 클래스? → #10 = Shipment
  • 무슨 메서드? → #15 = calculate(int) → BigDecimal

Methodref 1개가 결국 4개 항목 (Class + Utf8 + NameAndType + 두 Utf8)을 참조.

3.3 시각화 — 항목 간 그래프

바이트코드:
  invokevirtual #20

#20 (Methodref) ──┬──► #10 (Class) ──► #11 (Utf8) = "com/ilic/Shipment"
                  │
                  └──► #15 (NameAndType) ──┬──► #16 (Utf8) = "calculate"
                                            │
                                            └──► #17 (Utf8) = "(I)Ljava/math/BigDecimal;"

단일 메서드 호출에 5개 항목 참여.
→ 인덱스가 많아 보이는 이유.

3.4 InvokeDynamic — 람다와 String concat

Runnable r = () -> System.out.println("hi");
0: invokedynamic #5, 0    ← 람다 객체 생성
Constant Pool:
  #5 = InvokeDynamic  #0:#6
  #6 = NameAndType    #7:#8
  #7 = Utf8           run
  #8 = Utf8           ()Ljava/lang/Runnable;
  
  BootstrapMethods 섹션:
  #0: LambdaMetafactory.metafactory
  • 런타임에 JVM이 BootstrapMethod 호출
  • LambdaMetafactory가 람다 클래스 동적 생성
  • 결과 객체를 Stack에 push

→ Unit 3.1에서 살짝 본 invokedynamic 의 비밀.


4️⃣ 항목 간 참조 — 단순 값 vs 복합 참조

4.1 두 가지 유형

유형 1: 실제 값을 가진 항목
  - Utf8       → 바이트 데이터 직접 보유
  - Integer    → int 값
  - Float      → float 값
  - Long       → long 값
  - Double     → double 값

유형 2: 다른 항목을 참조하는 항목
  - String     → Utf8 1개 참조
  - Class      → Utf8 1개 참조
  - NameAndType → Utf8 2개 참조
  - Methodref  → Class 1개 + NameAndType 1개 참조
  - Fieldref   → Class 1개 + NameAndType 1개 참조
  - ...

4.2 왜 이렇게 쪼개졌나

public class Shipment {
    private String blNo;
    
    public String getBlNo() { return blNo; }
    public void setBlNo(String blNo) { this.blNo = blNo; }
}

세 곳에서 blNo 필드 참조 (생성자, getter, setter).
세 곳 모두 같은 Fieldref:

Constant Pool:
  #5 = Fieldref     #4.#15
  #4 = Class        #20  → Utf8 "com/ilic/Shipment"
  #15 = NameAndType #16:#17
  #16 = Utf8        blNo
  #17 = Utf8        Ljava/lang/String;

만약 모든 정보를 Fieldref 안에 다 넣으면:

  • 모든 필드 참조마다 클래스명, 필드명, 타입 다 반복
  • 같은 클래스명이 수십 번 중복

쪼개기:

  • "com/ilic/Shipment" 는 Utf8 1번
  • "Shipment 클래스" Class 항목 1번
  • "blNo" Utf8 1번
  • "String 타입" Utf8 1번
  • 필드 참조마다 위 항목들을 가리킴

중복 제거 + 재사용.

4.3 메서드의 경우

calc.add(1, 2);
calc.add(3, 4);

두 곳 모두:

invokevirtual #N    // 같은 인덱스

#N 항목:

#N  = Methodref   #C.#NT
#C  = Class       (Calc)
#NT = NameAndType (add:(II)I)

같은 메서드를 N번 호출해도 Constant Pool 항목은 변하지 않음.

4.4 ldc, ldc_w, ldc2_w

ldc #5      // 8-bit 인덱스 (#1 ~ #255)
ldc_w #500  // 16-bit 인덱스 (큰 풀)
ldc2_w #10  // long, double 전용

큰 클래스(Constant Pool 항목이 256개 초과)는 ldc_w 자동 사용.
→ 컴파일러가 알아서 선택.


5️⃣ javap -v로 상수 풀 직접 보기

5.1 실전 코드

public class ShipmentCalc {
    private int callCount = 0;
    
    public BigDecimal calculate(int weight) {
        callCount++;
        return BigDecimal.valueOf((long) weight * 100);
    }
}
javac ShipmentCalc.java
javap -c -v ShipmentCalc.class

5.2 출력 분석 (일부 발췌)

Constant pool:
  #1 = Methodref         #6.#23     // java/lang/Object."<init>":()V
  #2 = Fieldref          #5.#24     // ShipmentCalc.callCount:I
  #3 = Methodref         #25.#26    // java/math/BigDecimal.valueOf:(J)Ljava/math/BigDecimal;
  #4 = Class             #27        // ShipmentCalc
  #5 = Class             #28        // ShipmentCalc
  #6 = Class             #29        // java/lang/Object
  #7 = Utf8              callCount
  #8 = Utf8              I
  #9 = Utf8              <init>
  #10 = Utf8             ()V
  ...
  #23 = NameAndType      #9:#10     // "<init>":()V
  #24 = NameAndType      #7:#8      // callCount:I
  #25 = Class             #30       // java/math/BigDecimal
  #26 = NameAndType      #31:#32    // valueOf:(J)Ljava/math/BigDecimal;
  #27 = Utf8              ShipmentCalc
  #28 = Utf8              ShipmentCalc
  #29 = Utf8              java/lang/Object
  #30 = Utf8              java/math/BigDecimal
  #31 = Utf8              valueOf
  #32 = Utf8              (J)Ljava/math/BigDecimal;
  ...

5.3 한 줄씩 해독

#1 = Methodref  #6.#23   // java/lang/Object."<init>":()V
  • 타입: Methodref
  • 참조: #6 (Class) 와 #23 (NameAndType)
  • 의미: java/lang/Object 클래스의 <init> 메서드 (시그니처 ()V)
  • 사용처: 모든 생성자의 super() 호출
#2 = Fieldref  #5.#24   // ShipmentCalc.callCount:I
  • 타입: Fieldref
  • 참조: #5 (Class) 와 #24 (NameAndType)
  • 의미: ShipmentCalc 클래스의 callCount 필드 (타입 int)
  • 사용처: getfield, putfield 명령
#3 = Methodref  #25.#26   // java/math/BigDecimal.valueOf:(J)Ljava/math/BigDecimal;
  • 의미: BigDecimal의 valueOf(long) 메서드
  • 사용처: BigDecimal.valueOf(...) 호출 위치의 invokestatic

5.4 바이트코드와 매칭

public java.math.BigDecimal calculate(int);
    Code:
       0: aload_0
       1: dup
       2: getfield      #2         // Field callCount:I
       5: iconst_1
       6: iadd
       7: putfield      #2         // Field callCount:I
      10: iload_1
      11: i2l
      12: ldc2_w        #40        // long 100l
      15: lmul
      16: invokestatic  #3         // Method java/math/BigDecimal.valueOf:(J)Ljava/math/BigDecimal;
      19: areturn

#2 (Fieldref → callCount) → getfield/putfield에서 사용.
#3 (Methodref → BigDecimal.valueOf) → invokestatic에서 사용.

바이트코드의 모든 #N 이 풀 항목으로 풀이.

5.5 디스크립터 읽는 법

자주 보는 디스크립터:

디스크립터의미
Iint
Jlong
Ffloat
Ddouble
Bbyte
Sshort
Cchar
Zboolean
Vvoid (반환만)
[Iint[]
[[Iint[][]
Ljava/lang/String;String 객체 (참조 타입)
[Ljava/lang/String;String[]

메서드 디스크립터 = (매개변수들)반환타입:

calculate(int, BigDecimal) → BigDecimal
  (ILjava/math/BigDecimal;)Ljava/math/BigDecimal;

main(String[]) → void
  ([Ljava/lang/String;)V
  
toString() → String
  ()Ljava/lang/String;

→ 처음에 어렵지만 곧 익숙해진다.

5.6 javap 옵션 비교

# 옵션 1 — 바이트코드만
javap -c ShipmentCalc.class

# 옵션 2 — 상수 풀까지
javap -c -v ShipmentCalc.class

# 옵션 3 — private까지
javap -c -v -p ShipmentCalc.class

-v 옵션이 상수 풀을 보여줌.
→ Phase 3에서 가장 자주 쓸 명령은 javap -c -v -p.


6️⃣ 컴파일러가 상수 풀 만드는 과정

6.1 컴파일 단계에서의 흐름

.java 소스 파싱
   ↓ AST 생성
   ↓
Constant Pool Builder 시작
   ↓
모든 사용 자원 수집:
   - 사용하는 모든 클래스 → Class 항목
   - 호출하는 모든 메서드 → Methodref 항목
   - 접근하는 모든 필드 → Fieldref 항목
   - 모든 String 리터럴 → String 항목
   - 모든 int/long/float/double 큰 상수 → 해당 항목
   ↓
중복 제거 + 항목 간 참조 연결
   ↓
인덱스 부여 (#1, #2, ...)
   ↓
바이트코드 생성 시 이 인덱스로 명령어 작성
   ↓
.class 파일에 Constant Pool 섹션 + 바이트코드 저장

6.2 한 줄의 코드와 풀 항목

String s = "hello";

컴파일러가 추가하는 풀 항목:

#X = Utf8     "hello"   ← 실제 문자열 데이터
#Y = String   #X        ← 자바 String 개념 항목

바이트코드:

ldc #Y
astore_1

6.3 한 메서드 호출과 풀 항목

shipment.calculate(100);

컴파일러가 추가하는 풀 항목:

#A = Utf8         "com/ilic/Shipment"
#B = Class        #A
#C = Utf8         "calculate"
#D = Utf8         "(I)Ljava/math/BigDecimal;"
#E = NameAndType  #C:#D
#F = Methodref    #B.#E

바이트코드:

aload_1                    // shipment
bipush 100
invokevirtual #F

→ 메서드 호출 1번에 풀 항목 최대 6개 생성 (이미 다른 것이 있다면 재사용).

6.4 컴파일 타임 상수 폴딩

int x = 1 + 2 + 3;
int y = "Hello, " + "World";

컴파일러:

  • 1 + 2 + 36으로 미리 계산
  • "Hello, " + "World""Hello, World"로 결합
Constant Pool:
  #X = Integer 6
  #Y = String  #Z
  #Z = Utf8    "Hello, World"

바이트코드:

bipush 6       // x = 6
ldc #Y         // y = "Hello, World"

→ 두 개의 문자열이 풀에 각각 등록되는 게 아니라, 합쳐진 결과만 등록.

6.5 효율성 검증

public class Demo {
    String a = "test";
    String b = "test";
    String c = "test";
    
    void m1() { System.out.println("test"); }
    void m2() { return "test".length(); }
}

"test" 가 5번 등장.
Constant Pool:

#5 = String  #6
#6 = Utf8    "test"

단 2개 항목 (String + Utf8).
5번의 사용 모두 ldc #5.

6.6 상수 풀 크기 제한

Constant Pool 최대 항목 수: 65535 (16-bit 인덱스)

매우 큰 클래스 (자동 생성 코드, 거대 enum 등)에서 한계 도달 가능:

error: too many constants in constant pool

해결:

  • 클래스 분할
  • 코드 제너레이터 설정 조정 (lombok 등)
  • 명시적 import 줄이기 (영향 미미)

→ 실무에서 만날 일은 매우 드물지만 알아둘 만함.


7️⃣ ILIC 실무 — 상수 풀 분석 사례

7.1 사례 1 — 클래스 크기 의심 시

ILIC 코드베이스의 한 클래스가 비정상적으로 큼:

ShipmentEntity.class — 200KB

원인 추측: 자동 생성 코드, lombok 어노테이션 폭증.

javap -v ShipmentEntity.class | grep -c "^  #"
# Constant Pool 항목 개수

만약 50000개 이상 → 너무 많은 외부 참조 사용.
→ 분리, 리팩토링 검토.

7.2 사례 2 — String 리터럴 보안 검사

public class Config {
    private static final String DB_PASSWORD = "secret123";   // ❌
}

이 코드를 컴파일하면 Constant Pool에 "secret123" 가 평문으로 박힘.

strings Config.class | grep -i password
# secret123

JAR에 비밀번호 박지 말 것. 환경 변수, Vault, KMS 사용.

7.3 사례 3 — 라이브러리 의존 추적

# 외부 라이브러리 .class를 분석해 사용하는 클래스 확인
javap -v MysteryLib.class | grep "Class " | head -20

→ "이 클래스가 어느 다른 클래스를 참조하나" 한눈에 확인.
→ 의존성 그래프 작성에 활용.

7.4 사례 4 — String concat의 진화

Java 8:

String s = "Hello, " + name;

Constant Pool:

#5 = String  "Hello, "

바이트코드:

new StringBuilder
dup
ldc #5
invokespecial StringBuilder.<init>(String)
aload_1
invokevirtual StringBuilder.append(String)
invokevirtual StringBuilder.toString()

Java 9+:

String s = "Hello, " + name;

Constant Pool:

#5 = String      "Hello, "
#6 = InvokeDynamic #0:#7
#7 = NameAndType  ...
BootstrapMethods:
  #0: StringConcatFactory.makeConcatWithConstants

바이트코드:

ldc #5
aload_1
invokedynamic #6  ← 런타임에 최적 전략 선택

→ 같은 코드의 .class 가 자바 버전에 따라 다르게 컴파일.
javap -v 로 확인 가능.

7.5 사례 5 — Spring @RestController 의 진실

@RestController
@RequestMapping("/api/shipments")
public class ShipmentController { ... }

javap -v 출력 일부:

RuntimeVisibleAnnotations:
  0: #15(#16=s#17)
    org.springframework.web.bind.annotation.RestController
  1: #18(#16=s#19)
    org.springframework.web.bind.annotation.RequestMapping(
      "/api/shipments"
    )

→ 어노테이션도 Constant Pool에 보존됨.
→ Spring이 런타임에 이 정보를 읽어 매핑 (Reflection).

7.6 사례 6 — Compiled Java vs Kotlin

data class Shipment(val blNo: String, val eta: LocalDate)

Kotlin 컴파일러가 만드는 .class:

  • 일반 Java 클래스와 거의 동일한 Constant Pool 구조
  • 추가로 Metadata 어노테이션 (Kotlin 전용 정보)
  • equals, hashCode, toString, copy, componentN 메서드 자동 생성
javap -c -v Shipment.class | head -50

→ Kotlin의 마법을 직접 확인.


8️⃣ 흔한 실수 + 디버깅

실수 1 — Constant Pool과 String Pool 혼동

질문: "Constant Pool은 어디 있나요?"

❌ "Heap입니다" → String Pool과 혼동
✅ ".class 파일 안 + 로딩 시 Method Area"

기억할 것:

  • Constant Pool: 정적 메타 정보 (컴파일 타임)
  • String Pool: 동적 String 인스턴스 (런타임)

실수 2 — 인덱스 직접 코드에 박기

// ❌ 절대 시도 X
// 컴파일러는 인덱스를 자동 부여. 사용자가 제어 불가

→ 박승제씨가 직접 인덱스를 다룰 일은 없음. 읽을 때만.

실수 3 — 같은 String이 여러 번 등록될 거라 오해

public class Demo {
    String a = "test";
    String b = "test";
    String c = "test";
}

"test"가 풀에 3번 들어갈까? → 단 1번.
컴파일러가 중복 제거.

실수 4 — Utf8과 String 항목을 같다고 착각

#5 = Utf8     "hello"
#6 = String   #5
  • Utf8 = 바이트 데이터 (텍스트)
  • String = 자바 String 객체 개념

같은 텍스트라도 용도가 다름:

  • 클래스명 → Utf8 직접 사용 (Class 항목이 참조)
  • 자바 String 리터럴 → String 항목 거쳐서 사용

실수 5 — Constant Pool 크기 무시

자동 생성 코드 (gRPC stubs, JPA metamodel 등) 시 풀 크기 폭증 가능.

error: too many constants in constant pool

→ 클래스 분할, 코드 제너레이터 설정 조정.

실수 6 — Reflection 결과를 풀 위치로 착각

clazz.getName();    // "com.ilic.Shipment"

이 결과는 런타임에 Klass 정보에서 가져옴.
.class 파일의 Constant Pool에서 직접 읽지는 않음.
JVM이 미리 풀 정보를 Method Area에 로딩해두고 거기서 가져옴.

디버깅 — 클래스 파일 분석 도구

# 1. javap — 표준
javap -c -v -p Class.class

# 2. ASM Bytecode Viewer (IDE)
# 3. JBE (Java Bytecode Editor)
# 4. JOL (객체 레이아웃과 함께)

운영에서 가장 자주 쓰는 건 단연 javap.


9️⃣ 면접 질문 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Constant Pool과 String Pool 차이?정적 메타정보(.class+Method Area) vs 동적 String 인스턴스(Heap)
상수 풀 항목 14가지 카테고리?텍스트/리터럴, 클래스/타입, 멤버 참조, 동적 호출
Methodref 1개가 실제 참조하는 항목 수?5개 (자기 + Class + NameAndType + 두 Utf8)
같은 "hello"가 N번 등장하면 풀에 몇 번?1번. 컴파일러가 중복 제거
Utf8과 String 항목의 차이?바이트 데이터 vs 자바 String 객체 개념
디스크립터 (I)Ljava/math/BigDecimal; 의미?int 받아서 BigDecimal 반환하는 메서드
javap -v 의 v 옵션?verbose. 상수 풀까지 표시
InvokeDynamic의 사용처?람다, String concat, switch 패턴
어노테이션 정보도 풀에 저장되나?예. RuntimeVisibleAnnotations 속성
상수 풀 최대 크기?65535 항목 (16-bit 인덱스)

9.2 자기 점검 체크리스트

기본 이해

  • Constant Pool과 String Pool의 차이를 안다
  • 14가지 항목 타입을 카테고리로 안다
  • 단순 값 항목과 복합 참조 항목을 구분한다
  • Methodref가 어떻게 다른 항목을 참조하는지 안다
  • 자바 디스크립터를 읽을 수 있다 (I, J, Ljava/lang/String; 등)

실전 적용

  • javap -c -v 출력의 상수 풀을 해석할 수 있다
  • 바이트코드의 #N을 풀 항목으로 추적할 수 있다
  • String 리터럴 보안 검사를 수행할 수 있다
  • Kotlin과 Java의 .class 차이를 확인할 수 있다
  • Spring 어노테이션이 풀에 저장되는 모습을 안다

면접 대비 — 5분 답변

  • 두 풀의 결정적 구분
  • 상수 풀 항목 카테고리와 의의
  • Methodref의 5개 항목 참조 메커니즘
  • 디스크립터 읽는 법
  • 컴파일러의 상수 풀 생성 과정

🎯 핵심 요약 — 3줄 정리

1. Constant Pool ≠ String Pool

  • Constant Pool: .class 파일 안의 정적 메타 정보 (컴파일 타임)
  • String Pool: Heap의 동적 String 인스턴스 (런타임)
  • 둘 다 "풀" 이지만 완전히 다른 개념

2. 14가지 항목 타입 + 단순/복합 참조

  • Utf8/Integer/Float/Long/Double: 실제 값
  • String/Class/NameAndType: 다른 항목 참조
  • Methodref/Fieldref: 복합 참조 (Class + NameAndType)
  • InvokeDynamic: 람다, String concat, switch 패턴

3. 효율성과 일관성

  • 같은 값은 단 1번 저장
  • 같은 참조는 같은 인덱스로 공유
  • 바이트코드의 #N이 모두 여기 가리킴
  • 컴파일 타임 상수 폴딩으로 추가 최적화

📚 다음으로...

Unit 3.3 — 심볼 참조 (Symbolic Reference)

이번 Unit에서 Constant Pool의 정적 구조를 봤다면, 다음은 런타임에 어떻게 실제 주소로 변환되는가.

  • 심볼 참조 vs 실제 참조의 차이
  • 클래스 로딩의 Resolution 단계 상세
  • Lazy Resolution — 사용 시점에 해소
  • Direct Reference로의 변환 메커니즘
  • 다른 JVM에서 같은 .class가 동작하는 이유 (Late Binding)

Phase 3 진행 상황

🎯 Phase 3 — 바이트코드와 상수 풀 ★ 2주차의 정점
  ✅ Unit 3.1 바이트코드란 무엇인가
  ✅ Unit 3.2 상수 풀의 생성과 구조 ← 여기
  ⏭ Unit 3.3 심볼 참조
  ⏭ Unit 3.4 바이트코드 실전 분석

Phase 3 완주 후 갖게 될 능력

javap -c -v ShipmentService.class

이 출력을 한 줄도 빠짐없이 이해하고, 박승제씨가:

  • 운영 사고 시 .class 파일 직접 분석
  • 라이브러리 버그를 바이트코드로 디버깅
  • Spring AOP/JPA Proxy의 생성 코드 검증
  • 컴파일러 옵션의 효과 직접 측정
  • 면접에서 바이트코드 질문에 자신감

2주차 진행 상황

✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
✅ Phase 2 — JVM 메서드 실행 메커니즘 (2.1 ~ 2.4 완주)
🎯 Phase 3 — 바이트코드와 상수 풀 (정점, 2/4 진행)
⏭ Phase 4 — G1 GC 심화
⏭ Phase 5 — 컬렉션 내부 구조
⏭ Phase 6 — Reflection & Iterator
⏭ Phase 7 — Buffer
profile
Software Developer

0개의 댓글