Data Flow 설계하기: Latency와 Consistency를 고려해보자

2026. 2. 20. 10:53·[Spring] - Study/Project - CoinFlow(비트코인 차트)

실시간 차트 서비스에서 가장 중요한 것은 무엇일까요 속도? 정확성?

이 질문에서 시작된 고민이 Dual-Path Architecture라는 결과물로 이어지기까지의 과정을 기록합니다.


안녕하세요 오늘은 CoinFlow 서비스를 구축하기 위해 Data Flow 를 어떻게 설계했는지 설명해드리고자 합니다.

 

그럼 바로 시작하겠습니다!

 

초기 설계


처음에는 데이터 정합성과 정확성을 최우선으로 생각하여, 모든 데이터를 DB에 먼저 저장하고, 그 다음에 보여주는 구조로 설계했습니다.

 

금융 데이터인 만큼 데이터의 유실이나 오차를 절대 허용해서는 안 된다는 생각에, RDBMS를 Single Source of Truth 로 삼고자 했습니다.

 

초기 Data Flow 설계

데이터 흐름은 매우 직관적이었습니다.

  1. Collector: 외부 거래소(Binance)에서 실시간 체결(Tick) 데이터를 수집 및 적재합니다.
  2. Consumer: 큐에서 데이터를 하나씩 꺼내 가공합니다.
  3. Database: 가공된 데이터를 RDBMS(PostgreSQL)에 INSERT합니다.
  4. API/Socket: 클라이언트 요청 시 DB에서 데이터를 조회하여 응답합니다.

언뜻 보기에는 데이터 정합성을 보장하는 안전한 구조처럼 보입니다. 하지만...치명적인 문제가 존재했습니다.

 

초기 설계의 치명적인 문제점


설계를 하고 시나리오를 계속해서 생각해보니, 실시간 틱 처리를 위한 과정에서 한계점이 보였습니다.

 

1. RDBMS는 실시간 틱 데이터를 감당할 수 없다

틱(Tick) 데이터는 초당 수백 건씩 쏟아지는 Time-series Data입니다. 이를 RDBMS에 직접 저장하려고 했을 때, 다음과 같은 문제점이 있었습니다.

  • IOPS 한계 (Disk I/O 병목):
    • RDBMS는 데이터의 무결성(ACID)을 보장하기 위해 매 Insert마다 로그(WAL)를 쓰고 디스크에 동기화합니다.
    • 초당 수백 번의 Insert가 발생하면 디스크 I/O가 포화 상태가 되어, 정작 중요한 조회 쿼리(Select)까지 덩달아 느려지는 리소스 잠식 현상이 발생합니다.
  • 인덱싱 비용:
    • 시계열 데이터는 시간 순서로 정렬해서 조회해야 하므로 event_time 컬럼에 인덱스가 필수입니다.
    • 하지만 인덱스를 걸면 데이터 1개를 저장할 때 인덱스 트리(B-Tree)도 같이 재정렬해야 합니다.
    • 데이터가 쌓일수록 인덱스 트리가 거대해지고, 재정렬 비용이 기하급수적으로 늘어나 Insert 속도가 점점 느려집니다.
  • Latency:
    • Insert -> Transaction Start -> Lock -> Write WAL -> Index Update -> Commit
    • 이 모든 과정을 거치는 데 최소 수 십 ms가 소요됩니다.
    • 이미 DB에 저장된 시점에는 이미 지나간 과거 가격이 되어버려, 사용자에게 즉각적인 반응성을 제공할 수 없습니다.

 

2. 근본적인 정책에 대한 고민

우리는 주식이나 코인 차트를 볼 때 무엇을 가장 중요하게 생각할까요?

 

바로 지금 이 순간 얼마에 체결되었는지입니다.

 

매수 버튼을 누르려는 순간의 가격이 가장 중요하지, 그 데이터가 DB에 안전하게 기록되었는지는 사용자의 관심사가 아닙니다.

 

현재가를 사용자에게 0.1초라도 빨리 보여주는 것. 이것이 차트 서비스의 본질이라고 판단했습니다.

 

그래서 WebSocket을 통해 Tick 데이터를 Stream 하는 방식이 필요하다고 생각했습니다.

 

해결책: Dual-Path Architecture (The Solution)


저는 이 문제를 해결하기 위해 시스템을 속도 담당(Fast-Path)과 정확성 담당(Slow-Path)으로 분리했습니다.

 

🏃‍♂️ Fast-Path (Speed Layer)

