2주차 Unit 2.2 — Static vs 인스턴스 메서드 호출 경로

Psj·2026년 5월 15일

F-lab

목록 보기
57/240

Unit 2.2 — Static vs 인스턴스 메서드 호출 경로

F-LAB JAVA · 2주차 · Phase 2 · JVM 메서드 실행 메커니즘


📌 학습 목표

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

  • 메서드 호출의 4가지 케이스 (Static→Static, Static→Instance, Instance→Static, Instance→Instance) 의 메모리 동작 차이는?
  • main() 안에서 인스턴스 메서드를 직접 호출할 수 없는 메모리적 이유는?
  • 객체가 있어야 한다는 말의 정확한 메모리 의미는?
  • this는 어디서 와서 어디로 가는가?
  • 같은 인스턴스 메서드 안에서 다른 인스턴스 메서드 호출 시 this의 변화는?
  • Spring Bean이 모두 인스턴스인데 어떻게 효율적으로 동작하나?

🎯 핵심 한 문장

메서드 호출의 4가지 경로는 "객체 참조(this)가 어디서 오는가" 와 "VMT를 거치는가" 의 조합으로 결정된다.
Static 메서드는 객체와 무관(Static Zone 직접), 인스턴스 메서드는 반드시 객체의 Class Pointer → VMT를 거친다.
this라는 보이지 않는 매개변수가 이 모든 차이의 핵심이다.

비유 — 네 가지 통화 방식

경로비유자원
Static → Static공용 헬프데스크에서 다른 공용 헬프데스크로 사내 전화직원증 불필요
Static → Instance공용 헬프데스크에서 특정 직원에게 전화직원 번호(객체) 알아야 함
Instance → Static자기 자리에서 공용 헬프데스크로 전화자유롭게 가능
Instance → Instance자기 자리에서 같은 부서 동료에게 전화자기 직원증(this)으로 부서 매뉴얼(VMT) 찾기

직원증(this)이 있으면 모든 자원에 접근 가능. 없으면(static 메서드 안) 자기 자원만.


🧭 9개 섹션 로드맵

1. 4가지 호출 경로 — 큰 그림
2. Static → Static — 가장 단순한 경로
3. Static → Instance — 객체가 있어야 시작
4. Instance → Static — 자유로운 경로
5. Instance → Instance — this 매개변수의 마법
6. 메모리 추적 종합
7. ILIC 실무 — Service · Util · Entity의 호출 패턴
8. 흔한 실수와 디버깅
9. 면접 질문 + 자기 점검

1️⃣ 4가지 호출 경로 — 큰 그림

1.1 메서드 종류 × 메서드 종류 = 4 케이스

              호출 대상
            Static    Instance
   ┌──────────────────────────┐
호 │Static  │  ①  │   ②     │
출 ├────────┼─────┼──────────┤
자 │Inst.   │  ③  │   ④     │
   └──────────────────────────┘

① Static → Static    : 가장 단순
② Static → Instance  : 객체 필요 (가장 까다로움)
③ Instance → Static  : 자유로움
④ Instance → Instance: this 매개변수의 메커니즘

1.2 비대칭의 본질 — Unit 1.3의 정밀화

Unit 1.3에서 본 규칙:

Static은 인스턴스를 못 부르고, 인스턴스는 둘 다 부를 수 있다.

이번 Unit에서 정확한 메커니즘을 본다:

Static 메서드 실행 중:
  스택 프레임의 LVA[0] = 첫 매개변수 (this 없음)
  → "어느 객체의 데이터?" 라는 질문에 답할 수 없음
  → 인스턴스 멤버 접근 불가

Instance 메서드 실행 중:
  스택 프레임의 LVA[0] = this (자동 전달됨)
  → "어느 객체?" 명확
  → 인스턴스 멤버 자유 접근
  → Static도 자유 접근 (this 무시하면 됨)

1.3 4가지 경로의 invoke 명령 매핑

호출자대상invoke 명령VMT?
StaticStaticinvokestatic
StaticInstanceinvokestatic(생성자) + invokevirtual✓ (instance 호출)
InstanceStaticinvokestatic
InstanceInstanceinvokevirtual

→ 호출자가 누구든 호출 대상이 결정.
→ Unit 1.5의 invoke 4종 + Unit 2.1의 2단계가 여기 적용.

1.4 4가지를 한 번에 본 코드

public class Demo {

    static int staticCounter;
    int instanceCounter;

