Fontes Fintech

저지연 트레이딩 시스템에서 Memory Ordering과 Atomic Operation이 중요한 이유Trading | Engineering — Deep Dive #10 본문

엔지니어링 : Engineerings

저지연 트레이딩 시스템에서 Memory Ordering과 Atomic Operation이 중요한 이유Trading | Engineering — Deep Dive #10

폰테스 핀테크 :: 금융거래의 안전한 미래 2026. 10. 5. 23:37

저지연 트레이딩 시스템에서 Memory Ordering과 Atomic Operation이 중요한 이유

Trading Engineering — Deep Dive #10

Introduction

저지연 트레이딩 시스템의 성능을 이야기하다 보면 어느 순간 이런 질문에 도달하게 됩니다.

여러 CPU Core에서 동시에 실행되는 Thread들은 서로의 데이터를 어떻게 정확하게 볼 수 있을까?

단순한 프로그램에서는 다음과 같이 생각하기 쉽습니다.

Thread A
   ↓
Memory Write
   ↓
Thread B
   ↓
Memory Read

 

하지만 실제 멀티코어 CPU에서는 훨씬 복잡합니다.

각 CPU Core는 자신만의 Cache를 가지고 있고, Store Buffer와 Load/Store 처리 구조를 사용하며, Compiler 역시 프로그램의 의미를 보존하는 범위에서 명령어의 실행 순서를 재배치할 수 있습니다.

따라서 저지연 멀티스레드 시스템에서는 단순히

"변수를 공유하면 된다."

라고 생각해서는 안 됩니다.

우리는 다음 문제를 함께 고려해야 합니다.

  • Atomic Operation
  • Memory Ordering
  • Memory Barrier
  • Cache Coherency
  • Acquire / Release
  • Lock-Free Data Structure
  • Ring Buffer
  • False Sharing

이 글에서는 이 개념들이 실제 트레이딩 시스템에서 어떻게 연결되는지 살펴보겠습니다.

자, 딥다이브의 마지막 연재입니다.

 


1. 멀티코어 CPU에서는 Memory가 단순하지 않다

현대 CPU에서는 여러 Core가 동시에 실행됩니다.

              CPU
       ┌───────┴───────┐
       │               │
     Core 0          Core 1
       │               │
     Cache            Cache
       │               │
       └───────┬───────┘
               │
             Memory

예를 들어 Thread A가 데이터를 변경하고 Thread B가 그 데이터를 읽는다고 해보겠습니다.

Thread A                 Thread B

data = 100
   │
   └──────────────→     read data

개념적으로는 매우 단순해 보입니다.

하지만 실제 CPU에서는 Cache Coherency, Store Buffer, Memory Ordering 등이 관여합니다.

즉,

"코드에 적힌 순서"와 "다른 CPU가 관찰하는 순서"를 항상 동일하게 생각해서는 안 됩니다.


2. Compiler와 CPU는 왜 순서를 바꾸는가?

프로그램이 다음과 같다고 가정해 보겠습니다.

data = 100;
ready = 1;

사람은 자연스럽게 다음과 같이 생각합니다.

1. data를 100으로 변경
2. ready를 1로 변경

그리고 다른 Thread에서:

if (ready == 1) {
    use(data);
}

 

라고 하면 data == 100이라고 기대할 수 있습니다.

하지만 멀티코어 환경에서는 이 관계를 단순하게 가정해서는 안 됩니다.

Compiler와 CPU는 성능을 위해 내부적으로 메모리 접근을 재배치하거나 지연시킬 수 있기 때문입니다.

따라서 우리가 원하는 것은 단순한 실행 순서가 아니라:

다른 Thread가 관찰할 수 있는 Memory Ordering을 명확하게 정의하는 것

입니다.


3. Atomic Operation

이 문제를 해결하는 가장 기본적인 도구 중 하나가 Atomic Operation입니다.