일단 보여준다. 저장은 나중에 하고.

  • 목표: 0.1초 이내에 사용자에게 시세 전달 (Zero Latency).
  • 기술: Redis Stream (tick:raw)
  • 흐름:
    1. Collector가 거래소 틱을 받자마자 Redis Stream에 꽂아넣습니다. (XADD)
    2. TickRawStreamConsumer가 대기하고 있다가 즉시 데이터를 소비합니다.
    3. 복잡한 계산 없이 바로 WebSocket으로 전달합니다.
    4. DB 저장 과정 생략!
// TickRawStreamConsumer.java (Fast-Path)
@Override
public void onMessage(MapRecord<String, String, String> message) {
    // 1. Redis Stream에서 틱 꺼냄
    TickDto tick = mapToDto(message);

    // 2. DB 저장 없이 바로 소켓으로 전송 (비동기)
    sessionManager.getSubscribers(symbol).forEach(session -> {
        session.send(Flux.just(session.textMessage(json)));
    });
}

 

🐢 Slow-Path (Accuracy Layer & Correction)

늦더라도 확실한 정답을 알려준다.

  • 목표: 데이터 정합성 보장 (Strong Consistency).
  • 기술: Redis Pub/Sub (candle:closed)
  • 흐름:
    1. Aggregator 앱이 1분 동안 들어온 틱을 메모리에서 열심히 계산(Aggregation)합니다.
    2. 1분이 딱 끝나는 순간(00초), DB에 저장하고 확정된 캔들(CandleClosedEvent)을 발행합니다.
    3. CandleClosedStreamConsumer가 이걸 받아서 WebSocket으로 쏩니다.
    4. 프론트엔드는 이 데이터를 받아서 기존에 대충(?) 그려놨던 캔들을 정확한 값으로 덮어씌웁니다.
// CandleClosedStreamConsumer.java (Slow-Path)
@Override
public void onMessage(Message message, byte[] pattern) {
    // 1. Redis Pub/Sub으로 확정된 1분 봉 데이터 수신
    CandleClosedEvent event = deserialize(message);

    // 2. 클라이언트에게 확정된 데이터 전송 -> 프론트엔드 차트 보정(Correction) 수행
    broadcastToSubscribers(event);
}

 

 

추가적으로 고민했던 부분 : WebSocket Multiplexing

여기서 또 하나의 기술적 고민이 있었습니다.


Fast-Path용 소켓이랑 Slow-Path용 소켓을 따로 뚫어야 하나?

  • No! 브라우저 연결 제한(Connection Limit)과 리소스 낭비를 막기 위해 Single Connection으로 처리했습니다.
  • 하나의 WebSocket 세션으로 Tick 데이터(실시간)와 CandleClosed 데이터(보정용)를 모두 보냅니다.
  • 프론트엔드는 메시지 타입(type: tick vs type: candle_closed)만 보고 구분해서 처리하면 됩니다.

 

장애 대응 및 한계점 

 

물론 이 설계가 완벽한 것은 아닙니다.

하지만 시스템에서 발생할 수 있는 주요 장애 시나리오에 대한 대응책을 어느 정도 생각해봤습니다.

 

장애 시나리오: WebSocket 서버가 죽는다면?

  • 문제: 실시간 시세를 뿌려주던 WAS가 비정상 종료되거나 재배포를 위해 내려갑니다.
  • 대응 (Replay Strategy):
    1. Redis Stream은 로그(Log)입니다: 컨슈머가 죽어도 데이터는 Redis에 그대로 남아있습니다.
    2. 재시작 시점 복구: 서버가 다시 살아나면, 마지막으로 읽었던 Offset(Last Consumed ID)부터 다시 읽어들입니다.
    3. 밀려있던 틱들이 순식간에 재처리되면서 클라이언트는 잠시 멈췄던 차트가 "빠르게 감기(Fast-forward)" 되듯이 최신 상태로 동기화됩니다.

 