    public static void staticMethod1() {           // 호출자: Static
        staticCounter++;                            // ① Static → Static
        // instanceCounter++;                       // ❌ Static → Instance 직접 불가
        
        Demo d = new Demo();                        // 객체 생성 (Static → Instance 생성자)
        d.instanceMethod1();                        // ② Static → Instance
    }

    public void instanceMethod1() {                // 호출자: Instance
        staticCounter++;                            // ③ Instance → Static (자유)
        instanceMethod2();                          // ④ Instance → Instance (this 통해)
    }

    public void instanceMethod2() {
        instanceCounter++;                          // 현재 this의 필드 접근
    }

    public static void staticMethod2() {}
}

이번 Unit에서 이 4가지를 메모리 다이어그램으로 정밀 추적.


2️⃣ Static → Static — 가장 단순한 경로

2.1 코드

public class FreightUtils {

    public static BigDecimal applyFuelSurcharge(BigDecimal base, BigDecimal rate) {
        return base.multiply(BigDecimal.ONE.add(rate));
    }

    public static BigDecimal calculateTotal(BigDecimal base, BigDecimal weight) {
        BigDecimal subtotal = base.multiply(weight);
        BigDecimal withFuel = applyFuelSurcharge(subtotal, FUEL_RATE);  // ← Static → Static
        return withFuel;
    }
}

2.2 호출 흐름

calculateTotal(BASE, WEIGHT) 호출 중:
  현재 스택 프레임:
    LVA[0] base       ← 첫 매개변수
    LVA[1] weight     ← 두 번째 매개변수
    LVA[2] subtotal   ← 지역변수
    
  applyFuelSurcharge(subtotal, FUEL_RATE) 호출:
    1. Operand Stack에 인자 push
       Stack: [subtotal_ref, FUEL_RATE_ref]
    2. invokestatic FreightUtils.applyFuelSurcharge
    3. JVM 동작:
       - FreightUtils 클래스의 Static Zone 직접 조회
       - applyFuelSurcharge 바이트코드 발견
       - 새 프레임 생성, 인자를 LVA에 매핑
    4. 새 프레임:
       LVA[0] base = subtotal_ref      ← 호출자의 인자가 0번부터 시작
       LVA[1] rate = FUEL_RATE_ref
       (this 없음! static 메서드)

2.3 메모리 그림

Method Area:
  FreightUtils 클래스
    Static Zone:
      • applyFuelSurcharge() 바이트코드
      • calculateTotal() 바이트코드

Stack:
  ┌────────────────────────────────┐
  │ applyFuelSurcharge() 프레임      │  ← 새 프레임
  │   LVA[0] base = subtotal_ref    │
  │   LVA[1] rate = FUEL_RATE_ref   │
  ├────────────────────────────────┤
  │ calculateTotal() 프레임          │
  │   LVA[0] base                   │
  │   LVA[1] weight                 │
  │   LVA[2] subtotal               │
  └────────────────────────────────┘

Heap: (지금은 무관)

2.4 핵심 특징

  • Heap 안 거침 — 객체가 필요 없으므로
  • VMT 안 거침 — Static Zone에서 직접 호출
  • this 없음 — LVA[0]부터 첫 매개변수
  • 가장 빠름 — JIT가 인라이닝하기도 쉬움

2.5 같은 클래스 안에서 클래스명 생략

public class FreightUtils {
    public static BigDecimal a() { ... }
    public static BigDecimal b() {
        return a();             // 클래스명 생략 가능
        // FreightUtils.a();    // 명시도 가능
    }
}

같은 클래스 안의 static → static 호출은 클래스명 생략 가능.
→ JVM 입장에선 차이 없음. 컴파일러가 동일한 invokestatic 생성.


3️⃣ Static → Instance — 객체가 있어야 시작

3.1 코드

public class App {
    public static void main(String[] args) {
        // 객체 없이는 인스턴스 메서드 못 부름
        // calculate(100);    ❌ 컴파일 에러

        Shipment s = new Shipment("BL-001");    // 객체 생성
        BigDecimal fare = s.calculate(100);     // Static → Instance
        System.out.println(fare);
    }
}

3.2 왜 컴파일 에러인가 — 메모리 관점

main() 실행 중:
  스택 프레임:
    LVA[0] args (String[])
    (this 없음 — static)

만약 calculate(100); 을 직접 호출하면:
  - 호출 대상이 instance 메서드
  - VMT를 거쳐 어느 객체의 메서드를 부를지 결정해야 함
  - 그런데 객체 참조가 없음 → "어느 객체?" 모름
  - 컴파일러가 차단

