Skip to content

IoC와 DI 그 관계를 정리해보다

프레임워크를 왜 쓰는지는 알아야 밥 벌어먹고 산다.

Makonea
·2026년 3월 7일·14분

객체 지향 프로그래밍은 프로그래밍 언어를 설계하는 새로운 방식일 뿐만 아니라 프로그램을 설계하는 완전히 다른 방식이기도 합니다.
이 논문은 스몰토크(Smalltalk)로 시스템을 설계하는 것이 어떤 것인지 설명합니다. 특히, 객체 지향 프로그래밍의 주요 동기 중 하나가 소프트웨어 재사용이기 때문에,
이 논문은 클래스가 어떻게 개발되어야 재사용 가능하게 되는지를 설명합니다.

IoC란 무엇인가?

제어 반전, 프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게되는 소프트 웨어 디자인 패턴을 말한다.

Inversion of Control 이라고 부르는데, 전통적인 프로그래밍에서 흐름은 프로그래머가 작성한 프로그램이 외부 라이브러리의 코드를 호출해 이용한다.

하지만 제어 반전이 적용된 구조에서는 외부 라이브러리의 코드가 프로그래머가 작성한 코드를 호출한다.

IoC의 시작은

1988년 Ralph E. JohnsonBrian Foote가 작성한 "Designing Reusable Classes"는

OOP의 재사용성을 체계적으로 다룬 기념비적인 논문이다.

이 논문에서 두 저자는 OOP의 원칙을 다음과 같이 정의한다.

다형성 Polymorphism : 다양한 객체들이 동일한 메시지를 통해 상호작용할 수 있는 특성

프로토콜 Protocol : 객체들이 제공해야 할 서비스나 메서드 집합, 즉 인터페이스를 명확히 정의하는 것

상속 Inheritance : 기존 클래스의 특성과 행동을 새로운 클래스가 물려받아 확장하거나 수정할 수 있는 메커니즘

추상 클래스 Abstract Classes : 구체적인 구현 없이 서브클래스가 반드시 구현해야 하는 인터페이스나 기본 구조를 정의하는 클래스

이 정의는 오늘날 우리가 쓰는 OOP 용어 체계와 다소 다르다. 이는 당시 OOP가

앨런 케이(Alan Kay)의 메시지 중심 개념에서 Booch 등이 정립한 현대적 개념으로

이행하는 과도기였기 때문이다.

This inversion of control gives frameworks

the power to serve as extensible skeletons

이 논문은 현대적 프레임워크의 개념을 정립한 논문 중 하나로, IoC를 다음과 같이 설명한다. 프레임워크는 애플리케이션 코드가 직접 호출하는 라이브러리와 달리, 프레임워크 자체가 사용자가 정의한 메서드를 호출하는 방식으로 동작한다.

논문은 이를 "inversion of control"이라 명명하며, 이것이 프레임워크를 확장 가능한 골격(extensible skeleton)으로 만드는 핵심이라고 설명한다.

FrameWorks = Component + Inversion of Control

이 논문은 현대적 프레임워크의 개념을 정립한 논문 중 하나로, IoC를 프레임워크가

애플리케이션의 흐름을 제어하는 방식으로 설명한다. 기존 프로그램이 개발자가 직접

흐름을 제어하는 것과 달리, 외부(프레임워크)가 흐름을 제어하고 개발자는 필요한

부분만 구현하는 방식이다.

다만 이 논문이 IoC 개념을 최초로 창안한 것은 아니다. 그 이전에도 유사한 개념이

산발적으로 쓰이고 있었으나, Designing Reusable Classes(1988)가 이를 체계적으로

정리하고 용어를 확립한 논문으로 평가받는다.

쉽게 예시를 들어보자

전통적인 프로그래밍 방식에서는 객체가 필요한 시점에서 스스로를 생성하거나, 다른 객체를 생성하여 사용한다.

즉 프로그램의 흐름은 객체(Object) 자신이 주도하게 된다.

Java
// 전통적인 방식 (객체를 내부에서 직접 생성하는 구조)
class B {

    // B 클래스 내부에서 A를 직접 생성하고 사용한다
    public void doSomethingWithA() {
        A a = new A(); // B가 A 객체를 직접 생성함 (강한 결합)
        a.action();    // 생성된 A 객체의 메서드를 호출
    }
}

class A {

    // A 클래스의 기능 메서드
    public void action() {
        System.out.println("A의 액션"); // 콘솔에 메시지 출력
    }
}

