Nextree

세 가지 DI 컨테이너로 향하는 저녁 산책

Nextree Aug 06, 2014 0 Comments

애플리케이션 코드를 작성할 때, 특정기능이 필요하면 라이브러리를 호출하여 사용하곤 합니다. 프로그램의 흐름을 제어하는 주체가 애플리케이션 코드인 셈이지요. 하지만 프레임워크(Framework) 기반의 개발에서는 프레임워크 자신이 흐름을 제어하는 주체가 되어, 필요할 때마다 애플리케이션 코드를 호출하여 사용합니다.

프레임워크에서 이 제어권을 가지는 것이 바로 컨테이너(Container)입니다. 객체에 대한 제어권이 개발자로부터 컨테이너에게 넘어가면서 객체의 생성부터 생명주기 관리까지의 모든 것을 컨테이너가 도맡아서 하게됩니다. 이를 일반적인 제어권의 흐름이 바뀌었다고 하여 IoC(Inversion of Control : 제어의 역전)라고 부릅니다.

1. DI(Dependency Injection)이해하기

여기서 좀 더 자세히 들어가 DI라는 개념을 살펴봅니다. DI는 클래스 사이의 의존관계를 빈 설정 정보를 바탕으로 컨테이너가 자동적으로 연결해주는 것을 말합니다. 개발자들은 제어를 담당할 필요없이 빈 설정 파일에 의존관계가 필요하다는 정보만 추가해주면 됩니다. 그러면 오브젝트 레퍼런스를 외부(Container)로부터 주입 받아서, 실행 시에 동적으로 의존관계가 생성됩니다. 결국 컨테이너가 흐름의 주체가 되어서 애플리케이션 코드에 의존관계를 주입해주는 것입니다.

이해를 돕기 위해 DI가 적용되지 않은 코드와 적용된 코드를 비교해 보겠습니다.

IoC/DI가 적용되지 않은 경우

yrkim-140701-framework-02

package kr.co.nextree;

public class Foo{  
    private Bar bar;

    public Foo() {
        bar = new SubBar();
    }
}

[예제 1] IoC/DI가 적용되지 않은 경우

위의 코드를 보시면 IoC/DI가 적용되지 않을 경우, Bar 인터페이스를 구현하는 구체적인 클래스의 이름(SubBar)을 애플리케이션 코드에서 바로 등장시켜 초기화합니다. 이 경우, 동적으로 구현 클래스를 정해주기 어렵습니다.

IoC/DI가 적용된 경우

yrkim-140701-framework-03

// 컨테이너
<beans>  
    <bean id="bar" class="kr.co.nextree.SubBar">
    <bean id="foo" class="kr.co.nextree.Foo">
        <property name="bar" ref="bar"/>
    </bean>
</beans>  
// 애플리케이션 코드
package kr.co.nextree;

public class Foo {  
    private Bar bar;

    public void setBar(Bar bar) {
        this.bar = bar;
    }
}

[예제 2] IoC/DI가 적용된 경우

IoC/DI가 적용될 경우, 우선 사용할 객체들을 컨테이너에 등록합니다. 그리고 애플리케이션 코드에서 해당 객체를 setter함수의 매개변수로 받아와서 실행 시에 동적으로 의존관계를 설정해줍니다. 이 경우엔 Bar 인터페이스를 구현하는 구체적인 클래스의 이름이 애플리케이션 코드에 등장하지 않아, 동적으로 구현클래스를 정해줄 수 있게 됩니다.

따라서 IoC/DI를 사용하면 객체를 생성할 때에, 해당 객체가 참조하고 있는 다른 객체에 대한 종속성을 애플리케이션 코드 외부(Container)에 설정하게 함으로써 결합도(coupling)는 낮추면서 유연성과 확장성은 향상시킬 수 있습니다.


참고) 결합도(coupling) yrkim-140803-dicontainer-02 * 소프트웨어 공학에서, 결합도(coupling) 또는 의존도는 어떤 모듈이 다른 모듈에 의존하는 정도를 나타내는 것이다. * 결합도는 보통 응집도(cohesion)과 대비된다. 낮은 결합도는 종종 높은 응집도와 관련이 있으며, 그 역도 마찬가지이다. (중략) 낮은 결합도는 종종 구조화가 잘 된 컴퓨터 시스템의 지표이며, 좋은 설계이며, 높은 응집도를 겸비하면, 높은 가독성과 유지보수성이라는 일반적인 목표를 이루게 된다.
출처 : http://ko.wikipedia.org/wiki/%EA%B2%B0%ED%95%A9%EB%8F%84


