본문 바로가기

Spring

[Spring 공식문서] Spring Core 1.4

😅 우선 글을 작성하기에 앞서 이 글은 스프링을 처음 공부하는 입장에서 Spring 공식 문서를 자의적으로 해석한 글이며 (번역기의 도움을 받아) 제게 필요하다고 생각되는 부분을 우선적으로 해석할 예정이라 중간중간 빈 부분이 있을 수 있으며 틀린 내용이 포함되었을 수 있습니다. 잘못된 부분이 있을 때 피드백 남겨주시면 능력껏 반영하도록 하겠습니다.

 

1.4 Dependencies

  일반적인 어플리케이션들은 하나의 객체(스프링에서의 빈)로 구성되어있지 않습니다. 간단한 프로그램이라도 몇몇 객체들이 서로 협력하여 사용자들에게 기능을 제공합니다. 해당 파트는 당신이 어떻게 독립되어 있는 수많은 빈들이 협력하도록 하여 원하는 기능을 제공할 수 있는지 설명합니다.

 

1.4.1 Dependency Injection (의존성 주입)

  의존성 주입은 객체들이 자신들이 정의한 의존성에 따라 진행되는 절차입니다. (이 때 객체들의 의존성은 생성자, 팩토리 메서드의 인자, 객체의 인스턴스 생성 후 인스턴스 메서드를 사용한 주입으로만 정의될 수 있습니다.) 스프링 컨테이너는 빈을 생성할 때 빈들의 종속성을 주입합니다. 이 과정은 기본적으로 빈이 자체적으로 클래스 또는 서비스 로케이터 패턴을 직접 사용하여 종속성의 인스턴스화 또는 위치를 제어하는 방식과 반대(Inverse of Control)됩니다.

  코드는 DI 원칙을 사용하여 더 깔끔해지고, 객체에 그들의 종속성이 제공될 때 분리가 더욱 효과적입니다. 객체는 자신이 어느 객체에 의존하는지 볼 수 없고 종속성의 위치나 클래스를 알 수 없습니다. 결과적으로 당신의 클래스들은 더 테스트하기 쉬워지고, 특히 종속성이 인터페이스나 추상화 클래스에 존재할 때 stub이나 mock을 통해 단위 테스트를 쉽게 수행할 수 있게 됩니다.

의존성 주입은 두 가지의 주요한 방식이 있습니다.

  • Constructor-based dependency injection
  • Setter-based dependency injection

 

Constructor-based Dependency Injection

생성자 기반 의존성 주입은 스프링 컨테이너가 각각의 의존성을 의미하는 여러 인자를 전달받는 생성자를 호출하면서 이루어지게 됩니다. 특정한 인자들을 받는 정적 팩토리 메서드를 호출하여 빈을 생성할 때도 거의 동일한 방식으로 진행됩니다. 아래 예시는 생성자 주입으로 의존성 주입이 되는 예시를 보여줍니다.