3.3 객체 생성으로 해결

Shipment s = new Shipment("BL-001");

객체 생성의 의미 (Unit 1.5 6장 복습):
1. Heap에 메모리 할당
2. Object Header 설정 (Class Pointer 포함)
3. 생성자 호출 (Static → Instance의 특수 케이스 — <init> 메서드)
4. 객체 참조 반환 → 변수 s에 저장

→ 이제 main의 스택 프레임에 객체 참조가 있음.

3.4 인스턴스 메서드 호출

s.calculate(100);

호출 흐름:

1. main 프레임의 Operand Stack에 push:
   Stack: [s_ref, 100]

2. invokevirtual Shipment.calculate

3. JVM 동작:
   - s_ref가 가리키는 객체의 Class Pointer 확인
   - Shipment Klass의 VMT 조회
   - calculate(I) 슬롯 발견
   - 새 프레임 생성
   - LVA[0] = s_ref (this)      ← ★ 첫 번째 슬롯에 this 자동 전달
   - LVA[1] = 100

4. calculate 프레임:
   LVA[0] this = s_ref
   LVA[1] weight = 100

3.5 메모리 그림

Method Area:
  Shipment 클래스
    Class Metadata Zone:
      VMT[N] calculate(I) → Non-Static Zone의 바이트코드 위치
    Non-Static Zone:
      • calculate() 바이트코드
      • <init>() 바이트코드

Heap:
  ┌──────────────────────────┐
  │ Shipment @0x7f4a2c01      │
  │   Mark Word               │
  │   Class ptr → Shipment    │
  │   blNo = "BL-001"         │
  └──────────────────────────┘
                ▲
                │ this
                │
Stack:          │
  ┌────────────┴──────────────┐
  │ calculate() 프레임          │
  │   LVA[0] this = 0x7f4a2c01 │  ← 자동 전달됨
  │   LVA[1] weight = 100      │
  ├────────────────────────────┤
  │ main() 프레임                │
  │   LVA[0] args              │
  │   LVA[1] s = 0x7f4a2c01    │
  └────────────────────────────┘

this가 calculate 프레임의 LVA[0]에 자동 들어감.
→ 이제 calculate 내부에서 this.blNo, this.calculate2() 등 자유롭게 접근 가능.

3.6 main에서 인스턴스 메서드를 부르는 모든 자바 코드

자바 프로그램의 표준 구조:

public class App {
    public static void main(String[] args) {
        new App().run();        // ① 객체 만들고 인스턴스 메서드
        // 또는
        App app = new App();
        app.run();              // ② 명시적
    }

    public void run() {         // 여기부터 실제 비즈니스 로직
        // ...
    }
}

@SpringBootApplication
public class IlicApplication {
    public static void main(String[] args) {
        SpringApplication.run(IlicApplication.class, args);
        //              ↑ static. 내부에서 Spring이 인스턴스(Bean)들을 만듦
    }
}

모든 Java 앱은 "static 시작 → instance 인계" 패턴.
→ Spring은 그 인계 과정을 자동화한 것.


4️⃣ Instance → Static — 자유로운 경로

4.1 코드

public class ShipmentService {

    private final ShipmentRepository repository;

    public ShipmentService(ShipmentRepository repository) {
        this.repository = repository;
    }

    public BigDecimal calculate(Shipment s) {
        // Instance → Static 호출
        BigDecimal base = FreightPolicy.getBaseRate();          // ✓
        BigDecimal surcharge = FreightUtils.applyFuel(base);    // ✓
        
        return Math.max(base, surcharge);                       // ✓
    }
}

4.2 왜 자유로운가

calculate 메서드 실행 중:
  스택 프레임:
    LVA[0] this = ShipmentService 객체
    LVA[1] s = Shipment 객체
    ...

FreightUtils.applyFuel(base) 호출:
  → 대상이 Static 메서드
  → 객체 참조 필요 없음 → this 무시 가능
  → invokestatic FreightUtils.applyFuel
  → Static Zone 직접 호출

this가 있어도 사용 안 하면 그만. Static 호출은 객체와 무관.

4.3 메모리 그림

Method Area:
  ShipmentService Klass
    Non-Static Zone: calculate() 바이트코드
  
  FreightUtils Klass
    Static Zone: applyFuel() 바이트코드   ← 직접 호출 가능

Heap:
  ShipmentService 객체

