1. 소개

효율성과 성능은 특히 대량의 데이터를 스트리밍할 때 현대 데이터 서비스의 두 가지 중요한 측면입니다. 확실히 성능이 좋은 인코딩으로 메시지 크기를 줄이는 것이 이를 달성하는 열쇠입니다.

그러나 사내 인코딩/디코딩 알고리즘은 번거롭고 취약하여 장기적으로 유지 관리하기 어려울 수 있습니다.

운 좋게도 Simple Binary Encoding 은 Custom형 인코딩/디코딩 시스템을 실용적인 방식으로 구현하고 유지하는 데 도움이 될 수 있습니다.

이 예제에서는 SBE(Simple Binary Encoding)의 용도와 코드 샘플과 함께 사용하는 방법에 대해 설명합니다.

2. SBE란 무엇입니까?

SBE는 짧은 대기 시간 스트리밍을 지원하기 위해 메시지를 인코딩/디코딩하는 이진 표현입니다. 또한 금융 데이터 인코딩 표준인 FIX SBE 표준의 참조 구현입니다.

2.1. 메시지 구조

스트리밍 의미 체계를 유지하려면 메시지는 역추적 없이 순차적으로 읽거나 쓸 수 있어야 합니다 . 이는 역참조, 위치 포인터 처리, 추가 상태 관리 등과 같은 추가 작업을 제거하고 최대 성능과 효율성을 유지하기 위해 하드웨어 지원을 더 잘 활용합니다.

SBE에서 메시지가 어떻게 구성되어 있는지 살펴보겠습니다.

  • 헤더: 메시지 버전과 같은 필수 필드를 포함합니다. 필요한 경우 더 많은 필드를 포함할 수도 있습니다.
  • 루트 필드: 메시지의 정적 필드입니다. 블록 크기는 미리 정의되어 있으며 변경할 수 없습니다. 선택 사항으로 정의할 수도 있습니다.
  • 반복 그룹: 컬렉션 형식의 프레젠테이션을 나타냅니다. 그룹에는 필드와 더 복잡한 구조를 나타낼 수 있는 내부 그룹이 포함될 수 있습니다.
  • 가변 데이터 필드: 미리 크기를 결정할 수 없는 필드입니다. 문자열 및 Blob 데이터 형식이 두 가지 예입니다. 그들은 메시지의 끝에 있을 것입니다.

다음으로 이 메시지 구조가 중요한 이유를 살펴보겠습니다.

2.2. SBE(Not)는 언제 유용합니까?

SBE의 힘은 메시지 구조에서 비롯됩니다. 데이터에 대한 순차적 액세스에 최적화되어 있습니다. 따라서 SBE는 숫자, 비트 집합, 열거형 및 배열과 같은 고정 크기 데이터에 적합합니다 .

SBE의 일반적인 사용 사례는 SBE가 특별히 설계된 금융 데이터 스트리밍(대부분 숫자와 열거형 포함)입니다.

반면에 SBE는 string 및 blob과 같은 가변 길이 데이터 유형에 적합하지 않습니다 . 그 이유는 우리가 정확한 데이터 크기를 미리 알지 못할 가능성이 높기 때문입니다. 따라서 스트리밍 시간에 메시지의 데이터 경계를 감지하는 추가 계산이 필요합니다. 밀리초의 대기 시간에 대해 이야기하는 경우 이것이 우리의 비즈니스를 방해할 수 있다는 것은 놀라운 일이 아닙니다.

SBE는 여전히 String 및 Blob 데이터 유형을 지원하지만 가변 길이 계산의 영향을 최소로 유지하기 위해 항상 메시지 끝에 배치됩니다 .

3. 라이브러리 설정

SBE 라이브러리를 사용하려면 pom.xml 파일 에 다음 Maven 의존성 을 추가해 보겠습니다.

<dependency>
    <groupId>uk.co.real-logic</groupId>
    <artifactId>sbe-all</artifactId>
    <version>1.27.0</version>
</dependency>

4. 자바 스텁 생성

Java 스텁을 생성하기 전에 분명히 메시지 스키마를 구성해야 합니다. SBE는 XML을 통해 스키마를 정의하는 기능을 제공합니다 .

다음으로 샘플 시장 거래 데이터를 전송하는 메시지에 대한 스키마를 정의하는 방법을 살펴보겠습니다.

4.1. 메시지 스키마 생성

우리의 스키마는 FIX 프로토콜 의 특별한 XSD 를 기반으로 하는 XML 파일입니다. 이것은 우리의 메시지 형식을 정의할 것입니다.

