안녕하세요, 오늘은 거래량 계산 방법에 대해 소개하고자 합니다!
CoinFlow에서 가장 많이 수신되고, 가장 빈번하게 계산되는 데이터는 체결 틱(Tick)의 거래량과 가격입니다.
보통 많은 사람들이 1주를 사면 거래량도 1개 증가하는 것으로 알고 계시지만, 사실 0.xx 단위로도 거래가 될 수 있습니다.
그렇기에 매 틱마다 들어오는 거래량 소수점 데이터(예: 0.00123456 BTC)를 1분 봉, 5분 봉으로 쉴 새 없이 더해야 합니다.
물론 아직 초당 수만 건 정도의 Tick 데이터가 수신되지는 않습니다. 하지만 추후에 종목을 늘린다면 초당 수만 건의 거래량 데이터는 충분히 처리해야 합니다. 이를 해결하기 위해 어떤 방식들이 있었고, 최종적으로 제가 적용한 방식이 무엇이었는지 소개해드리고자 합니다.
(단일 스레드 기준)
Option 1: Double (부동 소수점) - 빠르지만 위험한 선택
- 장점(직접적인 하드웨어 지원):
- JVM 바이트코드 레벨: Java 소스코드에서
double타입의 덧셈(a + b)을 수행하면, 컴파일 시dadd라는 단일 바이트코드 단위 연산으로 변환됩니다. - JIT 컴파일 및 CPU 실행 레벨: 런타임에 JIT 컴파일러는 이
dadd명령어를 타겟 CPU의 하드웨어 부동소수점 연산 장치 명령어로 직접 매핑합니다. - 즉, 별도의 객체 메모리 할당(
new)이나 힙 영역 접근 없이 CPU의 전용 레지스터 안에서 즉각적으로 연산이 끝나기 때문에 연산 속도와 CPU 효율 면에서는 다른 방식보다 압도적으로 빠릅니다.
- JVM 바이트코드 레벨: Java 소스코드에서
- 단점(IEEE 754 표준 한계):
- 컴퓨터는 IEEE 754 표준을 따릅니다. 이로 인해
0.1과 같은 단순한 십진수도 이진수로는 무한 소수가 되어 메모리에 저장될 때 미세한 값이 잘려 나갑니다. - 결과적으로
0.1 + 0.2를 계산하면0.30000000000000004와 같은 오차가 발생합니다. 1분에 수만 건의 틱(Tick) 거래량이 누적되는 금융 시스템에서 이 미세한 오차가 누적되면 결국 데이터 정합성 문제 로 이어집니다.
- 컴퓨터는 IEEE 754 표준을 따릅니다. 이로 인해
- 테스트:
// VolumeScalingTest.java 일부
@Test
@DisplayName("IEEE 754: Double 타입 단순 연산 시 부동소수점 오차 발생 검증")
void double_SimpleAddition_FloatingPointError_Test() {
// Given
double volume1 = 0.1;
double volume2 = 0.2;
// When
double sum = volume1 + volume2;
// Then (0.3이 아닌 0.30000000000000004 반환)
assertThat(sum).isNotEqualTo(0.3);
assertThat(sum).isEqualTo(0.30000000000000004);
}
- 결론: 테스트 결과를 보게 되면, 0.1 + 0.2 가 0.3을 반환하지 않습니다. 따라서 금융/트레이딩 시스템에서 해당
double연산은 부적절하다고 판단했습니다.
Option 2: BigDecimal - 정확하지만 무거운 선택
- 장점: Java에서 소수점 오차 없이 정확한 연산을 하려면
BigDecimal이 제일 정확합니다. - 단점(성능 병목):
BigDecimal은 객체이기에, 한 번 더할 때마다 새로운 인스턴스가 생성됩니다.- 초당 수만 건의 틱이 들어오는 상황에서 매번
new BigDecimal()을 만들고add()를 호출하면, GC 오버헤드가 발생하여 시스템 성능 저하(STW)가 발생할 수 있습니다.
- 테스트: GC OOM 벤치마크
힙 메모리를 제한하지 않고, 실제 운영 환경과 가장 비슷한 4GB 환경에서 테스트를 진행해봤습니다.
// BigDecimalGCLimitProfiler.java 일부 - JVM Default Heap 한계 측정
public static void main(String[] args) {
final BigDecimal tickVolume = new BigDecimal("0.0001");
BigDecimal accumulator = BigDecimal.ZERO;
try {
while (true) {
accumulator = accumulator.add(tickVolume);
// 누적되는 틱 데이터가 메모리에 상주하는 상황
currentChunk[chunkIndex++] = accumulator;
}
} catch (OutOfMemoryError e) {
System.err.println("OutOfMemoryError 발생!");
}
}
- 결론:


