2주차 Unit 3.4 — 바이트코드 실전 분석

Psj·2026년 5월 15일

F-lab

목록 보기
63/240

Unit 3.4 — 바이트코드 실전 분석

F-LAB JAVA · 2주차 · Phase 3 · 바이트코드와 상수 풀
🎯 2주차의 정점 — javap -c -v 출력을 처음부터 끝까지 분석한다


📌 학습 목표

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

  • javap -c -v ClassName.class 출력의 모든 줄을 해독할 수 있는가?
  • dup 명령이 정확히 왜 필요한가? (커리큘럼 자기 점검 질문)
  • invokespecial / invokevirtual / invokestatic / invokeinterface 의 결정적 차이는?
  • 같은 자바 코드가 Java 8, 11, 17, 21 에서 어떻게 다르게 컴파일되나?
  • 운영에서 클래스 파일을 받아 30분 안에 정확한 진단을 작성할 수 있는가?
  • Phase 3 졸업 — 바이트코드 읽기 전문가 가 되었는가?

🎯 핵심 한 문장

Phase 3의 모든 학습이 이 한 줄에 응축된다:

javap -c -v -p ClassName.class

이 출력을 박승제씨가 처음부터 끝까지 이해할 수 있다면, 2주차의 정점에 도달한 것이다.
Phase 1 메모리 구조, Phase 2 메서드 호출 메커니즘, Phase 3 바이트코드 + 상수 풀 + 심볼 참조 — 모든 것이 한 화면에서 만난다.

비유 — 의사의 X-Ray 판독 능력

단계비유
소스 코드 작성환자와 대화로 증상 듣기
.class 생성X-Ray 촬영
javap -v 출력X-Ray 사진
이번 Unit의 능력X-Ray를 보고 정확히 진단

비전문가도 X-Ray 사진은 볼 수 있다. 읽을 수 있는가가 차이.
박승제씨도 Phase 3까지 와서 이제 읽을 수 있는 능력을 갖춘다.


🧭 9개 섹션 로드맵

1. Phase 3 종합 — 정점 진입
2. 분석할 실전 ILIC 코드
3. .class 파일 헤더 완전 해독
4. Constant Pool 완전 해독
5. 생성자 바이트코드 — 한 줄씩
6. 인스턴스 메서드 바이트코드 — 한 줄씩
7. ILIC 운영 사고 시뮬레이션 — 실제 디버깅
8. Java 버전별 차이 + 흔한 함정
9. 면접 질문 + Phase 3 졸업 시험

1️⃣ Phase 3 종합 — 정점 진입

1.1 지금까지 배운 것들 한자리에

Unit 3.1 — 바이트코드란 무엇인가
  ↓ 스택 머신, .class 구조, javap 도구, 8개 명령 카테고리

Unit 3.2 — 상수 풀의 생성과 구조
  ↓ 14가지 항목, Methodref의 5개 참조, 디스크립터

Unit 3.3 — 심볼 참조
  ↓ Symbolic vs Direct, Resolution 5단계, Late Binding

Unit 3.4 — 바이트코드 실전 분석 (이번)
  ↓ 모든 것을 종합한 한 클래스 분석

1.2 박승제씨가 가질 능력

이 Unit이 끝나면:

  1. 운영에서 받은 .class 파일을 즉시 분석 가능
  2. 라이브러리 버그 원인을 바이트코드로 추적 가능
  3. Spring AOP/JPA의 생성 코드 검증 가능
  4. 컴파일러 옵션 효과 직접 측정 가능
  5. 면접에서 바이트코드 질문 자신감

1.3 분석할 도구 — 다시 정리

# 가장 자주 쓸 명령
javap -c -v -p ClassName.class

# 옵션 의미:
# -c  : 바이트코드 표시
# -v  : verbose (상수 풀, 메타데이터)
# -p  : private 멤버 포함

대안:

  • IntelliJ IDEA: View → Show Bytecode
  • 또는: 외부 도구 (CFR, JD-GUI)

박승제씨는 IntelliJ를 쓰니까 IDE 통합이 가장 편할 것.
하지만 운영 서버에선 javap 명령어가 필수.


2️⃣ 분석할 실전 ILIC 코드

2.1 분석 대상

ILIC의 실전 클래스 (단순화 버전):

package com.ilic.shipment;

import java.math.BigDecimal;

public class ShipmentCalculator {

    private static final BigDecimal FUEL_RATE = new BigDecimal("0.15");
    private int callCount = 0;

