DI IoC·Container📚 전체 맵DI 실제코드
07d · DI 완전 이해 · 시리즈 4/5

DI
3 WAYS

생성자 주입 · 필드 주입 · Setter 주입 — 각각 JVM 메모리에서 무슨 일이 일어나는가 · @RequiredArgsConstructor 원리 · 순환참조 문제와 해결

Big Picture First
지금 어디 있나 — 전체 흐름 속 위치
07a 전체 조감도
07b 클래스·필드·생성자·final
07c IoC + Container
07d DI 3가지 방식 ← 지금 여기
07e 실제 프로젝트 코드
📌 07d에서 배울 것

07c에서 스프링이 Bean을 어떻게 만드는지 봤습니다.
이제 "만든 Bean을 어떻게 넣어주는가" — 즉 주입 방식 3가지를 비교합니다.
각 방식이 JVM 메모리에서 어떻게 다르게 동작하는지, 왜 생성자 주입이 권장되는지, Lombok @RequiredArgsConstructor가 뭘 하는지, 순환참조가 왜 생기고 어떻게 막는지 전부 다룹니다.

Overview
DI 3가지 방식 — 한눈에 비교
방식 ①
생성자 주입
✅ 권장 — 실무 표준
장점
  • final 사용 가능 → 불변성
  • 객체 생성 시 즉시 주입 → 누락 불가
  • 컴파일 에러로 누락 감지
  • 테스트 시 Mock 주입 명확
  • 순환참조 조기 감지
단점
  • 의존성 많으면 생성자 길어짐
    (→ Lombok으로 해결)
방식 ②
필드 주입
⚠️ 비권장
장점
  • 코드가 가장 짧고 편함
단점
  • final 못 씀 → 나중에 바뀔 수 있음
  • 테스트 시 주입 방법이 없음
  • 스프링 없으면 동작 불가
  • 순환참조 발견 늦음 (런타임)
  • 누락돼도 컴파일 에러 없음
방식 ③
Setter 주입
제한적 사용
장점
  • 선택적 의존성에 유용
  • 나중에 변경 가능 (장점이자 단점)
단점
  • final 못 씀
  • 주입 안 해도 객체 생성됨 → 누락 위험
  • NullPointerException 위험
Method 01
생성자 주입 — 실행 순서 + JVM 메모리 완전 추적
생성자 주입 — 단계별 메모리 상태 주석
@Service
public class OrderService {

    private final PaymentService paymentService;
    // [읽기 단계] "paymentService 필드가 있고, final이다" 파악
    // [이 시점 Heap] 아직 OrderService 객체 없음, 필드도 없음

    public OrderService(PaymentService paymentService) {
    // ↑ [읽기 단계] "PaymentService 없이는 만들 수 없다"는 생성자 정보 파악
    //   스프링: "OrderService 만들려면 PaymentService Bean 먼저 있어야 함"
    //
    // [스프링 Bean 생성 단계 — 실제 실행]
    // Step 1: PaymentService Bean 이미 Heap에 있음 (0x1A2B)
    // Step 2: new OrderService(paymentServiceBean) 호출
    // Step 3: Stack에 생성자 프레임 push
    //         매개변수 paymentService = 0x1A2B (Stack 지역변수)
    // Step 4: Heap에 OrderService 객체 공간 확보 (0x3C4D)
    //         → paymentService 필드 공간 포함 (아직 null)

        this.paymentService = paymentService;
        // Step 5: this(0x3C4D)의 paymentService 필드 = Stack의 0x1A2B
        //         → Heap 0x3C4D 안의 필드가 Heap 0x1A2B를 가리킴
        //         → DI 완료!
        //         → final이라 이 순간 이후 절대 변경 불가 (컴파일러 보장)

    // Step 6: 생성자 프레임 pop → Stack 지역변수(0x1A2B 참조) 소멸
    //         단, Heap의 OrderService.paymentService 필드는 0x1A2B 그대로 유지
    }

    public void order() {
        // [요청 처리 시 — 매 호출마다]
        // Step 7: Stack에 order() 프레임 push
        paymentService.pay();
        // Step 8: this(현재 OrderService Bean 0x3C4D).paymentService = 0x1A2B
        //         → Heap의 PaymentService Bean.pay() 실행
        //         → Stack에 pay() 프레임 push → 실행 → pop
        // Step 9: order() 프레임 pop
    }
}

