Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Archives
Today
Total
관리 메뉴

디지안의 개발일지

FEP 통신 내재화(with Spring Integration TCP) 본문

etc

FEP 통신 내재화(with Spring Integration TCP)

안덕기 2024. 7. 3. 21:58

FEP란

FEP란 Front End Processor의 약자로 원래 메인프레임에서 통신 과부하를 경감시키기 위해 전처리 작업을 하는 과정을 말한다. 하지만 금융권에서는 의미가 조금 와전되어 B2B 연계(대외계)를 FEP라고 부른다.

예를 들어, 금융 상품을 이용하는 사람들은 보통 하나의 금융 기관에서만 금융 상품을 이용하지 않는다. 은행은 국민은행, 증권은 신한투자증권, 보험은 카카오페이손해보험 거기서 더 나아가 은행도 하나의 은행만 사용하는 것이 아니라 토스뱅크, 카카오뱅크, 우리은행등 여러 은행을 사용할 수 있다. 각 금융기관들은 자기 은행에 대한 정보만 가지고 있는데 사용자는 한눈에 자기 자산에 대해서 알 수 있으면 더 편리할 것이다. 그래서 정부에서 모든 금융 자산에 대해서 한눈에 볼 수 있도록 하는 시스템을 만들었고 이를 마이데이터 사업이라고 부른다.

결론적으로 우리가 카카오페이, 토스, 뱅크샐러드 같은 앱에서 우리가 금융자산을 모아서 보려면 어느 곳에서는 금융정보를 모아야 한다. 각 기관과 직접 통신할 수도 있지만 모든 기관과 직접 통신하는 것은 쉬운 일이 아니기 때문에 특정 기관에서 데이터를 모아주는 역할을 하는데 대표적으로 한국신용정보원이 있다.

FEP 통신

금융권은 법적인 제재가 심하기 때문에 예전 기술에서 현재 기술로 넘어가는 것이 쉽지 않다. 그 대표적인 산물이 HTTP 통신을 하지 못하고 TCP를 사용하고 페이로드를 Json과 같은 형태를 사용하지 못하고 Fixed String이라는 고정된 길이의 문자열을 사용한다. 자세한 설명은 토스 영상을 보면 알 수 있다.

내재화하게 된 이유

FEP 통신을 하기 위해서는 HTTP와 Json으로 통신한 것을 TCP와 Fixed String으로 변환해줄 중간 모듈이 필요했다. 그래서 회사에서 별도의 솔루션을 사용하고 있었다. 하지만 이 솔루션 툴은 개발자를 위한 툴이 아니라 운영툴이었다. 직접적으로 운영툴이 어떤지는 보안 문제가 있을거 같아서 스크린샷을 보여줄 수 없지만 다음과 같은 문제점이 있었다.

  1. 운영 툴이 매우 불편하여 개발자들이 기피하는 업무가 되었다.
  2. 회사의 모든 워크로드는 k8s로 운영되고 있지만 솔루션은 ec2로 운영되고 있어 모니터링 하는데에 불편함이 있다.
  3. CI/CD 툴을 통해 배포할 수 없는 구조라 배포 과정이 복잡하다.

토스영상을 감명 깊게 보고 우리도 충분히 만들 수 있을거라 생각하여 만들어보았다.

기술

기술을 선택하는데 있어서 중요하게 여긴 것은 다른 사람들도 충분히 쉽게 코드를 작성할 수 있게 하는 것이었다. 그래서 Java 진영에서 친숙하게 사용하는 Spring 프로젝트를 적극적으로 사용하려고 노력하였다.

  • Kotlin
  • Kotlin Reflection
  • Spring Integration

전문

FEP 통신에서는 페이로드를 전문이라고 한다. 우리가 일반적으로 만드는 웹 애플리케이션은 Java Object를 Json으로 변경하는 Serilization 과정과 Json을 Java Object로 변경해주는 Deserialization 과정을 Spring이 대신해준다.(보통 jackson 라이브러리를 사용한다.) 하지만 전문은 그런 라이브러리가 없기 때문에 직접 만들어야 했다. 토스와 마찬가지로 애노테이션 방식을 통해서 구현을 하였다.