2. DI의 세가지 유형

마틴 파울러가 그의 저서인 Inversion of Control Containers and the Dependency Injection pattern에 제시한 세가지 DI패턴이 일반적인 DI의 적용 방법입니다. 각 예제는 마틴 파울러의 홈페이지(http://martinfowler.com/articles/injection.html)에 제시된 것입니다.

1) 생성자를 이용한 의존성 삽입 (Constructor Injection : type 1 IoC)

필요한 의존성을 모두 포함하는 클래스의 생성자를 만들고 그 생성자를 통해 의존성을 주입한다.

즉 생성자에 파라미터를 만들어두고 이를 통해 DI 컨테이너가 의존할 오브젝트 레퍼런스를 넘겨주는 방식입니다.

// PicoContainer
class MovieLister...  
    public MovieLister(MovieFinder finder) {
        this.finder = finder;       
    }

class ColonMovieFinder...  
    public ColonMovieFinder(String filename) {
        this.filename = filename;
    }

[예제 3] 생성자를 이용한 의존성 삽입

2) setter() 메소드를 이용한 의존성 삽입 (Setter Injection : type 2 IoC)

의존성을 입력받는 세터(setter) 메소드를 만들고 이를 통해 의존성을 주입한다.

setter()메소드는 외부에서 오브젝트 내부의 attribute값을 변경하려는 용도로 주로 사용됩니다. 핵심기능은 파라미터로 전달된 값을 내부의 인스턴스 변수에 저장하는 것입니다. 스프링에서 지지하는 DI방식으로, 외부에서 제공받은 오브젝트 레퍼런스를 저장해뒀다가 내부의 메소드에서 사용하게 하는 DI방식에서 활용하기에 적당합니다. 아래의 예제와 함께 위쪽에서 언급된 IoC/DI가 적용된 경우의 예제 또한 setter() 메소드를 통한 DI방식입니다.

// Spring Container
class MovieLister...  
    private MovieFinder finder;
    public void setFinder(MovieFinder finder) {
        this.finder = finder;
    }

class ColonMovieFinder...  
    public void setFilename(String filename) {
        this.filename = filename;
    }

[예제 4] setter()메소드를 이용한 의존성 삽입

3) 초기화 인터페이스를 이용한 의존성 삽입 (Interface Injection : type 3 IoC)

의존성을 주입하는 함수를 포함한 인터페이스를 작성하고 이 인터페이스를 구현하도록 함으로써 실행시에 이를 통하여 의존성을 주입한다.

가장 먼저 Injection을 하기 위한 인터페이스를 정의한 후, 구현하면 DI가 이루어지도록 합니다. 이는, 스프링이 지원하지 않는 DI방식입니다.

// Avalon
public interface InjectFinder {  
    void injectFinder(MovieFinder finder);
}

class MovieLister implements InjectFinder...  
    public void injectFinder(MovieFinder finder) {
        this.finder = finder;
    }

public interface InjectFinderFilename {  
    void injectFilename (String filename);
}

class ColonMovieFinder implements MovieFinder, InjectFinderFilename......  
    public void injectFilename(String filename) {
        this.filename = filename;
    }

[예제 5] 초기화 인터페이스를 이용한 의존성 삽입

3. DI Container의 종류

1) Spring Container

yrkim-140803-dicontainer-03
이미지 출처 : http://www.javatpoint.com/spring-tutorial

IoC/DI가 자바의 표준 프로그래밍 모델로 발전해오며 다양한 종류의 IoC/DI프레임워크가 등장했습니다. 하지만 그들중 단연 독보적인 프레임워크는 바로 Spring입니다. 유연하고 강력한 기능으로 다양하게 컨테이너를 사용할 수 있기 때문에 대부분 Spring을 선택하곤 합니다.

스프링 IoC 컨테이너가 관리하는 객체를 빈(bean)이라고 부릅니다. 그래서 스프링에서는 이 빈(bean)들을 관리한다는 의미로 컨테이너를 빈 팩토리(Bean Factory)라고 부릅니다(팩토리 디자인 패턴 구현). 이렇게 오브젝트의 생성과 오브젝트 사이의 런타임(Run-time) 관계를 설정하는 DI관점으로 볼 때는 컨테이너를 빈 팩토리라고 합니다. 하지만 스프링의 DI container는 단순한 DI작업보다 더 많은 일을 합니다. 그래서 DI기능에 엔터프라이즈 애플리케이션을 개발하는 데 필요한 여러 가지 컨테이너 기능을 추가하여 애플리케이션 컨텍스트(Application Context)라고 부르기도 합니다. 스프링에서는 이러한 컨테이너를 일반적으로 구성(Configuration)정보를 담아 xml로 구성합니다.

