Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 4장 IoC 컨테이너 #3

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.




4.4 의존성

일반적인 엔터프라이즈 어플리케이션은 하나의 객체(또는 스프링 용어로는 빈)로 이루어지지 않는다. 가장 간단한 어플리케이션에서 조차도 엔드유저가 하나의 긴밀한 어플리케이션처럼 느낄 수 있도록 함께 동작하는 약간의 객체들이 있다. 다음 섹션은 목표를 이루기 위해 객체들이 협력하는 완전히 구현된 어플리케이션에서 독립적인 다수의 빈을 어떻게 정의하는지 설명한다.

4.4.1 의존성 주입

의존성 주입 (DI)은 객체들이 같이 동작할 객체들의 의존성을 생성자 아규먼트와 팩토리 메서드의 아규먼트와 생성되거나 팩토리 메서드에서 리턴된 객체 인스턴스에 설정된 프로퍼티만으로 정의하는 과정이다. 그 다음 컨테이너는 빈을 생성할 때 빈들의 의존성을 주입한다. 이 과정은 기본적으로 빈 스스로 인스턴스화를 제어하거나 자신의 의존성을 클래스의 생성자를 직접 사용해서 결정하는 것과는 거꾸로 된 것(그래서 제어의 역전 (IoC)라고 부른다.)이다. 또는 서비스 로케이터 패턴도 있다.

DI 원리를 적용하면 코드는 더 깔끔해지고 객체들이 의존성과 함께 제공될 때 디커플링이 더욱 효과적이다. 객체는 자신의 의존성을 검색하지 않고 의존성의 위치나 클래스를 알지 못한다. 이처럼 클래스들은 테스트하기 쉬워지고 특히 유닛 테스트에서 사용하는 스텁(stub)이나 목(mock) 구현체를 사용할 수 있는 인터페이스나 추상 기반 클래스에 의존성이 있을 때 더욱 쉬워진다.

DI에는 2가지의 큰 변형이 존재한다. 생성자 기반의 의존성 주입과 Setter 기반의 의존성 주입이다.

4.4.1.1 생성자 기반의 의존성 주입

생성자 기반의 DI는 의존성을 나타내는 다수의 아규먼트로 생성자를 호출하는 컨테이너에서 이뤄진다. 빈을 생성하려고 아규먼트를 지정해서 static 팩토리 메서드를 호출하는 것과 거의 비슷하다. 이 말은 생성자에 대한 아규먼트와 static 팩토리 메서드에 대한 메서드를 비슷하게 다룬다는 의미다. 다음 예제는 생성자 주입으로만 의존성 주입이 될 수 있는 클래스를 보여준다. 클래시는 특별한 클래스가 아니고 컨테이너에서 특정한 인터페이스나 기반 클래스, 어노테이션에 의존성이 없는 POJO다.


public class SimpleMovieLister {

  // SimpleMovieLister는 MovieFinder에 의존성이 있다
  private MovieFinder movieFinder;

  // 스프링 컨테이너가 MovieFinder를 '주입'할 수 있도록 하는 생성자
  public SimpleMovieLister(MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }

  // 주입된 MovieFinder를 실제로 '사용'하는 비즈니스 로직은 생략했다...
}

생성자 아규먼트 결정

생성자 아규먼트는 아규먼트의 타입을 사용해서 결정한다. 빈 정의의 생성자 아규먼트에 잠재적인 불확실성은 전혀 없고 빈 정의에서 정의된 생성자 아규먼트의 순서는 빈이 인스턴스화 될 때 적절한 생성자에 전달할 아규먼트의 순서이다. 다음 클래스를 보자.


package x.y;

public class Foo {

  public Foo(Bar bar, Baz baz) {
      // ...
  }
}


Bar 클래스와 Baz 클래스가 상속관계가 아니라고 가정하면 잠재적인 불확실성이 존재하지 않는다. 그러므로 다음의 설정은 잘 동작하고 생성자 아규먼트 인덱스나 <constructor-arg/> 요소에 명시적으로 타입을 지정할 필요가 없다.


<beans>
  <bean id="foo" class="x.y.Foo">
      <constructor-arg ref="bar"/>
      <constructor-arg ref="baz"/>
  </bean>

  <bean id="bar" class="x.y.Bar"/>
  <bean id="baz" class="x.y.Baz"/>