public calss SimpleMovieLister {
	// SimpleMovieLister는 MovieFinder에 대해 의존성을 가집니다
  private final MovieFinder movieFinder;

  // 생성자를 통해 컨테이너가 MovieFinder를 주입해줍니다
  public SimpleMovieLister(MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }

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

 

Constructor Argument Resolution

Constructor argument resolution 매칭은 argument들의 타입을 사용해서 이루어집니다. 빈 정의의 생성자 인자에 모호한 부분이 없다면, 빈 정의에 적힌 생성자의 순서가 실제 빈이 만들어질 때 생성자의 인자에 전달되는 순서가 됩니다.

package x.y;

public class ThingOne {

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

 위 예시에서 ThingTwo와 ThingThree 클래스가 서로 상관관계나, 모호함이 없다고 가정해봅시다. 그렇다면 아래의 configuration(구성 정보)는 잘 작동할 것이고 당신은 추가적인 생성자 인자 index나 type 등을 <constructor-arg/> 요소에 구체적으로 작성할 필요가 없습니다.

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

  다른 빈이 참조되면 타입이 알려지기 때문에 매칭이 이루어집니다. (앞의 예시와 같이) <value>true</value>와 같이 기본형이 사용되는 경우, 스프링은 value의 타입을 결정할 수 없기 때문에 추가적인 도움 없이 매칭시킬 수 없습니다. 다음 예시를 살펴봅시다.

package examples;

public class ExampleBean {

    // 모든 것에 대한 답을 계산하기 위한 year의 숫자
    private final int years;

    // 모든 것에 대한 답
    private final 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>

  value의 모호함을 없애기 위해서 name 속성을 활용할 수도 있습니다.

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

  이 작업을 즉시 수행하려면 스프링이 생성자에서 인자의 이름을 볼 수 있도록 debug flag(?)를 허용한 채로 컴파일해야 합니다. Debug flag를 사용하여 컴파일하는 것을 원하지 않는다면, @ConstructorProperties JDK 어노테이션을 사용하여 명시적으로 생성자 인자의 이름을 알려줄 수 있습니다. 예시는 다음과 같습니다.

package examples;

public class ExampleBean {

    // Fields omitted

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

 

Setter-based Dependency Injection

  Setter 기반 의존성 주입은 스프링 컨테이너가 생성자나 팩토리 메서드를 통해 인자가 전달되지 않는 객체의 인스턴스를 생성한 뒤에 setter 메서드를 호출하는 경우에 이루어집니다.

public class SimpleMovieLister {

    // SimpleMovieLister는 MovieFinder에 대해 의존성을 가진다
    private MovieFinder movieFinder;

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

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

  ApplicationContext는 자신이 관리하는 빈에 대해 생성자 기반과 setter 기반 DI를 지원합니다. 또한 생성자 기반 방식을 통해 일부 의존성이 이미 주입된 후에도 setter 기반 의존성 주입을 지원합니다. 당신은 properties의 형식을 마꿔주기 위해 PropertyEditor를 사용하여 BeanDefinition을 구성할 수 있습니다. 하지만 대부분의 스프링 사용자들은 해당 방법을 사용하는 대신 XML 빈 정의, annotated components(ex. @Component, @Controller) 혹은 자바 기반 어노테이션(ex. @Bean)을 사용합니다. 위의 요소들은 내부적으로 BeanDefinition 인스턴스로 변환시킨 뒤 스프링 컨테이너에 올려줍니다.

(참고) Constructor-based or setter-based DI?

  생성자 기반 방식과 setter 기반 방식은 혼합하여 사용할 수 있으므로, 필수적인 종속성은 생성자 기반으로 선택적 종속성은 setter 기반으로 관리할 수 있습니다. @Autowired 어노테이션을 사용하면 setter 기반 방식 역시 필수 종속성을 관리할 수 있습니다. 하지만, 일반적으로 생성자 기반 방식을 사용할 것을 권장합니다.

  스프링 팀 역시 생성자 기반 방식을 지지하는데, 이는 애플리케이션 구성 요소를 불변 객체로 구현하고 필수 종속성 객체가 null이 아님을 보장하기 때문입니다. 또한 생성자 주입 구성 요소는 항상 초기화된 상태로 클라이언트에게 전달됩니다. 부가적으로, 많은 수의 생성자 인수는 좋지 않은 코드이기 때문에 클래스가 너무 많은 책임을 가지고 있는지 확인할 수 있고 더 나은 관심사 분리를 위해 리팩토링되어야할 필요가 있음을 암시할 수 있습니다.

  Setter 메서들르 통한 주입은 반드시 클래스 내의 종속성이 필요한 value에 합리적인 dafault 값을 부여했을 때 사용하는 것을 권장합니다. 그렇지 않으면, 종속성을 주입할 때마다 null 체크를 필요로 합니다. setter 기반 방식의 이점 중 하나는 객체의 인스턴스가 만들어진 후 종속 객체를 재구성하거나 다시 주입할 수 있다는 것입니다.

 

Dependency Resolution Process

스프링 컨테이너는 다음과 같은 순서로 빈 종속성 해결을 수행합니다:

  • Application은 모든 빈들의 정보를 가지는 구성 메타데이터를 가지고 초기화되고 만들어집니다. 구성 메타데이터는 XML, 자바 코드, 어노테이션 등을 통해 구체화될 수 있습니다.
  • 빈에 대해, 각각의 종속성은 properties나 생성자나 정적 팩토리 메서드의 인자로 표현됩니다. 이러한 종속성 요소들은 빈이 실제로 만들어질 때 각 빈에 전달됩니다.
  • 각 property 또는 생성자 인수는 설정할 값의 실제 정의이거나 스프링 컨테이너 내의 다른 빈에 대한 참조입니다.
  • 값의 각 property 또는 생성자 인수는 처음 정의된 형식에서 해당 property 또는 생성자 인수의 실제 유형으로 변환됩니다. 기본적으로 스프링은 문자열 형식으로 제공된 값을 int, long, String, boolean등과 같은 모든 내장 유형으로 변환할 수 있습니다.
  • 스프링 컨테이너는 생성될 때 각 빈들의 구성을 검증합니다. 하지만 각 빈들의 properties는 실제로 빈이 생성될 때까지 설정되지 않습니다. Singleton 범위이고 사전 인스턴스화(기본값)로 설정된 Bean은 컨테이너가 생성될 때 생성됩니다. 빈의 범위는 Bean Scopes에 정의됩니다. 그렇지 않은 빈의 경우 요청되었을 때만 빈이 만들어집니다. 빈의 생성은 빈의 종속성 및 해당 종속성의 종속성이 생성되고 할당되어야 하는 빈의 그래프를 유발할 수 있습니다. 이러한 종속성 간의 해결 불일치는 늦게 나타날 수 있습니다. 즉, 영향을 받는 Bean을 처음 생성할 때 나타날 수 있습니다.

(참고) Circular dependencies (순환 종속)

  만약 당신이 주로 생성자 주입을 사용한다면, 해결 불가능한 순환 종속 상황을 만들 수 있습니다. 예를 들어 Class A는 Class B의 인스턴스를 필요로 하며 생성자 주입을 통해 주입받습니다. Class B는 Class A의 인스턴스를 필요로 하며 생성자 주입을 통해 주입받습니다. 만약 당신이 클래스 A, B가 서로를 주입받도록 빈을 구성하면, 스프링 IoC 컨테이너는 런타임에 순환 참조를 발견하고, BeanCurrentlyInCreationException을 던지게 됩니다.

  이에 대해 가능한 해결법 중 하나로 일부 클래스를 생성자 주입 대신 setter 메서드를 통해 주입되도록 코드를 수정하는 것입니다.

당신은 일반적으로 스프링이 모든 것을 잘해줄 것이라고 믿을 수 있습니다. 스프링은 존재하지 않는 빈에 대한 참조나 순환 참조와 같은 configuration 문제를 컨테이너 load-time에 찾아냅니다. 스프링은 빈이 실제로 생성될 때, 즉 최대한 천천히 properties를 설정하고 종속성을 해결합니다. 이는 올바르게 로드된 컨테이너도 객체 또는 종속성 중 하나를 생성하는 데 문제가 있는 객체를 요청할 때 나중에 예외를 발생시킬 수 있음을 의미합니다. 일부 configuration 문제들이 뒤늦은 시점에 발견될 수 있기 때문에 ApplicationContext는 기본적으로 싱글톤 빈을 미리 인스턴스합니다. 실제로 빈이 요청되기 전에 이러한 빈을 생성하는 데 약간의 선행 시간과 메모리를 사용하면 늦은 시점이 아닌, ApplicationContext이 생성될 때 configuration 문제를 발견하게 됩니다. 당신은 또한 이런 기본 동작을 재정의하여 싱글톤 빈이 사전 인스턴스화되지 않고 느리게 초기화되도록 할 수 있습니다.

 

Examples of Dependency Injection

다음 예시는 setter 기반 DI를 위해 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"/>
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;
    }
}

아래의 생성자 기반 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"/>
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;
    }
}

스프링은 또한 정적 팩토리 메서드를 통해 종속성을 주입하는 방식을 관리해줄 수 있다.

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

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

 

1.4.2. Dependencies and Configuration in Detail

1.4.3. Using depends-on

 

1.4.4. Lazy-initialized Beans

  기본적으로 ApplicationContext는 모든 싱글톤 빈을 초기화 단계에서 우선적으로 구성하고 생성합니다. 일반적으로 이런 사전 인스턴스화는 구성 단계나 주변 환경으로 인한 오류가 즉시 발견되기 때문에, 오류가 뒤늦게 발견되는 경우에 비교하면 이상적입니다. 만약 우선적인 초기화가 바람직하지 않다면 빈 정의를 lazy-initialized(지연 초기화)로 설정해주어 사전 인스턴스화를 방지할 수 있습니다. 지연 초기화된 빈은 스프링 컨테이너가 구성할 때 생성되는 것이 아닌 실제로 처음 요청될 때 생성됩니다. XML에서 이런 설정은 <bean/> 태그의 lazy-init 속성을 조절해줍니다.

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>

  다만 지연 초기화된 빈을 지연 초기화되지 않은 싱글톤 빈이 의존하는 경우 ApplicationContext는 지연 초기화하지 않고 사전 인스턴스화합니다.

 

1.4.5. Autowiring Collaborators

  스프링 컨테이너는 서로 협력하는 빈들 사이의 관계를 자동으로 연결해 줄 수 있습니다. (Autowiring) 스프링 컨테이너는 ApplicationContext를 조사해서 자동으로 빈들 사이의 관계를 해결해줄 수 있습니다. Autowiring의 이점은 다음과 같습니다.

  • 속성이나 생성자 파라미터를 지정해야 할 필요성을 줄일 수 있습니다.
  • 객체가 변화할 때 구성을 업데이트해줄 수 있습니다. 예를 들어, 클래스에 의존성을 추가할 필요가 있을 때, 구성을 수정할 필요없이 자동으로 의존성을 충족시켜줍니다.
  • XML 기반 구성 메타데이터를 사용할 때, <bean/> 의 autowire 속성에서 autowire를 사용하도록 설정해줄 수 있습니다. Autowiring 기능은 네 개의 모드를 가집니다. 당신은 빈마다 autowiring을 지정하고, autowiring할 항목을 선택할 수 있습니다. 다음은 네 가지 모드에 대해 설명합니다.

Autowiring modes

no

  • (Default) Autowiring 하지 않습니다. 빈 참조는 반드시 ref 속성에서 정의되어야 합니다. 빈들간의 협력관계를 명시적으로 지정하는 건 네어 능력과 명확성이 향상되므로, 대규모 배포에서는 기본 설정을 변경하지 않는 것을 추천합니다. 시스템의 구조를 어느 정도 문서화합니다.

byName

  • property 이름을 통해 autowring합니다. 스프링은 연결되어야 할 필요가 있는 property와 같은 이름을 가진 빈을 찾습니다.

byType

  • Autowired되어야 할 property 유형의 빈이 컨테이너 내에 하나만 있는 경우 해당 property를 연결합니다. 만약 하나 이상 존재할 경우, byType autowiring을 사용할 수 없다는 심각한 예외가 발생합니다. 만약 매칭되는 빈이 없다면 아무런 일도 벌어지지 않습니다.

constructor

  • byType 방식과 비슷하지만 생성자 인자에 적용됩니다. 만약 스프링 컨테이너 내에 일치하는 생성자 인자 타입이 정확히 하나만 존재하지 않는다면 예외가 발생합니다.

 

Limitations and Disadvantages of Autowiring

Autowiring은 프로젝트 전체에서 일관적으로 사용될 때 가장 잘 작동합니다. 일반적으로 autowiring을 사용하지 않으면서 일부 빈들을 위해 사용하는 것은 다른 개발자들에게 혼란을 줄 수 있습니다. Autowiring의 한계와 단점에 대해 고려해봅시다:

  • property와 constructor-arg 설정의 명시적인 의존성은 항상 autowiring 설정을 오버라이딩합니다. 당신은 기본형 타입, Strings, Classes와 같은 속성들을 autowire할 수 없습니다. 이는 설계적인 한계점입니다.
  • Autowiring은 명시적인 연결보다 덜 정확합니다. 스프링은 예상되지 않은 결과를 만들지 않기 위해 모호한 경우에 대해 추측을 피하도록 주의합니다.
  • 스프링 컨테이너에서 문서를 생성할 수 있는 도구에 빈들 간 연결 정보가 제공되지 않을 수 있습니다.
  • 컨테이너 내의 여러 빈 정의는 autowired될 세터 메서드 또는 생성자 파라미터에 의해 지정된 유형과 일치할 수 있습니다. 다만, 단일 값을 기대하는 의존성의 경우 모호함이 임의적으로 해결되지 않습니다.(?) 고유한 빈 정의를 사용할 수 없다면 예외가 발생합니다.

Excluding a Bean from Autowiring

  스프링에서 당신은 빈 객체마다 autowiring을 하지 않도록 제외시킬 수 있습니다. XML 형식에서 <bean/> 요소의 autowire-candidate 속성을 false로 설정하면 됩니다. 컨테이너는 특정 빈 정의를 autowiring 인프라에서 사용할 수 없도록 만듭니다. (@Autowired를 사용했더라도)

  당신은 또한 빈 이름에 대한 패턴 매칭에 기반에 autowire 후보를 제한할 수 있습니다. 최상위 계층 <bean/> 요소는 default-autowire-candidates 속성 내에서 하나 이상의 패턴에 대해 받을 수 있습니다. 예를 들어, ‘Repository’로 끝나는 이름을 가진 모든 빈에 대해 autowire 후보 상태를 제한하기 위해서 *Repository 값을 제공할 수 있습니다. 여러 개의 패턴에 대해서, comma로 구분되는 리스트로 그것들을 정의할 수 있습니다. 다만 빈 정의의 autowire-candidate 속성의 명시적인 true나 false 값이 언제나 우선입니다. 그런 빈들에 대해서, 패턴 매칭 법칙은 적용되지 않습니다.

 

1.4.6. Method Injection

  대부분의 어플리케이션에서, 스프링 컨테이너 내의 대부분의 빈은 싱글톤입니다. 싱글톤 빈이 다른 싱글톤 빈과 협력하거나 싱글톤이 아닌 빈이 다른 싱글톤이 아닌 빈과 협력할 때, 당신은 보통 하나의 빈은 다른 빈의 속성으로 정의해 종속성을 처리해줍니다. 문제는 빈들의 생명주기가 다를 때 발생합니다. 싱글톤 빈 A가 싱글톤이 아닌 빈 B를 필요로 한다고 가정해봅시다. 스프링 컨테이너는 빈 A를 단 한 번만 만들고, 따라서 A의 속성을 설정할 기회를 한 번만 얻게 됩니다. 스프링 컨테이너는 필요할 때마다 빈 B의 새 인스턴스를 빈 A에 제공할 수 없습니다.

  이 문제의 해결책은 IoC(제어 역전)을 일부 포기하는 겁니다. 당신은 ApplicationContextAware 인터페이스를 구현하여 빈 A가 컨테이너를 알도록 만든 뒤 빈 A가 새로운 B 인스턴스를 필요로 해서 요청할 때마다 컨테이너에게 요청을 할 수 있도록 `getBean("B")와 같은 메서드를 만들 수 있습니다. 다음 예시는 이러한 방법의 예시를 보여줍니다:

package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * stateful Command-style class를 사용하여 일부 처리를 수행하는 클래스
 */
public class CommandManager implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public Object process(Map commandState) {
        // 적절한 Command의 새 인스턴스을 가져온다
        Command command = createCommand();
        // Command 인스턴스의 상태를 설정한다
        command.setState(commandState);
        return command.execute();
    }

    protected Command createCommand() {
        // 스프링 API에게 의존성을 가지는 것을 볼수 있다!!
        return this.applicationContext.getBean("command", Command.class);
    }

    public void setApplicationContext(
            ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

  위의 예시는 비즈니스 코드가 스프링 프레임워크를 알고 연관되어 있어 이상적이지는 않습니다. 스프링 IoC 컨테이너의 발전된 기능인 method injection을 사용하면 이러한 경우를 깔끔하게 해결할 수 있습니다.

 

Lookup Method Injection

  Lookup method injection은 스프링 컨테이너가 자신이 관리하는 빈들의 메서드를 오버라이드한 뒤 컨테이너 내의 다른 명명된 빈들에 대한 결과를 반환할 수 있는 능력입니다. Lookup은 전형적으로 위 섹션의 내용(빈 A, 빈 B)와 같이 프로토타입 빈을 포함합니다. 스프링 프레임워크는 메서드를 재정의하는 하위 클래스를 동적으로 생성하기 위해 CGLIB 라이브러리의 바이트코드 생성을 사용하여 method injection을 구현합니다.

  • 동적 서브클래싱이 작동하려면, 스프링 빈 컨테이너 서브클래스는 final이 될 수 없고 재정의되는 메서드 역시 final이 될 수 없습니다.
  • 추상 메서드가 있는 클래스를 단위 테스트하려면 클래스를 직접 하위 클래스로 만들고 추상 메서드의 stub 구현을 제공해야 합니다.
  • 구체적인 클래스를 선택해야 하는 component scan을 위해서 콘크리트 메서드(?) 역시 필요로 합니다.
  • 추가적인 주요 제한사항은 lookup 메서드는 팩토리 메서드, 특히 @Bean 구성 클래스의 메서드와 함께 쓰일 수 없다는 것입니다. 이 경우 스프링 컨테이너가 인스턴스 생성을 담당하지 않으므로 인스턴스에서 런타임으로 서브클래스의 생성을 할 수 없기 때문입니다.
  • 예시 코드로 제시된 CommandManager 클래스의 경우, 스프링 컨테이너는 동적으로 createCommand() 메서드의 구현을 재정의합니다. CommandManager 클래스는 수정된 아래의 예시에서 스프링과 어떠한 종속성을 가지고 있지 않습니다:
package fiona.apple;

// 더 이상 스프링 요소를 import하지 않는다!

public abstract class CommandManager {

    public Object process(Object commandState) {
        // 적절한 Command 인터페이스의 새 인스턴스를 가져온다
        Command command = createCommand();
        // Command 인스턴스의 상태를 설정한다.
        command.setState(commandState);
        return command.execute();
    }

    // 좋아... 근데 이 메서드의 구현은 어디에 있는 거지?
    protected abstract Command createCommand();
} 

  주입받아야 할 메서드를 포함하는 클래스에서(위의 예시에서는 CommandManager) 메서드는 주입을 위해서 다음 형식의 signature를 요구합니다.

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

  만약 추상 메서드라면, 동적으로 생성된 하위 클래스가 해당 메서드를 구현합니다. 그렇지 않으면, 하위 클래스가 원래 클래스에 정의된 콘크리트 메서드를 재정의합니다. 다음 예시를 생각해봅시다:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>

  commandManager로 분류되는 빈은 myCommand 빈의 새로운 인스턴스가 필요할 때마다 자신의 createCommand() 메서드를 호출합니다. 실제로 myCommand 빈이 필요한 경우 프로토타입으로 배포할 때 주의해야 합니다. 해당 빈이 싱글톤인 경우 매번 myCommand 빈의 동일한 인스턴스가 반환됩니다. 대신 어노테이션 기반 구성 모델에서, 당신은 @Lookup을 통해 lookup 메서드를 사용할 수 있습니다:

public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup("myCommand")
    protected abstract Command createCommand();
}

또는, 보다 관용적으로 조회 메서드의 선언된 반환 유형에 대응되는 대상 빈에 의존할 수도 있습니다:

public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup
    protected abstract Command createCommand();
}

  추상 클래스가 기본적으로 무시되는 스프링의 component scan 규칙과 호환되도록 하려면, 통상적으로 이러한 어노테이션 기반 lookup 메서드는 구체적인 stub 구현으로 선언해야 합니다. 이러한 제약은 명시적으로 등록되거나 명시적으로 import된 빈에는 적용되지 않습니다.