    public BigDecimal calculate(int weight, BigDecimal baseRate) {
        callCount++;
        BigDecimal subtotal = baseRate.multiply(BigDecimal.valueOf(weight));
        return subtotal.multiply(BigDecimal.ONE.add(FUEL_RATE));
    }

    public int getCallCount() {
        return callCount;
    }
}

2.2 왜 이 코드인가

이 한 클래스 안에 박승제씨가 배운 모든 메커니즘:

  • 인스턴스 필드 (callCount) → Unit 1.5
  • static 필드 (FUEL_RATE) → Unit 1.3
  • static 초기화 블록 패턴 (필드 선언 초기값)
  • 메서드 매개변수 (this 자동 전달) → Unit 2.2
  • 인스턴스 메서드 (calculate) → Unit 2.1
  • static 메서드 호출 (BigDecimal.valueOf) → Unit 1.5의 invokestatic
  • 인스턴스 메서드 호출 (multiply) → invokevirtual
  • 객체 생성 (new BigDecimal) → Unit 2.4
  • 상수 풀 전 항목 → Unit 3.2
  • 심볼 참조 → Unit 3.3

2.3 컴파일 및 분석

javac ShipmentCalculator.java
javap -c -v -p ShipmentCalculator.class > analysis.txt

이제 출력을 한 줄씩 보자.


3️⃣ .class 파일 헤더 완전 해독

3.1 javap 출력 시작

Classfile /Users/seungje/ShipmentCalculator.class
  Last modified May 15, 2026; size 567 bytes
  SHA-256 checksum 8a3c2f1e...
  Compiled from "ShipmentCalculator.java"
public class com.ilic.shipment.ShipmentCalculator
  minor version: 0
  major version: 65
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #21                         // com/ilic/shipment/ShipmentCalculator
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 2, methods: 3, attributes: 1

3.2 각 줄 해독

minor version: 0
major version: 65

Java 21로 컴파일. (Unit 3.1의 버전표: 65 = Java 21)
→ Java 17 서버에서 실행 시 UnsupportedClassVersionError 가능.

flags: (0x0021) ACC_PUBLIC, ACC_SUPER

→ 클래스 접근 플래그:

  • ACC_PUBLIC = 0x0001 (public)
  • ACC_SUPER = 0x0020 (구식 호환 플래그, Java 1.0 호환)
  • 합쳐서 0x0021

다른 플래그 값:

  • ACC_FINAL = 0x0010 (final class)
  • ACC_INTERFACE = 0x0200 (인터페이스)
  • ACC_ABSTRACT = 0x0400 (abstract)
  • ACC_ENUM = 0x4000 (enum)
this_class: #21
super_class: #2

→ 자기 클래스명 = Constant Pool의 #21
→ 부모 클래스명 = Constant Pool의 #2 (Object)

interfaces: 0, fields: 2, methods: 3, attributes: 1

→ 구현 인터페이스 0개
→ 필드 2개 (FUEL_RATE static, callCount instance)
→ 메서드 3개 (생성자 + calculate + getCallCount)
→ attribute 1개 (SourceFile)

3.3 Magic Number 확인 (hexdump)

hexdump -C ShipmentCalculator.class | head -2
00000000  ca fe ba be 00 00 00 41  00 2c 0a 00 02 00 03 ...
          ↑ Magic         ↑ Ver=65 ↑ Constant Pool 항목 수=44
  • 0xCAFEBABE: Magic Number (Unit 3.1)
  • 0x41 = 65 (Java 21)
  • 그 다음 2바이트가 Constant Pool 항목 개수

javap로 본 것과 일치.


4️⃣ Constant Pool 완전 해독

