실행흐름 완결📚 전체 맵DI 클래스·생성자
07a · DI 완전 이해 · 시리즈 1/5

DI
OVERVIEW

의존성 주입 전체 조감도 — 왜 필요한가 · JVM 메모리 어디에 들어가나 · 전체 흐름 한눈에

Series Plan
07 시리즈 전체 계획

전체 그림을 보고 → 파고들고 → 다시 전체 그림으로 나오는 구조로 반복합니다. 이 파일(07a)이 항상 돌아오는 기준점입니다.

07a · 현재 NOW
전체 조감도
  • DI가 왜 필요한가 — 문제부터
  • JVM 메모리 3영역과 DI 관계
  • 전체 실행 흐름 한눈에
  • 고기집 비유 전체 연결
  • 07b~07e 안내
07b
클래스·필드·생성자·final
  • 클래스 = 설계도, 왜 실행 안 되나
  • 필드 선언 시 메모리 상태
  • 생성자 실행 순서 + Stack 흐름
  • final이 메모리에서 의미하는 것
  • this가 가리키는 것 정확히
07c
Spring Container — Bean 생성 전체
  • @Service 스캔 → Bean 등록 순서
  • Heap에 Bean 올라가는 정확한 시점
  • 의존성 그래프 — 누가 먼저 만들어지나
  • Singleton 보장 원리
  • ApplicationContext 구조
07d
DI 3가지 방식 비교
  • 생성자 주입 — 실행 순서 + 메모리
  • 필드 주입 — @Autowired 동작 원리
  • Setter 주입 — 언제 쓰나
  • @RequiredArgsConstructor 원리
  • 순환참조 문제와 해결
07e
1차~3차 실제 코드에서 DI
  • 1차: UserService, BoardController DI
  • 2차: JwtUtil 주입 흐름
  • 3차: SecurityConfig DI 구조
  • 심화: AOP, @Transactional과 DI 관계
  • 전체 Bean 의존성 그래프
Why DI
DI가 왜 필요한가 — 문제부터 시작

DI를 이해하려면 DI가 없었을 때 어떤 문제가 생겼는지부터 봐야 합니다. 해결책을 알기 전에 문제를 먼저 느껴야 이해가 됩니다.

❌ DI 없을 때 — 문제
강한 결합 (Tight Coupling)
public class OrderService {
    // 직접 new로 만듦
    PaymentService ps =
        new PaymentService();

    public void order() {
        ps.pay();
    }
}
문제 1 — 결합도 높음
OrderService가 PaymentService를 직접 생성. 결제 방식 바꾸려면 OrderService 코드도 수정해야 함.

문제 2 — 테스트 불가
가짜(Mock) 결제 서비스로 교체하고 싶어도 내부에서 new로 만들어서 바꿀 방법이 없음.

문제 3 — 중복 생성
OrderService 100개 만들면 PaymentService도 100개 생성. 낭비.
✅ DI 있을 때 — 해결
느슨한 결합 (Loose Coupling)
@Service
public class OrderService {
    private final PaymentService ps;

    // 외부에서 받음 (스프링이 넣어줌)
    public OrderService(
            PaymentService ps) {
        this.ps = ps;
    }
}
해결 1 — 결합도 낮음
OrderService는 PaymentService를 직접 만들지 않음. 스프링이 넣어줌.

해결 2 — 테스트 가능
테스트 시 가짜 결제 서비스(MockPaymentService)를 생성자에 넣어서 테스트 가능.

해결 3 — Singleton 보장
스프링이 PaymentService를 딱 하나만 만들어서 여러 곳에 공유. 낭비 없음.
📌 DI 한 줄 정의

필요한 객체를 직접 만들지(new) 않고, 외부(스프링)에서 만들어서 넣어주는 것.
"내가 만들지 않겠다. 누군가 만들어서 나한테 줘라." — 이게 DI의 핵심입니다.

고기집 비유: 주문 들어올 때마다 사장이 직접 고기 잘라서 준비하던 걸 → 알바(스프링)가 미리 그램수대로 진열대에 세팅해두는 것.