// 테스트 코드
public class Main {

    public static void main(String[] args) {

        B b = new B();        // B 객체 생성
        b.doSomethingWithA(); // B가 내부에서 A를 생성하고 action() 실행
    }
}

반면 IoC를 적용하면 객체는 자신이 사용할 객체를 직접 생성하거나 찾지 않는다.

대신 외부 컨테이너가 필요한 객체를 생성하고, 객체간의 의존관계를 설정하여 제공한다.

이를 통해 객체는 자신의 핵심로직에만 집중 할 수 있게된다

예시는 이것을 가장 단편적으로 잘 보여주는

IoC중 가장 기초중 하나인 생성자 주입이다.

Java
// A 클래스 (의존성 제공자, Dependency Provider)
class A {

    // A 클래스의 기능 메서드
    public void action() {
        System.out.println("A의 동작");
    }
}

// B 클래스 (의존성 소비자, Dependency Consumer)
// 생성자(Constructor)를 통해 A를 주입받는 구조
class B {

    private A a; // 의존성 객체 (A에 대한 참조)

    // 생성자 주입 (Constructor Injection)
    // 외부에서 A 객체를 전달받아 내부 필드에 저장
    public B(A a) {
        this.a = a;
    }

    // 주입된 A 객체를 사용하는 메서드
    public void doSomethingWithA() {
        a.action(); // 전달받은 A 인스턴스의 메서드 호출
    }
}

// 외부 컨테이너 역할을 하는 클래스
public class Main {

    public static void main(String[] args) {

        A a = new A();   // 외부에서 A 객체 생성 (컨테이너 역할)
        B b = new B(a);  // B 객체 생성 시 A를 전달하여 의존성 주입

        b.doSomethingWithA(); // B가 주입받은 A를 사용하여 메서드 실행
    }
}

사실 이렇게 예시만 덩그러니 두면 사실 이해가 어려울 것이다.

그러니까 IoC의 핵심 개념을 소개한다.

Don't Call, We'll Call You - Sugarloaf 1974

IoC의 일반적 핵심 개념을 요약하면 다음과 같다.

The Mesa Programming Environment -Richard E. Sweet 1985

할리우드 원칙(HollyWood principle):

할리우드 원칙의 이름은 할리우드 영화 제작사의 오디션 관행에서 유래했지만, 그 문구 자체는 Sugarloaf의 1974년 곡 "Don't Call Us, We'll Call You"에서 먼저 등장한 표현이기도 하다. 1983년 메사 프로그래밍 환경에 대해서 설명한 논문에서 최초로 등장한 후, 현대적 OOP의 아버지인 4인방 중 한명인 존 블라시디스(John Vlissides)가 C++ 리포트에 쓰고, 마틴파울러가 인용해서 유명해진 원칙이다.

이 원칙은 할리우드 영화 제작사가 오디션 배우에게 전화하지말고, 연락처 주시면 우리가 전화드릴게요에서 유래된 개념이다.

쉽게 요약하면 다음과 같다.

1. 배우(호출자)가 감독(프레임워크)에게 능동적으로 연락해서 출연기회를 구걸하는 것이 아니라, 감독이 필요한 경우 필요한 배우를 직접 선택해서 '캐스팅 전화(Call Back)' 한다는 것으로. 제어 권한이 배우가 아닌 감독에게 있다는 점을 강조한 비유이다.

어쨌건 IoC는 제어의 역전을 뜻하고, 프로그램의 흐름 제어가 개발자에게서 프레임워크로 넘어가는 것을 의미하는데, 개발자는 프레임워크가 제공하는 규칙과 API를 따라 코드를 작성하고, 프레임 워크는 객체의 생명 주기와 상호작용을 관리한다.

IoC 구현 패턴을 크게 자주보이는 것을 기준으로 보면 3가지로 볼 수 있는데

아래의 패턴들은 코드 레벨 또는 객체간의 상호작용을 통해 '흐름 제어'를 역전시키는 IoC 패턴이다

콜백(Callback)

-이벤트 핸들링, GUI 프레임워크등에서 많이 볼 수 있는 그것

템플릿 메서드 패턴(Template Method Pattern)

-상위 클래스에서 전체 알고리즘 흐름(템플릿)을 정의하고, 하위 클래스가 일부 단계를 오버 라이드(Override)하는 패턴

Publisher-Subscriber 패턴