예를 들어 여러 Thread가 하나의 counter를 증가시킨다고 생각해 보겠습니다.

단순한 코드는:

counter++;

입니다.

하지만 여러 Thread가 동시에 실행하면:

Thread A              Thread B

Read counter
                      Read counter
Add 1
                      Add 1
Write counter
                      Write counter

 

와 같은 문제가 발생할 수 있습니다.

이를 Race Condition이라고 합니다.

Atomic operation을 사용하면 특정 연산을 다른 Thread와 충돌 없이 수행할 수 있습니다.

C11에서는 <stdatomic.h>를 사용할 수 있습니다.

#include <stdatomic.h>

atomic_int counter;

atomic_fetch_add(&counter, 1);

이렇게 하면 단순한 counter++와는 다른 동기화 의미를 갖게 됩니다.


4. Atomic이라고 해서 모든 문제가 해결되는 것은 아니다

여기서 중요한 함정이 있습니다.

Atomic Operation ≠ 전체 Memory Synchronization

Atomic 변수 하나를 사용한다고 해서 주변의 모든 Memory Access가 자동으로 올바른 순서로 정렬되는 것은 아닙니다.

예를 들어:

data = 100;
atomic_store(&ready, 1);

와

if (atomic_load(&ready)) {
    printf("%d\n", data);
}

 

사이에는 단순히 ready가 Atomic이라는 사실 이상의 문제가 있습니다.

중요한 것은:

ready를 통해 data의 변경까지 어떤 순서로 다른 Thread에 전달할 것인가?

입니다.

여기서 등장하는 것이 Memory Ordering입니다.


5. Memory Ordering

C11 Atomic은 여러 Memory Order를 제공합니다.

대표적으로:

memory_order_relaxed
memory_order_acquire
memory_order_release
memory_order_acq_rel
memory_order_seq_cst

 

각각의 의미와 비용이 다릅니다.

가장 단순하게 생각하면:

Relaxed

Atomic 연산 자체의 원자성은 보장하지만 강한 순서 관계는 제공하지 않습니다.

Acquire

이 Atomic Load 이후의 Memory Access가 특정 순서 관계를 갖도록 합니다.

Release

이 Atomic Store 이전의 Memory Access가 특정 순서 관계를 갖도록 합니다.

Acquire + Release

읽기와 쓰기 양쪽의 ordering을 함께 표현합니다.

Sequentially Consistent

가장 강한 ordering 모델 중 하나입니다.

따라서 저지연 시스템에서는 무조건 가장 강한 ordering을 사용하는 것이 정답이 아닙니다.


6. Acquire / Release

저지연 시스템에서 특히 중요한 개념이 Acquire / Release입니다.

대표적인 Producer / Consumer 구조를 생각해 보겠습니다.

Producer:

data = 100;

atomic_store_explicit(
    &ready,
    1,
    memory_order_release
);

Consumer:

if (atomic_load_explicit(
        &ready,
        memory_order_acquire)) {

    use(data);
}

여기서 핵심은 ready 자체가 아닙니다.

Release와 Acquire 사이에 Memory Ordering 관계가 형성된다는 것입니다.

개념적으로:

Producer                     Consumer

data = 100
    │
    │
release store ───────────→ acquire load
                               │
                               ↓
                           use(data)

Consumer가 Release Store와 대응되는 Acquire Load를 통해 값을 관찰하면, Producer가 그 전에 수행한 관련 Memory Write를 올바른 순서로 관찰할 수 있습니다.

이 패턴은 Lock-Free Queue나 Ring Buffer에서 매우 중요합니다.


7. Memory Barrier

Memory Barrier 또는 Memory Fence는 CPU와 Compiler가 Memory Access를 처리하는 순서에 제약을 두기 위한 중요한 메커니즘입니다.

개념적으로:

Before Barrier
      │
      │
 ─────┼─────
      │
Memory Barrier
      │
 ─────┼─────
      │
After Barrier

중요한 점은 Barrier가 단순히

