자바 백엔드 입문 15편 — 의존성 주입이 왜 필요한가

2026-05-16자바 백엔드 입문

자바 백엔드 입문 15편. Phase 2 IoC 컨테이너 시작. 객체가 의존성을 직접 만들 때 생기는 결합 문제를 풀고, IoC와 DI의 발상을 결혼식 사회자 비유로 풀어쓴 학습 노트.

📚 자바 백엔드 입문 · 15편 — 의존성 주입이 왜 필요한가

이 글은 자바 백엔드 입문 시리즈 59편 중 15편이에요. 14편에서 첫 Bean을 만들어 "Hello, Spring" 응답을 봤다면, 이번 15편부터 Phase 2 IoC 컨테이너 7편 의 시작입니다. Spring의 진짜 본질인 의존성 주입(DI) 이 왜 만들어졌는지, 그 발상의 배경을 풀어 가요. 여기를 단단히 잡으면 11편 ApplicationContext, 12편 @Autowired, 13편 Java Config가 한 번에 와닿아요.

의존성 주입이 어렵게 들리는 이유

처음 "의존성 주입(Dependency Injection)" 이라는 단어를 들으면 두 가지가 한꺼번에 헷갈립니다.

첫째, 단어 자체가 추상적이에요. "의존성" 도 어렵고, "주입" 도 어디서 어디로 주입한다는 건지 안 잡혀요.

둘째, "왜 굳이 이런 걸 발명했나" 가 안 보입니다. 그냥 객체가 필요한 다른 객체를 직접 new 해서 쓰면 안 되나? 이 질문이 풀려야 의존성 주입에 마음이 붙어요.

이 글에서는 의존성 주입을 결혼식 사회자 비유로 풀어 갑니다. 끝까지 따라오시면 "왜 Spring이 결국 거대한 객체 조립 기계가 됐는지" 가 한 번에 박혀요.

객체가 의존성을 직접 만드는 코드의 문제점

먼저 "의존성 주입 없이" 짠 코드를 봐요. 결제 처리하는 OrderService 가 결제 게이트웨이를 직접 만든다고 합시다.

public class OrderService {
    private KakaoPayGateway paymentGateway;

    public OrderService() {
        this.paymentGateway = new KakaoPayGateway();   // 직접 생성
    }

    public void placeOrder() {
        paymentGateway.pay(10000);
    }
}

문제없어 보이죠? 그런데 다음 시나리오들이 다 깨져요.

1. 결제 게이트웨이를 토스로 바꾸려면 OrderService 코드를 직접 수정해야 함

KakaoPayGatewayTossPayGateway 변경하려면 OrderService.java 를 열어 한 줄 고쳐야 해요. 이런 코드가 회사 안에 50개 있다면 50개 다 수정. 만약 카카오와 토스를 동시에 지원하려면 — 코드 자체가 완전히 갈아엎혀야 해요.

2. 테스트가 거의 불가능

OrderService 단위 테스트를 짜려고 하면 "실제 카카오페이 API에 연결" 이 일어나요. 테스트할 때마다 실제 결제가 발생하는 거예요. "가짜 결제 게이트웨이" 로 갈아끼울 수가 없거든요. OrderService 코드 안에 new KakaoPayGateway() 가 박혀 있으니.

3. 결합도(Coupling)가 너무 강함

OrderService"카카오페이가 어떻게 동작하는지" 까지 알아야 해요. 카카오페이 API가 한 줄이라도 바뀌면 OrderService 도 손을 봐야 합니다. 두 클래스가 너무 가깝게 얽혀 있어요.

이 세 문제가 1990~2000년대 자바 백엔드를 괴롭혔어요. 그 답이 의존성 주입이었습니다.

의존성 주입 — 객체를 만드는 책임을 밖으로

의존성 주입(Dependency Injection) 의 핵심 발상은 한 줄이에요. 객체가 자기 의존성을 직접 만들지 말고, 외부에서 받기만 하라. 위 OrderService 를 의존성 주입 방식으로 다시 쓰면 이렇게 돼요.

public class OrderService {
    private PaymentGateway paymentGateway;   // 인터페이스 타입

    public OrderService(PaymentGateway paymentGateway) {   // 생성자로 받음
        this.paymentGateway = paymentGateway;
    }

    public void placeOrder() {
        paymentGateway.pay(10000);
    }
}

핵심 변화 세 가지:

  1. KakaoPayGateway 가 사라지고 PaymentGateway 인터페이스로 (3편 인터페이스의 활용)
  2. new 가 사라짐OrderService 가 결제 게이트웨이를 직접 생성하지 않음
  3. 생성자로 받음 — 누군가 외부에서 "이 결제 게이트웨이 쓰라" 며 넘겨줘야 함

이렇게 짜면 위 세 문제가 다 풀려요.

  • 결제 게이트웨이 바꾸기? — OrderService 코드 안 건드리고, 외부에서 다른 구현체를 넘겨주기만 하면 끝
  • 테스트? — 테스트용 가짜 PaymentGateway 구현체를 만들어 넘겨주면 됨
  • 결합도? — OrderService"PaymentGateway 인터페이스만 알면 됨" — 카카오·토스가 어떻게 동작하는지 몰라도 OK

결혼식 사회자 비유 — 인쇄소 직접 가지 마라

비유로 풀어볼게요. 결혼식 사회자가 "청첩장이 필요해" 라고 할 때 두 시나리오를 비교해봐요.

시나리오 A: 사회자가 직접 인쇄소를 찾아간다