-주제Subect가 특정 이벤틀 발생시켰을때, 등록된 Subscriber에게 알림을 보내는 방식

(Spring의 IoC 컨테이너 모식도)

아래의 패턴은 객체 생성과 의존성 연결(주입)을 프레임워크나 컨테이너에게 위임해, 사용자는 “필요한 의존 관계”만 정의하면 자동으로 주입받게 하는 방식이다

IoC 컨테이너(IoC Container)

IoC를 실제로 구현하고 관리하는 역할을 하는 것이 IoC 컨테이너인데, IoC 컨테이너는 다음과 같은 역할을 한다.

1.객체의 생성 및 관리: 애플리케이션에서 필요한 객체들을 생성하고 그 생명 주기를 관리. (객체의 생성, 초기화, 소멸 등)

2.의존성 주입(Dependency Injection, DI): 객체 간의 의존 관계를 분석하고, 필요한 객체를 다른 객체에게 주입한다.

이를 통해 객체들은 서로 결합도를 낮추고 독립적으로 동작할 수 있다

3.객체의 검색 및 제공: 필요에 따라 컨테이너에 등록된 객체를 검색하여 제공

이러한 IoC의 장점은

-결합도 감소: 객체간의 의존성을 줄이고, 의존성을 줄이고, 코드 재사용성을 높임

-유연성 향상: 설정파일이나 다른 매커니즘을 통해 의존성을 쉽게 변경할 수 있음

-코드 단순화:객체 생성 및 의존성 관리 코드가 줄어들어 코드가 간결해진다.

근데 갑자기 이 글을 읽는 독자들은 갑자기 IoC 이야기하다가 의존성 주입(DI)이 라는 단어가 나와서 놀랐을 것이다.

이제 DI가 무엇인지 설명하겠다.

우선 IoC라는 것은 너무 설명이 넓고 많은 범위를 이야기하여 이해를 어렵게 하지 않는가?

그래서 우리 선배 프로그래머이자 위대한 성현 마틴 파울러는 여러사람들간의 논의 끝에 하나의 글을 작성하게 된다.

그것이 바로 프로그래밍 역사에 길이 남을 명문인

Inversion of Control Containers and the Dependency Injection pattern(2004)이다.

이 에세이에서 IoC의 다양한 형태를 정리하고, 그중에서 IoC의 한 패턴으로써 Dependency Injection(DI) 라는 개념을 정의한다.

제어의 역전이라는 용어는 너무 일반적이어서 사람들이 혼란스러워합니다.
그렇기때문에 다양한 IoC 옹호자들과 많은 논의를 거쳐 의존성 주입이라는 이름을 사용하기로 결정했습니다.

그리고 여기서 DI형태 중 특별한 형태 3가지를 예시로 들어보겠다.

일반적인 3가지 DI의 형태

Java
// ============================
// 1. 생성자 주입 (Constructor Injection)
// ============================

// A 클래스 (의존성을 제공하는 객체, Dependency Provider)
class A {
    public void action() {
        System.out.println("A의 동작"); // A 객체가 수행하는 실제 로직
    }
}

// B 클래스 (의존성을 사용하는 객체, Dependency Consumer)
// 생성자를 통해 A를 주입받는다.
class B {

    // B가 필요로 하는 의존성
    private A a;

    // 생성자를 통해 A를 외부에서 전달받는다.
    public B(A a) {
        this.a = a; // 전달받은 A 객체를 내부 필드에 저장
    }

    // A의 기능을 사용하는 메서드
    public void doSomethingWithA() {
        a.action(); // 주입된 A 객체의 메서드를 호출
    }
}

// 외부 컨테이너 (의존성 생성 및 연결 담당)
public class Main {

    public static void main(String[] args) {

        // 컨테이너가 A 객체를 생성
        A a = new A();

        // 컨테이너가 B 객체를 생성하면서 A를 주입
        B b = new B(a);

        // B의 기능 실행
        b.doSomethingWithA();
    }
}

생성자 주입(Constructor Injection): 객체 생성시 생성자를 통해 의존성 주입 -(Spring,DI 컨테이너에서 많이 사용)

Java
// ============================
// 2. Setter 주입 (Setter Injection)
// ============================

// A 클래스 (의존성 제공자)
class A {

    public void action() {
        System.out.println("A의 동작"); // 실제 동작 수행
    }
}

// B 클래스 (의존성 소비자)
// Setter 메서드를 통해 의존성을 주입받는다.
class B {