이제 스키마 파일을 생성해 보겠습니다.

<?xml version="1.0" encoding="UTF-8"?>
<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
  package="com.baeldung.sbe.stub" id="1" version="0" semanticVersion="5.2"
  description="A schema represents stock market data.">
    <types>
        <composite name="messageHeader" 
          description="Message identifiers and length of message root.">
            <type name="blockLength" primitiveType="uint16"/>
            <type name="templateId" primitiveType="uint16"/>
            <type name="schemaId" primitiveType="uint16"/>
            <type name="version" primitiveType="uint16"/>
        </composite>
        <enum name="Market" encodingType="uint8">
            <validValue name="NYSE" description="New York Stock Exchange">0</validValue>
            <validValue name="NASDAQ" 
              description="National Association of Securities Dealers Automated Quotations">1</validValue>
        </enum>
        <type name="Symbol" primitiveType="char" length="4" characterEncoding="ASCII" 
          description="Stock symbol"/>
        <composite name="Decimal">
            <type name="mantissa" primitiveType="uint64" minValue="0"/>
            <type name="exponent" primitiveType="int8"/>
        </composite>
        <enum name="Currency" encodingType="uint8">
            <validValue name="USD" description="US Dollar">0</validValue>
            <validValue name="EUR" description="Euro">1</validValue>
        </enum>
        <composite name="Quote" 
          description="A quote represents the price of a stock in a market">
            <ref name="market" type="Market"/>
            <ref name="symbol" type="Symbol"/>
            <ref name="price" type="Decimal"/>
            <ref name="currency" type="Currency"/>
        </composite>
    </types>
    <sbe:message name="TradeData" id="1" description="Represents a quote and amount of trade">
        <field name="quote" id="1" type="Quote"/>
        <field name="amount" id="2" type="uint16"/>
    </sbe:message>
</sbe:messageSchema>

스키마를 자세히 살펴보면 <types><sbe:message> 두 가지 주요 부분이 있음을 알 수 있습니다 . 먼저 <types> 정의를 시작하겠습니다 .

첫 번째 유형으로 messageHeader 를 만듭니다 . 필수 항목이며 4개의 필수 필드도 있습니다.

<composite name="messageHeader" description="Message identifiers and length of message root.">
    <type name="blockLength" primitiveType="uint16"/>
    <type name="templateId" primitiveType="uint16"/>
    <type name="schemaId" primitiveType="uint16"/>
    <type name="version" primitiveType="uint16"/>
</composite>
  • blockLength : 메시지의 루트 필드에 예약된 총 공간을 나타냅니다. 문자열 및 blob과 같은 반복 필드 또는 가변 길이 필드는 계산하지 않습니다.
  • templateId : 메시지 템플릿의 식별자입니다.
  • schemaId : 메시지 스키마의 식별자입니다. 스키마에는 항상 템플릿이 포함됩니다.
  • version : 메시지를 정의할 때 메시지 스키마의 버전입니다.

다음으로 열거형 Market 을 정의합니다 .

<enum name="Market" encodingType="uint8">
    <validValue name="NYSE" description="New York Stock Exchange">0</validValue>
    <validValue name="NASDAQ" 
      description="National Association of Securities Dealers Automated Quotations">1</validValue>
</enum>

우리는 스키마 파일에 하드 코딩할 수 있는 잘 알려진 교환 이름을 보유하는 것을 목표로 합니다. 그들은 자주 변경되거나 증가하지 않습니다. 따라서 < enum> 유형 이 여기에 적합합니다.

encodingType="uint8" 을 설정 하여 단일 메시지에 시장 이름을 저장하기 위한 8비트 공간을 예약합니다. 이를 통해 2^8 = 256개의 서로 다른 시장(0에서 255까지)을 지원할 수 있습니다. 이는 부호 없는 8비트 정수의 크기입니다.

바로 다음에 또 다른 유형인 Symbol 을 정의 합니다. AAPL(Apple), MSFT(Microsoft) 등과 같은 금융 상품을 식별하는 3자 또는 4자 문자열입니다.

<type name="Symbol" primitiveType="char" length="4" characterEncoding="ASCII" description="Instrument symbol"/>

보시다시피 characterEncoding=”ASCII” (7비트, 최대 128자) 로 문자를 제한하고 4자 이상을 허용하지 않도록 length=”4″ 로 대문자를 설정합니다. 따라서 가능한 한 크기를 줄일 수 있습니다.

