Dependency Injection

정의

의존성 주입(DI)은 객체가 자신의 의존성(함께 작업하는 다른 객체)을 생성자 인자, 팩토리 메서드 인자, 또는 생성 후 설정되는 프로퍼티를 통해서만 정의하는 프로세스다. 컨테이너가 Bean을 생성할 때 이 의존성들을 주입한다.

이 프로세스는 Bean이 직접 클래스를 생성하거나 Service Locator 패턴을 사용하여 의존성을 제어하는 것의 **역전(Inversion)**이다 — 이것이 IoC(Inversion of Control)라는 이름의 유래다.

DI를 적용하면 코드가 깔끔해지고 디커플링이 효과적이다. 객체가 의존성의 위치나 클래스를 알지 못하므로, 특히 의존성이 인터페이스나 추상 클래스인 경우 스텁/모의 구현을 사용한 단위 테스트가 쉬워진다.

DI는 두 가지 주요 변형이 있다: 생성자 기반 DISetter 기반 DI.

생성자 기반 DI (Constructor-based)

컨테이너가 의존성을 나타내는 인자들을 가진 생성자를 호출하여 수행한다. static 팩토리 메서드에 특정 인자를 전달하는 것도 거의 동일하게 취급된다.

public class SimpleMovieLister {

    // SimpleMovieLister는 MovieFinder에 의존
    private final MovieFinder movieFinder;

    // Spring 컨테이너가 MovieFinder를 주입할 생성자
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

이 클래스는 컨테이너 전용 인터페이스, 기반 클래스, 어노테이션에 대한 의존성이 없는 순수 POJO다.


생성자 인자 해결 (Constructor Argument Resolution)

생성자 인자 매칭은 인자의 타입으로 수행된다. Bean 정의의 생성자 인자에 모호함이 없으면, 정의된 순서대로 적절한 생성자에 전달된다.

public class ThingOne {
    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

ThingTwoThingThree가 상속 관계가 아니면 모호함이 없으므로 인덱스나 타입을 명시할 필요 없이 동작한다:

<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg ref="beanTwo"/>
        <constructor-arg ref="beanThree"/>
    </bean>
    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>
</beans>

하지만 <value>true</value> 같은 단순 타입을 사용하면 Spring이 값의 타입을 판별할 수 없어 타입 매칭이 불가능하다:

public class ExampleBean {
    private final int years;
    private final String ultimateAnswer;

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

이 경우 세 가지 방식으로 모호함을 해결할 수 있다:

1) 타입 매칭 (type)

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>

2) 인덱스 지정 (index) — 0부터 시작

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg index="0" value="7500000"/>
    <constructor-arg index="1" value="42"/>
</bean>

같은 타입의 인자가 두 개인 경우에도 인덱스로 모호함을 해결할 수 있다.

3) 파라미터 이름 (name)

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>

이름 기반 매칭이 동작하려면 -parameters 플래그로 컴파일해야 한다. 그렇지 않으면 @ConstructorProperties 어노테이션으로 명시적으로 이름을 지정할 수 있다:

@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
    this.years = years;
    this.ultimateAnswer = ultimateAnswer;
}

Setter 기반 DI (Setter-based)

컨테이너가 인자 없는 생성자(또는 인자 없는 static 팩토리 메서드)를 호출하여 Bean을 인스턴스화한 뒤, setter 메서드를 호출하여 수행한다.

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    // Spring 컨테이너가 MovieFinder를 주입할 setter
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

ApplicationContext는 관리하는 Bean에 대해 생성자 기반과 Setter 기반 DI를 모두 지원한다. 생성자로 일부 의존성을 주입한 후 Setter로 추가 의존성을 주입하는 것도 가능하다.

설정은 BeanDefinition 형태로 관리되며, PropertyEditor 인스턴스를 사용하여 프로퍼티를 변환한다. 대부분의 Spring 사용자는 이 클래스들을 직접 다루지 않고 XML 정의, 어노테이션 컴포넌트(@Component, @Controller 등), Java 설정(@Configuration@Bean 메서드)을 사용한다.

생성자 vs Setter: 어떤 것을 선택할까?

생성자 기반과 Setter 기반을 혼합할 수 있으므로, 다음 원칙을 따르는 것이 좋다:

구분생성자 주입Setter 주입
용도필수 의존성선택적 의존성 (합리적인 기본값이 있는 경우)
불변성객체를 **불변(immutable)**으로 구현 가능재설정/재주입 가능
null 안전성필수 의존성이 null이 아님을 보장의존성 사용 코드 곳곳에서 not-null 체크 필요
초기화 상태항상 완전히 초기화된 상태로 반환부분 초기화 상태 가능
단점생성자 인자가 많으면 bad code smell (책임이 너무 많음)
활용 사례일반적인 애플리케이션 컴포넌트JMX MBean 등 재설정이 필요한 경우

