Method Injection(한글)

대부분의 애플리케이션 시나리오에서 컨테이너에 있는 대부분의 Bean은 싱글톤입니다. 싱글톤 Bean이 다른 싱글톤 Bean과 협업해야 하거나 비싱글톤 Bean이 다른 비싱글톤 Bean과 협업해야 하는 경우, 일반적으로 한 Bean을 다른 Bean의 속성으로 정의하여 의존성을 처리합니다. Bean의 라이프사이클이 서로 다른 경우 문제가 발생합니다. 싱글톤 Bean A가 A에서 메서드를 호출할 때마다 비싱글톤(프로토타입) Bean B를 사용해야 한다고 가정해 보겠습니다. 컨테이너는 싱글톤 Bean A를 한 번만 생성하므로 속성을 설정할 기회가 한 번만 주어집니다. 컨테이너는 Bean A가 필요할 때마다 Bean B의 새 인스턴스를 제공할 수 없습니다.

해결책은 제어의 역전을 포기하는 것입니다. ApplicationContextAware 인터페이스를 구현하고 컨테이너에 대한 getBean("B") 호출을 통해 Bean A가 필요할 때마다 (일반적으로 새로운) Bean B 인스턴스를 요청하여 컨테이너를 인식하도록 만들 수 있습니다. 다음 예제는 이 접근 방식을 보여줍니다:

  • Java

  • Kotlin

package fiona.apple;

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

/**
 * 상태 저장 Command-style 클래스를 사용하여 일부 처리를 수행하는 클래스입니다.
 */
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() {
		// Spring API 종속성에 주목하세요!
		return this.applicationContext.getBean("command", Command.class);
	}

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

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

// A class that uses a stateful Command-style class to perform
// some processing.
class CommandManager : ApplicationContextAware {

	private lateinit var applicationContext: ApplicationContext

	fun process(commandState: Map<*, *>): Any {
		// grab a new instance of the appropriate Command
		val command = createCommand()
		// set the state on the (hopefully brand new) Command instance
		command.state = commandState
		return command.execute()
	}

	// notice the Spring API dependency!
	protected fun createCommand() =
			applicationContext.getBean("command", Command::class.java)

	override fun setApplicationContext(applicationContext: ApplicationContext) {
		this.applicationContext = applicationContext
	}
}

비즈니스 코드가 Spring 프레임워크를 인식하고 결합되어 있기 때문에 앞의 방식은 바람직하지 않습니다. Spring IoC 컨테이너의 다소 고급 기능인 메서드 주입을 사용하면 이 사용 사례를 깔끔하게 처리할 수 있습니다.

메서드 주입의 동기에 대한 자세한 내용은 이 블로그 항목에서 확인할 수 있습니다.

Lookup 메서드 주입(Lookup Method Injection)

Lookup 메서드 주입은 컨테이너가 컨테이너에서 관리되는 Bean의 메서드를 재정의하고 컨테이너의 다른 명명된 Bean에 대한 조회 결과를 반환하는 컨테이너의 기능입니다. 조회에는 일반적으로 이전 섹션에 설명된 시나리오에서와 같이 프로토타입 Bean이 포함됩니다. Spring 프레임워크는 CGLIB 라이브러리에서 바이트코드 생성을 사용하여 메서드를 재정의하는 서브클래스를 동적으로 생성함으로써 이 메서드 주입을 구현합니다.

  • 이 동적 서브클래싱이 작동하려면 Spring Bean 컨테이너가 서브클래싱하는 클래스가 final 일 수 없으며, 재정의할 메서드도 final 일 수 없습니다.

  • abstract 메서드가 있는 클래스를 단위 테스트하려면 클래스를 직접 서브클래싱하고 abstract 메서드의 스텁 구현을 제공해야 합니다.

  • 컴포넌트 스캐닝에도 구체적인 메서드가 필요하며, 이를 위해서는 구체적인 클래스가 필요합니다.

  • 또 다른 주요 제한 사항은 Lookup 메서드가 팩토리 메서드, 특히 구성 클래스의 @Bean 메서드에서는 작동하지 않는다는 것입니다. 이 경우 컨테이너가 인스턴스 생성을 담당하지 않으므로 런타임에 생성된 서브클래스를 즉시 생성할 수 없기 때문입니다.

이전 코드 조각의 CommandManager 클래스의 경우, Spring 컨테이너는 createCommand() 메서드의 구현을 동적으로 재정의합니다. 재작업된 예제에서 볼 수 있듯이 CommandManager 클래스에는 Spring 종속성이 없습니다:

  • Java

  • Kotlin

package fiona.apple;

// 더 이상 스프링을 가져오지 않아도 됩니다!

public abstract class CommandManager {

	public Object process(Object commandState) {
		// 적절한 명령 Command의 새 인스턴스를 가져옵니다.
		Command command = createCommand();
		// (Command) 명령 인스턴스에서 상태를 설정합니다.
		command.setState(commandState);
		return command.execute();
	}

	// 좋아요... 하지만 이 방법의 구현은 어디에 있나요?
	protected abstract Command createCommand();
}
package fiona.apple

// no more Spring imports!

abstract class CommandManager {

	fun process(commandState: Any): Any {
		// grab a new instance of the appropriate Command interface
		val command = createCommand()
		// set the state on the (hopefully brand new) Command instance
		command.state = commandState
		return command.execute()
	}