Stack:
  ┌────────────────────────────────┐
  │ applyFuel() 프레임               │  ← static, this 없음
  │   LVA[0] base                   │
  ├────────────────────────────────┤
  │ calculate() 프레임               │
  │   LVA[0] this (사용 안 함)        │
  │   LVA[1] s                      │
  │   LVA[2] base                   │
  └────────────────────────────────┘

this는 calculate에만 있고, applyFuel은 무관.

4.4 Static 멤버 접근

public class ShipmentService {
    public BigDecimal calculate() {
        return BASE_RATE.multiply(quantity);   // ← Static 필드 접근
    }
    
    private static final BigDecimal BASE_RATE = new BigDecimal("100");
}

BASE_RATE는 ShipmentService 클래스의 Static Zone에 있음.
→ 인스턴스 메서드에서 자유롭게 접근 가능 (this 없이도).

4.5 ILIC 실무에서의 패턴

@Service
public class ShipmentService {

    public Shipment create(ShipmentRequest req) {
        // Static utility 자유 호출
        String blNo = ShipmentUtils.generateBlNo();         // Static
        LocalDate eta = DateUtils.calculateEta(req);        // Static
        BigDecimal fare = FreightCalculator.calc(req);      // Static
        
        return new Shipment(blNo, eta, fare);
    }
}

→ Instance 메서드 안에서 Util 클래스의 Static 메서드 자유롭게 호출.
→ 가장 흔한 패턴.


5️⃣ Instance → Instance — this 매개변수의 마법

5.1 코드

public class Shipment {

    private String blNo;
    private BigDecimal weight;
    private LocalDate eta;

    public BigDecimal calculateTotal() {
        BigDecimal base = calculateBase();      // ① 같은 객체의 메서드
        BigDecimal fuel = calculateFuel();      // ② 같은 객체의 메서드
        return base.add(fuel);
    }

    private BigDecimal calculateBase() {
        return weight.multiply(new BigDecimal("100"));   // this.weight 접근
    }

    private BigDecimal calculateFuel() {
        return calculateBase().multiply(FUEL_RATE);      // 또 다른 인스턴스 호출
    }
}

5.2 같은 객체 안의 호출 — this의 전달

calculateTotal();calculateBase();

JVM이 실제로 컴파일하는 것:

this.calculateTotal();this.calculateBase();   ← 호출자의 this를 그대로 전달

5.3 호출 흐름 상세

calculateTotal() 실행 중:
  스택 프레임:
    LVA[0] this = 0x7f4a2c01 (Shipment 객체)
    LVA[1] base
    LVA[2] fuel

calculateBase() 호출:
  1. Operand Stack에 this 푸시
     Stack: [0x7f4a2c01]
  2. invokevirtual Shipment.calculateBase
  3. JVM 동작:
     - 0x7f4a2c01의 Class Pointer → Shipment Klass
     - VMT에서 calculateBase 슬롯 찾기
     - 새 프레임 생성
     - LVA[0] = 0x7f4a2c01 (같은 this)
  4. calculateBase 실행
     - this.weight 접근 (LVA[0]의 객체의 weight 필드)

호출자의 this가 호출 대상에게 전달.
→ 같은 객체의 메서드 체인 = 같은 this가 계속 전달.

5.4 메모리 그림

Heap:
  ┌──────────────────────────┐
  │ Shipment @0x7f4a2c01      │
  │   blNo = "BL-001"         │
  │   weight = 1500           │
  └──────────────────────────┘
        ▲      ▲
        │      │ 모두 같은 this
        │      │
Stack:  │      │
  ┌─────┴──────┴──────────────┐
  │ calculateBase() 프레임      │
  │   LVA[0] this = 0x7f4a2c01 │  ← calculateTotal과 같은 this
  ├────────────────────────────┤
  │ calculateTotal() 프레임      │
  │   LVA[0] this = 0x7f4a2c01 │
  │   LVA[1] base = ?          │
  └────────────────────────────┘

5.5 다른 객체의 인스턴스 메서드 호출

public class ShipmentService {

    public BigDecimal totalForAll(List<Shipment> shipments) {
        BigDecimal sum = BigDecimal.ZERO;
        for (Shipment s : shipments) {
            sum = sum.add(s.calculateTotal());    // ← 다른 객체의 메서드
        }
        return sum;
    }
}

호출 흐름:

totalForAll() 실행 중:
  LVA[0] this = ShipmentService 객체 (0x...A1)
  LVA[1] shipments = List 객체 (0x...A2)
  LVA[2] sum
  LVA[3] s = Shipment 객체 (0x...B1, B2, ...)

