1.4.1 의존성 주입 by ks

의존성 주

의존성 주입(=Dependency injection (DI)) 은 오브젝트가 오직 생성자, 팩토리 메소드에 인자를 넣어서 또는 팩토리 메소드로부터 반환되거나 생성된 후에 오브젝트 인스턴스에 프로퍼티를 세팅해서 의존성을 정의하는 과정이다. (즉, 동작하는 것들과 함께 다른 오브젝트들과 같은 것들) 그리고 나서 컨테이너는 빈을 생성할 때 의존성을 주입한다. 이 과정은 근본적으로 가령 제어의 역전과 같이 빈 스스로 인스턴스를 제어하거나 Service Locator Pattern또는 클래스의 직접적인 생성자를 사용하여 소유권을 갖는 것에 의존하는 위치를 제어하는 빈의 역전이다.

Service Locator Pattern

디자인 패턴의 하나. 강력한 추상화 레이어(abstraction layer)를 포함하는 프로세스를 캡슐화(encapsulate)한 것. 이 패턴은 service locator라는 central registry를 사용한다. 모든 클래스가 서비스 로케이터에 종속된다. DI 원리(제어의 역전)를 토대로 구현한 패턴이래. 그렇지만 최근들어와서 안티패턴이라는 말이 있어.

참조 : https://edykim.com/ko/post/the-service-locator-is-an-antipattern/

마틴 파울러라는 유명한 프로그래머가 있는데 그 사람이 설명해서 유명해진 패턴.

코드는 DI 원리를 사용해 더 깔끔하고 오브젝트들이 의존성으로 제공될 때 디커플링이 더효과적이다.

Coupling은 객체지향 관점에서 (다른 관점은 잘 모르겠고 ㅜㅜ) 최대한 줄여야한다고 해. 분리시키기 어려워지니까. 그래서 Coupling - Decoupling 이란 용어를 사용

오브젝트는 의존성을 찾지않고 의존성이 있는 클래스나 위치를 찾지도 않는다. 결과적으로 부분적으로 의존성이 인터페이스나 unit test에 사용되는 stub, mock를 구현하는 추상클래스에 기초하고 있을 때 클래스가 더 테스트하기 쉬워진다.

JUnit은 자바 코드 테스트하는 가장 유명한 프레임워크이다. 테스트를 위해 사용하는 오브젝트가 Stub, Mock, Fake등이 있는데 이들을 모두 테스트 더블(test double)이라고 한다. stub는 상태확인, mock는 동작확인에 사용된다. 이들은 실제 오브젝트가 아니라 가상 오브젝트가 실제 오브젝트를 대체하여 실행된다.

참조 : https://medium.com/@SlackBeck/mock-object%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-85159754b2ac

DI는 생성자 기반 주입, setter 기반 주입으로 나눌 수 있. Constructor-based dependency injection and Setter-based dependency injection.

  1. 생성자 기반 의존성 주입

생성자 기반 DI는 컨테이너가 각 대표하는 의존에 인자들을 가진 생성자를 주입하는 것으로 완성된다. 빈을 생성하기 위한 구체적인 인자들을 가진 정적 팩토리 메소드는 거의 동등하고 이 논의는 정적 팩토리 메소드와 유사하고 생성자에 인자들을 다룬다(?). 아래 예제는 생성자 주입으로 의존성을 주입하는 클래스를 보여준다.

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on a MovieFinder
    private MovieFinder movieFinder;

    // a constructor so that the Spring container can inject a MovieFinder
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}

(이 클래스에 특별한건 없다. POJO이고 클래스 또는 어노테이션에 기반한 컨테이너에 의존하지 않는다.)

POJO (Plain Old Java Object)

스프링 프레임워크는 POJO 방식의 프레임워크이다. 빈 클래스를 통해 생성된 객체. 클래스의 상속, 인터페이스의 구현, 어노테이션의 사용을 강제하지 않는 자바 객체.

생성자 인자 해결

생성자 인자 해결 매칭은 인자 타입을 사용해서 수행된한다. 만약 빈 정의의 생성자 인자에 잠재적 모호성이 존재하지 않는다면, 빈 정의 안에 정의된 생성자 인자 주문은 빈이 인스턴스화 되었을 때 그 인자들이 적절한 생성자에 공급된 것이다. 아래 클래스를 보면,

package x.y;

