2주차 Unit 1.3 — Method Area의 3개 존

Psj·2026년 5월 12일

F-lab

목록 보기
52/230

Unit 1.3 — Method Area의 3개 존

F-LAB JAVA · 2주차 · Phase 1 · ★ 2주차 핵심 Unit


📌 학습 목표

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

  • Method Area는 왜 하나의 박스가 아니라 3개 존으로 나뉘는가?
  • Class Metadata Zone · Static Zone · Non-Static Zone 각각의 역할은?
  • main()은 어느 존에 있나? 왜 거기에 있어야 하나?
  • static 메서드가 인스턴스 메서드를 직접 호출할 수 없는 이유는?
  • 객체 100개를 만들면 메서드 바이트코드는 몇 번 복제되는가?
  • Spring @Bean 정적 팩토리 메서드와 인스턴스 메서드의 메모리 차이는?

🎯 핵심 한 문장

Method Area는 "클래스가 무엇인지(선언)" 와 "어떻게 실행되는지(바이트코드)" 를 분리해서 저장한다.
— 선언 정보는 Class Metadata Zone 에, 실행 코드는 Static Zone / Non-Static Zone 에 따로.
이 분리가 객체 N개를 만들어도 바이트코드는 1개만 존재하는 비밀이다.

비유 — 도서관의 3개 책꽂이

도서관 비유누가
Class Metadata Zone도서 카드 카탈로그 (책 목차/색인)"이 책은 어떤 책인가" 정보만
Static Zone누구나 열람 가능한 공용 자료실회원증 없이도 접근
Non-Static Zone회원만 접근 가능한 자료실회원증(=객체)이 있어야 들어감

회원증이 없는 사람은 공용 자료실(static)만 이용 가능.
회원만 회원 자료실(non-static)에 들어가는데, 그 안에서 공용 자료실도 자유롭게 갈 수 있음.

→ 이게 "static은 인스턴스를 못 부르지만, 인스턴스는 static을 부를 수 있다" 의 본질.


🧭 9개 섹션 로드맵

1. Method Area를 왜 쪼개야 했나
2. Class Metadata Zone     — 클래스의 신분증
3. Static Zone             — 객체 없이 살 수 있는 공간
4. Non-Static Zone         — 객체가 있어야 의미가 있는 공간
5. main()의 비밀 — 왜 Static인가
6. 접근 권한의 비대칭 — Static ↛ Instance
7. ILIC 실무 — Service · Util · @Bean 메서드
8. 흔한 실수 8가지
9. 면접 질문 + 자기 점검

1️⃣ Method Area를 왜 쪼개야 했나

1.1 1주차 + Unit 1.2에서 본 Method Area

Method Area:
  • 클래스 정보
  • static 변수
  • 메서드 바이트코드
  • 상수 풀

→ 한 박스 안에 다 들어있다고 봤다. 큰 그림으로는 맞다.

1.2 그런데 실제로는 — 3개의 분리된 공간

┌──────────────────────────────────────────┐
│  Method Area                             │
│                                          │
│  ┌────────────────────────────────────┐ │
│  │  Class Metadata Zone               │ │
│  │  • 클래스명, 부모, 인터페이스          │ │
│  │  • 필드 선언 (이름, 타입, 접근제어자)   │ │
│  │  • 메서드 시그니처 (이름, 매개변수, 반환)│ │
│  │  • 어노테이션, 제네릭 정보              │ │
│  │                                    │ │
│  │  ★ "무엇이 있는가" — 선언만           │ │
│  └────────────────────────────────────┘ │
│                                          │
│  ┌────────────────────────────────────┐ │
│  │  Static Zone                       │ │
│  │  • static 메서드 바이트코드           │ │
│  │  • static 필드 (클래스 변수)          │ │
│  │  • static 초기화 블록                │ │
│  │                                    │ │
│  │  ★ "객체 없이도 실행 가능한 코드"      │ │
│  └────────────────────────────────────┘ │
│                                          │
│  ┌────────────────────────────────────┐ │
│  │  Non-Static Zone                   │ │
│  │  • 인스턴스 메서드 바이트코드          │ │
│  │  • 생성자 바이트코드                  │ │
│  │  • 인스턴스 초기화 블록                │ │
│  │                                    │ │
│  │  ★ "객체가 있어야 의미 있는 코드"      │ │
│  └────────────────────────────────────┘ │
└──────────────────────────────────────────┘

1.3 왜 이렇게 분리했나 — 3가지 이유

