F-LAB JAVA · 2주차 · Phase 1 · ★ 2주차 핵심 Unit
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
main()은 어느 존에 있나? 왜 거기에 있어야 하나?@Bean 정적 팩토리 메서드와 인스턴스 메서드의 메모리 차이는?Method Area는 "클래스가 무엇인지(선언)" 와 "어떻게 실행되는지(바이트코드)" 를 분리해서 저장한다.
— 선언 정보는 Class Metadata Zone 에, 실행 코드는 Static Zone / Non-Static Zone 에 따로.
이 분리가 객체 N개를 만들어도 바이트코드는 1개만 존재하는 비밀이다.
| 존 | 도서관 비유 | 누가 |
|---|---|---|
| Class Metadata Zone | 도서 카드 카탈로그 (책 목차/색인) | "이 책은 어떤 책인가" 정보만 |
| Static Zone | 누구나 열람 가능한 공용 자료실 | 회원증 없이도 접근 |
| Non-Static Zone | 회원만 접근 가능한 자료실 | 회원증(=객체)이 있어야 들어감 |
회원증이 없는 사람은 공용 자료실(static)만 이용 가능.
회원만 회원 자료실(non-static)에 들어가는데, 그 안에서 공용 자료실도 자유롭게 갈 수 있음.
→ 이게 "static은 인스턴스를 못 부르지만, 인스턴스는 static을 부를 수 있다" 의 본질.
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. 면접 질문 + 자기 점검
Method Area:
• 클래스 정보
• static 변수
• 메서드 바이트코드
• 상수 풀
→ 한 박스 안에 다 들어있다고 봤다. 큰 그림으로는 맞다.
┌──────────────────────────────────────────┐
│ Method Area │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Class Metadata Zone │ │
│ │ • 클래스명, 부모, 인터페이스 │ │
│ │ • 필드 선언 (이름, 타입, 접근제어자) │ │
│ │ • 메서드 시그니처 (이름, 매개변수, 반환)│ │
│ │ • 어노테이션, 제네릭 정보 │ │
│ │ │ │
│ │ ★ "무엇이 있는가" — 선언만 │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Static Zone │ │
│ │ • static 메서드 바이트코드 │ │
│ │ • static 필드 (클래스 변수) │ │
│ │ • static 초기화 블록 │ │
│ │ │ │
│ │ ★ "객체 없이도 실행 가능한 코드" │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Non-Static Zone │ │
│ │ • 인스턴스 메서드 바이트코드 │ │
│ │ • 생성자 바이트코드 │ │
│ │ • 인스턴스 초기화 블록 │ │
│ │ │ │
│ │ ★ "객체가 있어야 의미 있는 코드" │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
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개. 절대 복제 안 됨.
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이 접근 권한을 명확히 통제 가능.
클래스 로딩 단계 (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이 클래스 로딩 시 어디까지 준비할지 명확히 알 수 있도록 분리.
@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에서 깊이)
public BigDecimal calculate(int weight) { // ← Metadata: 시그니처
BigDecimal base = new BigDecimal("100"); // ← Static/Non-Static Zone: 바이트코드
return base.multiply(BigDecimal.valueOf(weight));
}
calculate라는 메서드가 있고, int를 받아 BigDecimal을 반환한다" 까지만{ ... })의 바이트코드는 다른 존에 저장→ 이게 2단계 호출 메커니즘의 출발점.
1. Metadata에서 "이런 메서드가 있다" 확인
2. Static/Non-Static Zone에서 실제 바이트코드 찾아서 실행
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에서 깊이).
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 다이어그램의 정밀화).
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
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에 객체가 없어도 모든 것이 완결된다.
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);
}
}
언제 실행되나?
new, Class.forName() 등이 트리거몇 번 실행되나?
→ Spring 빈 초기화나 설정 상수 세팅에 유용.
→ 단, 예외 던지면 ExceptionInInitializerError → 클래스가 영원히 사용 불가 상태.
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의 한 자리를 가리킴.
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)에.
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의 메서드가 객체별 데이터에 접근할 수 있는 메커니즘.
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의 같은 메서드를 가리킴
→ 메서드 코드는 클래스에 속한다. 객체에 속하지 않는다.
→ 객체는 데이터(인스턴스 변수)만 가지고, 메서드는 클래스의 자산.
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 다형성의 내부.
public class App {
public static void main(String[] args) { // ← 왜 static?
// ...
}
}
public static void main(String[] args) — 자바를 배운 첫날부터 본 시그니처.
왜 static 인가?
JVM 시작
↓
App.class 로딩 (Method Area에 적재)
↓
"main 메서드를 호출하자"
↓
??? 객체를 어떻게 만들지?
- new App() 을 호출하려면 → main이 먼저 실행돼야 함
- 닭이 먼저냐 달걀이 먼저냐
해결책: main을 Static Zone에 둔다 → 객체 없이 호출 가능.
JVM 시작
↓
App.class 로딩
↓
Static Zone에 main() 바이트코드 적재
↓
객체 없이 main() 직접 호출 ✓
→ 자바 프로그램의 진입점이라는 특수성 때문에 강제로 static.
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. 같은 이유.
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.
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. 그 안에서 비로소 인스턴스들이 만들어진다.
| 호출자 | 대상 | 가능? |
|---|---|---|
| Static | Static | ✓ |
| Static | Instance | ❌ (객체 있어야) |
| Instance | Static | ✓ |
| Instance | Instance | ✓ (같은 객체 또는 다른 객체) |
비대칭: Static → Instance 만 막힘. 나머지 셋은 모두 가능.
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() {}
}
Static Zone (객체 없이 존재)
↓ 가능
Instance Zone (객체가 있어야 의미)
↑ 가능
Static Zone의 코드: 객체 없이 호출됨 → this 없음 → 인스턴스 메서드/필드의 데이터를 어디서 찾을지 모름.
Instance Zone의 코드: 객체로 호출됨 → this 있음 → 그 객체의 인스턴스 데이터를 알고, Static Zone의 공유 데이터도 알 수 있음.
public static void staticMethod() {
instanceCount++;
// ↑
// 어느 객체의 instanceCount?
// 스레드가 100개의 객체 중 어느 것을 가리키는지 알 수 없음
}
→ 정보 부족. 컴파일러가 막아주는 것.
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) 같은 패턴.
@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);
}
}
왜 인스턴스 메서드?
repository, calculator 사용메모리 위치:
process() 바이트코드는 Non-Static ZoneShipmentService Bean(객체)은 Heappublic 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() 형태로 호출 → 의도가 명확메모리 위치:
@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?
ShipmentConfig Bean을 만들고, 그 인스턴스의 메서드를 호출해서 다른 Bean 생성→ Spring이 어떻게 static과 instance를 골라 쓰는지 이해 = Bean 라이프사이클 이해.
@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();
조합:
Shipment.create)s.calculate)→ Effective Java의 권장 패턴.
→ 메모리 측면에서: create()는 Static Zone, calculate()는 Non-Static Zone.
| 상황 | static? | 이유 |
|---|---|---|
| 외부 상태 없음, 순수 계산 | static | Math.max 패턴 |
| Spring 의존성(Repository 등) 사용 | instance | DI 받아야 함 |
| 객체 생성 팩토리 | static | 자기 자신 인스턴스화 전에 호출 |
| Entity의 비즈니스 로직 | instance | 객체 상태 변경 |
| 상수 조회 | static | 값만 반환 |
| 라이프사이클 콜백 (@PostConstruct) | instance | Bean이 만들어진 후 |
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++// ❌
public class FreightUtils {
public static BigDecimal calc(...) { ... }
}
FreightUtils u = new FreightUtils(); // 의미 없는 객체
// ✅
public final class FreightUtils {
private FreightUtils() {
throw new AssertionError();
}
// ...
}
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);
}
}
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 어노테이션 붙이면 컴파일 에러.
public class App {
public static void main(String[] args) {
process(); // ❌ 컴파일 에러
}
public void process() { ... } // 인스턴스 메서드
}
해결: 진짜 의도 확인.
process()를 static으로new App().process()this. 누락으로 가리기public class Order {
private int count;
public void increment(int count) { // 매개변수 이름 같음
count = count + 1; // ❌ 매개변수만 증가
// this.count = this.count + 1; // ✓ 의도
}
}
→ Unit 1.1 흔한 실수와 같은 패턴.
public class ShipmentManager {
private static ShipmentManager instance; // ❌ 안전하지 않은 싱글톤
public static ShipmentManager getInstance() {
if (instance == null) {
instance = new ShipmentManager(); // 멀티스레드 race condition
}
return instance;
}
}
문제:
해결:
// 1. Spring Bean 사용 (가장 권장)
@Component
public class ShipmentManager { ... }
// 2. enum 싱글톤 (Effective Java 권장)
public enum ShipmentManager {
INSTANCE;
public void doWork() { ... }
}
@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으로 빼는 게 정답.
| 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 인스턴스보다 먼저 만들어져야 함 |
this 매개변수의 정체를 안다main()이 왜 static인가1. Method Area는 3개 존으로 분리된다
2. static과 instance의 비대칭 접근
this 없어서)main()이 static인 이유: JVM이 객체를 못 만들기 때문3. 메서드는 클래스 자산, 객체는 데이터만 가진다
this 매개변수 자동 전달이번 Unit에서 메서드 바이트코드가 어디에 저장되는지 봤다면, 다음은 그 메서드가 호출될 때 Stack에서 무슨 일이 일어나는가.
StackOverflowError이번 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의 핵심.