public class ThingOne {

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

ThingTow와 ThingThree 클래스를 추측하면 상속과 연관되지 않았지만 잠재적 모호성도 존재하지 않는다. 그러므로 아래 configuraton은 잘 작동하고 <contructor-arg/> element로 명시적으로 생성자 인자 인덱스나 타입을 구체화할 필요가 없다.

<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>

생성자 인자 타입 매칭

예측하는 시나리오에선 컨테이너는 만약 type attribute를 사용해서 생성자 인자의 타입을 명시적으로 구체화한다면 간단한 타입으로 매칭할 수 있다. 아래 예제를 보면,

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

생성자 인자 인덱스

생성자 인자의 인덱스를 명시적으로 구체화하기 위해 index attribute를 사용할 수 있다. 아래의 예제를 보자.

(ExampleBean의 생성자에서 인자를 처음부터 차례로 1,2,...로 인덱스를 붙이고 주입.)

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

게다가 여러 값들이라서 모호해지면 같은 타입의 두 인자를 가진 생성자의 모호성을 인덱스를 구체화해서 해결한다.

index는 0이 기본이다.

생성자 인자 이름

모호성을 풀기위해 생성자 파라미터 이름을 사용할 수 있다.

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

박스 밖에서 동작하게 만들기 위해선 debug 플래그를 사용해서 컴파일 해야만 하고 Spring은 생성자로부터 파라미터 이름을 찾는다는 것을 명심해야한다. 만약 debug 플래그랑 컴파일 하기를 원하지 않는다면 @ContructorProperties 를 사용해라. 아래는 샘플이다.

@ConstructorProperties 은 속성의 명칭을 지정해주는 어노테이션인데 직접 애성자를 사용할땐 쓸일이 거의없다.

package examples;

public class ExampleBean {

    // Fields omitted

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

2. setter 기반 의존성 주입

Setter 기반 DI는 빈을 인스턴스화 하기 위해 인자가 없는 정적 팩토리 메소드 또는 인자가 없는 생성자를 주입하고 나서 setter메소드를 부른다.

아래 예제는 순수하게 pure setter 주입으로 의존성 주입을 할 수 있는 것을 보여준다.

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on the MovieFinder
    private MovieFinder movieFinder;

    // a setter method so that the Spring container can inject a MovieFinder
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}

ApplicatonContext는 몇몇 의존이 이미 생성자로 주입된 후에도 생성자와 setter 기반 DI를 지원한다. 한 포맷에서 다른 포맷으로 프로퍼티를 변환하기 위해 PropertyEditor 인스턴스와 연결하는 BeanDefinition의 형식에서 의존성을 구성한다. 하지만 대부분 Spring 사용자들은 이 클래스들을 직접적으로 쓰지 않는다(즉, 프로그래밍적으로) 오히려 XML에서 bean정의를 사용하거나 annotaion을 쓴 컴포넌트(즉, @Component, @Controller처럼) 를 사용하거나 @Configuration 클래스에서 @Bean으로 선언한 메소드를 사용한다. 이 소스들은 내부적으로 BeanDefinition의 인스턴스로 변환되고 전체 Spring IoC 컨테이너 인스턴스에서 로드된다.

생성자 기반 DI 와 setter 기반 DI

생성자, setter DI를 둘다 섞어서 쓸 수 있기 때문에 선택적 의존성을 위한 configuration 메소드 또는 setter 메소드, 그리고 필수적인 의존성을 위한 생성자를 사용하는건 좋은 방법이다. setter 메소드에 @Required 의 사용하는 것은 프로퍼티를 필요한 의존성이 되도록 만들 수 있다.

Spring team은 일반적으로 생성자 주입을 요구된 의존성이 null이 아닌 것을 확신하고 불면하는 오브젝트로서 어플리케이션 컴포넌트를 구체화하도록 한다고 얘기한다. 게다가 생성자 주입 컴포넌트는 항상 완전 초기 상태에 클라이언트 코드를 반환한다. 다른 면으로는 많은 생성자 인자는 좋지 않고, 너무 낳은 책임을 클래스가 가지는 것은 리팩토링 해야한다고 한다.

setter 주입은 우선적으로 클래스 안에 디폴트 값을 할당할 수 있는 선택적 의존성을 위해 사용된다. 반면에 not-null은 의존성을 사용하는 코드라면 어디든지 수행되어야만 한다. setter 주입의 한가지 장점은 setter 메소드는 나중에 재주입하거나 재설정(reconfiguration)을 할 수 있도록 해준다. JMX MBean을 통한 관리는 그러므로 setter 주입이 강제적이다.

DI 를 사용하는 것은 부분적 클래스를 이해하도록 한다. 때떄로 소스를 가지고 있지 않는 써드파티클래스들을 다룰 때 이 선택은 이루어진다. 예를 들어서 만약 써드파티 클래스가 어떠한 setter 메소드를 노출하지 않는다면 생성자 주입은 오로지 DI 의 형태로 가능하다.

의존성 해결 과정

컨테이너는 아래와 같이 빈 의존성을 수행한다.

  1. ApplicationContext는 모든 빈을 설명한 configuration 메타데이터로 초기화하여 생성된다. Configuration 메타데이터는 XML, java, annotation으로 구체화된다.

  2. 각 빈들은 프로퍼티, 생성자 인자, 또는 정적 팩토리 메소드(평범한 생성자 대신에 사용한다면) 의 형태로 의존성이 표현된다.

  3. 각 프로퍼티나 생성자 인자는 사실 세팅 또는 컨테이너 안에서 또다른 빈들을 참조하기 위한 값들의 정의다.