개선되어야 할 부분

  1. Consumer 장애:
    • Redis Stream에 데이터는 계속 쌓이는데 Consumer가 오랫동안 죽어있으면 Memory Overflow가 발생할 수 있습니다.
    • -> DLQ(Dead Letter Queue) 도입 및 모니터링 알람이 필요합니다.
  2. External API(거래소) 장애:
    • 바이낸스 서버 자체가 터져서 틱이 안 들어오는 경우는 우리가 손쓸 수 없습니다.
    • -> Circuit Breaker를 도입하여, 외부 장애 시 "점검 중" 상태로 전환하거나 다른 거래소로 Failover 하는 전략이 필요합니다.
  3. 늦게 들어오는 틱 처리:
    • 네트워크 지연으로 인해 먼저 발생한 틱(10:00:00)이 나중에 발생한 틱(10:00:05)보다 늦게 도착할 수 있습니다.
    • 이때 두 가지 전략을 사용하는 방법을 생각해봤습니다.
      • Fast-Path (실시간): 정렬 포기 (Zero-latency).
        • 정렬을 위해 기다리면(Buffering) 실시간성이 떨어지므로, 순서 역전을 허용하거나 과거 데이터를 무시합니다.
      • Slow-Path (정확성): 로직 보정 (Correctness).
        • 집계 시점에는 Event Time을 기준으로 Open/Close 가격을 재산정하여 최종 데이터의 정합성을 맞춥니다. 

 

결론: 최종 데이터 흐름 (Data Flow)


최종적으로 만들어진 전체적인 데이터 흐름을 다이어그램으로 정리했습니다.

최종 Data Flow

🎨 다이어그램 분석 (Color Code)

다이어그램의 각 컴포넌트는 역할에 따라 색상으로 구분되어 있습니다.

  • 🔴 Execution Modules (Red): 실제 비즈니스 로직을 수행하는 핵심 모듈들입니다.
  • 🟢 External API (Green): 바이낸스(Binance)와 같은 외부 데이터 소스입니다. 우리가 제어할 수 없는 영역입니다.
  • 🔵 Client (Blue): 클라이언트 영역입니다. WebSocket을 통해 실시간 데이터를 수신합니다.
  • 🟡 Database (Yellow): 영구적인 데이터 저장을 담당하는 PostgreSQL입니다.
  • 🟣 Message Queue (Purple): 메시지 큐 입니다.
    • Stream: Fast-Path, 대량 데이터(Tick)
    • Pub/Sub: Slow-Path, 이벤트 전파(Closed Candle)

 

이 구조 덕분에 사용자는 빠른 반응 속도를 경험하고, 시스템은 데이터 정합성을 지키는 구조를 만들 수 있었습니다.


 

오늘은 사용자 경험을 향상시킴과 동시에 정합성을 보장할 수 있는 설계에 대해 고민해봤던 과정을 기록해봤습니다.

더욱 좋은 방법이 있거나 잘못된 부분이 있으면 댓글로 남겨주시면 감사하겠습니다!

'[Spring] - Study > Project - CoinFlow(비트코인 차트)' 카테고리의 다른 글

[Data Flow] 실시간 차트 데이터 흐름도 재설계: 프론트엔드 연산 제거와 완벽한 정합성 보장하기  (0) 2026.02.28
초당 수만 건의 틱 데이터, 거래량은 어떻게 집계해야 할까? (BigDecimal vs Long)  (1) 2026.02.24
CoinFlow 프로젝트 시작 ~.~  (0) 2026.02.13
'[Spring] - Study/Project - CoinFlow(비트코인 차트)' 카테고리의 다른 글
  • [Data Flow] 실시간 차트 데이터 흐름도 재설계: 프론트엔드 연산 제거와 완벽한 정합성 보장하기
  • 초당 수만 건의 틱 데이터, 거래량은 어떻게 집계해야 할까? (BigDecimal vs Long)
  • CoinFlow 프로젝트 시작 ~.~
moonwhistle
moonwhistle
  • moonwhistle
    OrangeBanana
    moonwhistle
  • 전체
    오늘
    어제
    • 분류 (113)
      • [Spring] - Study (11)
        • CS (0)
        • Project - 모각밥(모여서 각자 밥먹기) (7)
        • Project - CoinFlow(비트코인 차트) (4)
      • 오픈소스 (1)
      • 📖 DB (1)
      • JAVA (6)
      • 우아한테크코스[프리코스] (15)
      • [Spring] - 멘토링 (30)
        • 미션 (13)
        • 개념 (16)
      • 알고리즘 (2)
      • 💬 생각생각 (3)
        • F-lap (2)
      • 통신 (34)
        • 네트워크 프로토콜 (18)
        • 데이터통신 (16)
      • 용접 (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 카테고리
    • 초록스터디
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    multimodule
    코인
    에프랩
    data
    GC
    jmm
    Flow
    멀티모듈
    spring
    동시성
    garbage collection
    Java
    KAFKA
    F-Lab
    volatile
    트랜잭션
    redis
    후기
    Synchronized
    설계
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
moonwhistle
Data Flow 설계하기: Latency와 Consistency를 고려해보자
상단으로

티스토리툴바