티스토리 뷰

스프링

[Spring Integration] 소개 및 튜토리얼

시리어스강 2023. 3. 18. 21:13

사내에서 배치 작업을 스케줄링이나 HTTP API가 아닌 DB polling로 기동하길 원하는 요구사항이 있어, 어떤 방식으로 구현하면 좋을지 찾다보니 Spring Integration를 활용할 수 있을 것 같아 학습 겸 Baeldung 자료를 기반으로 소개 및 튜토리얼 자료를 작성해보려 한다.

 

그리고 Spring Integration에 대한 기본적인 개념을 학습한 후에 이를 활용한 DB polling에 대한 포스팅을 해야겠다.

 

 

1. 소개

이 글에서는 실용적인 예제를 통해 Spring Integration의 핵심 개념을 소개한다. Spring Integration엔터프라이즈 아키텍처에서 시스템들과 프로세스들 간의 상호 연결성을 크게 향상시킬 수 있는 많은 요소들을 제공한다.

 

그리고 널리 사용되는 디자인 패턴을 구현하여, 개발자가 반복된 작업을 하지 않도록 지원한다.

 

엔터프라이즈 어플리케이션에서 Spring Integration이 활용될 수 있는 부분을 알아보고 다른 대안보다 선호되는 이유도 살펴볼 것이다. 그리고 Spring Integration 기반의 어플리케이션을 쉽게 개발하기 위한 툴들도 알아볼 것이다.

 

 

2. 준비

gradle 의존성

dependencies {
	implementation 'org.springframework.integration:spring-integration-core:5.5.16'
	implementation 'org.springframework.integration:spring-integration-file:5.5.16'
}

 

 

3. 메세지 패턴

Spring Integration의 기본 패턴 중 하나는 메세징이다. 이 패턴은 미리 정의된 채널을 통해 기존의 시스템이나 프로세스로부터 하나 이상의 시스템들이나 프로세스들로 보내지는 메세지(데이터)를 중심으로 이루어진다.

 

역사적으로, 이 패턴은 다음과 같은 방식으로 서로 다른 시스템들을 유연하게 통합했다.

  • 통합과 관련된 시스템을 대부분 분리한다.
  • 통합과 관련된 시스템이 서로의 프로토콜, 형식 그리고 기타 구현의 세부 사항에 대해 의존하지 않도록 한다.
  • 통합과 관련된 구성 요소의 개발과 재사용을 권장한다.

 

4. 메세지 패턴 예시

지정된 디렉토리에서 다른 디렉토리로 json 파일을 복사하는 기본적인 예제를 살펴볼 것이다.

 

4-1. @Configuration

아래 코드는 Service Activator, Integration ChannelInbound Channel Adapter를 구성한다. 각 구성 요소들에 대해서는 뒤에서 살펴볼 것이다.

@Configuration
@EnableIntegration
public class BasicIntegrationConfig {
    private final String srcDirectory;
    private final String destDirectory;
    private final String filePattern;

    public BasicIntegrationConfig() {
        srcDirectory = BasicIntegrationConfig.class.getClassLoader().getResource("source").getPath();
        destDirectory = BasicIntegrationConfig.class.getClassLoader().getResource("destination").getPath();
        filePattern = "*.json";
    }

    @Bean
    public MessageChannel fileChannel() {
        return new DirectChannel();
    }

    @Bean
    @InboundChannelAdapter(value = "fileChannel", poller = @Poller(fixedDelay = "1000"))
    public MessageSource<File> fileReadingMessageSource() {
        FileReadingMessageSource sourceReader = new FileReadingMessageSource();
        sourceReader.setDirectory(new File(srcDirectory));
        sourceReader.setFilter(new SimplePatternFileListFilter(filePattern));
        return sourceReader;
    }

    @Bean
    @ServiceActivator(inputChannel = "fileChannel")
    public MessageHandler fileWritingMessageHandler() {
        FileWritingMessageHandler handler = new FileWritingMessageHandler(
                new File(destDirectory)
        );
        handler.setFileExistsMode(FileExistsMode.REPLACE);
        handler.setExpectReply(false);
        return handler;
    }
}

 

4-2. 메인 메소드