이유 1 — 선언과 실행 코드의 분리

public class Shipment {
    private String blNo;                          // ← 선언만

    public BigDecimal calculate() {                // ← 시그니처 선언
        BigDecimal base = new BigDecimal("100"); // ← 실행 코드 (바이트코드)
        return base.multiply(BigDecimal.valueOf(2));
    }
}

Class Metadata = "이 클래스는 blNo 라는 String 필드와 calculate() 라는 메서드가 있다" (선언)
Static / Non-Static Zone = "그 메서드가 실제로 실행하는 명령들" (바이트코드)

→ 둘은 별개의 정보. 하나의 메서드 = 1개의 시그니처 + 1개의 바이트코드.
→ 객체 100개 만들어도 시그니처도 1개, 바이트코드도 1개. 절대 복제 안 됨.

이유 2 — Static vs Non-Static의 접근 가능성 분리

public class App {
    public static void main(String[] args) {     // Static Zone
        // 객체 없이도 실행 가능
        utilMethod();                            // ✓ static 호출 OK

        Member m = new Member();                 // 객체 생성
        m.greet();                               // ✓ 객체 있어야 호출 가능
    }

    static void utilMethod() { ... }            // Static Zone
}

class Member {
    void greet() { ... }                        // Non-Static Zone
}

Static Zone에 있는 코드는 객체 없이도 실행 가능 — JVM이 시작하자마자 main()을 호출할 수 있는 이유.
Non-Static Zone의 코드는 객체(=this)가 필요 — 생성자가 호출되어 객체가 만들어진 후에만 실행 가능.

→ 이 둘을 메모리 단위로 분리해두면 JVM이 접근 권한을 명확히 통제 가능.

이유 3 — 클래스 로딩 단계 최적화

클래스 로딩 단계 (JVM이 .class 파일을 읽을 때):

1. Loading       — 바이트 읽기
2. Linking
   - Verification  — 바이트코드 검증
   - Preparation   — Class Metadata + Static Zone 메모리 확보
   - Resolution    — 심볼 참조 해소 (Phase 3에서)
3. Initialization — Static Zone의 static 변수 초기화 + static 블록 실행

Non-Static Zone은 클래스 로딩 시점에 메모리에 적재되지만, 실제 실행은 객체 생성 후.
Static Zone은 클래스 로딩 직후 바로 실행 가능 상태.

→ JVM이 클래스 로딩 시 어디까지 준비할지 명확히 알 수 있도록 분리.


2️⃣ Class Metadata Zone — 클래스의 신분증

2.1 무엇이 들어가나

@Entity
@Table(name = "shipments")
public class Shipment extends BaseEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id @GeneratedValue
    private Long id;

    private String blNo;

    public BigDecimal calculate(int weight) { ... }
    public void setBlNo(String blNo) { ... }
}

Class Metadata Zone에 저장되는 것:

Shipment 클래스 정보:
  ├─ 클래스명: "com.ilic.shipment.Shipment"
  ├─ 부모: "com.ilic.common.BaseEntity"
  ├─ 인터페이스: [java.io.Serializable]
  ├─ 어노테이션: [@Entity, @Table(name="shipments")]
  ├─ 제네릭 정보: (없음)
  │
  ├─ 필드 선언:
  │   ├─ id          : Long      (private, @Id, @GeneratedValue)
  │   ├─ blNo        : String    (private)
  │   └─ serialVersionUID : long (private static final)
  │
  ├─ 메서드 시그니처:
  │   ├─ calculate(int) : BigDecimal   (public)
  │   ├─ setBlNo(String) : void        (public)
  │   └─ ...
  │
  └─ 상수 풀 참조 (Phase 3에서 깊이)

2.2 "선언만" 이라는 것의 의미

public BigDecimal calculate(int weight) {        // ← Metadata: 시그니처
    BigDecimal base = new BigDecimal("100");    // ← Static/Non-Static Zone: 바이트코드
    return base.multiply(BigDecimal.valueOf(weight));
}
  • Metadata에는 "calculate라는 메서드가 있고, int를 받아 BigDecimal을 반환한다" 까지만
  • 실제 메서드 본문({ ... })의 바이트코드는 다른 존에 저장

→ 이게 2단계 호출 메커니즘의 출발점.
1. Metadata에서 "이런 메서드가 있다" 확인
2. Static/Non-Static Zone에서 실제 바이트코드 찾아서 실행

2.3 Reflection이 보는 것이 바로 Metadata

Class<?> clazz = Shipment.class;

