의존성 주입 전체 조감도 — 왜 필요한가 · JVM 메모리 어디에 들어가나 · 전체 흐름 한눈에
전체 그림을 보고 → 파고들고 → 다시 전체 그림으로 나오는 구조로 반복합니다. 이 파일(07a)이 항상 돌아오는 기준점입니다.
DI를 이해하려면 DI가 없었을 때 어떤 문제가 생겼는지부터 봐야 합니다. 해결책을 알기 전에 문제를 먼저 느껴야 이해가 됩니다.
public class OrderService { // 직접 new로 만듦 PaymentService ps = new PaymentService(); public void order() { ps.pay(); } }
@Service public class OrderService { private final PaymentService ps; // 외부에서 받음 (스프링이 넣어줌) public OrderService( PaymentService ps) { this.ps = ps; } }
필요한 객체를 직접 만들지(new) 않고, 외부(스프링)에서 만들어서 넣어주는 것.
"내가 만들지 않겠다. 누군가 만들어서 나한테 줘라." — 이게 DI의 핵심입니다.
고기집 비유: 주문 들어올 때마다 사장이 직접 고기 잘라서 준비하던 걸 →
알바(스프링)가 미리 그램수대로 진열대에 세팅해두는 것.
DI가 일어나는 전체 과정을 7단계로 나눕니다. 각 단계에서 어떤 코드가 실행되고 JVM 메모리가 어떻게 변하는지 함께 봅니다.
| # | 단계 | 무슨 일 | JVM 메모리 | 코드 |
|---|---|---|---|---|
| ① | 파일 읽기 (컴파일) | .java → .class 변환. 클래스 구조 파악. | 아직 아무것도 없음 | javac 컴파일러 |
| ② | JVM 시작 + 클래스 로딩 | .class 파일을 JVM이 읽어서 Method Area에 적재 | Method Area — 클래스 정보, 메서드 바이트코드 | ClassLoader |
| ③ | Spring Container 시작 | @Service/@Component 스캔 → 관리 대상 목록 작성 | Heap — ApplicationContext 생성 | ComponentScan |
| ④ | Bean 생성 (DI 준비) | 의존성 순서대로 객체 생성. PaymentService 먼저, OrderService 나중. | Heap — PaymentService 객체, OrderService 객체 | BeanFactory |
| ⑤ | DI 실행 | OrderService 생성자 호출 시 PaymentService 주입 | Heap — OrderService.paymentService 필드에 참조 연결 | 생성자 주입 |
| ⑥ | 메서드 호출 | order() 호출 | Stack — order() 프레임 push | 개발자 코드 |
| ⑦ | 실행 완료 | pay() 실행 → "결제 완료!" 출력 → Stack 정리 | Stack — 프레임 pop, 지역변수 소멸 | — |
① ~ ② 단계: 클래스를 읽는 것. 정의만 파악. 아직 아무것도 실행 안 됨.
@Service를 읽는 것도 "아 이 클래스가 있구나" 파악하는 단계.
③ ~ ⑤ 단계: Spring Container가 실제로 객체를 Heap에 만들고 DI 처리.
이때 생성자가 처음으로 실행됩니다.
⑥ ~ ⑦ 단계: 비로소 우리가 원하는 메서드가 실행. Stack에 프레임 쌓임.
━━━━━━━━━━━ Heap ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [ PaymentService Bean ] ← 주소: 0x1A2B pay() 메서드 참조 → Method Area의 pay() 코드 [ OrderService Bean ] ← 주소: 0x3C4D paymentService 필드 → 0x1A2B ← Heap의 PaymentService 가리킴 order() 메서드 참조 → Method Area의 order() 코드 [ ApplicationContext ] beans 맵: { "paymentService" → 0x1A2B, ← PaymentService Bean 참조 "orderService" → 0x3C4D ← OrderService Bean 참조 } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ order() 호출 시 Stack: [ order() 프레임 ] 암묵적으로: this → 0x3C4D (OrderService Bean) this.paymentService → 0x1A2B → pay() 실행
변수가 직접 객체를 품는 게 아닙니다.
변수는 Heap에 있는 객체의 주소(참조값)만 갖고 있습니다.
private final PaymentService paymentService; 이 필드는
PaymentService 객체 자체를 담는 게 아니라 → Heap 어딘가에 있는 PaymentService 객체의 주소를 담습니다.
그래서 OrderService Bean이 하나이고, PaymentService Bean이 하나여도
수천 번 order()를 호출해도 항상 같은 PaymentService 객체를 사용합니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 1: 파일 읽기 (정의 파악, 실행 아님) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // [읽기 ①] @Service 읽음 → 스프링 관리 대상으로 등록 예약 @Service // [읽기 ②] 클래스 정의 읽음 → Method Area에 PaymentService.class 적재 public class PaymentService { // [읽기 ③] 메서드 정의 읽음 → 바이트코드 Method Area에 저장 // 아직 실행 안 됨 public void pay() { System.out.println("결제 완료!"); } } // [읽기 ④] @Service 읽음 → 스프링 관리 대상으로 등록 예약 @Service // [읽기 ⑤] 클래스 정의 읽음 → Method Area에 OrderService.class 적재 public class OrderService { // [읽기 ⑥] 필드 선언 읽음 → "PaymentService 타입 진열대 있구나" 파악 // 아직 값 없음, Heap에 아무것도 없음 private final PaymentService paymentService; // [읽기 ⑦] 생성자 정의 읽음 → "PaymentService 없이 못 만들겠구나" 파악 public OrderService(PaymentService paymentService) { this.paymentService = paymentService; } // [읽기 ⑧] 메서드 정의 읽음 → 바이트코드 저장. 아직 실행 안 됨 public void order() { paymentService.pay(); } } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 2: Spring Container 가동 (자동 처리) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // [스프링 ⑨] PaymentService에 의존하는 것 없음 → 먼저 생성 // 내부: new PaymentService() → Heap에 객체 생성 // 주소: 0x1A2B에 저장됨 // 기본 생성자(자동) 실행: public PaymentService() { } ← 컴파일러가 생성 // [스프링 ⑩] OrderService 생성 시도 → 생성자에 PaymentService 필요 // ⑨에서 만든 PaymentService(0x1A2B) 꺼내서 생성자에 주입 // 내부: new OrderService(paymentServiceBean) // → 생성자 실행: this.paymentService = 0x1A2B 저장 // → final 필드 확정. 이후 절대 변경 불가 // → Heap에 OrderService 객체 생성 완료 // [스프링 ⑪] ApplicationContext에 두 Bean 등록 // { "paymentService": 0x1A2B, "orderService": 0x3C4D } // → DI 완료 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 3: 실제 실행 (메서드 호출 시) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // [실행 ⑫] orderService.order() 호출 // Stack: order() 프레임 push // this = OrderService Bean(0x3C4D) // [실행 ⑬] this.paymentService.pay() 실행 // this.paymentService = 0x1A2B // → Heap의 PaymentService Bean 접근 // Stack: pay() 프레임 push // [실행 ⑭] "결제 완료!" 출력 // pay() 프레임 pop → order() 프레임 pop // Stack 정리 완료