위 그림들은 각각 4GB 기본 메모리를 모두 소진할 때까지의 부하 테스트 결과와 프로파일링 타임라인입니다.
먼저 타임라인 그래프를 보면, 시간이 지남에 따라 메모리가 가파르게 상승하고, 한계에 다다르면 가비지 컬렉션을 수행하는 패턴이 1~2초 간격으로 발생하고 있습니다.
타임라인에 기반하여 테스트 결과를 분석한 결과, GC 과정에서 발생하는 Stop-The-World(시스템 일시 멈춤) 현상이 Latency Spike를 유발한다는 것을 파악했습니다.(쉽게 말해, 카페에 커피 주문이 미친 듯이 쏟아지는데, 직원이 바닥을 닦겠다고 1분마다 홀 장사를 멈춰버리는 꼴입니다. 잠깐의 청소 시간 동안 밖에는 대기 줄이 폭발적으로 밀리게 되고, 직원은 만들다가 화나서 그만두겠죠?)
결과적으로 실제 환경에서 초당 수천만 개의 틱이 들어오진 않더라도, 종목 수가 늘어나고 트래픽이 몰리는 장기 운영 환경에서는 OOM 을 충분히 발생시킬 수 있습니다. 또한, 메모리 관리 측면에서도 좋지 않은 선택이라고 판단했습니다.
해결책: 고정 소수점 기반의 Long Scaling 전략 도입
- 아이디어: 소수점 위치를 고정시켜 버리면, 소수를 정수처럼 취급해서 매우 빠르고 오차 없이 계산할 수 있지 않을까?
- 알고리즘 문제를 풀 때 부동소수점 오차를 피하기 위해 특정 배수를 곱해 정수로 치환하여 계산하는 것과 동일한 원리입니다.
- 구현 방법 (VolumeScaler 도입):
- Delimiter: 암호화폐 거래의 대다수는 소수점 8자리까지의 숫자를 나타냅니다. 그래서 delimiter를
10^8 (100,000,000)로 정해줬습니다. - 값이 들어올 때: 외부 (업비트 등)에서 들어온 거래량
0.12345678에10^8을 곱해 순수 정수인12345678L(long)로 변환하여 메모리(OhlcAccumulator)에 누적합니다. - 연산: 기본 자료형인
long의 단순+연산만 수행하므로 GC 부하가 거의 없습니다.(volume = Math.addExact(volume, vol);) - 값을 내보낼 때: 1분 봉이 마감되어 DB에 저장하거나 클라이언트(프론트엔드)로 응답을 내려줄 때만 다시
10^8로 나누어BigDecimal로 복원합니다.
- Delimiter: 암호화폐 거래의 대다수는 소수점 8자리까지의 숫자를 나타냅니다. 그래서 delimiter를
- 테스트: 연산 속도 - BigDecimal vs Long scaling
// VolumeScalingTest.java 일부 - 1천만 번 누적 성능 비교
// 1. BigDecimal 성능 측정
long startBd = System.currentTimeMillis();
for (int i = 0; i < iterationCount; i++) {
accumulatedBd = accumulatedBd.add(bdTickVolume);
}
long bdDuration = System.currentTimeMillis() - startBd;
// 2. Long 성능 측정
long startLong = System.currentTimeMillis();
for (int i = 0; i < iterationCount; i++) {
accumulatedLong = Math.addExact(accumulatedLong, longTickVolume);
}
long longDuration = System.currentTimeMillis() - startLong;
assertThat(longDuration).isLessThan(bdDuration);

위 이미지는 테스트 결과입니다.
BigDecimal은 약 59ms가 소요된 반면, 객체 생성이 없는 Long 연산은 단 3ms만에 처리를 완료했습니다.
결과적으로 Long Scaling 연산이 BigDecimal 보다 빠르며, 메모리 낭비도 없기에 해당 방식을 사용하게 되었습니다.
최종 결과
- 성능: 무거운
BigDecimal객체 생성과 불완전한double연산 대신,long의 단순+연산만 수행하는 방식으로 성능 최적화를 구현했습니다. - Overflow 방어: 만약 특정 코인의 폭발적 거래로 거래량이
long의 최대값(약 922경)을 초과할 경우를 대비해 방어 로직을 구성해봤습니다.- 단순
+연산의 위험성: Java에서 두long값의 합이 범위를 넘어가면 시스템은 예외를 던지지 않고 오버플로우로 인해 음수로 바뀌어버립니다. 이는 금융/트레이딩 시스템에서 치명적인 문제라고 생각했습니다. Math.addExact()적용:단순+대신Math.addExact()연산을 사용하여 오버플로우 발생 시 즉시ArithmeticException을 던지도록처리했습니다. (거래량이 992경을 넘지는 않을 것으로 생각하고 구현했습니다.
- 단순
// OhlcAccumulator.java 일부
public synchronized void apply(BigDecimal price, long vol, Instant eventTime) {
// ... 가격, 시간 갱신 로직 생략 ...
// volume overflow 검증 및 누적
volume = Math.addExact(volume, vol);
}
이번 포스팅은 여러개의 tick 데이터 거래량을 집계하는 방식에 대해 고민한 점을 적어봤습니다.
잘못된 부분이나 더 좋은 방법이 있다면 댓글 남겨주시면 감사하겠습니다.
'[Spring] - Study > Project - CoinFlow(비트코인 차트)' 카테고리의 다른 글
| [Data Flow] 실시간 차트 데이터 흐름도 재설계: 프론트엔드 연산 제거와 완벽한 정합성 보장하기 (0) | 2026.02.28 |
|---|---|
| Data Flow 설계하기: Latency와 Consistency를 고려해보자 (0) | 2026.02.20 |
| CoinFlow 프로젝트 시작 ~.~ (0) | 2026.02.13 |