본문 바로가기

IT 살이/04. 기술 - 프로그래밍

개발 프레임워크 만들기 대장정 23 - DI( Dependencies Injection ) 설정

IoC( Inverse of Control  제어권의 역전, 역제어)와 DI( Dependencies Injection)은 같은 의미로 사용된다. 시간적으로 보면 IoC라는 용어가 먼저 나왔고 뒤에 Martin Fowler라는 사람이 개념상 더 적절하지 않냐면서 내놓은 것이 DI다. 필자의 눈에는 차이점을 잘 모르겠고, 개발자에게는 당장 별로 중요한 차이는 아닐듯하다. 컨테이너가 객체를 생성할때 그 객체가 필요로 하는 의존 객체들을 자동 생성해서 할당해준다는 것이다.

컨테이너가 (대상)객체들을 생성할때 의존객체들을 할당해주기 위해서는 대상객체는 의존 객체들을 외부에서 받아들일 수 있는 public 입구(?)가 있어야 한다. 그래야 컨테이너가 대상 객체를 생성해서 공개된 입구로 객체를 할당해 줄 수 있다. 대상객체 내부에서 private으로 생성하는 객체들에 대해서는 컨테이너가 할당해줄 수 없다. 공개된 입구는 어떤 것이 있는가. 바로 public 생성자,public setter 속성,  public 메소드가 있다. 즉 contructor injection, setter injection, method injection DI가 수행될 수 있기 위해서는 각각에 해당하는 public API가 있어야 한다.

constructor injection : 파라미터가 없는 기본 constructor밖에 없다면 DI고 뭐고 수행될 것이 없겠지만, 만약 파라미터가 있는 공개 constructor라면 constructor injection의 대상이 될 수 있다. 즉 대상 객체를 컨테이너가 생성할때, 파라미터에 해당하는 필요한 인자들이 있다면 컨테이너가 알아서 스스로 인자들을 생성해서 대상 객체를 생성하는데 사용하는 것을 constructor injection이라고 한다.

namespace X.Y

{

    public class Foo

    {

        public Foo(Bar bar, Baz baz)

        {

            //...

        }

    }

    public class Bar

    {

        //...

    }


    public class Baz

    {

        //...

    }

}

코드에서는 Foo의 객체를 생성할때 Bar타입의 bar객체와 Baz 타입의 baz객체를 이용하고 있다. 코드에서 Foo객체를 요구하면 bar, baz를 자동으로 생성해서 이것들을 이용해서 Foo객체를 생성해서 반환해준다.

constructor injection의 대상이 되려면 대상 객체와 constructor의 파라미터들이 모두 컨테이너가 인식할 수 있도록 설정되어 있어야 한다. 설정 방법은 조금 후에 앞에서 봤던 예제의 web.config의 내용으로 알아볼 것이다.

setter injection : 대상객체가 필요한 의존 객체를 대상 객체에 할당해 주기 위해서는 당연히 getter 속성 메소드는 이용할 수 없다. 따라서 property injection이라고 해도 setter injection을 의미하게 된다. 그러나 property injection같은 용어는 아직 보지 못했다. setter 메소드를 이용해서 의존 객체를 할당하는데, 물론 쓰기 가능한 모든 속성을 자동 할당하지는 않는다. 대상 객체를 생성하고 나서 그 객체의 속성중에서 설정을 통해서 요청한 속성에 대해서만 injection이 발생한다.

대상객체를 의존 객체들로 초기화하기 위해서 constructor DI를 사용할지 setter DI를 사용할지에 대한 규칙은 없다. 직접 개발을 하고 있는 상태라면 개발자의 기호에 맞게 사용하면 될 것 같다. 그러나 이미 개발되어 있는 3th파티 클래스의 코드를 구할 수가 없다면 이미 구현되어 있는 상태에 따라 달라질 수 밖에 없다. 예를 들어 공개된 속성이 없고 모든 의존 객체들을 대상 객체 constructor에서 모두 받는다면 어쩔 수 없이 contructor DI를 사용해야 할 것이다.

constructor injection, setter injection에 비해서 method injection은 그렇게 많이 사용되지 않는다. 이것에 대해서는 추후에 알아보겠다.

이제 constructor, setter injection에 대한 설정을 알아보도록 하자.