// ═══ 최종 Heap 상태 ═══════════════════════════════════════
// 0x1A2B: PaymentService Bean (서버 살아있는 한 유지)
// 0x3C4D: OrderService Bean
//         ↳ paymentService 필드 = 0x1A2B (영구 연결, 변경 불가)
// ══════════════════════════════════════════════════════════
📌 생성자 주입이 "안전"한 이유 — 메모리 관점

① Heap 연결이 생성 시점에 완성: 객체가 Heap에 생기는 순간 모든 의존성이 연결됨. "반쪽짜리 객체"가 없음.

② final + 컴파일러 강제: 생성자에서 초기화 안 하면 컴파일 에러. 런타임 전에 발견.

③ 테스트 시 Heap 직접 제어: new OrderService(mockPaymentService)로 가짜 객체를 직접 Heap에 넣어서 테스트 가능. 스프링 없어도 됨.

Method 02
필드 주입 — 편하지만 왜 위험한가
필드 주입 — 동작 원리와 문제점
@Service
public class OrderService {

    @Autowired
    // ↑ 스프링한테 "이 필드에 맞는 Bean 알아서 넣어줘" 표시
    private PaymentService paymentService;
    // ↑ final 없음 → 나중에 다른 값으로 바뀔 수 있음

    // 생성자가 없음 → 컴파일러가 기본 생성자 자동 추가:
    // public OrderService() { } ← 매개변수 없음

    // 스프링 실행 순서:
    // Step 1: new OrderService() → Heap에 객체 생성
    //         ↑ 이 시점에 paymentService = null (아직 아무것도 없음)
    //         ↑ "반쪽짜리 객체" 상태!
    // Step 2: 생성 완료 후, 스프링이 리플렉션으로 필드에 강제 주입
    //         Reflection: private 필드도 강제로 접근 가능한 Java 기능
    //         orderServiceBean.paymentService = paymentServiceBean; (강제 할당)
    //         이제서야 paymentService = 0x1A2B

    public void order() {
        paymentService.pay();
        // 만약 Step 2가 일어나기 전에 order()가 호출되면?
        // paymentService = null → NullPointerException!
        // (스프링 정상 환경에서는 거의 안 일어나지만 테스트 환경에서는 발생)
    }
}

// ─── 문제 1: 테스트 ───
// 스프링 없이 순수 Java 테스트:
OrderService os = new OrderService();
// paymentService = null!
// @Autowired는 스프링이 처리. 스프링 없으면 null 그대로.
// os.order() → NullPointerException 발생

// ─── 문제 2: final 불가 ───
// @Autowired private final PaymentService paymentService;
// → 컴파일 에러! final은 선언 시 또는 생성자에서만 초기화 가능
// → @Autowired는 생성자 이후에 주입하므로 final 불가
📌 필드 주입이 위험한 이유 — 메모리 관점

반쪽짜리 객체 문제: 생성자 없이 new OrderService()로 Heap에 객체 생성 → paymentService = null인 상태로 존재 → 그 후 스프링이 리플렉션으로 주입.
즉, 아주 잠깐이지만 null인 채로 Heap에 객체가 존재하는 순간이 있음.

테스트 불가: 리플렉션 주입은 스프링이 처리. 스프링 없는 단위 테스트에서 paymentService = null → NPE.

리플렉션 오버헤드: private 필드를 강제로 접근하는 리플렉션은 일반 메서드 호출보다 느림.

Method 03
Setter 주입 — 언제, 왜 쓰나
Setter 주입 — 동작 원리
@Service
public class NotificationService {

    private EmailService emailService; // final 없음

    @Autowired
    @Required // 필수 주입 표시 (선택)
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
        // ↑ Setter 메서드. 스프링이 이 메서드를 호출해서 주입.
        //
        // 스프링 실행 순서:
        // Step 1: new NotificationService() → Heap 객체 생성 (emailService = null)
        // Step 2: 스프링이 setEmailService(emailServiceBean) 호출
        //         → Stack에 setEmailService() 프레임 push
        //         → this.emailService = emailServiceBean 실행
        //         → 프레임 pop
        //
        // ⚠️ 문제: setEmailService()를 호출하지 않으면
        //    emailService = null → 나중에 사용 시 NPE
    }

    public void sendNotification() {
        // emailService가 null이면 여기서 NPE
        emailService.send("알림!");
    }
}