s.calculateTotal() 호출:
  1. Stack push: [0x...B1]   ← s의 참조 (this 아님!)
  2. invokevirtual Shipment.calculateTotal
  3. 새 프레임:
     LVA[0] this = 0x...B1   ← s의 참조가 calculateTotal의 this가 됨

호출자의 this(ShipmentService)와 호출 대상의 this(Shipment) 는 별개.
→ this는 "호출되는 객체"의 참조.

5.6 this의 다양한 사용

public class Shipment {
    private String blNo;

    public void setBlNo(String blNo) {
        this.blNo = blNo;       // ← 매개변수와 필드 구분
    }

    public Shipment withBlNo(String blNo) {
        this.blNo = blNo;
        return this;            // ← 메서드 체이닝 (builder 패턴)
    }

    public boolean equals(Object o) {
        if (this == o) return true;   // ← 참조 비교 (자기 자신?)
        // ...
    }
}

this의 본질:

  • 인스턴스 메서드의 숨겨진 첫 매개변수
  • LVA[0]에 자동 들어감
  • 코드에서 this.x 로 명시적으로 사용 가능
  • 일반적으로 생략 가능 (모호하지 않을 때)

6️⃣ 메모리 추적 종합

6.1 4가지 호출의 종합 시나리오

public class Demo {

    static int sCount = 0;
    int iCount = 0;

    public static void main(String[] args) {     // Static
        sCount++;                                 // ① S→S 변수 접근
        staticHelper();                           // ① S→S
        
        Demo d = new Demo();                      // ② S→I 생성자
        d.instanceWork();                         // ② S→I
    }

    public static void staticHelper() {           // Static
        sCount++;                                 // S에서 S 접근
    }

    public void instanceWork() {                  // Instance
        sCount++;                                 // ③ I→S
        iCount++;                                 // I→I 변수
        instanceHelper();                         // ④ I→I
    }

    private void instanceHelper() {               // Instance
        iCount++;
    }
}

6.2 시점별 메모리 상태

시점 1 — main 시작

Method Area:
  Demo 클래스
    Static Zone:
      sCount = 0
      main() 바이트코드
      staticHelper() 바이트코드
    Non-Static Zone:
      instanceWork() 바이트코드
      instanceHelper() 바이트코드
      <init>() 바이트코드

Heap: (비어있음)

Stack:
  main() 프레임
    LVA[0] args

시점 2 — staticHelper 호출 중

Stack:
  ┌──────────────────────────┐
  │ staticHelper() 프레임      │  ← 추가 (인자 없음, this 없음)
  ├──────────────────────────┤
  │ main() 프레임              │
  │   LVA[0] args             │
  └──────────────────────────┘

Method Area:
  Demo의 Static Zone:
    sCount = 2   ← 1번 증가 (main에서) + 1번 증가 (staticHelper에서)

시점 3 — new Demo() 직후

Heap:
  ┌──────────────────────┐
  │ Demo @0x7f4a2c01      │
  │   iCount = 0          │
  └──────────────────────┘

Stack:
  main() 프레임
    LVA[0] args
    LVA[1] d = 0x7f4a2c01    ← 참조 저장

시점 4 — instanceWork 호출 중

Stack:
  ┌──────────────────────────┐
  │ instanceWork() 프레임      │
  │   LVA[0] this = 0x7f4a2c01│  ← d의 참조가 this로 전달
  ├──────────────────────────┤
  │ main() 프레임              │
  │   LVA[0] args             │
  │   LVA[1] d = 0x7f4a2c01   │
  └──────────────────────────┘

진행 중인 작업:
  sCount++     → Static Zone의 sCount = 3
  iCount++     → Heap의 객체의 iCount = 1

시점 5 — instanceHelper 호출 중

Stack:
  ┌────────────────────────────┐
  │ instanceHelper() 프레임      │
  │   LVA[0] this = 0x7f4a2c01  │  ← 같은 this 전달
  ├────────────────────────────┤
  │ instanceWork() 프레임        │
  │   LVA[0] this = 0x7f4a2c01  │
  ├────────────────────────────┤
  │ main() 프레임                │
  └────────────────────────────┘

Heap의 객체의 iCount = 2

6.3 통찰

Static Zone 데이터: 모든 경로에서 동일 위치 접근 (sCount는 1개)
Heap 데이터: 객체별 위치 접근 (iCount는 객체마다 별도)
this 전달: 인스턴스 메서드 간 호출에서 자동 연쇄