Spring 팀은 생성자 주입을 권장한다. 불변 객체 구현이 가능하고, 필수 의존성이 null이 아님을 보장하기 때문이다.

서드파티 클래스를 다룰 때 setter 메서드가 없으면 생성자 주입만 가능한 경우도 있다. 결국 해당 클래스에 가장 적합한 방식을 선택하면 된다.

의존성 해결 프로세스

컨테이너는 다음 과정으로 Bean의 의존성을 해결한다:

  1. ApplicationContext가 모든 Bean을 기술하는 설정 메타데이터(XML, Java 코드, 어노테이션)로 생성·초기화된다
  2. 각 Bean의 의존성이 프로퍼티, 생성자 인자, 또는 정적 팩토리 메서드 인자 형태로 표현된다
  3. 각 프로퍼티/생성자 인자는 설정할 값의 실제 정의이거나 컨테이너 내 다른 Bean에 대한 참조
  4. 값인 프로퍼티/생성자 인자는 지정된 형식에서 실제 타입으로 변환된다 (Spring은 문자열을 int, long, String, boolean 등 내장 타입으로 자동 변환)

Spring 컨테이너는 생성 시 각 Bean의 설정을 검증한다. 그러나 Bean 프로퍼티 자체는 Bean이 실제로 생성될 때까지 설정되지 않는다.

  • Singleton 스코프 + pre-instantiated(기본값): 컨테이너 생성 시 함께 생성
  • 그 외 스코프: 요청 시 생성

Bean 생성은 의존성 그래프에 따라 연쇄적으로 발생할 수 있으며, 의존성 불일치는 영향받는 Bean이 처음 생성될 때 늦게 나타날 수 있다.

ApplicationContext가 기본적으로 singleton Bean을 사전 인스턴스화하는 이유가 여기에 있다. 약간의 초기 시간과 메모리를 들여 설정 문제를 일찍 발견하기 위함이다. 이 기본 동작을 오버라이드하여 singleton Bean을 지연 초기화(lazy initialization)할 수도 있다.


순환 의존성 (Circular Dependencies)

주로 생성자 주입 사용 시 해결할 수 없는 순환 의존성이 발생할 수 있다.

클래스 A가 생성자 주입으로 클래스 B의 인스턴스를 필요로 하고, 클래스 B가 생성자 주입으로 클래스 A의 인스턴스를 필요로 하면, Spring IoC 컨테이너가 런타임에 순환 참조를 감지하고 BeanCurrentlyInCreationException을 던진다.

해결 방법:

  • 일부 클래스의 소스 코드를 수정하여 생성자 대신 setter로 설정
  • 생성자 주입을 피하고 setter 주입만 사용 (권장하지 않음)

순환 의존성이 없는 일반적인 경우, 협력 Bean이 의존 Bean에 주입되기 전에 완전히 설정된다. 즉, Bean A가 Bean B에 의존하면 컨테이너는 B를 완전히 설정한 후 A의 setter 메서드를 호출한다. Bean은 인스턴스화되고, 의존성이 설정되고, 관련 라이프사이클 메서드(init 메서드, InitializingBean 콜백)가 호출된다.

DI 예제

Setter 기반 DI

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- 중첩 ref 요소를 사용한 setter 주입 -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>
    <!-- 간결한 ref 속성을 사용한 setter 주입 -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
public class ExampleBean {
    private AnotherBean beanOne;
    private YetAnotherBean beanTwo;
    private int i;

    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }
    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }
    public void setIntegerProperty(int i) {
        this.i = i;
    }
}

setter 메서드는 XML에서 지정한 프로퍼티와 매칭되도록 선언한다.


생성자 기반 DI

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>
    <constructor-arg ref="yetAnotherBean"/>
    <constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
public class ExampleBean {
    private AnotherBean beanOne;
    private YetAnotherBean beanTwo;
    private int i;

    public ExampleBean(
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        this.beanOne = anotherBean;
        this.beanTwo = yetAnotherBean;
        this.i = i;
    }
}

정적 팩토리 메서드 DI

생성자 대신 static 팩토리 메서드를 호출하도록 설정할 수도 있다. 팩토리 메서드의 인자도 <constructor-arg/>로 전달한다:

<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
    <constructor-arg ref="anotherExampleBean"/>
    <constructor-arg ref="yetAnotherBean"/>
    <constructor-arg value="1"/>
</bean>
public class ExampleBean {
    private ExampleBean(...) { ... }

    public static ExampleBean createInstance(
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        ExampleBean eb = new ExampleBean(...);
        // ...
        return eb;
    }
}

팩토리 메서드가 반환하는 클래스의 타입은 static 팩토리 메서드를 포함하는 클래스와 같은 타입일 필요는 없다. 인스턴스(비정적) 팩토리 메서드도 class 속성 대신 factory-bean 속성을 사용하는 것 외에는 본질적으로 동일하다.

관련 문서