F-LAB JAVA · 2주차 · Phase 2 · JVM 메서드 실행 메커니즘
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
main() 안에서 인스턴스 메서드를 직접 호출할 수 없는 메모리적 이유는?this는 어디서 와서 어디로 가는가?this의 변화는?메서드 호출의 4가지 경로는 "객체 참조(this)가 어디서 오는가" 와 "VMT를 거치는가" 의 조합으로 결정된다.
Static 메서드는 객체와 무관(Static Zone 직접), 인스턴스 메서드는 반드시 객체의 Class Pointer → VMT를 거친다.
this라는 보이지 않는 매개변수가 이 모든 차이의 핵심이다.
| 경로 | 비유 | 자원 |
|---|---|---|
| Static → Static | 공용 헬프데스크에서 다른 공용 헬프데스크로 사내 전화 | 직원증 불필요 |
| Static → Instance | 공용 헬프데스크에서 특정 직원에게 전화 | 직원 번호(객체) 알아야 함 |
| Instance → Static | 자기 자리에서 공용 헬프데스크로 전화 | 자유롭게 가능 |
| Instance → Instance | 자기 자리에서 같은 부서 동료에게 전화 | 자기 직원증(this)으로 부서 매뉴얼(VMT) 찾기 |
직원증(this)이 있으면 모든 자원에 접근 가능. 없으면(static 메서드 안) 자기 자원만.
1. 4가지 호출 경로 — 큰 그림
2. Static → Static — 가장 단순한 경로
3. Static → Instance — 객체가 있어야 시작
4. Instance → Static — 자유로운 경로
5. Instance → Instance — this 매개변수의 마법
6. 메모리 추적 종합
7. ILIC 실무 — Service · Util · Entity의 호출 패턴
8. 흔한 실수와 디버깅
9. 면접 질문 + 자기 점검
호출 대상
Static Instance
┌──────────────────────────┐
호 │Static │ ① │ ② │
출 ├────────┼─────┼──────────┤
자 │Inst. │ ③ │ ④ │
└──────────────────────────┘
① Static → Static : 가장 단순
② Static → Instance : 객체 필요 (가장 까다로움)
③ Instance → Static : 자유로움
④ Instance → Instance: this 매개변수의 메커니즘
Unit 1.3에서 본 규칙:
Static은 인스턴스를 못 부르고, 인스턴스는 둘 다 부를 수 있다.
이번 Unit에서 정확한 메커니즘을 본다:
Static 메서드 실행 중:
스택 프레임의 LVA[0] = 첫 매개변수 (this 없음)
→ "어느 객체의 데이터?" 라는 질문에 답할 수 없음
→ 인스턴스 멤버 접근 불가
Instance 메서드 실행 중:
스택 프레임의 LVA[0] = this (자동 전달됨)
→ "어느 객체?" 명확
→ 인스턴스 멤버 자유 접근
→ Static도 자유 접근 (this 무시하면 됨)
| 호출자 | 대상 | invoke 명령 | VMT? |
|---|---|---|---|
| Static | Static | invokestatic | ❌ |
| Static | Instance | invokestatic(생성자) + invokevirtual | ✓ (instance 호출) |
| Instance | Static | invokestatic | ❌ |
| Instance | Instance | invokevirtual | ✓ |
→ 호출자가 누구든 호출 대상이 결정.
→ Unit 1.5의 invoke 4종 + Unit 2.1의 2단계가 여기 적용.
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가지를 메모리 다이어그램으로 정밀 추적.
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;
}
}
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 메서드)
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: (지금은 무관)
this 없음 — LVA[0]부터 첫 매개변수public class FreightUtils {
public static BigDecimal a() { ... }
public static BigDecimal b() {
return a(); // 클래스명 생략 가능
// FreightUtils.a(); // 명시도 가능
}
}
같은 클래스 안의 static → static 호출은 클래스명 생략 가능.
→ JVM 입장에선 차이 없음. 컴파일러가 동일한 invokestatic 생성.
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);
}
}
main() 실행 중:
스택 프레임:
LVA[0] args (String[])
(this 없음 — static)
만약 calculate(100); 을 직접 호출하면:
- 호출 대상이 instance 메서드
- VMT를 거쳐 어느 객체의 메서드를 부를지 결정해야 함
- 그런데 객체 참조가 없음 → "어느 객체?" 모름
- 컴파일러가 차단
Shipment s = new Shipment("BL-001");
객체 생성의 의미 (Unit 1.5 6장 복습):
1. Heap에 메모리 할당
2. Object Header 설정 (Class Pointer 포함)
3. 생성자 호출 (Static → Instance의 특수 케이스 — <init> 메서드)
4. 객체 참조 반환 → 변수 s에 저장
→ 이제 main의 스택 프레임에 객체 참조가 있음.
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
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() 등 자유롭게 접근 가능.
자바 프로그램의 표준 구조:
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은 그 인계 과정을 자동화한 것.
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); // ✓
}
}
calculate 메서드 실행 중:
스택 프레임:
LVA[0] this = ShipmentService 객체
LVA[1] s = Shipment 객체
...
FreightUtils.applyFuel(base) 호출:
→ 대상이 Static 메서드
→ 객체 참조 필요 없음 → this 무시 가능
→ invokestatic FreightUtils.applyFuel
→ Static Zone 직접 호출
this가 있어도 사용 안 하면 그만. Static 호출은 객체와 무관.
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은 무관.
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 없이도).
@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 메서드 자유롭게 호출.
→ 가장 흔한 패턴.
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); // 또 다른 인스턴스 호출
}
}
calculateTotal();
↓
calculateBase();
JVM이 실제로 컴파일하는 것:
this.calculateTotal();
↓
this.calculateBase(); ← 호출자의 this를 그대로 전달
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가 계속 전달.
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 = ? │
└────────────────────────────┘
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는 "호출되는 객체"의 참조.
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의 본질:
this.x 로 명시적으로 사용 가능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++;
}
}
Method Area:
Demo 클래스
Static Zone:
sCount = 0
main() 바이트코드
staticHelper() 바이트코드
Non-Static Zone:
instanceWork() 바이트코드
instanceHelper() 바이트코드
<init>() 바이트코드
Heap: (비어있음)
Stack:
main() 프레임
LVA[0] args
Stack:
┌──────────────────────────┐
│ staticHelper() 프레임 │ ← 추가 (인자 없음, this 없음)
├──────────────────────────┤
│ main() 프레임 │
│ LVA[0] args │
└──────────────────────────┘
Method Area:
Demo의 Static Zone:
sCount = 2 ← 1번 증가 (main에서) + 1번 증가 (staticHelper에서)
new Demo() 직후Heap:
┌──────────────────────┐
│ Demo @0x7f4a2c01 │
│ iCount = 0 │
└──────────────────────┘
Stack:
main() 프레임
LVA[0] args
LVA[1] d = 0x7f4a2c01 ← 참조 저장
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
Stack:
┌────────────────────────────┐
│ instanceHelper() 프레임 │
│ LVA[0] this = 0x7f4a2c01 │ ← 같은 this 전달
├────────────────────────────┤
│ instanceWork() 프레임 │
│ LVA[0] this = 0x7f4a2c01 │
├────────────────────────────┤
│ main() 프레임 │
└────────────────────────────┘
Heap의 객체의 iCount = 2
Static Zone 데이터: 모든 경로에서 동일 위치 접근 (sCount는 1개)
Heap 데이터: 객체별 위치 접근 (iCount는 객체마다 별도)
this 전달: 인스턴스 메서드 간 호출에서 자동 연쇄
→ 4가지 경로 모두 결국 LVA[0]의 정체(this 있음/없음)로 차이가 결정.
@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% 패턴은 이 그래프 안에 있음.
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)@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 호출
조합의 매력:
Bean 종류와 호출:
@Controller → instance, 모든 요청에서 같은 객체 (싱글톤)
@Service → instance, 동일
@Repository → instance, 동일
호출 시:
Controller → Service → Repository
모두 instance → instance
invokevirtual 사용
하지만:
- Bean은 단 1개씩 → 객체 생성 비용 0 (앱 시작 시 한 번)
- this는 그 단일 인스턴스
- JIT가 인라인 캐시 → 다형성 비용 거의 0
- VMT 호출이 일반 함수 호출만큼 빠름
→ "Spring이 느리지 않은 이유" 의 메모리 메커니즘.
// 옵션 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>
메모리 비용:
→ Effective Java가 정적 팩토리를 권장하는 이유.
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(); // ✓ 객체 만들고 호출
}
public class App {
public static void method() {
System.out.println(this); // ❌ static에선 this 없음
}
}
→ Static 메서드의 LVA[0]은 첫 매개변수. this 슬롯 자체가 없음.
public class App {
private int count;
public static void increment() {
count++; // ❌ 어느 객체의 count?
}
}
→ static 메서드는 어느 객체에 속한지 모름 → 인스턴스 변수 접근 불가.
해결:
public static void increment(App app) {
app.count++; // ✓ 명시적 객체 전달
}
Shipment s = new Shipment();
s.staticMethod(); // ✓ 동작은 하지만
Shipment.staticMethod(); // ✓ 권장 — 의도 명확
JVM은 s.staticMethod() 도 Shipment.staticMethod() 로 컴파일.
→ 객체는 단지 클래스 정보 알리는 용도. 실제 동작은 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로.
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);
}
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();
}
| 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번 추가. 거의 무시할 수준 |
1. 4가지 호출 경로의 차이는 LVA[0]에 있다
2. this는 보이지 않는 첫 매개변수
this.x로 명시 가능, 보통 생략3. ILIC 실무 패턴
이번 Unit에서 4가지 경로를 봤다면, 다음은 가장 일반적인 케이스(Instance→Instance)를 한 줄 단위로 완전 추적.
Model2 m2 = new Model2(); int sum = m2.hap(1, 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주차의 정점