  4. 각 프로퍼티나 값 자체인 생성자 인자는 프로퍼티 혹은 생성자 인자의 사실적 타입에 대한 구체적 형태로 변환된다. 기본적으로 스프링은 값을 가령 int, long, String, boolean등과 같이 이미 만들어진 자료형에 문자열로 변환할 수 있다.

스프링 컨테이너는 각 빈들의 configuration을 검증한다. 하지만 빈 프로퍼티들은 스스로 빈이 사실적으로 생성되기 전까지 설정되지 않는다. (빈 안에 프로퍼티가 있는 것들은 생성되기 전에는 할당이 안된다는 의미인거같아) 스코프(scope)는 Bean Scope로서 정의된다. 반면에 빈은 오직 요청될 때 생성된다. 빈을 생성하면 잠재적으로 빈의 의존성과 의존성의 의존성이 생성되고 할당되어 빈 그래프가 생성된다. 의존성사이의 미스매치를 해결하는 방법에 대해서는 나중에 살펴보자.

빈 그래프 : 설정된 bean간의 관계도

순환 의존성

생성자 주입을 사용한다면 순환 의존성 시나리오가 발생할 수 도 있다.

예를 들어서 클래스 A가 생성자 주입할때 클래스 B인스턴스가 필요하고, 클래스 B는 클래스 A인스턴스를 주입받는다고 애각해보자. 만약 서로를 주입받아야한다면 스프링 IoC에서 런타임 시점에 순환 참조를 탐지하여 BeanCurrentlyInCreationException을 던진다.

하나의 가능한 해결책은 생성자보다 setter로 설정하는 것으로 도르르 바꾸는 것이다. 대안적으로 생성자 주입을 피하고 setter를 사용하는 것이 유일하다. 다른 라로는 비록 추천되지는 않지만 setter 주입으로 순환 의존성을 설정할 수있다.

전형적인 경우(순환 의존성이 아니라)와 달리, A와 B 빈 사이에 순환 의존성은 처음 초기화하기 전에 다른것으로 주입되도록 한다.

일반적으로 스프링은 올바르게 일하고 있다고 믿을 수 있다. 가령 존재하지 않는 빈을 참조하거나 컨테이너 로드 타임에 순환 의존같은 configuration 문제를 탐지한다. 스프링은 빈이 실제로 생길 때 프로퍼티를 세팅하고 가능한 한 늦게 의존성을 해결한다. 이는 올바르게 로드된 스프링 컨테이너가 나중에 사용자가 의존성중에 하나 또는 오브젝트를 생성하다가 문제가 생길 때 익셉션을 던질 수 있음을 의미한다. 예를 들어서 유효하지 않는 프로퍼티나 없는 결과로서 예외를 던진다. 잠재적으로 몇가지 configuration 이슈의 가시화를 미루는 이유는 ApplicationContext가 기본적으로 싱글 톤 빈을 미리 인스턴스 하는 이유다. 실제로 필요해지기 전에 이 빈들을 생성하기 위해 시간과 메모리에 선행하는 비용 측면에서 configuration 이슈는 ApplicationContext가 생성될때 발견된다.(?) 싱글톤 빈은 오히려 미리 인스턴스화하지 않고 늦게 초기화해서 기본 행위를 오버라이드 할 수 있다.

하나 혹은 그 이상의 빈들이 의존하는 빈으로 주입되지 않을 때 순환 의존성이 없다면 각 빈들은 전체적으로 의존하는 빈으로 주입되기 전에 설정(configuration)된다. 이는 만약 빈 A가 B에 의존성을 가지고 있다면 스프링 컨테이너가 전체적으로 A로 setter 메소드를 주입하기 전에 B를 완벽하게 설정(configuration)한다. 다른 말로 빈은 미리 초기화되지 않은 싱글톤 빈이 없다면 빈이 초기화되고, 의존성이 설정이 되고, 라이프사이클 메소드(가령 configured init method or the InitializingBean callback method) 가 주입된다.

의존성 주입

아래 예제는 XML기반의 setter기반 DI dlek. XML 설정의 일부이다.

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

    <!-- setter injection using the neater ref attribute -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

아래는 ExampleBean의 예제다

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 injection using the nested ref element -->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>

    <!-- constructor injection using the neater ref attribute -->
    <constructor-arg ref="yetAnotherBean"/>

    <constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

아래는 ExampleBean이다.

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;
    }
}

생성자 인자는 ExampleBean의 생성자에 인자로서 사용된다.

생성자를 사용하는 것 대신에 이제 다양한 예제를 살펴보자. 스프링은 스태틱 팩토리 메소드를 불러서 오브젝트 인스턴스를 반환한다.

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

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

아래는 ExampleBean class이다.

public class ExampleBean {

    // a private constructor
    private ExampleBean(...) {
        ...
    }

    // a static factory method; the arguments to this method can be
    // considered the dependencies of the bean that is returned,
    // regardless of how those arguments are actually used.
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

        ExampleBean eb = new ExampleBean (...);
        // some other operations...
        return eb;
    }
}

스태틱 팩토리 메소드에 인자는 <constructor-arg/> 요소를 지원한다. 사실 생성자가 사용하는것과 같은것이다. 팩토리 메소드에 의해 반환된 클래스의 타입은 스태틱 팩토리 메소드를 포함하는 클래스로서 같은 타입은 아니다. 이는 여기서 자세히 다루진 않는다.

Last updated