[Go] 실시간 ohlcv 차트(캔들 차트) 데이터 적재 서버 개발 in 블록체인 플랫폼 - Part 1. 문제편
블록체인 업계에서 핫했던 '밈 코인 발행 플랫폼'을 우리 회사에서도 개발하게 되었다.
개요
'밈 코인 발행 플랫폼'이란 사용자가 쉽게 자신만의 토큰을 생성하고 거래할 수 있는 서비스로, ERC-20이나 SPL 토큰 표준을 활용해 복잡한 스마트 컨트랙트 지식 없이도 누구나 토큰을 발행하고 이를 AMM(Automated Market Maker) 기반 DEX(Decentralized Exchange)에 자동으로 상장시킬 수 있는 생태계를 제공한다.
밈 코인 플랫폼이 업계에서 핫해진 이유는 Web3의 접근성 문제를 해결하며 진입 장벽을 낮춤으로써 일반 사용자들도 쉽게 토큰 이코노미에 참여할 수 있게 되었기 때문이다. 또한 밈 문화와 블록체인의 결합은 커뮤니티 기반의 강력한 네트워크 효과를 만들어내며, 토큰 발행자와 투자자 간의 P2P 유동성 풀 형성을 통해 새로운 형태의 자금 조달 메커니즘을 제시했다.
중요한 건 유저가 재미를 느낄 수 있는 실시간 반응형 사이트다. 특히 토큰 가격의 변동이 실시간으로 반영되어 시각적으로 보여지는 차트 시스템은 유저 경험의 핵심 요소이다. 트레이딩 플랫폼에서는 ms 단위의 지연도 사용자 경험을 저해할 수 있으므로, WebSocket 기반의 실시간 데이터 스트리밍과 효율적인 데이터 처리 파이프라인은 필수적이다. 사용자들은 자신이 발행하거나 투자한 토큰의 가격 변동을 실시간으로 확인하면서 즉각적인 피드백을 받길 원하며, 이러한 실시간성이 플랫폼의 성패를 가르는 중요한 요소가 된다.
익히 알려진 레퍼런스로는 솔라나 체인의 pump.fun, moonshot(트럼프 토큰이 출시된 곳), tron 체인의 sumPump, Sui와 Aptos 블록체인의 MovePump 가 있다. 들어가보면 굉장히 정신없는(?) 화면을 만나볼 수 있다.
프로젝트 배경
프로젝트는 시니어 개발자 1명, 그리고 나를 포함한 주니어 개발자 2명 이렇게 진행을 맡게 되었다. 시니어 개발자는 API에서 필요한 온체인 데이터를 쌓는 서버를, 주니어 개발자인 나와 다른 한 분은 API 서버 개발을 담당했다. (API 개발기는 따로 포스트를 올릴 예정이다.) 프로젝트 규모에 비해 턱없이 부족한 인력이지만 빠르게 완성을 진행해야 했기에 지금 목표를 달성할 수 있는 여력이 있는 팀원들로 구성이 되었다. 솔직히 밈 생태계에 약간의 흥미를 느끼고 있었기 때문에 프로젝트를 맡고 싶었는데 포함되게 되어서 기뻤다.
그러나 문제는 시니어 개발자분이 갑자기 퇴사를 하게 된 것에 있었다. 시니어 개발자분이 담당한 서버는 미완성인 상태였고, 배포까지는 단 일주일도 남지 않은 상태였다. 어찌저찌 동료가 서버가 구동은 될 수 있도록 고쳤으나 서버는 돌아가지만 쌓는 데이터가 이상하고, 정확하지 않았다. 특히, 문제는 차트 데이터였다ㅠㅠ
실시간성이 중요한 밈토큰 특성 상, 가격 데이터와 차트는 사용자 경험의 핵심 요소이다. 밈 코인 발행 플랫폼에서는 신규 발행된 토큰들이 초기 유동성 풀 형성 과정에서 극심한 가격 변동을 보이며, 이러한 초기 가격 형성 단계에서의 변동성(price volatility)은 특히 주목받는다. 새롭게 발행된 토큰이 처음 유동성 풀에 상장되는 순간부터 시작되는 가격 발견(price discovery) 과정은 매우 빠르게 진행되며, 유저들은 이 과정에서 실시간 가격 변동을 확인하며 참여 여부를 결정한다. 따라서 정확하고 신뢰할 수 있는 차트 데이터는 플랫폼의 신뢰성과 직결된다.
그런데 현재 차트가 제대로 쌓이지 않고 있던 것이다. 아예 시간 순차적으로 쌓지도 못할 뿐더러 가격 자체의 값도 이상했다.
그동안 팀에서 가격 및 차트 데이터를 개발한 것이 나였기에 내가 긴급하게 차트 적재 부분까지 담당하게 되었다. 문제는 나 또한 리니어 차트는 개발해본 적이 있어도, '캔들 차트'는 처음 만들어 본다는 것이었다.
설계 고려 사항
블록체인 데이터를 기반으로 한 차트 시스템은 일반적인 백엔드 서비스와는 설계 접근 방식이 근본적으로 다르다. 일반적인 차트 서비스는 자체 데이터베이스에서 생성된 데이터를 시각화하는 반면, 블록체인 기반 차트는 분산 원장에 기록된 이벤트 로그를 JSON-RPC 인터페이스를 통해 읽어와 재구성해야 한다.
우리 시스템의 경우, 블록에 저장된 이벤트 로그(참고로 EVM 기반이다)를 파싱하여 온체인 데이터를 수집하고, 특히 스마트 컨트랙트에서 발생하는 Buy, Sell 이벤트 발생 시 가격 변동을 감지하여 차트 데이터에 반영해야 했다.
이런 이유로 여러 이벤트 타입 중에서도 buy와 sell 타입의 이벤트에서만 차트 데이터를 갱신하는 방식으로 설계되어 있었다.
이러한 설계에서 고려해야할 문제가 여럿 있었다.
1. 시계열 데이터의 연속성
차트 데이터는 1분, 5분 등 일정한 시간 간격(time interval)으로 지속적으로 쌓여야 하는데, 이벤트 기반(event-driven)으로만 데이터를 쌓는다면 이벤트가 발생하지 않은 시간대에는 데이터가 존재하지 않게 된다. 따라서 이벤트가 없는 시간대에는 이전 가격 데이터를 그대로 유지하는 OHLCV 데이터 연속성 보장 로직이 필요했다.
2. 서버의 재기동 시나리오
블록체인 인프라 특성상 데이터 불일치(reorg와 같은 상황)나 RPC 노드 연결 문제 발생 시 데이터를 다시 쌓아야 하는 경우가 빈번하다. 따라서 재기동 시 특정 블록 번호나 최신 블록부터 차트 데이터를 재구성할 수 있는 메커니즘이 필요했다. 이를 위해서는 이미 차트에 반영된 트랜잭션을 추적하고, 재기동 시 중복 반영을 방지하는 멱등성(idempotency) 로직이 필수적이었다.
이러한 복잡한 요구사항들은 Go의 강력한 동시성 Primitives(goroutines, channels, mutexes)를 활용한 견고한 동시성 제어와 상태 관리 메커니즘을 필요로 했다. 특히 Go의 sync 패키지에서 제공하는 Mutex, RWMutex, WaitGroup과 같은 동기화 도구를 적절히 활용하여 경쟁 상태(race condition)를 방지하고 데이터 일관성을 유지해야 했다.
3. 아키텍처 설계의 제약 조건
이상적으로는 기능별로 서버를 분리하는 마이크로서비스 아키텍처가 좋지만, 온체인 데이터의 방대한 양과 RPC 노드 접근에 따른 네트워크 부담을 고려할 때 로그 수집 서버를 여러 대 운영하는 것은 rate limiting과 비용 측면에서 비효율적이었다. 인프라 비용의 제약으로 서버 분리가 불가능했고, 이로 인해 단일 서버 내에서의 효율적인 concurrent processing이 필요했다.
또한 "ETL 파이프라인 패턴"으로 온체인 데이터를 먼저 DB에 저장하고 이후에 차트 데이터를 생성하는 접근법은 실시간성이 떨어지는 문제가 있었다. 게다가 네트워크 지연이나 문제로 DB 데이터 적재가 지연된다면, 차트 데이터의 정합성(consistency)에 심각한 오류가 발생할 위험이 컸다. 이는 Kafka와 같은 메시지 큐를 사용하여 해결할 수도 있지만, 인프라 구성의 복잡성과 비용 증가 문제가 있었다.
기존 서버 설계
우선 내가 인수인계 받았을 때의 차트 시스템은 크게 세 개의 주요 컴포넌트로 구성되어 있었다.
1. 데이터 모델
type Chart struct {
Start int64
End int64
LastUpdateBlock int64
LastUpdatedTxIndex int64
TxHashes []common.Hash
Ohlcv map[common.Address]*Ohlcv
}
type Ohlcv struct {
Open decimal.Decimal
High decimal.Decimal
Low decimal.Decimal
Close decimal.Decimal
Volume decimal.Decimal
}
- start, end: Unix 타임스탬프 형식으로 각 캔들의 시작, 종료 시간을 나타내는 필드로, MongoDB 컬렉션의 복합 인덱스(Compound Index)로 사용
- ohlcv: 각 토큰의 Open, High, Low, Close, Volume 데이터로, 토큰 주소를 키로 가지는 맵 객체. 가격 데이터는 블록체인의 decimals를 고려하여 DB에는 string 타입으로 저장(big.Float의 직렬화 이슈 방지)
- last_updated_block, last_updated_tx_index: 마지막으로 가격을 업데이트한 블록과 트랜잭션 인덱스로, 특히 용량 문제로 big.Int 포인터 타입 사용
- tx_hashes: 이미 처리된 트랜잭션의 해시 목록으로, 트랜잭션 중복 처리를 방지하기 위한 블룸 필터(Bloom Filter) 대용
2. 차트 데이터 생성 파이프라인
시스템은 두 개의 핵심 고루틴으로 구성되어 있었다.
2.1 Base Chart Generator
func ChartBaseBatch(chartType ChartType) {
c := cron.New()
c.AddFunc(fmt.Sprintf("@every %ds", chartType.GetInterval()), func() {
if err := w.setBaseChart(chartType); err != nil {
return
}
})
// ...
}
각 차트의 시간 간격에 따라 cron job으로 데이터를 우선 생성:
- 정의된 시간 간격(1분, 5분 등)마다 정기적인 베이스 차트를 생성
- 이전 캔들의 종가(close price)를 기준으로 새로운 캔들의 초기 값을 설정
- 각 시간대별 차트(1분봉, 5분봉 등)에 대해 독립적인 처리를 수행
2.2 Event Handler
func handleTokenBoughtOrSoldEvent(
l types.Log, eventData any, eventType EventType, timestamp int64,
) ([]mongo.WriteModel, error) {
// ...
if chart1MModel, err = w.chartManager.MakeUpdateChartModel(l, eventData, timestamp, Chart1M, tradingType); err != nil {
// ...
}
// ...
}
블록체인 이벤트를 실시간으로 처리하는 역할을 담당:
- Buy/Sell 이벤트를 감지하여 실시간으로 차트 데이터를 업데이트
- 트랜잭션 단위로 가격 정보를 갱신하고 High, Low 가격을 조정
- 거래량(Volume) 데이터를 누적하여 처리
- MongoDB bulkwrite를 위한 WriteModel 생성
3. 데이터 관리
type ChartManager struct {
mongoDB *mongoDB.MongoDB
chart1MCache map[int64]*Chart
chart5MCache map[int64]*Chart
batchTimestamp map[ChartType]*int64
// ...
}
차트 데이터를 메모리 효율적으로 관리하기 위한 구조:
- MongoDB를 영구 저장소로 활용하여 차트 데이터의 지속성 보장
- 인메모리 캐시를 통한 성능 최적화로 빈번한 DB 접근 최소화
- 타임프레임별(1분, 5분 등) 독립적인 캐시 관리로 데이터 분리
주요 문제점
하지만 현재 코드에는 여러 심각한 문제점이 있었다...
1. 동시성 제어 미흡
이 구조가 정상적으로 작동하려면 event handler는 반드시 base chart가 먼저 데이터를 생성한 후에 동작해야 했다. base chart가 이전 캔들을 참조하여 ohlc 데이터를 생성해야만 event handler가 올바른 컨텍스트에서 가격을 업데이트할 수 있기 때문이다.
하지만 현실은 두 고루틴이 동시에 실행되고 있었고, 이로 인해 race condition이 발생하고 있었다.
차트 데이터의 불일치와 비정상적인 가격 표시의 원인은 이 race condition 즉, base chart가 데이터를 생성하기 전에 event handler가 먼저 실행되었기 때문이었다.
2. 서버 재기동 고려 부재
서버 재기동 시나리오를 고려한 로직이 부재하여, 재시작 시 데이터 정합성을 보장할 수 없는 상태였다. 블록체인에서 흔히 발생하는 체인 재구성(chain reorganization)이나 노드 동기화(node synchronization) 문제 발생 시, 적절한 에러 핸들링과 복구 메커니즘이 없어 심각한 데이터 불일치가 발생할 수 있었다. 이는 특히 중요한 데이터 손실이나 오류가 발생했을 때 복구가 어려운 심각한 설계 결함이었다.
3. 메모리 관리 이슈
차트 데이터는 시간이 지남에 따라 계속 증가하는데, 이를 적절히 관리하는 메커니즘이 없었다. 지속적인 사용량의 증가로 OOM(Out Of Memory)가 발생할 수 있었고, GC(Garbage Collection) 부하 증가로 인한 성능이 저하될 가능성도 있었다. 불필요한 과거 데이터까지 메모리에 유지가 되어 자원이 계속 낭비될 수 있었다.
글이 너무 길어져서 글을 나눴다!
해결 방안은 [Go] 실시간 ohlcv 차트(캔들 차트) 데이터 적재 서버 개발 in 블록체인 플랫폼 - Part 2. 해결편에서 계속..!