그 다음에는 가격 데이터에 대한 복합 유형이 필요합니다. 따라서 Decimal 유형을 만듭니다 .

<composite name="Decimal">
    <type name="mantissa" primitiveType="uint64" minValue="0"/>
    <type name="exponent" primitiveType="int8"/>
</composite>

Decimal 은 두 가지 유형으로 구성됩니다.

  • 가수 : 십진수의 유효 자릿수
  • 지수 : 소수점 이하 자릿수

예를 들어 가수=98765  및 지수=-3 값은 98.765를 나타냅니다.

다음으로 Market 과 매우 유사하게 값이 uint8 로 매핑되는 Currency 를 나타내는 또 다른 <enum> 을 만듭니다 .

<enum name="Currency" encodingType="uint8">
    <validValue name="USD" description="US Dollar">0</validValue>
    <validValue name="EUR" description="Euro">1</validValue>
</enum>

 마지막으로 이전에 생성한 다른 유형을 구성하여 Quote 를 정의 합니다.

<composite name="Quote" description="A quote represents the price of an instrument in a market">
    <ref name="market" type="Market"/>
    <ref name="symbol" type="Symbol"/>
    <ref name="price" type="Decimal"/>
    <ref name="currency" type="Currency"/>
</composite>

마지막으로 유형 정의를 완료했습니다.

그러나 여전히 메시지를 정의해야 합니다. 이제 메시지 TradeData 를 정의해 보겠습니다 .

<sbe:message name="TradeData" id="1" description="Represents a quote and amount of trade">
    <field name="quote" id="1" type="Quote"/>
    <field name="amount" id="2" type="uint16"/>
</sbe:message>

확실히 유형 면에서 사양 에서 더 자세한 내용을 찾을 수 있습니다 .

다음 두 섹션에서는 스키마를 사용하여 메시지를 인코딩/디코딩하는 데 최종적으로 사용하는 Java 코드를 생성하는 방법에 대해 설명합니다.

4.2. SbeTool 사용

Java 스텁을 생성하는 간단한 방법은 SBE jar 파일을 사용하는 것입니다. 그러면 유틸리티 클래스 SbeTool 이  자동으로 실행됩니다.

java -jar -Dsbe.output.dir=target/generated-sources/java 
  <local-maven-directory>/repository/uk/co/real-logic/sbe-all/1.26.0/sbe-all-1.26.0.jar 
  src/main/resources/schema.xml

명령을 실행하려면 로컬 Maven 경로로 자리 표시자 <local-maven-directory> 를 조정해야 한다는 점에 주의해야 합니다 .

성공적으로 생성되면 target/generated-sources/java 폴더에 생성된 Java 코드가 표시 됩니다.

4.3. Maven과 함께 SbeTool 사용

SbeTool 을 사용 하는 것은 충분히 쉽지만 Maven에 통합하여 더 실용적으로 만들 수도 있습니다.

따라서 다음 Maven 플러그인을 pom.xml 에 추가해 보겠습니다 .

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>1.6.0</version>
            <executions>
                <execution>
                    <phase>generate-sources</phase>
                    <goals>
                        <goal>java</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <includeProjectDependencies>false</includeProjectDependencies>
                <includePluginDependencies>true</includePluginDependencies>
                <mainClass>uk.co.real_logic.sbe.SbeTool</mainClass>
                <systemProperties>
                    <systemProperty>
                        <key>sbe.output.dir</key>
                        <value>${project.build.directory}/generated-sources/java</value>
                    </systemProperty>
                </systemProperties>
                <arguments>
                    <argument>${project.basedir}/src/main/resources/schema.xml</argument>
                </arguments>
                <workingDirectory>${project.build.directory}/generated-sources/java</workingDirectory>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>uk.co.real-logic</groupId>
                    <artifactId>sbe-tool</artifactId>
                    <version>1.27.0</version>
                </dependency>
            </dependencies>
        </plugin>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>build-helper-maven-plugin</artifactId>
            <version>3.0.0</version>
            <executions>
                <execution>
                    <id>add-source</id>
                    <phase>generate-sources</phase>
                    <goals>
                        <goal>add-source</goal>
                    </goals>
                    <configuration>
                        <sources>
                            <source>${project.build.directory}/generated-sources/java/</source>
                        </sources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

결과적으로 일반적인 Maven 새로 설치 명령은 Java 스텁을 자동으로 생성합니다 .

