본문 바로가기

Programing/Framework

[Spring] Boot 빈 의존성 사례 - Spring Integration (TCP)

하는 방법은 검색해보면 쉽게 찾을 수 있다.

글을 쓰는 이유는 실무에서 어떤 경우에 발생하는지 기록을 하기 위함이다.

 

스프링이 관리하는 객체, 즉 빈은 의존하고 있는 것이 명시적으로 드러날 때는 초기화 과정에서 필요한 빈들을 먼저 초기화를 해준다.

하지만 간접적으로 빈을 사용하는 경우 초기화 시점에 필요한 빈이 없을 수 있기 때문에 애플리케이션에서 실패가 발생한다.

 

내가 경험한 사례는 다음과 같다.

TCP/IP 전문 통신을 해야해서 Spring Integration프로젝트 중 TCP and UDP Support 기능을 이용했다.

Spring Integration 5.0 부터는 Java DSL을 통한 설정이 가능하기에 Configuration은 아래와 같이 구성했다.

@Configuration
@EnableIntegration
public class R2D2Configuration {
    @Bean
    public IntegrationFlow client(TcpMessageMapper mapper) {
        return IntegrationFlows.from(R2d2Gateway.class)
                .handle(Tcp.outboundGateway(Tcp.netClient(r2d2Properties.getServerAddress(), r2d2Properties.getServerPort())
                        .serializer(ByteArrayLfSerializer.INSTANCE)
                        .deserializer(ByteArrayNulSerializer.INSTANCE)
                        .mapper(mapper)))
                .get();
    }

실제 빈으로 주입받는 타입은 아래와 같은 인터페이스이다.

public interface R2D2Gateway {
    R2D2pprovalResponse approve(@Payload R2D2ApprovalRequest r2d2ApprovalRequest);

