포스트

스프링 핵심 원리 (3) - IoC, DI 컨테이너

스프링 핵심 원리 (3) - IoC, DI 컨테이너

1. 개요

이전 글에서 AppConfig를 만들어 구현 객체를 생성·연결하는 책임을 클라이언트로부터 분리했다. 이번 글에서는 이 변화를 좀 더 일반적인 용어로 정리해본다. AppConfig가 등장하면서 프로그램의 제어 흐름이 어떻게 바뀌었는지를 제어의 역전(IoC) 으로, 그 결과 객체 사이의 의존관계가 외부에서 연결되는 것을 의존관계 주입(DI) 으로, 그리고 이 역할을 대신 수행해주는 대상을 IoC 컨테이너(DI 컨테이너) 라는 용어로 이해해본다.


2. 제어의 역전 (IoC, Inversion of Control)

기존 프로그램에서는 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 즉 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 이 흐름이 오히려 자연스럽다.

1
2
3
public class OrderServiceImpl implements OrderService {
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); // 스스로 생성
}

반면 AppConfig가 등장한 이후 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다. OrderServiceImpl은 필요한 인터페이스를 호출할 뿐, 실제로 어떤 구현 객체가 그 뒤에서 동작할지 알지 못한다.

프로그램에 대한 제어 흐름의 권한은 모두 AppConfig가 쥐고 있다. 심지어 OrderServiceImpl 객체 자체도 AppConfig가 생성한다. AppConfigOrderService 인터페이스의 전혀 다른 구현 객체를 만들어 실행할 수도 있는데, OrderServiceImpl은 그런 사실도 모른 채 묵묵히 자신의 로직만 실행한다.

이처럼 프로그램의 제어 흐름을 자기 자신이 아니라 외부에서 관리하는 것을 제어의 역전(IoC) 이라 한다.

프레임워크 vs 라이브러리

IoC를 이해하면 프레임워크와 라이브러리의 차이도 명확해진다.

  • 프레임워크: 내가 작성한 코드를 프레임워크가 제어하고, 대신 호출·실행한다. (예: JUnit — 테스트 메서드를 내가 직접 호출하지 않고 JUnit이 대신 실행해준다)
  • 라이브러리: 내가 작성한 코드가 직접 제어 흐름을 담당하고, 필요할 때 라이브러리를 호출해서 사용한다.

3. 의존관계 주입 (DI, Dependency Injection)

OrderServiceImplDiscountPolicy 인터페이스에 의존하지만, 실제 어떤 구현 객체가 사용될지는 알지 못한다. 이 의존관계를 제대로 이해하려면 정적인 클래스 의존관계실행 시점에 결정되는 동적인 객체(인스턴스) 의존관계를 분리해서 생각해야 한다.

3.1 정적인 클래스 의존관계

클래스가 사용하는 import 코드만 보고도 의존관계를 판단할 수 있다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 클래스 다이어그램만으로 분석이 가능하다.

1
2
3
4
5
6
7
8
9
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

OrderServiceImplMemberRepository, DiscountPolicy에 의존한다는 사실은 코드만 봐도 알 수 있다. 하지만 이 클래스 의존관계만으로는 실제 어떤 구현 객체(MemoryMemberRepository인지 DbMemberRepository인지, FixDiscountPolicy인지 RateDiscountPolicy인지)가 주입될지는 전혀 알 수 없다.

3.2 동적인 객체 인스턴스 의존관계

애플리케이션 실행 시점(런타임) 에 실제로 생성된 객체 인스턴스의 참조가 연결되는 의존관계다.

1
2
3
4
5
6
public class AppConfig {
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(), discountPolicy();
    }
}

애플리케이션 실행 시점에 외부(AppConfig)에서 실제 구현 객체를 생성하고, 그 참조값을 생성자를 통해 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입(DI) 이라 한다.

의존관계 주입을 사용하면 클라이언트 코드를 전혀 변경하지 않고도, 클라이언트가 호출하는 대상의 타입 인스턴스를 바꿀 수 있다. 즉 정적인 클래스 의존관계는 그대로 둔 채, 동적인 객체 인스턴스 의존관계만 쉽게 변경할 수 있게 된다.


4. IoC 컨테이너, DI 컨테이너

AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다. 최근에는 의존관계 주입에 초점을 맞춰 주로 DI 컨테이너라고 부르며, 문맥에 따라 어셈블러(assembler)나 오브젝트 팩토리(object factory) 등으로 불리기도 한다.

용어초점
IoC 컨테이너프로그램의 제어 흐름을 외부에서 관리한다는 점
DI 컨테이너객체 사이의 의존관계를 외부에서 주입해준다는 점

지금까지 살펴본 AppConfig는 순수한 자바 코드로 직접 구현한 DI 컨테이너다. 스프링을 사용하면 AppConfig@Configuration, @Bean을 붙이는 것만으로 ApplicationContext라는 스프링 컨테이너가 이 역할을 대신 수행해준다.


5. 정리

AppConfig가 등장하면서 애플리케이션의 제어 흐름은 클라이언트가 아니라 AppConfig가 가져가게 되었고, 이를 제어의 역전(IoC) 이라 부른다. 그 결과 클라이언트와 구현 객체 사이의 실제 연결은 코드 작성 시점이 아니라 애플리케이션 실행 시점에 외부에서 이루어지는데, 이것이 의존관계 주입(DI) 이다.

여기서 핵심은 의존관계를 정적인 클래스 의존관계동적인 객체 인스턴스 의존관계로 나누어 보는 것이다. import 코드만으로 알 수 있는 정적 의존관계는 변하지 않지만, 실행 시점에 실제로 주입되는 구현 객체(동적 의존관계)는 AppConfig만 바꾸면 자유롭게 교체할 수 있다.

AppConfig처럼 객체의 생성과 의존관계 연결을 전담하는 대상을 IoC 컨테이너(DI 컨테이너) 라 하며, 스프링의 ApplicationContext가 바로 이 역할을 프레임워크 차원에서 제공하는 도구다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.