사회자가 "OO 인쇄소" 를 직접 찾아가서 청첩장을 주문하고 받아옵니다. 다음 결혼식엔 "XX 인쇄소" 가 더 싸다고 해도, 사회자가 또 직접 다른 인쇄소를 알아봐야 해요. 사회자가 결혼식 진행 + 인쇄소 협상 + 가격 비교 + 디자인 검토를 다 해야 하니까 정말 바빠요.

시나리오 B: 결혼식 회사가 청첩장을 사회자에게 "제공" 한다

사회자는 "청첩장 한 다발 필요해" 라고 말만 하면 결혼식 회사가 적절한 인쇄소에서 받아 사회자 손에 "건네줍니다". 사회자는 "어느 인쇄소에서 왔는지" 신경 안 써요. 받은 청첩장을 그대로 쓰면 됩니다.

시나리오 B가 의존성 주입이에요. 사회자가 OrderService, 청첩장이 PaymentGateway 의존성, 결혼식 회사가 IoC 컨테이너(Spring). 사회자는 "청첩장 필요해" 라고 인터페이스만 정의하고, 실제 청첩장을 어디서 어떻게 가져오는지는 결혼식 회사가 알아서 처리해요.

IoC와 DI의 관계

여기서 두 단어가 자주 같이 등장해요.

  • IoC (Inversion of Control, 제어의 역전)"객체 생성·생명주기·연결의 책임을 우리가 안 지고 컨테이너가 진다" 는 큰 개념
  • DI (Dependency Injection) — IoC를 실제로 구현하는 가장 흔한 방식. "의존성을 컨테이너가 객체에 주입"

비유로 풀면 — IoC가 "운전의 통제권을 회사 운전기사에게 넘긴다" 는 추상적 개념이라면, DI는 "운전기사가 정확히 어떤 자동차를 골라 어떤 코스로 운전하는가" 의 구체적 구현 방식이에요. IoC는 발상, DI는 그 발상을 자바 코드로 실현하는 방법.

💡 IoC = 큰 개념, DI = 구현 방식

시험에 "IoC와 DI가 같은 말이다" 가 보기로 나오면 X. DI는 IoC를 구현하는 한 방식. IoC를 다른 방식(Service Locator 등)으로 구현할 수도 있지만, Spring은 거의 100% DI를 씁니다.

의존성 주입 3가지 방식

자바에서 의존성을 주입하는 방식은 세 가지. 12편에서 깊이 다루는데, 미리 짧게 만나둘게요.

// 1. 생성자 주입 (권장, Spring 공식)
public OrderService(PaymentGateway paymentGateway) {
    this.paymentGateway = paymentGateway;
}

// 2. 세터 주입 (옵션 의존성에 적합)
@Autowired
public void setPaymentGateway(PaymentGateway paymentGateway) {
    this.paymentGateway = paymentGateway;
}

// 3. 필드 주입 (편하지만 비권장)
@Autowired
private PaymentGateway paymentGateway;

여기서 시험 함정 자주 나와요. "어떤 주입 방식이 가장 권장되나?" — 답은 생성자 주입(Constructor Injection) 이에요. 이유 세 가지: ① 필드를 final 로 만들 수 있어 불변성 보장 ② 객체 생성 시점에 의존성 누락 발견 ③ 테스트 시 명시적으로 의존성 넘기기 쉬움. 필드 주입은 "편해 보이지만" 이 세 장점을 다 잃어요.

한 줄 정리 — 의존성 주입 = "객체가 자기 의존성을 직접 만들지 말고 외부에서 받기만 한다" 는 발상. IoC는 큰 개념, DI는 구현 방식. Spring은 거의 100% 생성자 주입을 권장.

시험 직전 한 번 더 — 의존성 주입 입문자가 매번 헷갈리는 것

  • 의존성(Dependency) = 한 객체가 동작하려면 필요한 다른 객체
  • 의존성 주입(DI) = 의존성을 직접 만들지 말고 외부에서 받기
  • 직접 new 하면 문제 3가지 = ① 교체 어려움 ② 테스트 불가 ③ 강한 결합
  • DI는 인터페이스와 함께 써야 진짜 힘이 나옴 (3편 복기)
  • IoC = Inversion of Control (제어의 역전) — 큰 개념
  • DI = Dependency Injection — IoC를 구현하는 구체적 방식
  • IoC와 DI는 같은 말이 아님 — DI는 IoC를 구현하는 한 방법
  • 의존성 주입 3가지 = 생성자 주입 / 세터 주입 / 필드 주입
  • 생성자 주입이 Spring 공식 권장
  • 생성자 주입 장점 3가지 = final 가능 / 누락 발견 빠름 / 테스트 쉬움
  • 필드 주입(@Autowired private)은 편하지만 비권장 — final 불가 + 테스트 어려움
  • 결혼식 사회자 비유 = 사회자(객체) + 청첩장(의존성) + 결혼식 회사(IoC 컨테이너)
  • Spring Framework의 진짜 본질 = 의존성 주입 + Bean 관리
  • 의존성 주입 덕분에 한 클래스의 단위 테스트 가능 — 가짜 의존성 끼워 넣기
  • "카카오페이 → 토스로 결제 게이트웨이 교체" 가 코드 한 줄 안 건드리고 가능
  • DI 없으면 한국 회사 대형 시스템은 유지보수 불가능
  • 의존성 주입은 Spring 발명이 아니라 "디자인 패턴" 이지만, Spring이 가장 잘 구현
  • Java EE의 CDI(Contexts and Dependency Injection)도 비슷한 발상
  • IoC의 "Inversion" = 제어가 "내 손에서 컨테이너로 뒤집힘"
  • 생성자 주입은 Lombok @RequiredArgsConstructor와 조합하면 한 줄로 끝

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!