clazz.getName();                  // "com.ilic.shipment.Shipment"
clazz.getSuperclass();            // BaseEntity.class
clazz.getInterfaces();            // [Serializable.class]
clazz.getDeclaredFields();        // [id, blNo, serialVersionUID]
clazz.getDeclaredMethods();       // [calculate, setBlNo, ...]
clazz.getAnnotations();           // [@Entity, @Table]

Reflection은 Class Metadata Zone을 읽는 API.
→ Spring/JPA/Jackson이 이 정보로 동작 (2주차 Phase 6에서 깊이).

2.4 객체와의 관계

Heap:
  ┌─────────────────────┐
  │ Shipment 객체 #1     │
  │   Class ptr ────────┼──┐
  │   id = 1            │  │
  │   blNo = "BL-001"   │  │
  └─────────────────────┘  │
                           ▼
                  Method Area / Class Metadata Zone:
                  ┌─────────────────────┐
                  │ Shipment 클래스       │
                  │   필드 선언:           │
                  │     id, blNo, ...    │
                  │   메서드 시그니처:      │
                  │     calculate, ...   │
                  └─────────────────────┘

객체의 Class Pointer는 정확히 이 Metadata를 가리킨다 (Unit 1.2 다이어그램의 정밀화).


3️⃣ Static Zone — 객체 없이 살 수 있는 공간

3.1 무엇이 들어가나

public class Math2 {
    public static final double PI = 3.14159;       // ← static 변수
    private static int callCount = 0;              // ← static 변수

    static {                                        // ← static 초기화 블록
        callCount = 0;
        System.out.println("Math2 클래스 로딩");
    }

    public static double square(double x) {        // ← static 메서드 (바이트코드)
        callCount++;
        return x * x;
    }
}

Static Zone에 저장되는 것:

Math2 클래스의 Static Zone:
  ├─ static 필드의 실제 값:
  │   ├─ PI         = 3.14159
  │   └─ callCount  = 0
  │
  ├─ static 메서드 바이트코드:
  │   └─ square(double) : double
  │       0: dload_0
  │       1: dload_0
  │       2: dmul
  │       3: dreturn
  │
  └─ static 초기화 블록 바이트코드:
      └─ <clinit>() : void

3.2 객체 없이 접근 가능

double area = Math2.PI * Math2.square(5.0);
//             ↑              ↑
//        객체 없이 직접      객체 없이 직접

Method Area의 Static Zone에 바로 코드와 데이터가 있으므로 새 객체를 만들 필요가 없다.

실행 흐름:
  1. JVM이 Math2 클래스 로딩
  2. Static Zone에 PI=3.14159 적재, square() 바이트코드 적재
  3. Math2.PI 접근 → Static Zone의 PI 값 직접 읽음
  4. Math2.square(5.0) 호출 → Static Zone의 바이트코드 실행

→ Heap에 객체가 없어도 모든 것이 완결된다.

3.3 static 초기화 블록

public class Config {
    public static final Map<String, String> ROUTES;

    static {
        Map<String, String> m = new HashMap<>();
        m.put("SEOUL", "ICN");
        m.put("BUSAN", "PUS");
        m.put("INCHEON", "ICN");
        ROUTES = Collections.unmodifiableMap(m);
    }
}

언제 실행되나?

  • 클래스가 처음 사용되는 시점 (= 클래스 로딩 시점)
  • 메서드 호출, static 변수 접근, new, Class.forName() 등이 트리거

몇 번 실행되나?

  • 정확히 1번. JVM 생명주기 동안.

→ Spring 빈 초기화나 설정 상수 세팅에 유용.
→ 단, 예외 던지면 ExceptionInInitializerError → 클래스가 영원히 사용 불가 상태.

3.4 클래스 변수 (static 필드)의 진짜 위치

Unit 1.1에서 "클래스 변수는 Method Area에 1개" 라고 했다.
정확히는 Static Zone에 1개.

Method Area:
  ├─ Class Metadata Zone:
  │   └─ "Shipment 클래스에 totalCount 라는 static int 필드가 있다" (선언)
  │
  └─ Static Zone:
      └─ totalCount = 42   (실제 값)

→ 선언과 실제 저장이 다른 존에 분리.
→ 객체 100개가 같은 totalCount를 보는 이유: 모두 Static Zone의 한 자리를 가리킴.


4️⃣ Non-Static Zone — 객체가 있어야 의미 있는 공간

4.1 무엇이 들어가나

public class Shipment {
    private Long id;
    private String blNo;

