잊지 않겠습니다.

Dependency Injection

DI란 사용될 Object들이 의존성을 갖는 여러 객체들을 직접 자신이 얻어내는 것을 의미한다. DI는 여러분의 code를 느슨한 결합상태로 만들고, 테스트 하기 쉽게 만들며, 읽기 쉽게 만들어준다.

Java의 Official standard인 JSR-330에서 정의된 DI에 대해서도 역시 알아볼 것이다.

Inject some knowledge - understanding IoC and DI

IoC와 DI의 개념은 매우 헛갈리며, 이 둘을 섞어서 이야기하는 경우가 매우 많다. 이 둘에 대한 내용을 좀더 알아보기로 한다.

IoC

만약에 IoC를 사용하고 있지 않다면, program의 logic은 함수의 조합에 의하여 조절될 것이다. 매우 정밀한 design에 의해서 꾸며진 이 함수들은 여러 Object들에 의해서 재사용이 되어가며 사용되고 있을것입니다.
IoC를 사용한다면, 이에 대한 “central control” 이라는 개념자체를 뒤집게 됩니다. 호출자의 code에 의해 다뤄지는 program의 실행으로 구성이 되며 모든 Program의 Logic은 호출되는 subroutine에 의하여 encapsulated 되게 됩니다.
이는 Design Pattern 중 Hollywood Principal과 동일합니다. 여러분의 code가 호출하는 것이 아닌, 어떤 곳에서 여러분의 code를 호출하는 방식으로의 변경을 의미하게 됩니다.

Text 기반의 mud game이 있고, 이를 GUI Framework로 감싼 Version이 있다고 가정해보도록 합시다. GUI Framework는 어떠한 Logic도 가지고 있지 않습니다. “LEFT Click” 이라는 event가 호출이 되면, 이에 따른 GO LEFT 라는 command가 실행이 되는 것 뿐입니다. 이에 대한 실질적인 로직은 모두 Text 기반의 mud game안에 들어가 있습니다.

IoC에 대해서 이야기한다면, 다른 개념으로 생성자에 대한 접근을 볼 수 있습니다. 우리가 일반적인 개발방식으로 Program Logic을 생성한다면, 많은 생성자를 Call 하는 Code내에서 생성하게 되는 것을 볼 수 있습니다. 그런데, 이와 같은 방식을 뒤집에서 이미 생성되어 있는, 또는 생성할 방법이 결정나 있는 Factory Pattern을 통해 구성되는 객체들로 만들어진다면 이는 객체에 대한 Control을 객체에게 넘겨준 IoC가 적용된 상태라고 할 수 있습니다.

DI (Dependency Injection)

DI는 IoC의 일부입니다. 이는 여러분의 code안에 있는 객체의 dependency를 code 바깥에서 code가 생성되거나 실행될 때, 주입(Inject)하는 것에 촛점이 맞춰져있습니다.

IoC Container를 Spring에서 이야기한다면, ApplicationContext가 그 역활을 합니다.
DI의 경우, @Autowired가 그 일을 담당하게 됩니다.

DI를 구성하는 경우 다음과 같은 장점을 갖습니다.

Loose coupling

많은 코드들 내부에서 가지고 있는 new를 통한 객체의 생성을 하지 않기 때문에, 객체에 대한 coupling이 작아지게 됩니다. interface를 통한 결합을 하게 된다면, 의존되는 객체에 대한 의존성을 제거하고, 객체의 행위에만 집중할 수 있게 됩니다.

Testability

Loose coupling을 통해, 단순화된 객체들은 Test를 행하기 쉽습니다. Loose coupling이 되지 않은 객체들은 각각의 생성자 및 모든 method의 호출 방법의 차이에 따른 Test가 매우 힘듭니다.

Greater cohesion (높은 응집성)

생성된 code들은 객체에 대한 생성방법이나 여러 부가적 initialize를 할 필요가 없기 때문에, Logic에 대한 높은 응집성을 갖게 됩니다. 이는, code에 대한 가독성을 높여주는 장점을 가지고 있습니다.

Reusable components

loose coupling에 의한 결과로서, 다른 객체에서 사용하기 쉬운 상태로 만들어주고, 이는 객체에 대한 재 사용성을 높여주게 됩니다.

Lighter code

이는 높은 응집성에 의하여 나온 결과입니다. dependency 된 객체에 대한 추가 코드는 더이상 존재하지 않고, 사용자가 작성한 code가 직접적으로 호출하는 부분만이 남겨져 있는 상태입니다. 가독성이 높아지고, 버그가 생길 구석이 좀 더 줄어들수 있습니다.

DI의 표준화

2004년 google Guice의 Bob Lee와 SpringSource의 Rod Johnson은 JSR-330(javax.inject)에 대하여 합의하게 됩니다. 이 합의로 인하여, 요구되는 Framework내에서 표준화된 DI를 사용할 수 있게 되었습니다. 가벼운 Guice Framework를 사용하다가, 보다 많은 기능을 가진 Spring Framework로의 전환이 자유롭게 된 것을 의미합니다.

실질적으로 위의 말은 거의 불가능에 가깝습니다. Guice와 Spring Framework의 성격이 IoC와 DI를 제공하는 것이 사실이지만, Project가 진행되어가는데 있어서, 기본적인 javax.inject의 기능만이 아닌 다른 기능을 이용해서 처리하게 되는 것이 일반적입니다. 왜냐면 javax.inject는 Guice와 Spring의 가장 기본적인 기능중 공통점만을 가지고 있기 때문입니다.
지금, javax를 이용하는 경우는, JNDI를 이용한 Resource의 관리 이외에는 크게 사용되고 있지 않습니다. Spring, Guice 만을 이용하게 되는 것이 일반적이지요.