"CPU를 멈춘다"

는 의미가 아니라는 것입니다.

Memory Ordering을 보장하기 위해 필요한 제약을 제공하는 것입니다.

C11에서는 Atomic의 Memory Order를 통해 이러한 의도를 보다 명확하게 표현할 수 있습니다.


8. Lock과 Lock-Free

멀티스레드 시스템에서 가장 익숙한 방법은 Lock입니다.

pthread_mutex_lock(&lock);

update_order_book();

pthread_mutex_unlock(&lock);

Lock은 매우 유용하고 안전한 도구입니다.

하지만 저지연 시스템에서는 다음과 같은 문제가 발생할 수 있습니다.

Thread A
    ↓
Lock 획득
    ↓
Critical Section
    ↓
Lock 해제

Thread B
    ↓
Lock 대기

Thread가 Lock을 기다리는 동안 latency가 증가할 수 있습니다.

특히 여러 Thread가 높은 빈도로 동일한 데이터를 접근한다면 Lock Contention이 문제가 됩니다.


9. Lock-Free는 "Lock이 없다"는 의미 이상이다

Lock-Free Data Structure는 여러 Thread가 공유 데이터를 처리하면서 전통적인 Mutex에 의존하지 않는 구조를 말합니다.

대표적인 구현 방법이 Atomic Operation + Memory Ordering입니다.

예를 들어 Producer와 Consumer 사이에 Ring Buffer를 사용할 수 있습니다.

Producer
   │
   ▼
┌─────┬─────┬─────┬─────┬─────┐
│  0  │  1  │  2  │  3  │  4  │
└─────┴─────┴─────┴─────┴─────┘
                  ▲
                  │
               Consumer

Producer는 새로운 데이터를 기록하고,

Consumer는 이미 publish된 데이터를 읽습니다.

여기서 중요한 것은 데이터 자체와 Producer/Consumer의 Index를 어떻게 동기화할 것인가입니다.


10. Ring Buffer와 Memory Ordering

간단한 SPSC(Single Producer, Single Consumer) Ring Buffer를 생각해 보겠습니다.

Producer                         Consumer

write data
    │
    ↓
buffer[index]
    │
    ↓
publish head ───────────────→ read head
                                  │
                                  ↓
                              read data

여기서 매우 중요한 순서가 있습니다.

Producer는 반드시:

1. Data Write
2. Head Publish

순서로 동작해야 합니다.

Consumer는:

1. Head Read
2. Data Read

순서로 동작해야 합니다.

이 관계가 잘못되면 Consumer가 아직 완전히 작성되지 않은 데이터를 읽을 가능성이 생깁니다.

따라서:

Data Write
     ↓
Release
     ↓
Publish Index
     ↓
Acquire
     ↓
Read Data

와 같은 Memory Ordering이 중요합니다.


11. 왜 Lock-Free Ring Buffer가 트레이딩 시스템에서 유용한가

트레이딩 시스템에서는 다음과 같은 데이터 흐름을 생각할 수 있습니다.

Market Data Handler
        │
        ▼
   Ring Buffer
        │
        ▼
    Strategy
        │
        ▼
   Risk Engine
        │
        ▼
   Order Manager

각 Thread가 자신의 역할을 담당하고 데이터 전달을 Ring Buffer로 구성하면 공유 상태를 줄일 수 있습니다.

특히 SPSC 구조에서는:

  • Producer 하나
  • Consumer 하나
  • 명확한 Data Ownership
  • 단순한 Index 관리
  • Mutex 최소화

등의 장점이 있습니다.

이는 앞서 살펴본 Cache Locality와 False Sharing 문제와도 직접 연결됩니다.


12. Atomic Index도 Cache를 고려해야 한다

Ring Buffer에서 다음과 같은 구조를 생각할 수 있습니다.

struct ring {
    atomic_size_t head;
    atomic_size_t tail;
};

논리적으로는 아무 문제가 없어 보입니다.

