클라우드 사용량 계량·정산 시스템
2025.06 – 현재 · 백엔드 개발 (선임) · 엔쓰리엔(주) 직지랩스
- Java 21
- Spring Boot 3.3
- Spring WebFlux
- R2DBC
- PostgreSQL
- Redis
- Reactor-Kafka
- Gradle
Overview
개요
클라우드 자원을 얼마나 썼는지 측정하고 요금을 계산하는 백그라운드 시스템입니다. 돈을 다루는 만큼 오차 없는 정확한 계산이 핵심이며, 트래픽이 몰려도 멈추지 않도록 비동기 방식으로 구현했습니다.
- 5분 스케줄 계량 워커: 컨테이너=초·스토리지=MB-초 계산, 슬롯 경계 클램핑 + 선형 보간으로 정밀 사용량 산출, AtomicBoolean 재진입 가드로 이중집계 차단
- 정수 고정소수점 PriceCalculator: 단가를 시/분/초 스케일(×10²/10⁴/10⁶)로 ROUND 연산해 부동소수 오차 없는 정산, effective_date 최신 요율을 LATERAL 조인으로 정확 반영
- Settle/SettleDetail 트랜잭션 정산 + 크레딧 차감 비동기 RPC(요청-응답 페어) — Kafka 실패가 DB 재시도를 유발하지 않도록 격리해 이중 차감 방지
- NicePay PG 위변조 검증(SHA-256 서명 + 금액 이중 검증) 후 승인→크레딧 충전 비동기 처리
- CQRS Read-side 사용량 대시보드: Redis 집계 뷰 조회로 일/주/월 시계열 가공(롤링 평균·현재 소비율, ISO 주차→월별 N주차 라벨) — 계량(생성)과 분리된 조회 계층
- 이중 R2DBC 데이터소스 분리(로그 Read / 계량·정산 Write) + 커넥션 풀 튜닝
Architecture
시스템 아키텍처
Metrics
성과 지표
정수 고정소수점
정산 정밀도
무오차 정산
선형보간 MB-초
계량
이중 R2DBC
비동기 DB
Read / Write 분리
Troubleshooting
트러블슈팅
선형 보간 기반 MB-초 스토리지 계량
문제 상황
스토리지 사용량은 외부에서 일정 간격으로 측정된 이산(discrete) 바이트 값으로만 들어왔다. 그러나 과금을 위한 계량은 5분 슬롯 단위 정확한 사용량(MB-초)이 필요했고, 측정 시점이 슬롯 경계와 일치하지 않아 단순 합산으로는 오차가 발생했다.
원인 가설
슬롯을 감싸는 앞뒤 측정점 두 개를 찾아 슬롯 중간 시각의 바이트 값을 선형 보간하면, 이산 측정값 사이를 정밀하게 추정할 수 있다. 계량 구간 ±여유 시간만큼 측정 로그를 더 조회하면 보간에 필요한 경계 측정점을 확보할 수 있다.
검증
계량 구간보다 넓게 볼륨 사용량 로그를 조회해 보간 경계를 확보하고, 슬롯 [start,end]를 감싸는 before/after 측정점으로 슬롯 중간 시각의 바이트를 b0 + (b1-b0)*midSec/totalSec로 추정하는 로직을 검증했다. 한쪽 측정점만 있으면 그 값, 둘 다 없으면 0으로 처리되는지, 최종 (보간 바이트 × 사용 초) / 1MB로 MB-초가 산출되는지 확인했다.
해결
- 슬롯 중간 시각 기준 선형 보간으로 이산 측정값 사이를 추정해 정밀 MB-초 사용량을 계산했다.
- 컨테이너는 슬롯 경계 클램핑으로 부분 사용 초를 정확히 잘라내 누적했다.
- 워크타입(컨테이너/스토리지)은 Strategy 패턴(
WorkTypeProcessor+ supports)으로 분리해 계량 로직을 확장 가능하게 구성했다.
멱등 증분 계량 (이중집계 방지)
문제 상황
계량 워커는 5분 스케줄로 반복 실행되는데, 이전 사이클이 길어져 다음 사이클과 겹치거나 워커가 재실행되면 같은 구간이 두 번 계량되어 과금 데이터가 부풀려질 위험이 있었다. 금융 도메인이라 이중집계는 치명적이었다.
원인 가설
마지막 계량 시점 이후 구간만 재계산하는 증분 계량과, 저장 단계의 멱등성(중복 PK는 무시)을 결합하면, 재실행/중복 사이클에도 동일 구간이 한 번만 반영된다. 사이클 진입 시 재진입 가드를 두면 동시 실행 자체를 막을 수 있다.
검증
AtomicBoolean.compareAndSet(false,true)로 이전 사이클 미완료 시 skip되는지, 마지막 계량 레코드(startAt) 이후부터 계산이 시작되는지 확인했다. 저장 시 중복 PK로 인한 DataIntegrityViolationException은 조용히 무시하고 그 외만 경고하는지, 항목 단위 에러가 onErrorResume으로 격리되어 전체 사이클이 보호되는지 점검했다.
해결
AtomicBoolean재진입 가드로 사이클 중복 실행을 차단했다.- 마지막 계량 시점 이후 구간만 재계산하는 증분 계량 + 중복 PK 무시로 재실행 멱등성을 확보했다(이중집계 방지).
- 대상 매핑을
concatMap으로 순차 처리하고 항목 단위 에러를 격리해 한 건의 실패가 사이클 전체를 중단시키지 않도록 했다.
정수 고정소수점 정산 (부동소수 오차 제거)
문제 상황
요금 정산은 사용량 × 단가로 계산되는데, 단가가 소수(달러/시간 등)라 부동소수 연산을 쓰면 누적 오차가 생겨 청구 금액이 어긋날 수 있었다. 또한 단가는 시점별로 바뀌므로 정산 시점에 유효한 요율을 정확히 적용해야 했다.
원인 가설
단가를 시/분/초 단위별로 정수 스케일(×10²/×10⁴/×10⁶)로 관리하고, 사용 초를 시·분·초로 분해해 ROUND 기반 정수 연산으로 합산하면 부동소수 오차 없이 정산할 수 있다. 요율 이력은 effective_date <= 정산시점의 최신 한 건을 조인하면 시점별 요율을 정확히 반영한다.
검증
사용량 집계를 단가 테이블과 LATERAL 조인해 effective_date 최신 요율을 가져오고, 초를 시/분/초로 분해해 시*H + ROUND(분*M/100) + ROUND(초*S/10000) 형태의 정수 연산으로 금액이 산출되는지 확인했다. 스토리지는 MB-초를 GB·분당 센트 단가로 환산해 ROUND 처리되는지 점검했다.
해결
- 단가를 시/분/초 스케일(×10²/10⁴/10⁶)로 두고
ROUND기반 정수 고정소수점 연산으로 부동소수 오차 없는 정산을 구현했다. - 시점별 요율은
effective_date최신값을 LATERAL 조인해 요율 이력을 정확히 반영했다. Settle+SettleDetail저장을 트랜잭션으로 원자화하고, 정산 금액이 0이면 후속 발행을 생략했다.
이중 차감 방지 크레딧 RPC
문제 상황
정산이 끝나면 크레딧을 차감해야 했는데, 크레딧 서비스는 별도 서비스라 비동기 메시징으로 차감을 요청했다. 차감 요청이 실패했을 때 정산을 통째로 재시도하면 이미 커밋된 정산 기록에 대해 차감이 두 번 일어날 수 있었다(이중 차감).
원인 가설
정산 기록(DB 커밋)과 크레딧 차감(메시징)을 분리하고, 차감 메시징 실패가 DB 재시도를 유발하지 않도록 격리하면 이중 차감을 막을 수 있다. 차감 성공 응답을 받았을 때만 정산을 "결제됨"으로 표시하고, 실패하면 미결제로 남겨 다음 사이클에서 재정산하면 정확히 1회만 차감된다.
검증
정산 금액>0일 때만 차감을 발행하고(key=정산ID), 발행 실패는 onErrorResume(empty)로 흡수해 DB 재시도를 유발하지 않는지 확인했다. 응답 토픽에서 key로 정산을 식별해 성공 시 markAsPaid, 실패 시 미결제 유지(재정산 대상)되는지, 타 서비스 메시지는 key 파싱 단계에서 무시되는지 점검했다.
해결
- 정산 기록(DB 커밋)과 크레딧 차감(비동기 RPC)을 분리하고, 차감 실패가 DB 재시도를 유발하지 않도록 격리해 이중 차감을 방지했다.
- 차감 성공 응답을 받았을 때만
markAsPaid로 결제 완료 처리하고, 실패 건은 미결제로 남겨 다음 사이클에서 재정산하도록 했다. - 별도 검증 워커가 5분 주기로 조직별 사용금액을 집계해 잔액·결제수단을 사전 점검하는 안전장치를 두었다.
NicePay 결제 위변조 검증
문제 상황
외부 PG(NicePay) 결제 콜백을 신뢰하고 그대로 크레딧을 충전하면, 콜백이 위변조될 경우 실제 결제 없이 크레딧이 충전될 수 있었다. 결제 금액 자체도 조작 가능성이 있어 검증이 필요했다.
원인 가설
PG가 제공하는 서명 규칙(SHA-256 해시)을 서버에서 동일하게 재계산해 콜백의 서명과 비교하고, 결제 준비 시 저장한 금액과 콜백 금액을 이중 비교하면, 위변조된 콜백을 걸러낼 수 있다.
검증
sha256(authToken + clientId + amount + secret)를 직접 계산해 콜백 서명과 일치하는지, 사전 저장 금액과 콜백 금액이 일치하는지 검증했다. 불일치 시 "위변조" 실패로 처리되고, 정상 건만 승인 API 호출 → 크레딧 충전 비동기 발행으로 이어지는지 확인했다.
해결
- PG 콜백에 대해 SHA-256 서명 위변조 검증 + 금액 이중 검증을 직접 구현해 결제 무결성을 보장했다.
- 검증 통과 건만 승인 처리 후 크레딧 충전을 비동기로 발행했다(요청-응답 상관 key로 충전 결과 회수).