	// okay... but where is the implementation of this method?
	protected abstract fun createCommand(): Command
}

주입할 메서드가 포함된 클라이언트 클래스(이 경우 CommandManager)에서 주입할 메서드에는 다음 형식의 서명이 필요합니다:

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

메서드가 'abstract' 인 경우 동적으로 생성된 서브클래스가 메서드를 구현합니다. 그렇지 않으면 동적으로 생성된 서브클래스가 원래 클래스에 정의된 구체적인 메서드를 재정의합니다. 다음 예제를 살펴보세요:

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

명령 관리자`로 식별된 Bean은 myCommand Bean의 새 인스턴스가 필요할 때마다 자체 createCommand() 메서드를 호출합니다. 실제로 필요한 경우 myCommand Bean을 프로토타입으로 배포하는 데 주의해야 합니다. 싱글톤인 경우, 매번 동일한 myCommand Bean 인스턴스가 반환됩니다.

또는 다음 예제에서 볼 수 있듯이 어노테이션 기반 컴포넌트 모델 내에서 @Lookup 어노테이션을 통해 Lookup 메서드를 선언할 수 있습니다:

  • Java

  • Kotlin

public abstract class CommandManager {

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

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

	fun process(commandState: Any): Any {
		val command = createCommand()
		command.state = commandState
		return command.execute()
	}

	@Lookup("myCommand")
	protected abstract fun createCommand(): Command
}

또는 더 관용적으로, Lookup 메서드의 선언된 반환 유형에 대해 대상 Bean이 확인되는 것에 의존할 수 있습니다:

  • Java

  • Kotlin

public abstract class CommandManager {

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

	@Lookup
	protected abstract Command createCommand();
}
abstract class CommandManager {

	fun process(commandState: Any): Any {
		val command = createCommand()
		command.state = commandState
		return command.execute()
	}

	@Lookup
	protected abstract fun createCommand(): Command
}

추상 클래스가 기본적으로 무시되는 Spring의 구성 요소 검색 규칙과 호환되도록 하려면 일반적으로 이러한 주석이 달린 조회 메서드를 구체적인 (역자설명 : 가짜구현이라고 생각하면 편함)스텁 구현으로 선언해야 합니다. 이 제한은 명시적으로 등록되거나 명시적으로 가져온 Bean 클래스에는 적용되지 않습니다.

다른 범위의 대상 Bean에 접근하는 또 다른 방법은 ObjectFactory/ Provider 주입 지점입니다. 범위가 지정된 Bean을 종속성으로 주입를 참조하세요.

또한 org.springframework.beans.factory.config 패키지에 있는 `ServiceLocatorFactoryBean`이 유용할 수도 있습니다.

임의 메서드 교체(Arbitrary Method Replacement)

Lookup 메서드 주입보다 덜 유용한 메서드 주입 형태는 관리되는 Bean의 임의 메서드를 다른 메서드 구현으로 대체할 수 있는 기능입니다. 이 기능이 실제로 필요할 때까지 이 섹션의 나머지 부분은 건너뛰셔도 됩니다.

XML 기반 구성 메타데이터를 사용하면 배포된 Bean에 대해 replaced-method 요소를 사용하여 기존 메서드 구현을 다른 메서드로 대체할 수 있습니다. 재정의하려는 `computeValue`라는 메서드가 있는 다음 클래스를 고려해 보겠습니다:

  • Java

  • Kotlin

public class MyValueCalculator {

	public String computeValue(String input) {
		// some real code...
	}

	// some other methods...
}
class MyValueCalculator {

	fun computeValue(input: String): String {
		// some real code...
	}

	// some other methods...
}

다음 예제에서 볼 수 있듯이 org.springframework.beans.factory.support.MethodReplacer 인터페이스를 구현하는 클래스는 새로운 메서드 정의를 제공합니다:

  • Java

  • Kotlin

/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
public class ReplacementComputeValue implements MethodReplacer {

	public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
		// get the input value, work with it, and return a computed result
		String input = (String) args[0];
		...
		return ...;
	}
}
/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
class ReplacementComputeValue : MethodReplacer {

	override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
		// get the input value, work with it, and return a computed result
		val input = args[0] as String;
		...
		return ...;
	}
}

원본 클래스를 배포하고 메서드 오버라이드를 지정하는 Bean 정의는 다음 예제와 유사합니다:

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
	<!-- arbitrary method replacement -->
	<replaced-method name="computeValue" replacer="replacementComputeValue">
		<arg-type>String</arg-type>
	</replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

재정의되는 메서드의 메서드 서명을 나타내기 위해 <replaced-method/> 요소 내에 하나 이상의 <arg-type/> 요소를 사용할 수 있습니다. 인수의 서명은 메서드가 오버로드되고 클래스 내에 여러 변형이 존재하는 경우에만 필요합니다. 편의를 위해 인수의 타입 문자열은 정규화된 타입 이름의 하위 문자열일 수 있습니다. 예를 들어 다음은 모두 java.lang.String 과 일치합니다:

java.lang.String
String
Str

인수의 수는 가능한 각 선택을 구분하기에 충분한 경우가 많으므로 이 단축키를 사용하면 인수 유형과 일치하는 가장 짧은 문자열만 입력할 수 있어 많은 타이핑을 절약할 수 있습니다.