4.1 풀 항목 모두 보기

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // java/math/BigDecimal
   #8 = Utf8               java/math/BigDecimal
   #9 = String             #10            // "0.15"
  #10 = Utf8               "0.15"
  #11 = Methodref          #7.#12         // BigDecimal."<init>":(Ljava/lang/String;)V
  #12 = NameAndType        #5:#13         // "<init>":(Ljava/lang/String;)V
  #13 = Utf8               (Ljava/lang/String;)V
  #14 = Fieldref           #21.#15        // ShipmentCalculator.FUEL_RATE:Ljava/math/BigDecimal;
  #15 = NameAndType        #16:#17        // FUEL_RATE:Ljava/math/BigDecimal;
  #16 = Utf8               FUEL_RATE
  #17 = Utf8               Ljava/math/BigDecimal;
  #18 = Fieldref           #21.#19        // ShipmentCalculator.callCount:I
  #19 = NameAndType        #20:#23        // callCount:I
  #20 = Utf8               callCount
  #21 = Class              #22            // com/ilic/shipment/ShipmentCalculator
  #22 = Utf8               com/ilic/shipment/ShipmentCalculator
  #23 = Utf8               I
  #24 = Methodref          #7.#25         // BigDecimal.valueOf:(J)Ljava/math/BigDecimal;
  #25 = NameAndType        #26:#27        // valueOf:(J)Ljava/math/BigDecimal;
  #26 = Utf8               valueOf
  #27 = Utf8               (J)Ljava/math/BigDecimal;
  #28 = Methodref          #7.#29         // BigDecimal.multiply:(LBigDecimal;)LBigDecimal;
  #29 = NameAndType        #30:#31
  #30 = Utf8               multiply
  #31 = Utf8               (Ljava/math/BigDecimal;)Ljava/math/BigDecimal;
  #32 = Fieldref           #7.#33         // BigDecimal.ONE:LBigDecimal;
  #33 = NameAndType        #34:#17
  #34 = Utf8               ONE
  #35 = Methodref          #7.#36         // BigDecimal.add:(LBigDecimal;)LBigDecimal;
  ...

4.2 카테고리별 해독

Utf8 항목 (실제 텍스트 데이터)

#4  = Utf8  java/lang/Object       ← 클래스명
#5  = Utf8  <init>                  ← 생성자 이름
#6  = Utf8  ()V                     ← 시그니처: void 반환
#10 = Utf8  "0.15"                  ← String 값
#16 = Utf8  FUEL_RATE               ← 필드 이름
#17 = Utf8  Ljava/math/BigDecimal;  ← 타입 디스크립터
#20 = Utf8  callCount               ← 필드 이름
#23 = Utf8  I                       ← int 타입
#26 = Utf8  valueOf                 ← 메서드 이름
#27 = Utf8  (J)Ljava/math/BigDecimal;  ← long 받아 BigDecimal 반환
#30 = Utf8  multiply
#31 = Utf8  (Ljava/math/BigDecimal;)Ljava/math/BigDecimal;
#34 = Utf8  ONE

모든 텍스트의 진짜 저장소.

Class 항목

#2  = Class  #4   // java/lang/Object
#7  = Class  #8   // java/math/BigDecimal
#21 = Class  #22  // com/ilic/shipment/ShipmentCalculator

→ 3개 클래스를 참조. Object(부모), BigDecimal(사용), 자기 자신.

NameAndType 항목

#3   = NameAndType  #5:#6      // <init>:()V         (Object 생성자)
#12  = NameAndType  #5:#13     // <init>:(String)V   (BigDecimal 생성자)
#15  = NameAndType  #16:#17    // FUEL_RATE:BigDecimal
#19  = NameAndType  #20:#23    // callCount:I
#25  = NameAndType  #26:#27    // valueOf:(J)BigDecimal
#29  = NameAndType  #30:#31    // multiply:(BigDecimal)BigDecimal

→ 각 멤버 (메서드/필드)의 이름 + 시그니처 쌍.

Methodref 항목

#1  = Methodref  #2.#3   // Object.<init>()
#11 = Methodref  #7.#12  // BigDecimal.<init>(String)
#24 = Methodref  #7.#25  // BigDecimal.valueOf(long)
#28 = Methodref  #7.#29  // BigDecimal.multiply(BigDecimal)
#35 = Methodref  #7.#36  // BigDecimal.add(BigDecimal)

→ 호출하는 메서드들. 각 항목이 Class + NameAndType 조합.

Fieldref 항목

#14 = Fieldref  #21.#15  // ShipmentCalculator.FUEL_RATE
#18 = Fieldref  #21.#19  // ShipmentCalculator.callCount
#32 = Fieldref  #7.#33   // BigDecimal.ONE

→ 접근하는 필드들.

String 항목

#9 = String  #10   // "0.15"

→ String 리터럴. Utf8을 한 단계 감쌌음.

4.3 풀의 효율성 검증