\par ?? \par ?? <\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 CommonLoggingAroundAdvice\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects\cf0 "\cf2 >\par ??\tab \tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 Level\cf0 "\cf2 \cf6 value\cf2 =\cf0 "\cf2 Debug\cf0 "\cf2 />\par ??\tab \par ?? \par ?? \par ??\tab \par ?? <\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 calculator\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services\cf0 "\cf2 />\par ?? <\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 calculatorWeaved\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop\cf0 "\cf2 >\par ??\tab \tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 target\cf0 "\cf2 \cf6 ref\cf2 =\cf0 "\cf2 calculator\cf0 "\cf2 />\par ??\tab \tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 interceptorNames\cf0 "\cf2 >\par ??\tab \tab \tab <\cf13 list\cf2 >\par ??\tab \tab \tab \tab <\cf13 value\cf2 >\cf0 CommonLoggingAroundAdvice\cf2 \par ??\tab \tab \tab \par ??\tab \tab \par ??\tab \par ??\par ??} -->

<objects xmlns="http://www.springframework.net">


   <!-- Aspect -->

   <object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

    <property name="Level" value="Debug"/>

  </object>

   <!-- Service -->

   <object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

  <object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

    <property name="target" ref="calculator"/>

    <property name="interceptorNames">

      <list>

        <value>CommonLoggingAroundAdvice</value>

      </list>

    </property>

  </object>

</objects>

<objects/>요소안에 3개의 객체가 등록되어 있다. 첫번째 등록된 객체를 보면, 어셈블리 Spring.Aspects에 포함되어 있는 Spring.Aspects.Logging.CommonLoggingAroundAdvice 타입이 CommonLoggingAroundAdvice라는 id로 등록되어 있다. 그리고 이 객체는 public 속성으로 Level 이 있는데, 이것의 값을 "Debug"로 설정하고 있다. 지금은 Spring.Aspects.Logging.CommonLoggingAroundAdvice 타입이 뭐하는 녀석인지 몰라도 된다.

애플리케이션의 코드상에서 이 객체( id="CommonLoggingAroundAdvice")를 요청하게 되면 컨테이너는 일단 주어진 타입 정보를 통해서 인스턴스를 하나 생성하고 나서 Level 속성에 Debug 값을 설정한다.
애플리케이션이 시작되면 CommonLoggingAroundAdvice 인스턴스가 하나 생성되고( 기본적으로 Spring.NET 컨테이너에 생성되는 객체는 singleton이다), Level 속성에 Debug값을 설정한다.

\par ?? \par ?? <\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 CommonLoggingAroundAdvice\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects\cf0 "\cf2 >\par ??\tab \tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 Level\cf0 "\cf2 \cf6 value\cf2 =\cf0 "\cf2 Debug\cf0 "\cf2 />\par ??\tab \par ?? \par ?? \par ??\tab \par ?? <\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 calculator\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services\cf0 "\cf2 />\par ?? <\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 calculatorWeaved\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop\cf0 "\cf2 >\par ??\tab \tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 target\cf0 "\cf2 \cf6 ref\cf2 =\cf0 "\cf2 calculator\cf0 "\cf2 />\par ??\tab \tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 interceptorNames\cf0 "\cf2 >\par ??\tab \tab \tab <\cf13 list\cf2 >\par ??\tab \tab \tab \tab <\cf13 value\cf2 >\cf0 CommonLoggingAroundAdvice\cf2 \par ??\tab \tab \tab \par ??\tab \tab \par ??\tab \par ??\par ??} -->
근데 CommonLoggingAroundAdvice 타입의 Level 속성을 보면 반환값의 타입이 enum LogLevel 라는 열거형을 사용하고 있다. 그러나 여기에 할당될 값은 value어트리뷰트에 문자열 "Debug"이 설정되어 있다. 문자열을 LogLevel이라는 열거형에 할당할 수 있나? 없다. 그럼 어떻게 ? 똑똑한 Spring.NET 컨테이너는 대상 객체의 Level 속성이 LogLevel 열거형이라는 것을 알고 "Debug"를 열거형의 문자열 값중의 하나로 인식해서 LogLevel.Debug를 속성에 할당해준다. 즉 문자열 "Debug"를 열거형 값 LogLevel.Debug로의 변환을 자동으로 해준다. 이 과정에 TypeConvert라는 것이 사용되고 있다는 것을 알아두고 나중에 더 깊이 있는 공부를 할 필요가 있겠다.