public static void main(String[] args) {
    AbstractApplicationContext context = new AnnotationConfigApplicationContext(BasicIntegrationConfig.class);
    context.registerShutdownHook();

    Scanner scanner = new Scanner(System.in);
    while(true) {
        System.out.print("exit ? [y/n] ");
        String input = scanner.nextLine().trim();

        if(input.equalsIgnoreCase("y")) {
            context.close();
            scanner.close();
            break;
        }
    }
    System.exit(0);
}

 

 

5. Spring Integration 구성 요소

5-1. Message

The org.springframework.integration.Message 인터페이스는 Spring Integration 컨텍스트 내에서 데이터의 전송 단위인 스프링 메세지를 정의한다.

 

이는 두 가지 주요 요소에 대한 accessor(getter)를 제공한다.

  • MessageHeaders: 기본적으로 org.springframework.integration.MessageHeaders 클래스에 정의된 메타 데이터를 송신하는데 사용할 수 있는 key-value 컨테이너
  • 메세지 페이로드: 전송할 가치가 있는 실제 데이터 → 위의 예시에서는 json
public interface Message<T> {
    T getPayload();
    MessageHeaders getHeaders();
}

 

 

5-2. Channel

Spring Integration에서 Channel은 통합 아키텍처에서 기본 채널이다. 메세지가 한 시스템에서 다른 시스템으로 전달되는 파이프 역할을 수행한다. 다시 말해, 통합 시스템 또는 프로세스가 다른 시스템으로 메세지를 송수신할 때 사용되는 파이프로 생각하면 된다.

 

Spring Integration에서 Channel은 필요에 따라 다양한 형태로 제공된다. 대부분 커스텀 없이 설정 및 사용이 가능하지만, 상황에 따라 커스터마이징도 가능하다.

 

 

P2P(Point-to-Point) Channel

P2P(Point-to-Point) Channel시스템과 구성 요소들 간에 일대일 통신을 위해 사용된다. 한 구성 요소는 다른 구성 요소가 확인할 수 있도록 메세지를 채널에 발행(publish)한다. 각 채널의 끝에는 하나의 구성 요소만 있을 수 있다.

 

채널 구성은 DirectChannel 인스턴스를 반환하는 것만큼 간단하다. 예제에서는 각각의 getter 메서드 이름으로 식별되는 세 개의 채널을 정의했다.

@Bean
public MessageChannel fileChannel1() {
    return new DirectChannel();
}

@Bean
public MessageChannel fileChannel2() {
    return new DirectChannel();
}

@Bean
public MessageChannel fileChannel3() {
    return new DirectChannel();
}

 

Publish-Subscribe(Pub-Sub) Channel

Publish-Subscribe(Pub-Sub) Channel시스템과 구성요소들 간의 일대다 통신을 설정하는데 사용된다. 이렇게 하면 이전에 생성한 3개의 direct channel에 모두 전달 가능하다.

 

따라서 다음과 같이 필요에 따라 P2P channelpub-sub channel 바꿀 수 있다.

@Bean
public MessageChannel pubSubFileChannel() {
    return new PublishSubscribeChannel();
}

@Bean
@InboundChannelAdapter(value = "pubSubFileChannel", poller = @Poller(fixedDelay = "1000"))
public MessageSource<File> fileReadingMessageSource() {
    FileReadingMessageSource sourceReader = new FileReadingMessageSource();
    sourceReader.setDirectory(new File(srcDirectory));
    sourceReader.setFilter(new SimplePatternFileListFilter(filePattern));
    return sourceReader;
}

 

예제에서는 pub-sub channel에 발행(publish)하기 위해 inbound channel adapter를 변환했다. 이렇게하면 우리는 원본 디렉토리에서 읽은 파일을 여러 디렉토리로 보낼 수 있다.

 

 

5-3. Bridge

Spring Integrationbridge직접 연결할 수 없는 두 개의 메세지 채널이나 어댑터를 연결하는데 사용된다. 예제에서는 bridge를 사용하여 pub-sub channel을 3개의 다른 P2P channel에 연결 할 수 있다. → P2P channel과 pub-sub channel은 직접 연결이 불가능하다.

@Bean
@BridgeFrom(value = "pubSubFileChannel")
public MessageChannel fileChannel1() {
    return new DirectChannel();
}