// Setter 주입이 유용한 경우:
// - 선택적 의존성: emailService가 없어도 동작해야 할 때
// public void sendNotification() {
//     if (emailService != null) emailService.send("알림!");
// }
// - 순환참조 해결이 필요한 레거시 코드 (거의 없음)
Comparison
3가지 방식 — 메모리 관점 완전 비교표
관점생성자 주입 ✅필드 주입 ⚠️Setter 주입 △
객체 생성 시 Heap 상태 완성 상태
생성자 실행 중 즉시 연결
반쪽 상태
먼저 생성 후 주입
반쪽 상태
먼저 생성 후 주입
final 사용 ✅ 가능
생성자에서 초기화
❌ 불가
생성 후 주입이라 불가
❌ 불가
생성 후 주입이라 불가
주입 시점 객체 생성 중 (동시에) 객체 생성 완료 후 (리플렉션) 객체 생성 완료 후 (메서드 호출)
누락 감지 컴파일 에러 런타임 NPE 런타임 NPE
테스트 new 생성자로 직접 주입 스프링 없으면 null Setter 호출로 주입 가능
순환참조 감지 시점 기동 시 즉시 감지 런타임 실행 시 감지 런타임 실행 시 감지
Lombok
@RequiredArgsConstructor — 실무에서 실제로 쓰는 방식

생성자 주입이 권장되지만 의존성이 많으면 생성자 코드가 길어집니다. 실무에서는 Lombok의 @RequiredArgsConstructor로 이를 해결합니다.

@RequiredArgsConstructor — 전/후 비교 + 원리
━━━━━━━━ Before (Lombok 없이 — 직접 생성자 작성) ━━━━━━━━

@Service
public class BoardService {

    private final BoardRepository boardRepository;
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;

    // 의존성 3개 → 생성자 직접 써야 함
    public BoardService(
            BoardRepository boardRepository,
            UserRepository userRepository,
            JwtUtil jwtUtil) {
        this.boardRepository = boardRepository;
        this.userRepository = userRepository;
        this.jwtUtil = jwtUtil;
    }
}

━━━━━━━━ After (Lombok 사용 — 실무 표준) ━━━━━━━━━━━━━

@Service
@RequiredArgsConstructor
// ↑ Lombok: final 필드를 모두 받는 생성자를 컴파일 시 자동 생성
//   → 위의 생성자 코드를 자동으로 만들어줌
//   → @Autowired 없어도 됨 (생성자가 하나뿐이면 스프링이 자동 인식)
public class BoardService {

    private final BoardRepository boardRepository;
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;
    // 이게 전부. 생성자 코드 없음.
}

━━━━━━━━ @RequiredArgsConstructor 동작 원리 ━━━━━━━━━━

// 컴파일 시 Lombok이 자동으로 아래 코드 생성 (보이지 않지만 .class에 포함)
public BoardService(
        BoardRepository boardRepository,
        UserRepository userRepository,
        JwtUtil jwtUtil) {
    this.boardRepository = boardRepository;
    this.userRepository = userRepository;
    this.jwtUtil = jwtUtil;
}

// 규칙:
// - final 필드 = 반드시 포함 (Required)
// - final 아닌 필드 = 제외
// - @NonNull 붙은 필드 = 포함 + null 체크 코드 추가
//
// JVM 메모리 관점: 완전히 동일
// 생성자 주입을 직접 쓴 것과 동일하게 동작
// Heap 연결, final 보장, Stack 프레임 생성/pop 모두 동일
📌 우리 프로젝트에서 쓴 패턴

1차~3차 프로젝트에서 @Service @RequiredArgsConstructor가 붙은 Service 클래스들은 전부 이 패턴입니다.
생성자 코드가 보이지 않아도 컴파일 시 자동 생성된 생성자가 있고, 스프링이 그 생성자로 DI를 처리합니다.

@Controller @RequiredArgsConstructor도 동일. 자세한 건 07e에서 실제 코드로 확인합니다.

Circular Dependency
순환참조 — 문제와 해결

A가 B를 필요로 하고, B가 A를 필요로 하는 상황. DI에서 가장 골치 아픈 문제입니다. 왜 생기고, 생성자 주입이 어떻게 이를 조기에 발견하는지 봅니다.