하지만 head와 tail이 동일한 Cache Line에 위치한다면 Producer와 Consumer가 서로 다른 변수를 업데이트하면서도 같은 Cache Line을 계속 무효화할 수 있습니다.

즉:

Atomic을 사용했는데도 Cache Contention이 발생할 수 있습니다.

이것이 앞서 Deep Dive #2에서 살펴본 False Sharing입니다.

따라서 저지연 Ring Buffer에서는 다음이 함께 고려되어야 합니다.

Atomic
   +
Memory Ordering
   +
Cache Line
   +
False Sharing
   +
Data Ownership

13. Data Ownership

저지연 시스템에서 매우 강력한 설계 원칙 중 하나가 Data Ownership입니다.

가능하다면 하나의 데이터 구조를 여러 Thread가 동시에 수정하도록 만들기보다:

Thread A
   │
   └── Owns Data A

Thread B
   │
   └── Owns Data B

처럼 데이터의 소유권을 명확하게 나누는 것이 좋습니다.

데이터 전달이 필요할 경우:

Thread A
   ↓
Message
   ↓
Ring Buffer
   ↓
Thread B

형태로 전달합니다.

이렇게 하면 공유 메모리에 대한 Lock Contention과 Cache Coherency 비용을 줄일 수 있습니다.


14. Trading System에 적용하면

실제 트레이딩 시스템을 단순화하면 다음과 같은 구조를 생각할 수 있습니다.

             Market Data
                  │
                  ▼
          Market Data Handler
                  │
                  ▼
             Ring Buffer
                  │
                  ▼
              Strategy
                  │
                  ▼
             Risk Check
                  │
                  ▼
            Order Manager
                  │
                  ▼
               FEP
                  │
                  ▼
              Exchange

 

각 단계가 독립적인 Thread 또는 Processing Stage로 구성될 수 있습니다.

이때 중요한 것은 모든 Thread가 모든 데이터를 공유하는 것이 아닙니다.

오히려:

각 Thread가 가능한 한 자신의 데이터를 소유하고, 필요한 데이터만 Message 형태로 전달하는 구조

가 저지연 시스템에서 유리할 수 있습니다.


15. Memory Ordering은 Latency와 Correctness의 문제다

Memory Ordering은 단순히 성능 최적화 기술이 아닙니다.

잘못 설계하면 데이터 자체가 잘못 전달될 수 있습니다.

예를 들어:

Producer

Write Order
     ↓
Publish

와

Consumer

Observe Publish
     ↓
Read Order

 

사이에 명확한 Ordering 관계가 없다면 Consumer가 Producer의 변경을 예상한 순서대로 관찰한다고 보장할 수 없습니다.

따라서 저지연 시스템에서 Memory Ordering은:

Correctness

와

Performance

두 가지를 동시에 다루는 문제입니다.


16. 가장 강한 Memory Order가 항상 좋은가?

그렇지 않습니다.

강한 ordering을 무조건 사용하는 것은 시스템의 동기화 비용을 증가시킬 수 있습니다.

반대로 너무 약한 ordering을 사용하면 프로그램의 동시성 의미가 깨질 수 있습니다.

따라서 중요한 것은:

필요한 synchronization guarantee만 제공하는 것

입니다.

예를 들어 단순한 통계 counter와 Producer/Consumer 데이터 publish는 요구사항이 다를 수 있습니다.

Statistics Counter
        ↓
   Relaxed Atomic

반면 데이터 publish에는:

Data Write
    ↓
Release
    ↓
Publish
    ↓
Acquire
    ↓
Data Read

 

와 같은 ordering이 필요할 수 있습니다. 즉,

Correctness를 만족하는 가장 약한 Memory Ordering

을 선택하는 것이 저지연 시스템에서 중요한 설계 원칙이 될 수 있습니다.


17. Lock-Free라고 항상 빠른 것은 아니다

여기에서도 주의할 점이 있습니다.