JDBCDataManager 컴포넌트를 통해 스프링의 DI를 살펴봅니다. [예제 6]의 JDBCDataManager 클래스에서는 스프링의 DI방식인 setter()메소드를 사용하고, [예제 7]과 같이 xml파일을 설정해줍니다. dataSource가 myDataSource빈을 참조하도록 설정해서 해당 빈들을 등록해주어, 런타임에 DI가 일어납니다.

public class JDBCDataManger {  
    private DataSource dataSource;

    public void setDataManager(DataSource dataSource {
        this.dataSource = dataSource;
    }

    public void getData() {
        //use dataSource for something
    }
}

[예제 6] JDBCDataManager

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" >  
    <property name="driverClassName">
        <value>com.mydb.jdbc.Driver</value>
    </property>
    <property name="url">
        <value>jdbc:mydb://server:port/mydb</value>
    </property>
    <property name="username">
        <value>root</value>
    </property>
</bean>  
<bean id="dataManager" class="example.JDBCDataManger">  
    <property name="dataSource">
        <ref bean="myDataSource"/>
    </property>
</bean>  

[예제 7] Spring xml 설정 정보

출처 : https://today.java.net/pub/a/today/2004/02/10/ioc.html

2) PicoContainer

yrkim-140803-dicontainer-04
이미지 출처 : http://www.picocontainer.com

PicoContainer는 DI자체에 집중한 상당히 작은 컨테이너 프레임워크입니다. 외부구성파일(Configuration xml 파일)도 없고 애노테이션(annotation)도 필요없습니다. 단지 컴포넌트를 컨테이너와 함께 등록하기만하면 됩니다. 따라서 굉장히 단순한 구성으로 DI를 구현할 수 있습니다. 하지만 이 때문에 복잡한 대형 프로젝트에는 적합하지 않는 DI 컨테이너로 알려져 있기도 합니다.

PicoContainer의 특성을 간단히 '임의의 객체의 인스턴스화'라고 할 수 있습니다. 이를 통해 많은 수의 클래스와 인터페이스간의 복잡한 의존관계들을 단순화시킬 수 있습니다.

yrkim-140803-dicontainer-06

이번에도 JDBCDataManager 컴포넌트를 통해 PicoContainer의 DI를 살펴봅니다. [예제 8]의 JDBCDataManager 클래스에서는 PicoContainer의 DI방식인 constructor를 사용합니다. 그리고 PicoContainer는 configuration파일을 사용하는 대신, [예제 9]와 같은 자바파일을 통해 설정합니다.

public class JDBCDataManger {  
    private DataSource dataSource;

    public JDBCDataManger(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void getData() {
        //use dataSource for something
    }
}

[예제 8] JDBCDataManager

//create a datasource bean
BasicDataSource dataSource = new BasicDataSource();  
dataSource.setDriverClassName("com.mydb.jdbc.Driver");  
dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");  
dataSource.setUsername("Bob");

//create the container
MutablePicoContainer pico = new DefaultPicoContainer();

//register components with container
ConstantParameter dataSourceParam = new ConstantParameter(dataSource);  
String key = "DataManager";  
Parameter[] params = {dataSourceParam};

/*
 * Now each request for a DataManager will instantiate
 * a JDBCDataManager object with our defined
 * dataSource object
 */
pico.registerComponentImplementation (key, JDBCDataManger.class,params);  

[예제 9] PicoContainer 설정
출처 : https://today.java.net/pub/a/today/2004/02/10/ioc.html

3) Guice

이미지 출처 : http://garbagecollected.org/2007/12/12/guice-book/