java/math/BigDecimal 텍스트는 풀에 단 1번 (#8).
하지만 코드에서 BigDecimal을 여러 번 사용:

  • new BigDecimal("0.15") (생성자)
  • BigDecimal.valueOf(weight) (static)
  • baseRate.multiply(...) (인스턴스 메서드)
  • BigDecimal.ONE (필드)
  • BigDecimal.add(...) (인스턴스 메서드)

모두 #7 (Class) 또는 #8 (Utf8) 을 공유.
중복 제거 효과 확실.


5️⃣ 생성자 바이트코드 — 한 줄씩

5.1 생성자 정의 (자동 생성)

ShipmentCalculator 클래스에 명시적 생성자 없음.
컴파일러가 자동으로 기본 생성자 생성:

public ShipmentCalculator() {
    super();
    this.callCount = 0;   // 명시적이지만 0이라 생략 가능
}

추가로 static 초기화 블록:

static {
    FUEL_RATE = new BigDecimal("0.15");
}

5.2 javap 출력 — 인스턴스 생성자

public com.ilic.shipment.ShipmentCalculator();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #18                 // Field callCount:I
         9: return
      LineNumberTable:
        line 9: 0

5.3 한 줄씩 해독

메타 정보:

descriptor: ()V       ← 매개변수 없음, void 반환
flags: ACC_PUBLIC     ← public
stack=2, locals=1, args_size=1
  • stack=2: Operand Stack 최대 2 슬롯 사용
  • locals=1: LVA 1 슬롯 (this 1개)
  • args_size=1: 매개변수 1개 (this 포함)

바이트코드 한 줄씩:

0: aload_0
  • LVA[0]을 Operand Stack에 push
  • LVA[0]은 항상 this (Unit 2.2)
  • Stack: [this]
1: invokespecial #1     // Object.<init>()
  • 메서드 호출: super()
  • #1은 Constant Pool의 Methodref → Object의 <init>
  • this를 인자로 (Stack에서 pop)
  • → Object 생성자 실행 (사실상 빈 작업)
  • Stack: []
4: aload_0
  • 다시 this 푸시 (다음 putfield용)
  • Stack: [this]
5: iconst_0
  • int 상수 0 푸시
  • Stack: [this, 0]
6: putfield #18         // callCount:I
  • 인스턴스 필드 쓰기
  • #18 = Fieldref → ShipmentCalculator.callCount
  • Stack에서 2개 pop: this와 값
  • this.callCount = 0 실행
  • Stack: []
9: return
  • void 반환
  • 생성자 종료

5.4 callCount = 0 가 왜 명시적?

박승제씨가 작성한 코드:

private int callCount = 0;

만약:

private int callCount;  // 명시적 = 0 없이

JVM은 어차피 0으로 초기화 (Unit 2.4). 그런데 컴파일러는 둘 다 putfield #18로 동일하게 처리.
명시적이든 아니든 같은 바이트코드.

→ Unit 2.4에서 본 "컴파일러가 필드 초기값을 생성자에 자동 삽입"의 증거.

5.5 static 초기화 블록 (<clinit>)

static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=3, locals=0, args_size=0
         0: new           #7         // class java/math/BigDecimal
         3: dup
         4: ldc           #9         // String "0.15"
         6: invokespecial #11        // BigDecimal.<init>(String)
         9: putstatic     #14        // FUEL_RATE
        12: return

<clinit> = class initializer. 클래스 초기화 시 1번만 호출.

한 줄씩:

0: new #7              // new BigDecimal
  • BigDecimal 클래스 정보 확인
  • Heap에 메모리 할당
  • Object Header 설정
  • Stack: [obj_ref]
3: dup                 // 복제
  • 다음 invokespecial이 소비할 것 + putstatic이 쓸 것
  • Stack: [obj_ref, obj_ref]
4: ldc #9              // "0.15"
  • String 리터럴 로드
  • Stack: [obj_ref, obj_ref, "0.15"]
6: invokespecial #11   // BigDecimal.<init>(String)
  • 생성자 호출
  • this(obj_ref) + "0.15"를 인자로
  • Stack: [obj_ref]
9: putstatic #14       // FUEL_RATE
  • static 필드 쓰기
  • #14 = ShipmentCalculator.FUEL_RATE
  • Stack의 obj_ref를 FUEL_RATE에 할당
  • Stack: []
12: return

→ Unit 2.4 6장 (new의 메모리 변화)을 직접 확인.


6️⃣ 인스턴스 메서드 바이트코드 — 한 줄씩

6.1 calculate 메서드

public BigDecimal calculate(int weight, BigDecimal baseRate) {
    callCount++;
    BigDecimal subtotal = baseRate.multiply(BigDecimal.valueOf(weight));
    return subtotal.multiply(BigDecimal.ONE.add(FUEL_RATE));
}

6.2 javap 출력

public java.math.BigDecimal calculate(int, java.math.BigDecimal);
    descriptor: (ILjava/math/BigDecimal;)Ljava/math/BigDecimal;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=4, locals=4, args_size=3
         0: aload_0
         1: dup
         2: getfield      #18                 // Field callCount:I
         5: iconst_1
         6: iadd
         7: putfield      #18                 // Field callCount:I
        10: aload_2
        11: iload_1
        12: i2l
        13: invokestatic  #24                 // Method BigDecimal.valueOf:(J)LBigDecimal;
        16: invokevirtual #28                 // Method BigDecimal.multiply:(LBigDecimal;)LBigDecimal;
        19: astore_3
        20: aload_3
        21: getstatic     #32                 // Field BigDecimal.ONE:LBigDecimal;
        24: getstatic     #14                 // Field FUEL_RATE:LBigDecimal;
        27: invokevirtual #35                 // Method BigDecimal.add:(LBigDecimal;)LBigDecimal;
        30: invokevirtual #28                 // Method BigDecimal.multiply:(LBigDecimal;)LBigDecimal;
        33: areturn
      LineNumberTable:
        line 12: 0
        line 13: 10
        line 14: 20

6.3 메타 정보

descriptor: (ILjava/math/BigDecimal;)Ljava/math/BigDecimal;

(int, BigDecimal) → BigDecimal

stack=4, locals=4, args_size=3
  • Operand Stack 최대 4
  • LVA 4 슬롯
  • 매개변수 3개 (this + weight + baseRate)

LVA 레이아웃:

LVA[0] this
LVA[1] weight (int)
LVA[2] baseRate (BigDecimal)
LVA[3] subtotal (지역변수)

6.4 callCount++ 의 5줄 분석 (PC 0~9)

0: aload_0
  • this 푸시
  • Stack: [this]
1: dup
  • this 복제
  • Stack: [this, this]
2: getfield #18  // callCount
  • top의 this에서 callCount 읽음
  • Stack: [this, callCount값]
5: iconst_1
  • 1 푸시
  • Stack: [this, callCount, 1]
6: iadd
  • 더하기 (top 2개)
  • Stack: [this, callCount+1]
7: putfield #18  // callCount
  • this.callCount = 결과
  • Stack: []

5개 명령어로 callCount++ 표현.
→ Unit 2.3에서 본 패턴 정확히 재현.

6.5 baseRate.multiply(BigDecimal.valueOf(weight)) 분석 (PC 10~19)

10: aload_2
  • baseRate 푸시 (LVA[2])
  • Stack: [baseRate]
11: iload_1
  • weight 푸시 (LVA[1])
  • Stack: [baseRate, weight(int)]
12: i2l
  • int → long 변환
  • Stack: [baseRate, weight(long)]
  • BigDecimal.valueOf(long) 이라 long 필요
13: invokestatic #24    // BigDecimal.valueOf(J)
  • static 메서드 호출
  • Stack의 long을 인자로
  • 결과 BigDecimal을 push
  • Stack: [baseRate, BigDecimal_from_weight]
16: invokevirtual #28   // BigDecimal.multiply(BigDecimal)
  • 인스턴스 메서드 호출
  • Stack의 baseRate를 this로, BigDecimal_from_weight를 인자로
  • baseRate.multiply(BigDecimal_from_weight) 실행
  • 결과 BigDecimal을 push
  • Stack: [subtotal]
19: astore_3
  • subtotal에 저장 (LVA[3])
  • Stack: []

invokestatic + invokevirtual 차이 직접 확인.

  • invokestatic: 객체 없이 호출. BigDecimal.valueOf
  • invokevirtual: 객체(top)를 this로 사용. baseRate.multiply(...)

6.6 subtotal.multiply(BigDecimal.ONE.add(FUEL_RATE)) 분석 (PC 20~30)

20: aload_3                  // subtotal
21: getstatic #32            // BigDecimal.ONE (static 필드)
24: getstatic #14            // FUEL_RATE (static 필드)
27: invokevirtual #35        // BigDecimal.ONE.add(FUEL_RATE)
30: invokevirtual #28        // subtotal.multiply(결과)
33: areturn                  // 반환

Stack 변화:

PC 20: [subtotal]
PC 21: [subtotal, ONE]
PC 24: [subtotal, ONE, FUEL_RATE]
PC 27: [subtotal, ONE.add(FUEL_RATE)]   ← invokevirtual이 ONE.add(...) 실행
PC 30: [subtotal.multiply(...)]         ← invokevirtual이 subtotal.multiply(...) 실행
PC 33: []                               ← areturn

6.7 getter — getCallCount

public int getCallCount();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #18         // callCount:I
         4: ireturn

3줄에 모든 게:
1. this 푸시
2. this의 callCount 읽기
3. int 반환

→ Java의 getter는 정말 단순.
→ JIT가 인라이닝 처리 → 거의 0 비용.


7️⃣ ILIC 운영 사고 시뮬레이션 — 실제 디버깅

7.1 시나리오 — 운영에서 에러 발생

2026-05-15 14:30:00 ERROR ShipmentService - 운임 계산 실패
java.lang.NoSuchMethodError: 
    'java.math.BigDecimal java.math.BigDecimal.multiply(java.math.BigDecimal)'
    at com.ilic.ShipmentCalculator.calculate(ShipmentCalculator.java:13)

박승제씨의 분석 시작.

7.2 1단계 — 에러 의미 파악

NoSuchMethodError:

  • 컴파일 시점엔 메서드 있었음
  • 런타임에 없음
  • → Resolution 실패 (Unit 3.3)

특히 BigDecimal.multiply(BigDecimal):

  • 표준 JDK 메서드
  • 사라질 리 없음

의심:

  • 잘못된 BigDecimal 클래스가 로딩되고 있다?
  • ClassLoader 격리 문제?

7.3 2단계 — 바이트코드 분석

# 운영 서버에서 클래스 추출
ssh prod-server
cd /var/log/dump/
jcmd <PID> Thread.print > thread.dump

# 실행 중인 JAR의 ShipmentCalculator.class 추출
unzip -o app.jar com/ilic/ShipmentCalculator.class

javap -c -v com/ilic/ShipmentCalculator.class > local-analysis.txt

local-analysis.txt의 Constant Pool 확인:

#28 = Methodref  #7.#29
#7  = Class      #8   // java/math/BigDecimal
#29 = NameAndType #30:#31
#30 = Utf8       multiply
#31 = Utf8       (Ljava/math/BigDecimal;)Ljava/math/BigDecimal;

→ 우리 클래스는 정상. BigDecimal.multiply(BigDecimal) 호출.

7.4 3단계 — 실제 로딩된 BigDecimal 확인

# 어느 BigDecimal이 로딩됐나?
jcmd <PID> VM.classloader_stats

# 또는
jcmd <PID> GC.class_stats | grep BigDecimal

출력:

java.math.BigDecimal       loaded by com.malicious.BadClassLoader

예상 외 결과: BigDecimal이 표준 JDK가 아닌 다른 ClassLoader에서 로딩됨!

7.5 4단계 — 원인 추적

# JAR 안에 BigDecimal이 있나?
unzip -l app.jar | grep BigDecimal

발견:

com/malicious/BigDecimal.class

누군가가 BigDecimal 이름의 클래스를 JAR에 넣음.

  • 우연한 충돌 (말도 안 됨)
  • 의도된 공격? 또는 잘못된 import?
# 어떤 의존성에서 왔나?
./gradlew dependencyInsight --dependency BigDecimal

→ 특정 라이브러리가 자체 BigDecimal 가지고 있음. 클래스 우선순위 문제.

7.6 5단계 — 해결

즉시 조치:
1. 문제 라이브러리 제거
2. 또는 ClassLoader 우선순위 조정
3. JDK BigDecimal이 먼저 로딩되게 보장

장기 조치:
1. 빌드 시 클래스 충돌 검출 도구 추가 (Maven Enforcer, Gradle Conflict Resolution)
2. JAR 무결성 검증

7.7 박승제씨의 능력

이 시나리오를 30분 내에 진단할 수 있게 된 것이 Phase 3 학습의 가치.

시간:        15:00 (에러 발생)
            ↓ 1분
        에러 메시지 확인
            ↓ 5분
        바이트코드 추출 + javap 분석
            ↓ 10분
        ClassLoader 추적
            ↓ 10분
        원인 발견 + 해결책 도출
시간:        15:30 (해결)

이게 바이트코드 읽기 능력의 진짜 가치.


8️⃣ Java 버전별 차이 + 흔한 함정

8.1 같은 코드, 다른 바이트코드

String s = "Hello, " + name + "!";

Java 8 컴파일:

new           #1   // StringBuilder
dup
invokespecial #2   // StringBuilder.<init>()
ldc           #3   // "Hello, "
invokevirtual #4   // StringBuilder.append(String)
aload_1            // name
invokevirtual #4   // StringBuilder.append(String)
ldc           #5   // "!"
invokevirtual #4
invokevirtual #6   // StringBuilder.toString()
astore_2

Java 9+ 컴파일:

ldc           #3   // "Hello, "
aload_1
ldc           #5   // "!"
invokedynamic #7   // makeConcatWithConstants
astore_2

→ Java 9+ 는 invokedynamic + StringConcatFactory 사용 (JEP 280).
→ 런타임에 JVM이 최적 전략 선택.

8.2 enum의 진화

Java 16 이전 — switch 표현식 등장 전:

switch (status) {
    case DRAFT: ...; break;
    case ACTIVE: ...; break;
}
lookupswitch  { ... }     // 일반적인 분기

Java 17+ — pattern matching switch (preview):

return switch (obj) {
    case Integer i -> "int: " + i;
    case String s -> "str: " + s;
    default -> "other";
};
invokedynamic #N   // SwitchBootstraps.typeSwitch

→ 점점 더 invokedynamic 활용 증가.

8.3 record (Java 16+)

public record Shipment(String blNo, BigDecimal freight) {}

javap -c -v 결과:

  • 자동 생성된 메서드들: blNo(), freight(), equals(), hashCode(), toString()
  • 각 메서드는 invokedynamic 으로 ObjectMethods 부트스트랩 사용
  • 매우 간결한 바이트코드
// 일반 클래스의 equals/hashCode/toString:
// 수십 줄의 바이트코드

// record:
0: aload_0
1: aload_1
2: invokedynamic #1  // ObjectMethods.bootstrap
7: areturn

→ 같은 동작을 훨씬 짧은 바이트코드로.

8.4 sealed class (Java 17+)

public sealed class Cargo
    permits DryCargo, ReeferCargo, HazmatCargo {}

.class 파일에 새 attribute:

PermittedSubclasses:
  DryCargo
  ReeferCargo
  HazmatCargo

→ JVM이 컴파일 시점에 자식 클래스 목록 검증.

8.5 흔한 함정 1 — 디버그 정보 누락

javac App.java
javap -c App.class

LocalVariableTable이 없으면:

0: aload_0
1: aload_1     // 변수 이름 모름

-g 옵션으로 컴파일하면:

0: aload_0     // this
1: aload_1     // shipment

→ 운영 디버깅 위해 -g:lines,vars 옵션 권장.

8.6 흔한 함정 2 — 인라인 캐시 무효화

// 호출 사이트
shipment.calculate(100);   ← 항상 ShipmentImpl1만 옴

JIT가 인라인 캐시 → 매우 빠름.

만약 어느 순간 다른 타입 등장:

shipment.calculate(100);   ← 이번엔 ShipmentImpl2

캐시 미스 → 재해소.
메가모픽(megamorphic) 호출 사이트가 되면 캐시 효과 사라짐.

→ 다형성을 너무 다양하게 쓰면 성능 손실. 보통 문제 안 됨.

8.7 흔한 함정 3 — Reflection 비용

Method m = clazz.getMethod("calculate", int.class);
m.invoke(obj, 100);

내부적으로:

  • 첫 호출: 매우 비쌈 (수십 μs)
  • 두 번째 이후: 캐시 사용 (수 μs)
  • JIT가 인라이닝하면: 거의 일반 호출만큼

Reflection 한 번 vs 매번 호출의 차이.

// ❌ 비효율
for (int i = 0; i < 100; i++) {
    clazz.getMethod("calc", int.class).invoke(obj, i);
}

// ✓ Method 객체 캐시
Method m = clazz.getMethod("calc", int.class);
for (int i = 0; i < 100; i++) {
    m.invoke(obj, i);
}

9️⃣ 면접 질문 + Phase 3 졸업 시험

9.1 면접 단골 질문 매핑

Q핵심 답변
dup 명령은 왜 필요?new 후 객체 참조를 invokespecial이 소비. astore도 써야 해서 복제
invokevirtual vs invokespecial vs invokestatic 차이?다형성 / 정적 확정 / 객체 무관 (Unit 1.5 4종)
getstatic vs getfield?static 필드 / 인스턴스 필드
Java 9+ String concat 바이트코드 차이?StringBuilder → invokedynamic (JEP 280)
LocalVariableTable의 역할?디버그용 변수명 매핑
i2l 명령?int → long 타입 변환
ireturn vs areturn?int 반환 / 참조 반환
record의 equals/hashCode 구현?ObjectMethods.bootstrap + invokedynamic
클래스 파일 verify의 역할?바이트코드 안전성 검증 (스택 일치, 점프 유효성)
Constant Pool 인덱스 시작?#1부터 (#0은 reserved)

9.2 자기 점검 체크리스트

Phase 3 종합

  • javap -c -v 출력을 모든 줄 해독 가능
  • Constant Pool 모든 항목 카테고리 식별 가능
  • 생성자 바이트코드를 따라갈 수 있다
  • 인스턴스 메서드 바이트코드를 따라갈 수 있다
  • dup, getfield, putfield, invoke* 등을 안다

실전 적용

  • 운영 클래스 파일을 분석해 사고 디버깅 가능
  • Java 버전별 바이트코드 차이 인식 가능
  • Spring AOP 프록시 클래스 분석 가능
  • Reflection 사용 시 성능 함정 안다
  • 인라인 캐시의 메가모픽 위험 안다

면접 대비

  • 5분 안에 javap 출력 한 화면 해석
  • dup이 필요한 이유 코드와 함께 설명
  • invoke 4종 차이 명확히
  • Java 8 vs 9 변화 (invokedynamic) 알기
  • 운영 사고 시 바이트코드 분석 가능

9.3 🎯 Phase 3 졸업 시험

다음 질문에 즉답할 수 있다면 Phase 3 졸업:

  1. 한 자바 코드 String s = a + b; 가 Java 8과 Java 11에서 어떻게 다르게 컴파일되는가?
  2. 바이트코드 invokevirtual #20 에서 #20이 정확히 무엇을 가리키는가?
  3. dup 명령이 새 객체 생성 시 왜 필요한가?
  4. NoSuchMethodError 가 발생했을 때 바이트코드와 Constant Pool에서 무엇을 확인하는가?
  5. Java 17의 record가 어떻게 적은 바이트코드로 equals/hashCode/toString을 구현하는가?

즉답 가능 = Phase 3 졸업 + 2주차 정점 달성.


🎯 핵심 요약 — 3줄 정리

1. javap -c -v -p 한 줄이 바이트코드 분석의 전부

  • Magic Number, Version, Constant Pool, Methods, Attributes
  • 박승제씨는 이제 모든 줄을 해독 가능

2. 자바 코드 ↔ 바이트코드는 1:N 매핑

  • callCount++ = 5개 바이트코드 명령
  • new Shipment("BL") = new + dup + ldc + invokespecial
  • + 연산 = Java 9+에선 invokedynamic

3. 운영 사고 디버깅의 강력한 무기

  • NoSuchMethodError → 바이트코드의 Methodref 확인
  • ClassLoader 격리 → 어느 .class가 로딩됐는지 추적
  • 라이브러리 버그 → 디컴파일 없이 바이트코드 직접 검증

🏆 Phase 3 완주 — 2주차의 정점 달성

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

→ 박승제씨는 이제 .class 파일 분석 전문가

Phase 3 후 박승제씨가 가진 능력

  • javap -c -v -p 출력을 처음부터 끝까지 해석
  • ✓ 모든 invoke 명령의 차이 명확히 구분
  • ✓ Constant Pool의 모든 14가지 항목 인식
  • ✓ 운영 사고 시 30분 내 바이트코드 진단
  • ✓ Spring AOP/JPA 생성 코드 검증
  • ✓ Java 버전별 컴파일 차이 분석
  • ✓ 면접에서 바이트코드 질문에 자신감

📚 다음으로...

Phase 4 — G1 GC 심화

이번 Phase에서 클래스 파일과 메서드 호출의 모든 것을 봤다면, 다음은 운영 환경의 메모리 관리.

1주차에서 GC 기본을 봤지만, 2주차 Phase 4는:

  • G1 GC의 리전(Region) 모델 정밀
  • 거대 리전(Humongous Region)
  • 정지시간 예측 모델
  • 운영 서버 GC 튜닝
  • GC 로그 분석

→ 박승제씨가 운영 서버의 OOM 트러블슈팅을 마스터하게 됨.

2주차 진행 상황

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

작성한 2주차 학습자료

Phase 1: 6개 Unit (1.1 ~ 1.6)
Phase 2: 4개 Unit (2.1 ~ 2.4)
Phase 3: 4개 Unit (3.1 ~ 3.4) ★ 정점
─────────────────────────────
누적: 14개 Unit
profile
Software Developer

0개의 댓글