    // B가 필요로 하는 의존성
    private A a;

    // Setter 메서드를 통해 A를 외부에서 주입
    public void setA(A a) {
        this.a = a;
    }

    // A의 기능을 사용하는 메서드
    public void doSomethingWithA() {

        // 의존성이 주입되었는지 확인
        if (a != null) {

            // 주입된 A 객체 사용
            a.action();

        } else {

            // 의존성이 주입되지 않은 경우
            System.out.println("A가 주입되지 않았습니다.");
        }
    }
}

// 외부 컨테이너
public class Main {

    public static void main(String[] args) {

        // 컨테이너가 A 객체 생성
        A a = new A();

        // 컨테이너가 B 객체 생성
        B b = new B();

        // Setter를 통해 A 주입
        b.setA(a);

        // B의 기능 실행
        b.doSomethingWithA();
    }
}

세터 주입(Setter Injection): 세터 메서드를 통해 의존성을 주입 (다양한 구성 가능, 주로 XML 등이 사용)

Java
// ============================
// 3. 인터페이스 주입 (Interface Injection)
// ============================

// 의존성 주입을 위한 인터페이스
// 특정 객체가 A를 주입받을 수 있음을 명시하는 계약(Contract)
interface InjectableA {

    // A를 주입받는 메서드
    void injectA(A a);
}

// A 클래스 (의존성 제공자)
class A {

    public void action() {
        System.out.println("A의 동작"); // 실제 로직 수행
    }
}

// B 클래스 (의존성 소비자)
// InjectableA 인터페이스를 구현하여 A 주입을 받는다.
class B implements InjectableA {

    // B가 필요로 하는 의존성
    private A a;

    // 인터페이스에서 정의한 주입 메서드 구현
    @Override
    public void injectA(A a) {
        this.a = a; // 전달받은 A를 내부 필드에 저장
    }

    // A의 기능을 사용하는 메서드
    public void doSomethingWithA() {

        // 의존성이 주입되었는지 확인
        if (a != null) {

            // 주입된 A 사용
            a.action();

        } else {

            // 의존성이 주입되지 않은 경우
            System.out.println("A가 주입되지 않았습니다.");
        }
    }
}

// 외부 컨테이너
public class Main {

    public static void main(String[] args) {

        // 컨테이너가 A 객체 생성
        A a = new A();

        // 컨테이너가 B 객체 생성
        B b = new B();

        // 인터페이스 메서드를 통해 A 주입
        b.injectA(a);

        // B 기능 실행
        b.doSomethingWithA();
    }
}

인터 페이스 주입(Interface Injection): 인터페이스를 통해 의존성을 주입한다. (필자가 자주 사용, 특정 모듈간의 강한 계약이 필요할때 주로 쓴다.)

주입 방식

장점

단점

사용 상황

생성자 주입 (Constructor Injection)

객체 생성 시점에 의존성이 주입되어 불변성 보장, 객체가 완전히 초기화된 상태로 생성됨

순환 의존성(Circular Dependency) 처리 어려움

반드시 필요한 의존성일 때

Setter 주입 (Setter Injection)

선택적 의존성(Optional Dependency) 지원, 순환 의존성 해결이 비교적 쉬움

객체 생성 이후에도 의존성이 변경될 수 있어 불변성 보장 어려움, 객체 상태가 바뀔 가능성

필수가 아닌 의존성일 때

인터페이스 주입 (Interface Injection)

특정 인터페이스를 통해 특정 객체만 주입 가능하도록 강제

구현이 번거롭고 코드 복잡도 증가, 실제 프레임워크에서 거의 사용되지 않음

특정한 주입 패턴을 강제해야 하는 경우


최종적으로 이제 이 글을 단 2줄로 요약하자

IoC (제어의 역전): 제어 흐름을 외부에 위임하는 상위 개념. 프레임워크, 콜백, 이벤트 등 다양한 방식으로 구현 가능.

DI (의존성 주입): IoC를 구현하는 하위 개념. 객체 간의 의존성을 외부에서 주입하는 특정 패턴.

마지막으로, IoC와 DI는 단순히 기술적 도구가 아닌, 소프트웨어 설계의 철학을 반영한 개념이다.

이를 통해 객체 간의 결합도를 낮추고, 코드의 유연성과 테스트 용이성을 높일 수 있다

이러한 원칙을 이해하고 적용한다면, 더 나은 소프트웨어를 설계하고 개발하는 데 큰 도움이 될 것이다.