또한 더 많은 구성 옵션 에 대해서는 SBE의 Maven 설명서 를 항상 살펴볼 수 있습니다.

5. 기본 메시징

Java 스텁이 준비되었으므로 이를 사용하는 방법을 살펴보겠습니다 .

우선 테스트를 위한 데이터가 필요합니다. 따라서 MarketData 라는 클래스를 만듭니다 .

public class MarketData {

    private int amount;
    private double price;
    private Market market;
    private Currency currency;
    private String symbol;

    // Constructor, getters and setters
}

MarketDataSBE가 생성 한 Market  및 Currency 클래스를  구성합니다 .

다음으로 나중에 단위 테스트에서 사용할 MarketData 개체를 정의해 보겠습니다 .

private MarketData marketData;

@BeforeEach
public void setup() {
    marketData = new MarketData(2, 128.99, Market.NYSE, Currency.USD, "IBM");
}

MarketData 가 준비되어 있으므로 다음 섹션에서 TradeData 에 쓰고 읽는 방법을 살펴보겠습니다 .

5.1. 메시지 작성

대부분 우리는 데이터를 ByteBuffer 에 쓰기를 원 하므로 생성된 인코더, MessageHeaderEncoderTradeDataEncoder 와 함께 초기 용량을 가진 ByteBuffer 를 만듭니다 .

@Test
public void givenMarketData_whenEncode_thenDecodedValuesMatch() {
    // our buffer to write encoded data, initial cap. 128 bytes
    UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocate(128));
    MessageHeaderEncoder headerEncoder = new MessageHeaderEncoder();
    TradeDataEncoder dataEncoder = new TradeDataEncoder();
    
    // we'll write the rest of the code here
}

데이터를 쓰기 전에 가격 데이터를 가수와 지수의 두 부분으로 구문 분석해야 합니다.

BigDecimal priceDecimal = BigDecimal.valueOf(marketData.getPrice());
int priceMantissa = priceDecimal.scaleByPowerOfTen(priceDecimal.scale()).intValue();
int priceExponent = priceDecimal.scale() * -1;

이 변환에 BigDecimal 을 사용했음을 주목해야 합니다 . 우리는 정밀도를 잃고 싶지 않기 때문에 금전적 가치를 다룰 때 BigDecimal 을 사용하는 것이 항상 좋은 습관 입니다.

마지막으로 TradeData 를 인코딩하고 작성해 보겠습니다 .

TradeDataEncoder encoder = dataEncoder.wrapAndApplyHeader(buffer, 0, headerEncoder);
encoder.amount(marketData.getAmount());
encoder.quote()
  .market(marketData.getMarket())
  .currency(marketData.getCurrency())
  .symbol(marketData.getSymbol())
  .price()
    .mantissa(priceMantissa)
    .exponent((byte) priceExponent);

5.2. 메시지 읽기

메시지를 읽기 위해 데이터를 작성한 동일한 버퍼 인스턴스를 사용합니다. 그러나 이번에 는 디코더인 MessageHeaderDecoderTradeDataDecoder 가 필요합니다.

MessageHeaderDecoder headerDecoder = new MessageHeaderDecoder();
TradeDataDecoder dataDecoder = new TradeDataDecoder();

다음으로 TradeData 를 디코딩합니다 .

dataDecoder.wrapAndApplyHeader(buffer, 0, headerDecoder);

마찬가지로 가격 데이터를 이중 값으로 만들기 위해 가수와 지수의 두 부분에서 가격 데이터를 디코딩해야 합니다. 확실히 BigDecimal 을 다시 사용합니다.

double price = BigDecimal.valueOf(dataDecoder.quote().price().mantissa())
  .scaleByPowerOfTen(dataDecoder.quote().price().exponent())
  .doubleValue();

마지막으로 디코딩된 값이 원래 값과 일치하는지 확인합니다.

Assertions.assertEquals(2, dataDecoder.amount());
Assertions.assertEquals("IBM", dataDecoder.quote().symbol());
Assertions.assertEquals(Market.NYSE, dataDecoder.quote().market());
Assertions.assertEquals(Currency.USD, dataDecoder.quote().currency());
Assertions.assertEquals(128.99, price);

6. 결론

이 기사에서는 SBE를 설정하고 XML을 통해 메시지 구조를 정의하고 이를 사용하여 Java에서 메시지를 인코딩/디코딩하는 방법을 배웠습니다.

언제나처럼 GitHub에서 모든 코드 샘플과 그 이상을 찾을 수 있습니다 .

Generic footer banner