Lock-Free ≠ Always Faster

Lock-Free 구조에서도 다음 비용이 발생할 수 있습니다.

  • Atomic Operation
  • Cache Coherency
  • Memory Ordering
  • CAS Retry
  • Cache Line Contention
  • Busy Waiting
  • 복잡한 Recovery Logic

특히 여러 CPU가 동일한 Atomic 변수에 반복적으로 접근하면 Cache Line이 Core 사이를 계속 이동할 수 있습니다.

따라서 Mutex 하나를 제거했다고 해서 반드시 latency가 줄어드는 것은 아닙니다.


18. 결국 모든 Deep Dive는 하나로 연결된다

이번 Deep Dive 시리즈에서 살펴본 기술을 하나의 흐름으로 연결해 보면 다음과 같습니다.

CPU Cache
    ↓
False Sharing
    ↓
CPU Affinity
    ↓
NUMA
    ↓
Polling
    ↓
Interrupt / NAPI
    ↓
NIC / RX Queue
    ↓
Kernel Networking
    ↓
Kernel Bypass
    ↓
UDP Market Data
    ↓
Order Book
    ↓
Strategy
    ↓
Atomic / Memory Ordering
    ↓
Lock-Free Ring Buffer
    ↓
Risk Check
    ↓
TCP Order Flow
    ↓
FEP
    ↓
Exchange

처음에는 서로 다른 기술처럼 보이지만 결국 하나의 질문으로 연결됩니다.

How can we move information through the system with minimum latency and predictable behavior?


19. FontesFintech Engineering Approach

저지연 시스템에서 중요한 것은 특정 기술을 사용하는 것이 아닙니다.

중요한 것은 전체 시스템에서 데이터가 어떻게 이동하는지를 이해하는 것입니다.

우리는 다음과 같은 원칙으로 접근할 수 있습니다.

Measure

실제 latency와 시스템 상태를 측정합니다.

Identify

CPU, Cache, Memory, Network, Lock 등 병목의 위치를 확인합니다.

Redesign

필요한 부분의 데이터 구조와 실행 구조를 변경합니다.

Benchmark

동일한 조건에서 변경 전후를 비교합니다.

Measure Again

실제 latency distribution이 개선되었는지 다시 확인합니다.

이 과정은 단순한 성능 최적화가 아닙니다.

Correctness → Performance → Predictability

를 함께 만족시키는 과정입니다.


Conclusion

저지연 트레이딩 시스템의 마지막 단계에서 결국 우리는 CPU의 가장 깊은 영역까지 내려가게 됩니다.

Cache

→ Memory

→ Atomic

→ Memory Ordering

→ Lock-Free

→ Data Ownership

그리고 이 모든 것은 결국 하나의 목적을 향합니다.

데이터를 빠르게 처리하는 것뿐만 아니라, 여러 CPU Core에서 예측 가능한 방식으로 데이터를 전달하는 것

입니다.

저지연 시스템에서는 단순히 CPU를 빠르게 사용하는 것만으로 충분하지 않습니다.

어떤 Core에서 실행되는가

어떤 Memory를 사용하는가

어떻게 데이터를 공유하는가

어떤 순서로 Memory를 관찰하는가

어떻게 Thread 사이에서 데이터를 전달하는가

까지 함께 설계해야 합니다.

결국 저지연 시스템의 핵심은 하나의 기술이 아닙니다.

Right Data. Right Core. Right Memory. Right Ordering.

그리고 우리가 반복해서 돌아가야 하는 원칙도 하나입니다.

Measure First. Optimize with Evidence.


이 연재를 하는 동안 ChatGPT에게 주제를 던져주고 대답을 확인하고 수정하고 다시 상호 피드백...

이런 시간 마저도 감탄스러운데,, 앞으로 대면할 엄청난 AI시대에도 우리모두 잘 적응하고, 잘 어울려 조화로운 삶이 되면 좋겠네요.