→ 4가지 경로 모두 결국 LVA[0]의 정체(this 있음/없음)로 차이가 결정.


7️⃣ ILIC 실무 — Service · Util · Entity의 호출 패턴

7.1 Spring 애플리케이션의 호출 패턴 매트릭스

@RestController
public class ShipmentController {
    
    private final ShipmentService service;  // Spring 주입
    
    @GetMapping("/{id}")
    public ShipmentResponse get(@PathVariable Long id) {
        Shipment s = service.findById(id);              // ① I→I (다른 Bean)
        return ShipmentResponse.from(s);                // ② I→S (정적 팩토리)
    }
}

@Service
public class ShipmentService {
    
    private final ShipmentRepository repository;
    
    public Shipment findById(Long id) {
        validate(id);                                   // ③ I→I (같은 객체)
        return repository.findById(id)                  // ④ I→I (다른 Bean)
            .orElseThrow(() -> new NotFoundException(id));
    }
    
    private void validate(Long id) {
        if (id == null) {
            throw new IllegalArgumentException("id required");
        }
    }
}

public final class ShipmentResponse {
    public static ShipmentResponse from(Shipment s) {  // Static (정적 팩토리)
        return new ShipmentResponse(...);              // Static→Instance 생성자
    }
}

호출 그래프:

Controller.get()        [I]
  ↓ I → I (Bean)
  Service.findById()    [I]
    ↓ I → I (same object, this)
    validate()          [I]
    ↓ I → I (Bean)
    repository.findById()  [I]
  ↓ I → S
  ShipmentResponse.from() [S]
    ↓ S → I (생성자)
    new ShipmentResponse()

→ ILIC 코드의 99% 패턴은 이 그래프 안에 있음.

7.2 Util 클래스 — Static의 정석

public final class BlNoUtils {

    private BlNoUtils() {
        throw new AssertionError("유틸 클래스");
    }

    public static String generate(String prefix, Long sequence) {  // Static
        return prefix + "-" + LocalDate.now().getYear()
            + "-" + String.format("%06d", sequence);
    }

    public static boolean isValid(String blNo) {                    // Static
        return blNo != null && blNo.matches("[A-Z]+-\\d{4}-\\d{6}");
    }
}

// 사용 — 모든 곳에서 자유롭게
@Service
public class ShipmentService {
    public Shipment create(...) {
        String blNo = BlNoUtils.generate("BL", nextSeq);   // I → S
        if (!BlNoUtils.isValid(blNo)) { ... }              // I → S
    }
}

왜 Static?

  • 외부 의존성 없음 (순수 함수)
  • 매번 객체 생성 불필요
  • 호출 경로 단순 (invokestatic)

7.3 Entity의 패턴 — Static Factory + Instance Methods

@Entity
public class Shipment {

    @Id @GeneratedValue
    private Long id;
    private String blNo;
    private LocalDate eta;

    protected Shipment() {}    // JPA용

    private Shipment(String blNo, LocalDate eta) {   // private 생성자
        this.blNo = blNo;
        this.eta = eta;
    }

    public static Shipment create(String blNo, LocalDate eta) {   // Static factory
        Objects.requireNonNull(blNo);
        if (eta.isBefore(LocalDate.now())) {
            throw new IllegalArgumentException("ETA must be future");
        }
        return new Shipment(blNo, eta);   // S → I (생성자)
    }

    public BigDecimal calculate() {                  // Instance
        return BASE_RATE.multiply(getWeight());      // I → S (final field) & I → I (this.getWeight)
    }

    public boolean isDelivered() {                   // Instance
        return status == ShipmentStatus.DELIVERED;
    }
}

// 사용
Shipment s = Shipment.create("BL-001", eta);   // S 호출
if (s.isDelivered()) { ... }                    // I 호출
s.calculate();                                  // I 호출

조합의 매력:

  • 객체 생성 = static factory (검증 포함)
  • 객체 동작 = instance method (this 활용)
  • 두 유형이 자연스럽게 협력

7.4 Spring Bean이 인스턴스인데 효율적인 이유

Bean 종류와 호출:
  @Controller  → instance, 모든 요청에서 같은 객체 (싱글톤)
  @Service     → instance, 동일
  @Repository  → instance, 동일

호출 시:
  Controller → Service → Repository
  모두 instance → instance
  invokevirtual 사용
  