@Inject annotation

@Inject annotation은 삽입(Inject)될 dependency를 지정합니다. @Inject는 다음과 같은 곳에서 사용가능합니다.

  • Constructor
  • Methods
  • Fields

@Qualifier annotation

@Qualifier annotation은 객체들의 identify를 지정합니다. 이는 뒤에 나올 @Named와 밀접한 관계를 갖습니다.

@Named annotation

@Inject와 같이 사용됩니다. @Qualifier에 의하여 지정된 이름을 가진 객체를 @Named에 의해서 지정하여 삽입(Inject)하게 됩니다.

@Scope annotation

삽입될 객체의 생명주기를 나타냅니다. 이는 다음과 같습니다.

  • @Scope가 선언되지 않는 경우, 삽입(Inject)가 발생할 때마다, 객체를 새로 생성하게 됩니다.
  • @Scope가 선언되는 경우, 삽입(Inject)가 발생될 때, 기존에 생성된 객체가 있는 경우에는 그 객체를 사용하게 됩니다. 이는 thread-safe해야지 될 필요성을 갖게 됩니다.

@Scope annotation은 각 IoC Framework에서 새롭게 정의되어서 사용되는 것이 일반적이니다. 또한 @Singleton annotation이 생긴후, JSR-330에서는 @Singleton을 이용한 객체 선언을 주로 해주는 경우가 더 많습니다.

@Singleton annotation

@Scope가 선언된것과 동일하게 동작합니다. 이는 거의 모든 DI Framework의 기본 동작입니다.

Provide< T > interface

T 객체에 대한 구현이 아닌 Provide<T>에 대한 구현을 요구하는 경우가 있습니다. 이런 경우, 다음과 같은 목적으로 사용되게 됩니다.

  • 여러 instance를 생성해서 사용하는 경우.
  • 객체를 사용할 때, Lazy Loading을 이용해서 객체를 얻어낼 필요가 있을 때.
  • circular dependency를 회피할 목적으로.

Google Guice를 이용한 DI sample code

/**
 * Created by ykyoon on 14. 8. 5.
 * code에 @Inject가 될 대상
 */
public class AgentFinder {
    public void doSomeThing() {
        System.out.println("Print SomeThing");
    }
}

/**
 * Created by ykyoon on 14. 8. 5.
 * @Inject를 이용해서 서비스 받을 대상
 */
public class HollyWoodService {
    private AgentFinder agentFinder;

    @Inject
    public HollyWoodService(AgentFinder agentFinder) {
        this.agentFinder = agentFinder;
    }
}

/**
 * Created by ykyoon on 14. 8. 5.
 * AgentFinder에 대한 dependency될 객체 설정 - Spring의 @Configuration과 거의 동일
 */
public class AgentFinderModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AgentFinder.class);
    }
}

위 코드에서 보면 알수 있듯이, AbstractModule을 이용한 Configuration이 행해진다는 것에 유의할 필요가 있다. DI를 사용하는 code를 JUnit test code로 작성하면 다음과 같다.

public class HollyWoodServiceTest {

    private Injector injector = null;

    @Before
    public void setUp() {
        injector = Guice.createInjector(new AgentFinderModule());
        assertThat(injector, is(not(nullValue())));
    }

    @Test
    public void getHollywoodService() {
        HollyWoodService ho = injector.getInstance(HollyWoodService.class);
        assertThat(ho, is(not(nullValue())));
    }
}

Google Guice의 AbstractModule을 보면 Spring의 @Configuration과 매우 유사합니다. 그리고 문법의 경우에는 .NET 진영의 ninject와 거의 유사합니다. (서로간에 영향을 받은건지, 아니면 ninject가 영향을 받은 건지는 잘 모르겠습니다.)

이번에는 객체의 생명주기를 결정하는 Scope입니다.

Scope를 이용한 객체 생명주기의 설정

AppModule내에서 객체의 생명주기를 결정 가능합니다.

public class AgentFinderModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AgentFinder.class)
                .annotatedWith(Names.named("primary"))
                .to(AgentFinderImpl.class)
                .in(Singleton.class);
        bind(HollyWoodService.class);
    }
}

위 구성으로 다음 테스트 코드를 실해하면 결과는 다음과 같습니다.

public class HollyWoodServiceTest {

    private Injector injector = null;

    @Before
    public void setUp() {
        injector = Guice.createInjector(new AgentFinderModule());
        assertThat(injector, is(not(nullValue())));
    }

    @Test
    public void getHollywoodService() {
        HollyWoodService ho1 = injector.getInstance(HollyWoodService.class);
        assertThat(ho1, is(not(nullValue())));
        HollyWoodService ho2 = injector.getInstance(HollyWoodService.class);
        System.out.println(ho1);
        System.out.println(ho2);
        assertThat(ho1 != ho2, is(true));

        System.out.println(ho1.getAgentFinder());
        System.out.println(ho2.getAgentFinder());
        assertThat(ho1.getAgentFinder() == ho2.getAgentFinder(), is(true));
    }
}
me.xyzlast.gg.hollywood.HollyWoodService@2b9627bc
me.xyzlast.gg.hollywood.HollyWoodService@65e2dbf3
me.xyzlast.gg.hollywood.AgentFinderImpl@7b49cea0
me.xyzlast.gg.hollywood.AgentFinderImpl@7b49cea0

Singleton으로 설정된 객체는 호출될 때, 기존의 객체가 있는 경우에는 그 객체를 계속해서 사용하게 됩니다. 그렇지만, 아무것도 설정되지 않은 객체의 경우에는 마치 new를 통해서 생성되는 객체와 동일한 패턴을 따르게 됩니다.


Posted by Y2K
,