    R2D2CancellationResponse cancel(@Payload R2D2CancellationRequest r2d2CancelRequest);
}

IntegrationFlowBuilder 에 의한 빈 초기화 과정

IntegrationFlowBuilder 의 from 인자로는 serviceInterface를 받는다.

이 넘겨받은 serviceInterface는 GatewayProxyFactoryBean의 생성자의 파라미터로 전달된다.

public class GatewayProxyFactoryBean extends AbstractEndpoint
		implements TrackableComponent, FactoryBean<Object>, MethodInterceptor, BeanClassLoaderAware {

	private volatile Class<?> serviceInterface;

	public GatewayProxyFactoryBean(Class<?> serviceInterface) {
		this.serviceInterface = serviceInterface;
	}

	@Override
	public Class<?> getObjectType() {
		return (this.serviceInterface != null ? this.serviceInterface : null);
	}

GatewayProxyFactoryBean 은 FactoryBean 인터페이스를 구현하는데, 객체타입을 반환하는 getObjectType() 에 전달받은 serviceInterface를 돌려준다.

IntegrationFlowBeanPostProcessor 에 의해 빈 후처리기가 호출되면 registerComponent라는 메서드를 통해 beanFactory에 빈으로 등록이 되게 된다.

public class IntegrationFlowBeanPostProcessor
		implements BeanPostProcessor, ApplicationContextAware, SmartInitializingSingleton {

	private Object processStandardIntegrationFlow(StandardIntegrationFlow flow, String flowBeanName) {
		String flowNamePrefix = flowBeanName + ".";
		if (this.flowContext == null) {
			this.flowContext = this.beanFactory.getBean(IntegrationFlowContext.class);
		}
		boolean useFlowIdAsPrefix = this.flowContext.isUseIdAsPrefix(flowBeanName);
		int subFlowNameIndex = 0;
		int channelNameIndex = 0;

		Map<Object, String> integrationComponents = flow.getIntegrationComponents();
		Map<Object, String> targetIntegrationComponents = new LinkedHashMap<>(integrationComponents.size());

		for (Map.Entry<Object, String> entry : integrationComponents.entrySet()) {
			Object component = entry.getKey();
			if (component instanceof ConsumerEndpointSpec) {
				// ..
			}
			else {
				if (noBeanPresentForComponent(component, flowBeanName)) {
					if (component instanceof AbstractMessageChannel || component instanceof NullChannel) {
						// ..
					}
					else if (component instanceof MessageChannelReference) {
						// ..
					}
					else if (component instanceof FixedSubscriberChannel) {
						// ..
					}
					else if (component instanceof SourcePollingChannelAdapterSpec) {
						// ..
					}
					else if (component instanceof StandardIntegrationFlow) {
						// ..
					}
					else if (component instanceof AnnotationGatewayProxyFactoryBean) {
						AnnotationGatewayProxyFactoryBean gateway = (AnnotationGatewayProxyFactoryBean) component;
						String gatewayId = entry.getValue();

						if (gatewayId == null) {
							gatewayId = gateway.getComponentName();
						}
						if (gatewayId == null) {
							gatewayId = flowNamePrefix + "gateway";
						}

						registerComponent(gateway, gatewayId, flowBeanName,
								beanDefinition -> {
									((AbstractBeanDefinition) beanDefinition)
											.setSource(new DescriptiveResource("" + gateway.getObjectType()));
								});

	private void registerComponent(Object component, String beanName, String parentName,
			BeanDefinitionCustomizer... customizers) {

		BeanDefinition beanDefinition =
				BeanDefinitionBuilder.genericBeanDefinition((Class<Object>) component.getClass(), () -> component)
						.applyCustomizers(customizers)
						.getRawBeanDefinition();

		((BeanDefinitionRegistry) this.beanFactory).registerBeanDefinition(beanName, beanDefinition);

		if (parentName != null) {
			this.beanFactory.registerDependentBean(parentName, beanName);
		}

		this.beanFactory.getBean(beanName);
	}

결국 R2D2Gateway 타입의 빈이 등록이 되는 것이다. (프록시 객체이다.)

R2D2Gateway 사용처

실제 R2D2Gateway를 사용하는 서비스는 아래와 같다. 생성자 주입을 받는다.

@Service
public class R2D2Service {
    private final R2D2Gateway r2d2Gateway;
    
    public R2D2Service(R2D2Gateway r2d2Gateway) {
    	this.r2d2Gateway = r2d2Gateway;
    }

IntelliJ IDEA 에서는 R2D2Gateway가 빈으로 표시된 것이 없기에 빨간 색으로 나온다.

Could not autowire. No beans of 'R2D2Gateway' type found.

인터페이스에 @Component 을 붙이면 IDE의 경고는 없어지지만 의존성 문제는 해결되지 않는다.

발생하는 에러

초기화시 아래와 같은 에러가 발생한다.

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 1 of constructor in com.tistory.namocom.service.r2d2Service required a bean of type 'com.tistory.namocom.R2D2Gateway' that could not be found.


Action:

Consider defining a bean of type 'com.tistory.namocom.R2D2Gateway' in your configuration.


Process finished with exit code 1

 

R2D2Configuration 에서 생성되는 빈의 타입은 IntegrationFlow 이지만 필요로 하는 것은 R2kGateway 타입이다.

따라서 아래와 같이 @Qualifier 를 지정하는 것은 마찬가지로 실패이다.

@Service
public class R2D2Service {
    private final R2D2Gateway r2d2Gateway;
    
    public R2D2Service(@Qualifier("client") R2D2Gateway r2d2Gateway) {
    	this.r2d2Gateway = r2d2Gateway;
    }

결국은 R2D2Service에 IntegrationFlow 에 대한 의존성을 거는 것으로 해결을 하였다.

@Service
@DependsOn("client")
public class R2D2Service {
    private final R2D2Gateway r2d2Gateway;
    
    public R2D2Service(R2D2Gateway r2d2Gateway) {
    	this.r2d2Gateway = r2d2Gateway;
    }

빈 이름이 너무 일반적인 이름(client)라서 Configuration 의 빈 이름을 바꾸는 것이 좋다고 생각이 들었다.

@Configuration
@EnableIntegration
public class R2D2Configuration {
    @Bean
    public IntegrationFlow r2d2Client(TcpMessageMapper mapper) {

그 외

ArgumentsHolder argsHolder at org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor

 

org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency 의 finally

org.springframework.beans.factory.support.ConstructorResolver#setCurrentInjectionPoint