1
문제 발생 상황
ServiceA ↔ ServiceB — 서로가 서로를 의존
@Service
public class ServiceA {
    private final ServiceB serviceB; // ← ServiceB 필요
    public ServiceA(ServiceB b) { this.serviceB = b; }
}

@Service
public class ServiceB {
    private final ServiceA serviceA; // ← ServiceA 필요
    public ServiceB(ServiceA a) { this.serviceA = a; }
}

// 스프링 Container 입장:
// ServiceA 만들려면 → ServiceB 필요
// ServiceB 만들려면 → ServiceA 필요
// ServiceA 만들려면 → ServiceB 필요
// → 무한 루프! 누구도 먼저 만들 수 없음
생성자 주입 시 → 기동 시 즉시 에러 (BeanCurrentlyInCreationException)
2
해결 방법 1 — 설계 변경 (가장 권장)
순환 의존성이 생긴 건 설계 문제 → 분리
ServiceA와 ServiceB가 서로를 필요로 한다면 → 공통 로직을 ServiceC로 분리 →
ServiceA → ServiceC, ServiceB → ServiceC (단방향 의존성으로 해결).

순환참조는 "이 두 클래스가 너무 강하게 묶여있다"는 신호. 설계를 다시 봐야 함.
3
해결 방법 2 — @Lazy (차선)
지연 주입 — "필요할 때만 만들어줘"
@Service
public class ServiceA {
    private final ServiceB serviceB;

    public ServiceA(@Lazy ServiceB b) {
        // @Lazy: "ServiceB 지금 당장 만들지 말고
        //         처음 사용할 때 만들어줘"
        // → 순환 고리를 끊음
        // → ServiceA 먼저 완성 → 나중에 ServiceB 주입
        this.serviceB = b;
    }
}
설계 변경이 어려울 때 임시 해결책. 근본 해결책은 아닙니다.
4
생성자 주입 vs 필드 주입 — 순환참조 감지 시점 차이
생성자 주입: 기동 시 발견 vs 필드 주입: 실행 중 발견
생성자 주입: 스프링 기동 시 Bean 생성 단계에서 순환 감지 → 서버 시작 실패 + 에러 메시지 →
코드를 배포하기 전에 발견 가능 (조기 감지, 훨씬 안전).

필드 주입: Bean은 기동 시 생성 성공 → 실제 메서드가 실행되면서 NullPointerException →
운영 중에 발생할 수 있음 (늦은 감지, 위험).
생성자 주입 → 기동 시 Heap에 올라가다 실패 (즉시 감지) 필드 주입 → Heap에 null로 올라가서 나중에 NPE (늦은 감지)
📌 생성자 주입이 "조기 감지"가 되는 메모리 이유

생성자 주입에서 OrderService를 Heap에 만들려면 생성자를 실행해야 함 →
생성자 실행 전에 PaymentService Bean이 Heap에 있어야 함 →
PaymentService Heap에 없으면 → 생성자 실행 불가 → 기동 실패.

즉, Heap에 올리는 과정 자체가 의존성 검증이 됩니다.
순환참조도 이 과정에서 "닭이 먼저냐 달걀이 먼저냐" 문제로 즉시 감지됩니다.

Back to Big Picture
전체 흐름 재확인 — 07d 위치 정리
Bean 생성 순서 결정 (07c)
생성자 주입: 생성과 동시에 Heap 연결 완성
final 확정 → 불변
@RequiredArgsConstructor → 자동 생성자
순환참조 → 기동 시 즉시 발견
📌 07d 핵심 요약
  • 생성자 주입: 객체 생성 중 Heap 연결 완성. final 보장. 컴파일 누락 감지. 테스트 용이. → 권장.
  • 필드 주입: 생성 후 리플렉션 강제 주입. 반쪽 객체 순간 존재. final 불가. 테스트 불가. → 비권장.
  • Setter 주입: 생성 후 Setter 메서드 호출. 선택적 의존성에만. → 제한적.
  • @RequiredArgsConstructor: Lombok. final 필드 생성자 자동 생성. JVM 동작은 생성자 주입과 동일.
  • 순환참조: 생성자 주입 → 기동 시 즉시 감지. 근본 해결은 설계 분리.
DI IoC·Container📚 전체 맵DI 실제코드