</beans>


또 다른 빈이 참조되되었을 때 타입을 알고 있으면 매치할 수 있다.(앞의 예제와 같이) <value>true<value>처럼 간단한 타입이 사용되면 스프링은 value의 타입을 결정할 수 없다. 그래서 다른 정보가 없으면 타입을 매치할 수 없다. 다음 클래스를 보자.


package examples;

public class ExampleBean {

  // Ultimate Answer를 계산하는 연도
  private int years;

  // 삶, 전세계, 모든 것들에 대한 대답
  private String ultimateAnswer;

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


생성자 아규먼트 타입 매칭

앞의 시나리오에서 type 속성을 사용해서 생성자 아규먼트의 타입을 명시적으로 지정하면 컨테이너는 간단한 타입의 타입매칭을 할 수 있다. 다음 예제를 보자.


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


생성자 아규먼트 인덱스

명시적으로 생성자 아규먼트의 인덱스를 지정하려면 index 속성을 사용해라. 다음 예제를 보자.

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


다수의 간단한 값이 가지는 불확실성을 해결하는데 추가로 같은 타입의 두 아규먼트를 가지는 생성자의 불확실성은 인덱스를 지정해서 해결한다. 참고로 index는 0부터 시작한다.

생성자 아규먼트 이름

스프링 3.0처럼 명확한 값을 위해 생성자 파라미터 이름도 사용할 수 있다.

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



이 코드가 동작하려면 스프링이 생성자에서 파라미터명을 검색할 수 있도록 debug 플래그를 사용 가능 하도록 해놓고 컨파일 해야 한다. debug 플래그를 사용 가능으로 해도 컴파일할 수 없다면(또는 debug 플래그를 사용하고 싶지 않다면) 생성자 아규먼트의 이름을 명시적으로 짓는 @ConstructorProperties JDK 어노테이션을 사용할 수 있다. 예제 클래스는 다음과 같다.


package examples;

public class ExampleBean {

  // 필드는 생략한다

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




4.4.1.2 Setter 기반의 의존성 주입

Setter 기반의 DI는 빈을 인스턴스화 하기 위해 아규먼트가 없는 생성자나 아규먼트가 없는 static 팩토리 메서드를 호출한 뒤에 빈의 setter 메서드를 호출하는 컨테이너로 이뤄진다.

다음 예제는 순수한 setter 주입을 사용해서만 의존성 주입이 되는 클래스를 보여준다. 이 클래스는 평범한 자바 클래스다. 컨테이너에 특정 인터페이스나 기반 클래스, 어노테이이션에 의존성이 없는 POJO이다.


public class SimpleMovieLister {

  // SimpleMovieLister는 MovieFinder에 의존성이 있다
  private MovieFinder movieFinder;

  // 스프링 컨테이너가 MovieFinder를 '주입'할 수 있는 setter 메서드
  public void setMovieFinder(MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }

  // 실제 주입된 MovieFinder를 '사용'하는 비즈니스 로직은 생략한다...
}



ApplicationContext는 빈이 관리하는 생성자 기반의 DI와 setter 기반의 DI를 지원한다. 생성자 방법으로 이미 주입된 의존성에 대해서 setter 기반의 DI도 지원한다. 프로퍼티를 하나의 형식에서 다른 형식으로 변환하는 PropertyEditor 인스턴스와 함께 사용하는 BeanDefinition의 형식으로 의존성을 설정한다. 하지만 대부분의 스프링 사용자들은 이러한 클래스들을 직접 사용하지 않고(프로그래밍적으로) 내부적으로 이러한 클래스의 인스턴스로 변환되는 XML 정의 파일을 사용해서 전체 스프링 IoC 컨테이너 인스턴스를 로드한다.

생성자 기반의 DI냐? setter 기반의 DI냐?

생성자 기반 DI와 Setter 기반 DI를 섞어서 사용할 수 있으므로 경험에 의하면 강제적인 의존성은 생성자 아규먼트를 사용하고 선택적인 의존성은 setter를 사용하는 것이 좋다. setter에 @Required 어노테이션을 사용해서 의존성이 꼭 필요한 setter를 만들 수 있다.

스프링 팀은 보통 setter 주입을 더 좋아한다. 왜냐하면, 생성자 아규먼트가 많아지면 다루기가 쉽지 않기 때문이다. 특히 프로퍼티가 선택적이면 더욱 다루기가 어렵다. setter 메서드는 나중에 다시 설정하거나 다시 주입해야 하는 클래스의 객체들을 만들 수도 있다. JMX MBean을 통한 관리는 강제적인 유즈케이스이다.

몇몇 순수주의자들은 생성자 기반의 주입을 좋아한다. 모든 객체의 의존성을 제공한다는 것은 객체가 항상 클라이언트한테 완전히 초기화된 상태를 리턴한다는 것을 의미한다. 이 방법은 단점은 객체가 재구성이나 재주입하기가 쉽지 않아진다는 것이다.

개별 클래스에 적절한 DI를 사용해라. 때때로 소스코드가 없는 서드파티 클래스를 다룰 때는 어떤 DI를 사용할 지 직접 선택해야 한다. 레거시 클래스는 어떤 setter 메서드도 제공하지 않을 수 있으므로 생성자 주입이 이용 가능한 유일한 DI일 것이다.


4.4.1.3 의존성 해결 과정

컨테이너는 빈의 의존성 처리를 다음과 같이 처리한다.
  1. ApplicationContext는 빈에 대한 모든 정보를 담고 있는 설정 메타데이터로 생성되고 초기화된다. 설정 메타데이터는 XML이나 자바 코드, 어노테이션으로 명시할 수 있다.
  2. 프로퍼티나 생성자 아규먼트, 일반적인 생성자 대신 사용하는 정적-팩토리 메서드의 형식으로 각 빈의 의존성을 나타낸다. 빈이 실제로 생성될 때 이러한 의존성을 빈에 제공한다.
  3. 각 프로퍼티나 생성자 아규먼트는 설정하려는 값에 대한 실제 정의이거나 컨테이너의 다른 빈에 대한 참조이다.
  4. 각 프로퍼티나 생성자 아규먼트의 값은 명시된 타입에서 해당 프로퍼티나 생성자 아규먼트의 실제 타입으로 변환된다. 기본적으로 스프링은 문자열 형식으로 제공된 값을 int, long, String, boolean 같은 내장된 모든 타입으로 변환할 수 있다.
스프링 컨테이너는 컨테이너가 생성될 때 빈 참조 프로퍼티가 유효한 빈을 참조하는지 등과 같은 각 빈의 설정에 대한 유효성을 확인한다. 하지만 빈 프로퍼티 자체는 빈이 실제로 생성될 때까지 설정되지 않는다. 싱글톤 스코프이면서 미리 초기화된(기본값) 값으로 설정되는 빈은 컨테이너가 생성되면서 생성된다. 스코프가 Section 4.5, “Bean scopes”에 정의되지 않으면 빈은 요청되었을 때만 생성된다. 빈을 생성하면서 빈의 의존성과 의존성에 대한 의존성(등등)이 생성되고 할당되는 것과 같은 빈의 그래프를 발생시킬 가능성이 있다.

보통 스프링이 일을 제대로 하고 있다고 신뢰할 수 있다. 스프링은 컨테이너는 존재하지 않는 빈에 대한 참조나 순환 의존성같은 설정 문제를 로딩하면서 찾아낸다. 스프링은 빈이 실제로 사용되는 순간까지 가능한한 늦게 프로퍼티들을 설정하고 의존성을 해결한다. 이 말은 객체나 객체의 의존성을 생성하는 데 문제가 있다면 제대로 로딩된 스프링 컨테이너가 객체가 요청되었을 때 예외를 생성할 수 있다는 의미이다. 예를 들어 빈은 누락되거나 유효하지 않은 프로퍼티같은 예외를 던진다. ApplicationContext가 기본적으로 미리 초기화된 싱글톤 빈으로 구현된 이유는 몇몇 설정 이슈를 나중에 발견할 수 있는 가능성이 있기 때문이다. 이러한 빈이 실제로 필요하기 전에 생성하기 위한 시간과 메모리에 대한 비용을 들이면 설정이슈를 나중에 발견하는 대신에 ApplicationContext를 생성할 때 발견할 수 있다. 싱글톤 빈이 미리 초기화되는 대신에 지연된 초기화를 사용하도록 이러한 기본적인 행동을 오버라이드할 수 있다.

순환 의존성

생성자 주입을 주로 사용한다면 해결할 수 없는 순환 의존성 시나리오가 발생할 가능성이 있다.

예를 들어 보자. 클래스 A가 생성자 주입으로 클래스 B의 인스턴스를 사용하고 클래스 B도 생성자 주입으로 클래스 A의 인스턴스를 사용한다. 서로 주입하는 클래스 A와 B의 빈을 설정하면 스프링 IoC 컨테이너는 런타임시에 이 순환 참조를 찾아내서 BeanCurrentlyInCreationException를 던진다.

어떤 클래스들의 소스코드를 생성자 대신에 setter를 이용한 설정으로 변경하는 것이 한가지 해결책이 될 수 있다. 아니면 생성자 주입을 피하고 setter 주입만 사용해라. 다시 말하자면 setter 주입으로 순환 의존성을 설정할 수 있지만 별로 권장하는 방법은 아니다.

(순환 의존성이 없는) 전형적인 경우와는 다르게 빈 A와 빈 B 사이의 순환 의존성은 스스로 완전히 초기화될 수 있도록 하나의 빈이 다른 빈에 우선적으로 주입되게 강제한다.(고전적인 닭이 먼저냐 달걀이 먼저냐 이야기이다.)

순환 의존성이 존재하지 않다면 의존하는 빈에 하나 이상의 연관된 빈을 주입할 때 각 연관 빈은 의존하는 빈에 주입하기 위해 먼저 완전히 설정된다. 이는 빈 A가 빈 B에 의존성이 있을 때 스프링 IoC 컨테이너는 빈 A에 setter 메서드를 호출하려고 빈 B를 먼저 완전히 설정한다는 의미이다. 다시 말하면 빈을 먼저 인스턴스화하고(미리 인스턴스화되는 싱글턴이 아니라면) 빈의 의존성이 설정하고 관련 있는 라이프 사이클 메서드(설정된 init 메서드나 InitializingBean 콜백 메서드 같은)를 호출한다.


4.4.1.4 의존성 주입 예제

다음 예제는 setter 기반의 DI를 위해 XML기반의 설정 메타데이터를 사용한다. 스프링 XML 설정파일에서 약간의 빈 정의를 지정했다.


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



앞의 예제에서 XML파일에서 명시된 프로퍼티에 대응되는 setter를 선언했다. 다음 예제는 생성자 기반의 DI를 사용한다.


<bean id="exampleBean" class="examples.ExampleBean">

<!-- 중첩된 <ref/>를 사용하는 생성자 주입 -->
<constructor-arg>
  <ref bean="anotherExampleBean"/>
</constructor-arg>

<!-- constructor injection using the neater 'ref' attribute -->
<!-- 더 세련된 'ref' 속성을 사용하는 생성자 주입 -->
<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;
  }
}