    public Shipment(String blNo) {                    // ← 생성자
        this.blNo = blNo;
    }

    public BigDecimal calculate(int weight) {         // ← 인스턴스 메서드
        return basicRate().multiply(BigDecimal.valueOf(weight));
    }

    private BigDecimal basicRate() {                  // ← 인스턴스 메서드
        return new BigDecimal("1000");
    }

    {                                                  // ← 인스턴스 초기화 블록
        System.out.println("Shipment 인스턴스 생성");
    }
}

Non-Static Zone에 저장되는 것:

Shipment 클래스의 Non-Static Zone:
  ├─ 인스턴스 메서드 바이트코드:
  │   ├─ calculate(int) : BigDecimal
  │   ├─ basicRate()    : BigDecimal
  │   ├─ getBlNo()      : String
  │   └─ setBlNo(String): void
  │
  ├─ 생성자 바이트코드:
  │   └─ <init>(String) : void
  │
  └─ 인스턴스 초기화 블록 바이트코드

⚠️ 인스턴스 변수(id, blNo)는 여기 없다. 선언은 Class Metadata에, 실제 값은 객체(Heap)에.

4.2 객체가 필요한 이유 — this 매개변수

public BigDecimal calculate(int weight) {
    return basicRate().multiply(BigDecimal.valueOf(weight));
    //     ↑
    //  실제로는 this.basicRate()
}

JVM 시각에서 보면:

// 우리가 쓴 코드
shipment.calculate(100);

// JVM이 실제로 호출하는 모양 (개념적)
Shipment.calculate(shipment, 100);
//                  ↑
//                  this 매개변수

인스턴스 메서드는 항상 첫 번째 숨겨진 매개변수로 this를 받는다.
→ 객체가 없으면 this를 전달할 수 없음 → 호출 자체가 불가능.

호출 시점의 Stack 프레임:
  ┌──────────────────────────┐
  │ calculate() 프레임         │
  │   this = 0x7f4a2c01      │  ← 자동 전달
  │   weight = 100           │
  └──────────────────────────┘

→ Unit 1.2 5장에서 본 this 매개변수의 정체.
→ Non-Static Zone의 메서드가 객체별 데이터에 접근할 수 있는 메커니즘.

4.3 객체 N개 만들어도 바이트코드는 1개

for (int i = 0; i < 100; i++) {
    Shipment s = new Shipment("BL-" + i);
    s.calculate(100);
}
Method Area / Non-Static Zone:
  calculate() 바이트코드  ← 1개

Heap:
  Shipment #1, #2, #3, ..., #100  ← 100개
  각 객체는 모두 같은 Class Pointer로 Method Area의 같은 메서드를 가리킴

메서드 코드는 클래스에 속한다. 객체에 속하지 않는다.
→ 객체는 데이터(인스턴스 변수)만 가지고, 메서드는 클래스의 자산.

4.4 다형성과의 연결 — Virtual Method Table (VMT)

class Animal {
    public void sound() { System.out.println("..."); }
}

class Dog extends Animal {
    @Override
    public void sound() { System.out.println("멍"); }
}

Animal a = new Dog();
a.sound();    // "멍"   ← 다형성

JVM의 동작:

Heap:
  Dog 객체
    Class ptr ──────┐
                    ▼
Method Area:
  Dog 클래스의 Non-Static Zone:
    sound() 바이트코드 → "멍" 출력

  Animal 클래스의 Non-Static Zone:
    sound() 바이트코드 → "..." 출력

→ JVM은 변수 타입(Animal)이 아니라 객체의 Class Pointer가 가리키는 클래스의 메서드를 호출.
→ Dog의 Class Pointer가 Dog 클래스를 가리키므로 Dog의 sound() 실행.

이를 위해 JVM은 각 클래스에 Virtual Method Table (VMT, 가상 메서드 테이블)을 둔다.

Dog 클래스의 VMT:
  toString()  → Object.toString  (또는 override 시 Dog.toString)
  equals()    → Object.equals
  sound()     → Dog.sound  ← override됨
  bark()      → Dog.bark

→ 다형성의 메모리 구현체. 1주차 Unit 2.4 다형성의 내부.


5️⃣ main()의 비밀 — 왜 Static인가

5.1 질문

public class App {
    public static void main(String[] args) {    // ← 왜 static?
        // ...
    }
}

public static void main(String[] args) — 자바를 배운 첫날부터 본 시그니처.
왜 static 인가?

5.2 답 — "JVM은 객체를 만들 수 없다"