Guice는 구글이 발표한 DI 프레임워크 입니다. Guice는 Java5이상을 위한 가벼운 DI container이며, 애노테이션(Annotation)을 사용하여 자바 객체를 구성하도록 DI를 구현합니다. 구현클래스가 인터페이스와 묶이도록 한 후, @Inject 애노테이션을 사용하여 생성자나 메소드, 필드에 dependency를 주입합니다. 동일한 인터페이스에 하나 이상의 구현이 필요한 경우, 사용자는 구현을 식별하는 커스텀 애노테이션(Custom Annotation)을 만들 수 있습니다. 그리고 주입시 그 애노테이션을 사용합니다. Guice는 이처럼 자바 애노테이션을 사용한 최초의 일반적인 DI framework입니다. Guice의 장점으로는 사용편의성이나 에러메시지의 확실성과 더불어 빠른 속도를 꼽을 수 있습니다.

Guice의 API중 Binder는 인터페이스와 구현클래스의 매핑을 담당하고, Module은 그런 Binding의 집합을 유지합니다. 다음의 예제는 이러한 API를 통해 Service인터페이스를 ServiceImpl이라는 구현클래스에 싱글톤으로 bind한 후, Client에서 @Inject 애노테이션을 사용해 DI하는 것을 보여줍니다.

public class MyModule implements Module {  
    public void configure(Binder binder) {
        binder.bind(Service.class)
        .to(ServiceImpl.class)
        .in(Scopes.SINGLETON);
    }
}

[예제 10] Module의 구현 클래스

public class Client {  
    private final Service service;

    // constructor를 통한 injection
    @Inject
    public Client(Service service) {
        this.service = service;
    }

    public void go() {
        service.go();
    }
}

[예제 11] @Inject 애노테이션을 통한 DI
출처 : Guice 1.0 User's Guide문서

Guice에서는 위와 같이 Constructor를 통한 injection을 선호하지만 Method, Field를 통해서도 injection이 가능합니다.

// Method를 통한 injection
public class FrogMan{  
    private Vehicle vehicle;

    @Inject
    public void setVehicle(Vehicle vehicle) {
        this.vehicle = vehicle;
    }
...

// Field를 통한 injection
public class FrogMan {  
    @Inject private Vehicle vehicle;
    public FrogMan(){}
...

[예제 12] Method / Field Injection
출처 : https://www.ibm.com/developerworks/java/library/j-guice/

4) 세가지 DI Container의 특징과 비교

  • Spring Container
    : 스프링은 애플리케이션 코드와 프레임워크 자체와의 결합도(coupling)가 다른 프레임워크에 비해 훨씬 낮다는 것을 특징으로 들수 있습니다. 또한 DI본연의 기능 뿐만이 아니라 유연하고 강력한 컨테이너기능을 쓸 수 있다는 장점이 있습니다. 그렇기 때문에 비교적 구성하기 어렵고 속도가 느려질 수 있다는 단점이 있음에도 불구하고 여전히 가장 많이 선택받는 프레임워크입니다.
  • PicoContainer
    : PicoContainer는 DI기능에 집중한 작은 컨테이너 프레임워크로서, 큰 오버헤드가 없이 단순하게 구성할 수 있습니다. 비교적 작은 프로젝트에 적합합니다.
  • Guice
    : Guice또한 단순하게 구성할수 있으며 Annotation, Generics등의 기능을 적극적으로 사용한다는 특징이 있습니다. 무엇보다 큰 장점은 속도가 빠르다는 것입니다.

Spring Framework, PicoContainer, Guice에 대한 오늘 날짜(2014.08.05)의 구글 트렌드 검색결과입니다. 역시 Spring Framework가 2005년부터 많은 사랑을 받기 시작한 이후로 지금까지 꾸준한 모습을 보여줍니다. Guice또한 2007년 이후부터 꾸준하게 사용되고 있고, PicoContainer는 두 프레임워크에 비해 관심이 저조함을 알 수 있습니다. 글로벌 트랜드는 Spring에 대한 관심이지속적인 하락세인데, 국내에서는 최근 들어 관심이 폭발하고 있습니다. 아무래도 전자정부 프레임워크의 영향이 크지 않았나 생각해봅니다.

맺음말

IoC/DI는 자바의 표준 프로그래밍 모델로서 아주 중요한 요소입니다. 이를 구현하기 위한 여러 프레임워크가 있습니다. DI container기능에만 집중한 프레임워크부터, DI 기능은 물론 복잡한 JEE애플리케이션을 보다 쉽게 구성하도록 돕는 프레임워크까지 다양합니다. 따라서 각 프레임워크의 특성을 이해하고, 목표 시스템에 잘 어울리는 것을 골라야 합니다. 감사합니다.

참고도서 및 사이트

Nextree

Read more posts by this author.