빈 정의에 명시된 생성자 아규먼트는 ExampleBean의 생성자에 대한 아규먼트로 사용된다.

이제 생성자 대신에 사용하는 다른 형태의 예제를 보자. 스프링은 객체의 인스턴스를 받으려고 static 팩토리 메서드를 호출한다.


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




public class ExampleBean {

  // private 생성자
  private ExampleBean(...) {
    ...
  }
  
  // 정적 팩토리 메서드. 이 팩토리 메서드의 아규먼트는 아규먼트가 실제로 
  // 어떻게 사용되는가에 상관없이 리턴되는 빈의 의존성으로 간주될 수 있다.
  public static ExampleBean createInstance (AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

      ExampleBean eb = new ExampleBean (...);
      // 다른 작업들...
      return eb;
  }
}



static 팩토리 메서드의 아규먼트들은 <constructor-arg/> 요소로 제공되고 이는 생성자가 실제로 사용되는 것과 완전히 같다. 이 예제에서는 같은 타입이었지만 팩토리 메서드가 리턴하는 클래스의 타입은 static 팩토리 메서드가 담고 있는 클래스와 같은 타입이어야 할 필요는 없다. 인스턴스 (static이 아닌) 팩토리 메서드는 본질적으로 동일하므로(class 속성 대신에 factory-bean 속성을 사용하는 것은 제외하고) 자세한 내용을 여기서 이야기하지 않을 것이다.
2012/03/03 02:06 2012/03/03 02:06