두번째 객체는 "calculator"라는 id로 Spring.Calculator.Services.AdvancedCalculator 타입의 객체가 등록되고 있다. 이 객체에서는 DI를 위한 설정은 없다. 단순히 객체 등록만 하고 있다.

세번째로 등록되는 객체에서도 두 개의 setter injection을 위한 설정이 있다.  ProxyFactoryObject 타입을 보면 공개된 두개의 속성 Target, InterceptorNames이 있다. 대소문자는 구분하지 않고 있다.  대상객체 ProxyFactoryObject의 Target 속성은 object 타입을 받는 속성이다. <property/>요소의 ref 어트리뷰트를 사용해서 Target 속성에 할당될 객체를 지정하고 있다. ref 어트리뷰트의 값으로는 XML에 설정된 다른 객체의 id(또는 name)값을 지정해주면 된다. 두번째 속성 InterceptorNames는 문자열 배열 string[]이다. 문자열 배열을 지정할때는 <list/> 하위 요소를 사용해서 필요한대로 <value/>요소를 사용해서 값을 추가하면 된다.

ProxyFactoryObject를 코드에서 요청할때 컨테이너는 인스턴스를 하나 생성하고나서 두개의 공개 속성을 통해서 초기화를 마친 후 준비된 객체를 코드에 반환한다.

예제에서는 나와 있지 않지만, 앞에서 보인 Foo 객체에 대한 constructor injection을 위한 설정을 보이면 다음과 같다.

\par ?? <\cf13 constructor-arg\cf2 \cf6 name\cf2 =\cf0 "\cf2 arg\cf0 "\cf2 \cf6 value\cf2 =\cf0 "\cf2 strArg\cf0 "\cf2 />\par ??\par ??<\cf13 obejct\cf2 \cf6 id\cf2 =\cf0 "\cf2 barObject\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 X.Y.Bar,X.Y\cf0 "\cf2 />\par ??<\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 bazObject\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 X.Y.Baz,X.Y\cf0 "\cf2 />\par ??} -->

<object id="fooObject" type="X.Y.Foo, X.Y">

  <constructor-arg name="bar" ref="barObject"/>

  <constructor-arg name="baz" ref="bazObject"/>

  <!--추가된 인자-->

  <constructor-arg name="arg" value="strArg"/>

</object>

<obejct id="barObject" type="X.Y.Bar,X.Y"/>

<object id="bazObject" type="X.Y.Baz,X.Y"/>

컨테이너가 생성되면서 설정된 모든 객체들에 대한 정보(Object definitions)들이 컨테이너에 로딩된다.  그리고 나서 DI가 수행되는 것은 실제로 대상 객체에 대한 인스턴스가 요청될때이다. DI가 수행될때 의존 객체들은 XML 설정 파일이 아니라 컨테이너의 로딩된 object definitions에서 검색된다. 따라서 XML상 의존 객체들이 대상 객체보다 뒤에 설정되어 있어도 상관없다.

앞에서 소개한 문서의 5.3. Dependencies 절을 보면 알겠지만, DI를 위한 설정 표현이 여러 버전이 있다. 예를 들어 다음과 같은 표현도 있다.

<property name="target">

   <ref object="calculator"/>

</property>

각기 표현마다 다른 장단점이 있을 수 있다. 또한 이곳에는 기타 여러가지 설정을 위한 요소 및 어트리뷰트에 대해서 설명한 내용이 있다. 한번 정독해보는 것도 좋을 듯 싶다.

오늘은 여기까지.  자자!

추가 설명

컨테이너에 등록을 했으면 이제 코드에서 객체를 얻는 API도 필요할 것이다. Spring 컨테이너에 등록( 또는 생성)되어 있는 객체에 대한 참조를 얻는 API는 다음과 같다.

IApplicationContext ctx = ContextRegistry.GetContext();

IAdvancedCalculator firstCalc = (IAdvancedCalculator) ctx.GetObject("calculatorService");

IApplicationContext는 System.Core.dll의 Spring.Context 네임스페이스에 포함되어 있다.