JVM 시작
   ↓
App.class 로딩 (Method Area에 적재)
   ↓
"main 메서드를 호출하자"
   ↓
??? 객체를 어떻게 만들지?
   - new App() 을 호출하려면 → main이 먼저 실행돼야 함
   - 닭이 먼저냐 달걀이 먼저냐

해결책: mainStatic Zone에 둔다 → 객체 없이 호출 가능.

JVM 시작
   ↓
App.class 로딩
   ↓
Static Zone에 main() 바이트코드 적재
   ↓
객체 없이 main() 직접 호출 ✓

자바 프로그램의 진입점이라는 특수성 때문에 강제로 static.

5.3 이 패턴이 등장하는 다른 곳

Java의 Entry Point들

public static void main(String[] args)              // 일반 자바 프로그램
public static void premain(String args, Instrumentation inst)  // Java Agent
public static void agentmain(String args, Instrumentation inst) // 동적 Agent

모두 static. 같은 이유.

Factory Method 패턴

public class Shipment {
    private final String blNo;

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

    public static Shipment of(String blNo) {   // ← static factory
        return new Shipment(blNo);
    }
}

Shipment s = Shipment.of("BL-001");   // 객체 없이 호출

of(), valueOf(), getInstance(), from()static factory method 패턴.
객체 없이 호출 가능해야 하므로 static.

5.4 main에서 인스턴스 메서드 호출하기

public class App {
    public static void main(String[] args) {
        // greet();              ❌ 컴파일 에러 — 인스턴스 메서드를 직접 못 부름
        App app = new App();
        app.greet();              // ✓ 객체 만든 후
    }

    public void greet() {         // 인스턴스 메서드
        System.out.println("hello");
    }
}

→ Spring 프로젝트의 main은 보통 한 줄.

@SpringBootApplication
public class IlicApplication {
    public static void main(String[] args) {
        SpringApplication.run(IlicApplication.class, args);
        // 여기부터 Spring이 인스턴스(Bean)들을 만들어 실행
    }
}

SpringApplication.run도 static. 그 안에서 비로소 인스턴스들이 만들어진다.


6️⃣ 접근 권한의 비대칭 — Static ↛ Instance

6.1 규칙

호출자대상가능?
StaticStatic
StaticInstance❌ (객체 있어야)
InstanceStatic
InstanceInstance✓ (같은 객체 또는 다른 객체)

비대칭: Static → Instance 만 막힘. 나머지 셋은 모두 가능.

6.2 코드로 보기

public class Service {

    private int instanceCount = 0;           // 인스턴스 변수
    private static int totalCount = 0;       // 클래스 변수

    public static void staticMethod() {      // Static Zone
        totalCount++;              // ✓ static → static
        // instanceCount++;        // ❌ static → instance (객체 없음)
        // instanceMethod();       // ❌ static → instance method
        
        Service s = new Service();
        s.instanceCount++;         // ✓ 객체 만든 후엔 가능
        s.instanceMethod();        // ✓
    }

    public void instanceMethod() {           // Non-Static Zone
        instanceCount++;           // ✓ instance → instance (this.instanceCount)
        totalCount++;              // ✓ instance → static
        staticMethod();            // ✓ instance → static
        anotherInstance();         // ✓ instance → instance (this.anotherInstance)
    }

    public void anotherInstance() {}
}

6.3 왜 비대칭인가 — 메모리 관점

Static Zone (객체 없이 존재)
   ↓ 가능
Instance Zone (객체가 있어야 의미)
   ↑ 가능

Static Zone의 코드: 객체 없이 호출됨 → this 없음 → 인스턴스 메서드/필드의 데이터를 어디서 찾을지 모름.
Instance Zone의 코드: 객체로 호출됨 → this 있음 → 그 객체의 인스턴스 데이터를 알고, Static Zone의 공유 데이터도 알 수 있음.

public static void staticMethod() {
    instanceCount++;
    //  ↑
    //  어느 객체의 instanceCount?
    //  스레드가 100개의 객체 중 어느 것을 가리키는지 알 수 없음
}

정보 부족. 컴파일러가 막아주는 것.

6.4 자주 헷갈리는 케이스

public class Foo {
    private int x = 10;            // 인스턴스
    private static int y = 20;     // static

    public static void a() {
        // System.out.println(x);  ❌
        System.out.println(y);     // ✓
    }

    public void b() {
        System.out.println(x);     // ✓
        System.out.println(y);     // ✓
    }