@FepMessage(maxLength = 55)
data class TestData(
    @FepProperty(justified = Justified.RIGHT, padding = Padding.SPACE, length = 10, no = 0)
    val rightSpace: String,
    @FepProperty(justified = Justified.LEFT, padding = Padding.SPACE, length = 15, no = 1)
    val leftSpace: String,
    @FepProperty(justified = Justified.RIGHT, padding = Padding.ZERO, length = 15, no = 2)
    val rightZero: String,
    @FepProperty(justified = Justified.LEFT, padding = Padding.ZERO, length = 15, no = 3)
    val leftZero: String,
) : FepTelegram

위 코드를 보면 각 프로퍼티마다 정렬, 패딩, 길이등을 정의한다. 그리고 몇번째에 프로퍼티가 위치할지도 정의내린다.

아래는 Java Object를 전문 데이터로 변경하면

TestData(rightSpace = "12345678", leftSpace = "", rightZero = "", leftZero = "")

아래와 같이 변환된다.

  12345678               000000000000000000000000000000

그러면 위와 같은 데이터를 TCP 통신을 통해 전달한다.

통신

우리가 사용하는 Spring Web MVC는 HTTP을 기반으로 하는 라이브러리다. 그렇기 때문에 HTTP 메소드, HTTP URL, Body, Header등등을 통해 원하는 클라이언트와 데이터를 전달 받는다. 하지만 FEP 통신에서는 TCP 통신을 하기 때문에 TCP을 연결을 할 라이브러리가 필요했다.

원래는 Virtual Thread + Java NIO나 Netty를 사용하려고 했으나 각각 단점이 아래와 같은 단점이 존재했다.

  • Virtual Thread + Java NIO : 가상 스레드를 사용하기 때문에 디버깅이 어렵고 low한 라이브러리를 사용하기 때문에 구현해야할 것이 많다. 안정적인 시스템을 만들기에는 시간이 많이 필요하다.
  • Netty : 개념부터가 어렵다. 그렇기 때문에 모두가 같이 개발하기에는 어려움이 존재할 수 있다고 생각이 들었다.

그래서 선택한 것은 Spring Integration이다. 코드가 Spring Web MVC와 비슷한 구조기 때문에 사용하는데 큰 무리가 없을 것이라고 생각이 들었다. 단점이라고 하면 아무래 blocking 모델이기 때문에 많은 트래픽을 처리하기에는 무리가 있을 수 있다. 하지만 우리 회사 정도의 규모에서는 그렇게 큰 문제가 될거라고 생각이 들지 않았다.

먼저 아래와 같이 TCP 관련 설정을 해줘야 한다.

@Configuration
@EnableIntegration
class TcpServerConfig(@Value("\${tcp.port}") private val port: Int) {
    @Bean
    fun connectionFactory(): AbstractServerConnectionFactory {
        return TcpNioServerConnectionFactory(port).apply {
            this.isSingleUse = true
        }
    }

    @Bean
    fun requestChannel(): MessageChannel {
        return DirectChannel()
    }

    @Bean
    fun replyChannel(): MessageChannel {
        return DirectChannel()
    }

    @Bean
    fun inboundGateway(connectionFactory: AbstractServerConnectionFactory): TcpInboundGateway {
        val tcpInboundGateway = TcpInboundGateway()
        tcpInboundGateway.setConnectionFactory(connectionFactory)
        tcpInboundGateway.setRequestChannel(requestChannel())
        tcpInboundGateway.setReplyChannel(replyChannel())
        return tcpInboundGateway
    }
}

그리고 우리가 Spring Web MVC에서 @RestController@RequestMapping 을 선언하는 것처럼 컨트롤러 클래스라는 것과 어떤 채널을 통해 값이 들어오게 될지 선언을 하면 된다.

@MessageEndpoint
class MessageEndpoint(
    private val facade: Facade,
) {
    @ServiceActivator(inputChannel = "requestChannel", async = "true")
    fun process(telegram: ByteArray): ByteArray {
        return facade.process(telegram)
    }
}

결론

클라이언트도 비슷한 방식으로 구현하면 된다. 하지만 아쉽게도 실제로는 업무에 투입하지 못하였다. 다른 프로젝트에 밀려서.. ㅠㅠ. 이 프로젝트가 직접적으로 투입되는 것에도 많은 공수가 들어가기 때문에 이미 사용하고 있는 솔루션을 사용하는 것으로 결론이 났다.