Big Picture
전체 실행 흐름 — 파일 읽기부터 메서드 실행까지

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, 지역변수 소멸
📌 핵심 구분 — "정의 파악" vs "실행"

① ~ ② 단계: 클래스를 읽는 것. 정의만 파악. 아직 아무것도 실행 안 됨.
@Service를 읽는 것도 "아 이 클래스가 있구나" 파악하는 단계.

③ ~ ⑤ 단계: Spring Container가 실제로 객체를 Heap에 만들고 DI 처리.
이때 생성자가 처음으로 실행됩니다.

⑥ ~ ⑦ 단계: 비로소 우리가 원하는 메서드가 실행. Stack에 프레임 쌓임.

JVM Memory + DI
JVM 메모리 3영역 — DI에서 각각 무슨 역할
① Method Area
클래스 설계도 — OrderService.class
메서드 바이트코드 — order(), pay() 코드
static 변수 — static으로 선언된 것들
@Service 정보 — 어노테이션 메타데이터
JVM 시작 시 클래스 로딩 → 여기 저장
모든 스레드 공유. GC 대상 아님.
DI에서 역할: 스프링이 여기서 클래스 구조 읽어서 Bean 만들 방법 파악
② Heap ← DI의 무대
PaymentService Bean — 딱 하나 (Singleton)
OrderService Bean — 딱 하나 (Singleton)
OrderService.paymentService 필드 — Heap의 PaymentService를 가리키는 참조
ApplicationContext — 모든 Bean 관리하는 컨테이너
DI가 일어나는 곳.
Bean 객체들이 여기 올라가고, 필드가 연결됨.
서버 살아있는 한 GC 안 됨 (참조가 있으므로)
③ Stack
order() 프레임 — order() 호출 시 생성
지역 변수 — 메서드 안 변수들
Heap 참조값 — Bean 객체 주소
메서드 실행 시마다 프레임 push.
메서드 완료 시 프레임 pop → 지역변수 소멸.
DI에서 역할: 메서드 실행 중 Heap의 Bean에 접근할 때 참조값 저장
DI 후 Heap 상태 — 참조 연결 구조
DI 완료 후 Heap 상태 (의사코드)
━━━━━━━━━━━ 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() 실행
📌 참조(Reference)란 — 이게 이해되면 메모리가 보임

변수가 직접 객체를 품는 게 아닙니다.
변수는 Heap에 있는 객체의 주소(참조값)만 갖고 있습니다.

private final PaymentService paymentService; 이 필드는
PaymentService 객체 자체를 담는 게 아니라 → Heap 어딘가에 있는 PaymentService 객체의 주소를 담습니다.

그래서 OrderService Bean이 하나이고, PaymentService Bean이 하나여도
수천 번 order()를 호출해도 항상 같은 PaymentService 객체를 사용합니다.

Analogy
고기집 비유 — 전체 연결
클래스
Class
간판 + 메뉴판. 정의만. 실제 장사 아님.
필드
Field
진열대 자리. 아직 고기 없음. 자리만 있음.
생성자
Constructor
장사 전 준비. 진열대에 고기 올려놓는 1회 작업.
final
final
진열대 잠금장치. 한번 올린 고기 절대 못 바꿈.
@Service
@Service
알바한테 관리 맡기는 표시. 없으면 알바가 모름.
Spring Container
IoC Container
알바/로봇. 표시 보고 고기 미리 세팅해두는 주체.
DI
Dependency Injection
알바가 장사 전에 진열대에 고기 올려두는 행위. 사장(개발자) 안 해도 됨.
Singleton
Singleton
고기 하나를 여러 테이블이 공유. 주문마다 새 고기 안 만듦.
메서드 호출
Method Call
손님이 주문. 이때서야 진열대 고기가 실제로 쓰임.
Full Code Flow
파일 읽기 → 스프링 → 실행 — 전체 코드 주석 완전판
PaymentService.java + OrderService.java — 전체 실행 순서 주석
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 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 정리 완료
실행흐름 완결📚 전체 맵DI 클래스·생성자