    public static void c(Foo foo) {
        System.out.println(foo.x); // ✓ 명시적 객체 전달
        System.out.println(y);     // ✓
    }
}

static 메서드라도 객체 참조를 받으면 인스턴스 멤버 접근 가능.
→ 유틸 클래스의 Utils.process(obj) 같은 패턴.


7️⃣ ILIC 실무 — Service · Util · @Bean 메서드

7.1 Service 메서드 — 인스턴스가 기본

@Service
public class ShipmentService {

    private final ShipmentRepository repository;          // 인스턴스 final
    private final FareCalculator calculator;              // 인스턴스 final

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

    public Shipment process(ShipmentRequest req) {        // ✓ 인스턴스 메서드
        Shipment shipment = new Shipment(req);
        BigDecimal fare = calculator.compute(shipment);
        shipment.setFreight(fare);
        return repository.save(shipment);
    }
}

왜 인스턴스 메서드?

  • Spring이 객체(Bean)를 만들어 주입repository, calculator 사용
  • DI 받은 의존성을 쓰려면 → 객체의 인스턴스 변수에 접근해야 함 → 인스턴스 메서드

메모리 위치:

  • Service 클래스 자체는 Method Area
  • process() 바이트코드는 Non-Static Zone
  • Spring이 만든 ShipmentService Bean(객체)은 Heap

7.2 Util 클래스 — Static의 정석

public final class FreightUtils {

    private FreightUtils() {                              // 인스턴스화 방지
        throw new AssertionError("유틸 클래스");
    }

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

    public static String formatBL(String blNo) {           // ✓ static
        return "BL-" + blNo.toUpperCase();
    }
}

// 사용
BigDecimal total = FreightUtils.applyFuelSurcharge(base, fuelRate);

왜 static?

  • 외부 상태(필드)에 의존하지 않음 → 순수 함수 성격
  • 매번 객체를 만들 필요 없음
  • FreightUtils.x() 형태로 호출 → 의도가 명확

메모리 위치:

  • 메서드 바이트코드는 Static Zone
  • 객체는 만들어지지 않음 → Heap 사용 0
  • 호출 비용 최소

7.3 Spring @Bean — Configuration의 정적/인스턴스 혼합

@Configuration
public class ShipmentConfig {

    @Bean
    public ShipmentService shipmentService(          // ✓ 인스턴스 메서드 (보통)
        ShipmentRepository repository,
        FareCalculator calculator
    ) {
        return new ShipmentService(repository, calculator);
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer    // ⚠ static!
        configurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

왜 어떤 @Bean은 static?

  • 일반 @Bean: Spring이 ShipmentConfig Bean을 만들고, 그 인스턴스의 메서드를 호출해서 다른 Bean 생성
  • BFPP(BeanFactoryPostProcessor) 같은 일부 빈은 Bean 생성 순서상 다른 @Bean보다 먼저 만들어져야 함 → static으로 선언해서 ShipmentConfig 인스턴스화 전에 호출 가능하게

→ Spring이 어떻게 static과 instance를 골라 쓰는지 이해 = Bean 라이프사이클 이해.

7.4 Entity의 정적 팩토리

@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(                    // ✓ static factory
        String blNo, LocalDate eta
    ) {
        Objects.requireNonNull(blNo, "blNo 필수");
        if (eta.isBefore(LocalDate.now())) {
            throw new IllegalArgumentException("ETA는 미래여야 함");
        }
        return new Shipment(blNo, eta);
    }

    public BigDecimal calculate() { ... }            // 인스턴스 메서드
}

// 사용
Shipment s = Shipment.create("BL-001", LocalDate.of(2026, 12, 1));
s.calculate();

조합:

  • 객체 생성: static factory (Shipment.create)
  • 객체 동작: instance method (s.calculate)

→ Effective Java의 권장 패턴.
→ 메모리 측면에서: create()는 Static Zone, calculate()는 Non-Static Zone.

7.5 ILIC의 메서드 분류 가이드

상황static?이유
외부 상태 없음, 순수 계산staticMath.max 패턴
Spring 의존성(Repository 등) 사용instanceDI 받아야 함
객체 생성 팩토리static자기 자신 인스턴스화 전에 호출
Entity의 비즈니스 로직instance객체 상태 변경
상수 조회static값만 반환
라이프사이클 콜백 (@PostConstruct)instanceBean이 만들어진 후

8️⃣ 흔한 실수 8가지

실수 1 — static에서 인스턴스 멤버 접근

public class App {
    private int count = 0;