@Bean
@BridgeFrom(value = "pubSubFileChannel")
public MessageChannel fileChannel2() {
    return new DirectChannel();
}

@Bean
@BridgeFrom(value = "pubSubFileChannel")
public MessageChannel fileChannel3() {
    return new DirectChannel();
}

 

위의 bean 구성은 pubSubFileChannel을 3개의 P2P channel에 연결한다. @BridgeFrom 어노테이션은 bridge를 정의하며 pub-sub channel을 구독(subscribe)해야 하는 모든 채널에 적용 가능하다.

 

위의 코드는 "pubSubFileChannel에서 fileChannel1, fileChannel2, fileChannel3으로 bridge를 생성하여 pubSubFileChannel의 메세지가 세 channel 모두에 동시에 전달될 수 있도록 한다."로 볼 수 있다.

 

 

5-4. Service Activator

Service Activator는 주어진 메소드에서 @ServiceActivator 어노테이션을 정의하는 POJO다. 이를 통해  Inbound channel에서 메세지를 수신할 때 POJO의 메서드를 실행할 수 있고, 외부 channel에 메세지를 보낼 수 있다.

 

스프링 공식 문서에서는 스프링의 관리를 받는 객체가 서비스 역할을 할 수 있도록 Input channel에 연결하기 위한 엔드포인트 유형이라고 설명하고 있다.

 

예제에서 Service Activatorinput channel로부터 파일을 받아 디렉토리에 write를 한다.

 

* 개인적으로 Baeldung과 스프링 공식 문서의 설명만으로는 직관적으로 개념을 받아들이기 어려웠는데, 해당 예제와 다른 사례들을 살펴보다보니 어떤식으로 활용할 수 있는지 이해할 수 있었다. 당연한 얘기지만, 짧은 설명만으로 납득이 되지 않는 개념은 예제를 찾아보는 것이 확실히 도움이 되는 것 같다.

 

 

5-5. Adapter

Adapter시스템 또는 데이터 소스에 plug-in 할 수 있는 엔터프라이즈 통합 패턴 기반의 구성 요소이다. 말 그대로 우리가 알고 있는 벽에 있는 소켓이나 전자 장치에 연결하는 어댑터라고 볼 수 있다.

 

이는 데이터베이스, FTP 서버, 메세징 시스템(ex. JMS, AMQP), 소셜 네트워크(ex. Twitter)와 같은 블랙박스 시스템에 재사용 가능한 연결을 할 수 있다. 이런 시스템에 연결해야 한다는 것은 Adapter는 이식과 재사용이 용이해야 한다는 의미이다.

 

Adapter는 크게 인바운드와 아웃바운드로 나뉜다. 예제를 기준으로 이들을 살펴보자.

 

 

Inbound Adapter

우선 Inbound Adapter는 이전에 살펴보았듯이, 외부 시스템으로부터 메세지를 가져오는데 사용된다. 예제에서는 파일시스템 디렉토리가 외부 시스템이 되는데, 다음과 같이 구성되었다.

  • @InboundChannelAdapter 어노테이션: Adapter가 메세지(예제에서는 json)를 보낼 Channel과 어댑터가 지정된 간격으로 디렉토리를 폴링하는데 활용이 되는 Poller를 구성
  • FileReadingMessageSource: 파일시스템 폴링과 관련된 처리를 구현한 Spring Integration 클래스

 

Outbound Adapter

Outbound Adapter는 메세지를 외부로 보내는데 사용된다. Spring Integration은 다양한 상황을 처리하기 위해 쉽게 사용할 수 있는 다양한 Adapter들을 지원한다.

 

 

6. 결론

Spring Integration에 대한 기본적인 활용법을 살펴보았다.

 

Spring Integration 코드는 JavaSE 내에서 독립형 프로젝트로 배포할 수 있을 뿐만 아니라 Jakarta EE 환경에 포함되어서 배포도 가능하다. EAI 중심 제품들이나 ESB(Enterprise Service Bus) 패턴들과 직접적으로 경쟁하진 않지만, ESB가 해결할 수 있는 문제들을 가벼운 방식으로 대체할 수 있는 방법이 될 것이다.

 

 

 

참고 자료 🙇‍♂️

댓글