하지만:
  - Bean은 단 1개씩 → 객체 생성 비용 0 (앱 시작 시 한 번)
  - this는 그 단일 인스턴스
  - JIT가 인라인 캐시 → 다형성 비용 거의 0
  - VMT 호출이 일반 함수 호출만큼 빠름

→ "Spring이 느리지 않은 이유" 의 메모리 메커니즘.

7.5 정적 팩토리 vs 생성자 — 메모리 관점

// 옵션 1 — public 생성자
Shipment s = new Shipment("BL-001", eta);
// 컴파일: new + invokespecial <init>

// 옵션 2 — 정적 팩토리
Shipment s = Shipment.create("BL-001", eta);
// 컴파일: invokestatic create → 내부에서 new + invokespecial <init>

메모리 비용:

  • 옵션 1: 객체 생성 1번
  • 옵션 2: 메서드 호출 1번 + 객체 생성 1번 → 미세하게 더 비쌈
  • 하지만 정적 팩토리는 이름, 검증, 캐싱, 서브타입 반환 등 유연성 제공

→ Effective Java가 정적 팩토리를 권장하는 이유.


8️⃣ 흔한 실수와 디버깅

실수 1 — main에서 인스턴스 메서드 호출 시도

public class App {
    public static void main(String[] args) {
        process();          // ❌ 컴파일 에러
    }
    
    public void process() { ... }
}

→ 메모리적으로 main은 static. process는 instance. 객체 없음.

해결:

public static void main(String[] args) {
    new App().process();    // ✓ 객체 만들고 호출
}

실수 2 — static 메서드에서 this 사용

public class App {
    public static void method() {
        System.out.println(this);    // ❌ static에선 this 없음
    }
}

→ Static 메서드의 LVA[0]은 첫 매개변수. this 슬롯 자체가 없음.

실수 3 — 인스턴스 변수를 static 메서드에서 직접 접근

public class App {
    private int count;
    
    public static void increment() {
        count++;            // ❌ 어느 객체의 count?
    }
}

→ static 메서드는 어느 객체에 속한지 모름 → 인스턴스 변수 접근 불가.

해결:

public static void increment(App app) {
    app.count++;            // ✓ 명시적 객체 전달
}

실수 4 — static 메서드를 객체로 호출 (스타일 문제)

Shipment s = new Shipment();
s.staticMethod();           // ✓ 동작은 하지만
Shipment.staticMethod();    // ✓ 권장 — 의도 명확

JVM은 s.staticMethod()Shipment.staticMethod() 로 컴파일.
→ 객체는 단지 클래스 정보 알리는 용도. 실제 동작은 static.

혼란 방지를 위해 클래스명.메서드명() 권장.

실수 5 — Spring Bean의 메서드를 static으로 만들기

@Service
public class ShipmentService {

    @Autowired
    private static ShipmentRepository repo;   // ❌ static에 주입 안 됨

    public static Shipment find(Long id) {    // ❌ AOP, 트랜잭션 안 됨
        return repo.findById(id).orElseThrow();
    }
}

→ Spring은 인스턴스 단위로 의존성 주입.
→ static 메서드는 AOP 프록시 안 거침 (Unit 2.1).

해결: 모든 Spring Bean 메서드는 instance로.

실수 6 — final + static + 가변 객체

public class Config {
    public static final List<String> routes = new ArrayList<>();
    //              ↑ static에 가변 컬렉션
}

// 어디서나
Config.routes.add("DANGEROUS");   // ❌ 전역 변경
Config.routes.clear();            // ❌ 다른 모든 코드에 영향

→ static 변수는 클래스 단위 1개. 가변이면 전역 변경 가능.

해결:

public static final List<String> routes = List.of("SEOUL", "BUSAN");
// 또는
private static final List<String> routes = new ArrayList<>();
public static List<String> getRoutes() {
    return Collections.unmodifiableList(routes);
}

실수 7 — 익명 인스턴스 메서드 호출의 NPE

ShipmentService service = getService();   // null 반환 가능
service.findById(1L);                     // ❌ NPE

// vs static은 NPE 없음
ShipmentUtils.format(blNo);               // ✓ 객체 무관

→ Instance 메서드는 객체 참조 필요 → null이면 NPE.
→ Optional, Objects.requireNonNull 등으로 방어.

디버깅 — 호출 경로 확인

# 1. 메서드 호출 추적 (간단한 방법)
log.info("Calling {}.{}", getClass().getSimpleName(), "methodName");

# 2. stack trace 출력
new Throwable().printStackTrace();