    public static void main(String[] args) {
        count++;    // ❌ Non-static field 'count' cannot be referenced from a static context
    }
}

해결:

  • 인스턴스 멤버가 필요하면 객체 생성: new App().count++
  • 또는 멤버를 static으로 변경 (주의: 멀티스레드)

실수 2 — 유틸 클래스에 인스턴스 만들 수 있게 둠

// ❌
public class FreightUtils {
    public static BigDecimal calc(...) { ... }
}

FreightUtils u = new FreightUtils();    // 의미 없는 객체

// ✅
public final class FreightUtils {
    private FreightUtils() {
        throw new AssertionError();
    }
    // ...
}

실수 3 — static 초기화 블록에서 예외

public class Config {
    public static final Map<String, String> ROUTES;

    static {
        ROUTES = loadFromFile();    // 파일 없으면 IOException
    }
}

static 블록에서 예외 발생 → ExceptionInInitializerError.
한 번 실패하면 그 클래스는 영원히 사용 불가 → 다른 모든 호출이 NoClassDefFoundError.

해결:

static {
    try {
        ROUTES = loadFromFile();
    } catch (IOException e) {
        ROUTES = Collections.emptyMap();    // fallback
        log.warn("Routes 로딩 실패, 빈 맵으로 시작", e);
    }
}

실수 4 — static 메서드를 override하려는 시도

class Parent {
    public static void greet() { System.out.println("Parent"); }
}

class Child extends Parent {
    public static void greet() { System.out.println("Child"); }  // override 아님!
}

Parent p = new Child();
p.greet();    // "Parent"  ← 다형성 안 됨

static 메서드는 다형성 대상이 아님. 클래스 단위 메서드라서 컴파일 타임에 결정.
@Override 어노테이션 붙이면 컴파일 에러.

실수 5 — main 메서드에서 인스턴스 메서드를 정적인 줄 알고 호출

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

    public void process() { ... }    // 인스턴스 메서드
}

해결: 진짜 의도 확인.

  • 한 번만 쓸 거면 process()를 static으로
  • 객체 상태가 필요하면 new App().process()

실수 6 — 인스턴스 메서드에서 this. 누락으로 가리기

public class Order {
    private int count;

    public void increment(int count) {    // 매개변수 이름 같음
        count = count + 1;                // ❌ 매개변수만 증가
        // this.count = this.count + 1;   // ✓ 의도
    }
}

→ Unit 1.1 흔한 실수와 같은 패턴.

실수 7 — Singleton을 static 변수로 만들기

public class ShipmentManager {

    private static ShipmentManager instance;    // ❌ 안전하지 않은 싱글톤

    public static ShipmentManager getInstance() {
        if (instance == null) {
            instance = new ShipmentManager();   // 멀티스레드 race condition
        }
        return instance;
    }
}

문제:

  • 멀티스레드 환경에서 2개 객체 생성 가능
  • Static 변수라 영원히 메모리 점유

해결:

// 1. Spring Bean 사용 (가장 권장)
@Component
public class ShipmentManager { ... }

// 2. enum 싱글톤 (Effective Java 권장)
public enum ShipmentManager {
    INSTANCE;
    public void doWork() { ... }
}

실수 8 — static 변수에 인스턴스 의존성 주입 시도

@Service
public class ShipmentService {

    @Autowired
    private static ShipmentRepository repository;    // ❌ static 필드에 주입 안 됨
    //     ↑
    //  Spring은 static 필드를 무시함 (Bean 인스턴스 단위 주입이므로)
}

해결: static 필드에는 주입 X. 인스턴스 필드로.
정말 static에서 써야 한다면 setter 주입의 변종 패턴:

private static ShipmentRepository repository;

@Autowired
public void setRepository(ShipmentRepository repo) {
    ShipmentService.repository = repo;   // 권장 X (테스트 어렵고, 안티패턴)
}

→ 이런 코드를 보면 보통 설계 문제. Bean으로 빼는 게 정답.


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

9.1 면접 단골 질문 매핑

Q핵심 답변
Method Area의 3개 존은?Class Metadata · Static · Non-Static
Class Metadata Zone에 무엇이?클래스 선언 정보 (필드 선언, 메서드 시그니처, 어노테이션)
Static Zone에 무엇이?static 메서드 바이트코드 + static 필드 실제 값
Non-Static Zone에 무엇이?인스턴스 메서드 바이트코드 + 생성자
main()이 static인 이유?JVM이 객체를 만들 수 없음. 객체 없이 호출 가능해야
static이 instance를 직접 호출 못 하는 이유?this가 없음 → 어느 객체의 데이터인지 모름
객체 100개 만들면 메서드 바이트코드 개수?1개. 메서드는 클래스 자산, 객체는 데이터만 가짐
인스턴스 메서드는 어떻게 객체 데이터에 접근?숨겨진 this 매개변수가 자동 전달됨
static 메서드를 override할 수 있나?❌ 클래스 단위라 컴파일 타임에 결정. 다형성 대상 X
Spring @Bean이 static인 경우는?BFPP 같은 빈은 Configuration 인스턴스보다 먼저 만들어져야 함

9.2 자기 점검 체크리스트

기본 이해

  • Method Area 3개 존의 이름과 역할을 안다
  • 선언(Metadata)과 실행 코드(Static/Non-Static Zone)의 분리를 이해한다
  • static 메서드와 인스턴스 메서드의 호출 메커니즘 차이를 안다
  • this 매개변수의 정체를 안다
  • 객체 N개를 만들어도 메서드는 1개임을 안다

실전 적용

  • static 메서드를 적절한 곳에 사용할 수 있다 (유틸, 팩토리)
  • Service에는 인스턴스 메서드를 쓰는 이유를 안다
  • Spring Bean과 static의 관계를 안다
  • static 초기화 블록의 위험을 안다
  • Singleton을 안전하게 구현할 수 있다 (Spring Bean 또는 enum)

면접 대비 — 5분 답변

  • Method Area 3개 존의 책임 분리
  • main()이 왜 static인가
  • static에서 인스턴스 멤버 접근 못 하는 이유
  • 객체와 메서드 바이트코드의 관계 (1:N)
  • Virtual Method Table과 다형성의 메모리 구현

🎯 핵심 요약 — 3줄 정리

1. Method Area는 3개 존으로 분리된다

  • Class Metadata Zone — 선언만 (필드 선언, 메서드 시그니처)
  • Static Zone — 객체 없이도 실행 가능한 코드/데이터 (static 메서드, static 필드)
  • Non-Static Zone — 객체가 있어야 의미 있는 코드 (인스턴스 메서드, 생성자)

2. static과 instance의 비대칭 접근

  • Static → Instance: ❌ (this 없어서)
  • Instance → Static: ✓
  • main()이 static인 이유: JVM이 객체를 못 만들기 때문

3. 메서드는 클래스 자산, 객체는 데이터만 가진다

  • 객체 100개 → 메서드 바이트코드 1개
  • 인스턴스 메서드 호출 = 숨겨진 this 매개변수 자동 전달
  • 다형성 = Class Pointer가 가리키는 클래스의 메서드 실행

📚 다음으로...

Unit 1.4 — Stack Area의 동작

이번 Unit에서 메서드 바이트코드가 어디에 저장되는지 봤다면, 다음은 그 메서드가 호출될 때 Stack에서 무슨 일이 일어나는가.

  • 스택 프레임의 정밀 구조 (Local Variable Array, Operand Stack, Frame Data)
  • LIFO 동작과 메서드 호출/반환
  • PC Register와 명령 추적
  • 멀티스레드 환경의 스택 분리
  • 재귀 호출의 한계와 StackOverflowError

2주차 진행 상황

  • ✅ Unit 1.1 자바 변수의 3종류
  • ✅ Unit 1.2 변수별 저장 위치
  • ✅ Unit 1.3 Method Area의 3개 존 ★ — 이 문서
  • ⏭ Unit 1.4 Stack Area의 동작
  • ⏭ Unit 1.5 Heap과 객체-Metadata 연결
  • ⏭ Unit 1.6 Literal Pool Area
  • ⏭ Phase 2 (메서드 실행 메커니즘)
  • Phase 3 (바이트코드와 상수 풀) ★ 2주차의 정점

Phase 3로 가는 길 — 미리보기

이번 Unit에서 본 "메서드 바이트코드" 가 Phase 3에서 실제로 어떻게 생겼는지 본다.

public BigDecimal calculate(int weight) {
    return BigDecimal.valueOf(100 * weight);
}

→ Phase 3에서:

0: getstatic     #2   // BigDecimal.valueOf
3: bipush        100
5: iload_1            // weight 로드
6: imul
7: i2l
8: invokestatic  #3   // BigDecimal.valueOf(long)
11: areturn

#2, #3 같은 인덱스가 상수 풀(Constant Pool) 의 정체. Phase 3의 핵심.

profile
Software Developer

0개의 댓글