# 3. AspectJ로 모든 메서드 호출 로깅 (개발 환경)
@Around("execution(* com.ilic..*.*(..))")
public Object logCall(ProceedingJoinPoint pjp) throws Throwable {
    log.debug("Entering {}", pjp.getSignature());
    return pjp.proceed();
}

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

9.1 면접 단골 질문 매핑

Q핵심 답변
메서드 호출 4가지 케이스?S→S, S→I, I→S, I→I
Static→Instance 직접 호출이 안 되는 이유?객체 참조 없음 → VMT 거쳐 갈 수 없음
this는 어디서 와서 어디로?호출 시 Operand Stack → 호출 대상의 LVA[0]
Instance→Instance에서 this 전달은?같은 객체면 호출자의 this 그대로, 다른 객체면 그 객체 참조
main이 static인 메모리적 이유?JVM 시작 시 객체 없음. 객체 없이 호출 가능해야
Instance→Static 호출이 자유로운 이유?static 호출은 객체 무관, this 무시하면 됨
s.staticMethod()Shipment.staticMethod()?컴파일러는 동일 처리. 스타일 차이
static 메서드를 객체로 호출하면 동작 차이?없음. JVM 입장에선 같은 invokestatic
Spring Bean을 static으로 만들면?DI 안 됨, AOP 안 됨. 모두 instance여야 함
정적 팩토리의 메모리 비용?메서드 호출 1번 추가. 거의 무시할 수준

9.2 자기 점검 체크리스트

기본 이해

  • 4가지 호출 케이스를 모두 안다
  • 각 케이스에서 LVA[0]이 무엇인지 안다
  • this가 호출 시 어떻게 전달되는지 추적할 수 있다
  • static과 instance의 메모리 경로 차이를 안다
  • main에서 instance 메서드를 부르려면 객체가 필요한 이유를 안다

실전 적용

  • Service 메서드는 instance, Util은 static 으로 구분 가능
  • Entity에 static factory + instance method 패턴 적용 가능
  • Spring Bean 메서드가 모두 instance여야 하는 이유를 안다
  • static 컬렉션의 위험을 인식한다
  • AOP가 안 먹는 경우 (static, private, self-invocation)을 진단할 수 있다

면접 대비 — 5분 답변

  • 4가지 호출 경로와 LVA[0] 차이
  • this의 메모리적 정체 (숨겨진 매개변수)
  • static과 instance의 디스패치 차이
  • Spring Bean이 instance인 메모리적 이유
  • 정적 팩토리 vs 생성자 선택 기준

🎯 핵심 요약 — 3줄 정리

1. 4가지 호출 경로의 차이는 LVA[0]에 있다

  • Static 메서드: LVA[0]은 첫 매개변수 (this 없음)
  • Instance 메서드: LVA[0] = this (자동 전달)
  • 이 차이가 모든 비대칭의 원인

2. this는 보이지 않는 첫 매개변수

  • 호출자의 Operand Stack에 객체 참조 push
  • invokevirtual 시 호출 대상의 LVA[0]으로 전달
  • 코드에서 this.x로 명시 가능, 보통 생략

3. ILIC 실무 패턴

  • Service/Repository/Controller: instance (Spring Bean)
  • Util/Helper: static (외부 의존성 없음)
  • Entity: static factory + instance methods
  • Spring DI는 모두 instance 기반 (static에 DI 불가)

📚 다음으로...

Unit 2.3 — 인스턴스 메서드 호출의 전 과정 (Case Study)

이번 Unit에서 4가지 경로를 봤다면, 다음은 가장 일반적인 케이스(Instance→Instance)를 한 줄 단위로 완전 추적.

  • Model2 m2 = new Model2(); int sum = m2.hap(1, 2); 의 전 과정
  • 5단계 추적: 바이트코드 로딩 → main 시작 → new → 객체 참조 저장 → 메서드 호출
  • JIT 최적화가 들어가면 어떻게 달라지는가
  • 운영 환경에서 호출 hot path 분석

2주차 진행 상황

✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
🚀 Phase 2 — JVM 메서드 실행 메커니즘
  ✅ Unit 2.1 메서드 호출의 2단계 처리
  ✅ Unit 2.2 Static vs 인스턴스 호출 경로 ← 여기
  ⏭ Unit 2.3 인스턴스 메서드 호출의 전 과정 (Case Study)
  ⏭ Unit 2.4 new 연산자의 실제 동작
⏭ Phase 3 — 바이트코드와 상수 풀 ★ 2주차의 정점
profile
Software Developer

0개의 댓글