Skip to content

zbnerd/probabilistic-valuation-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,935 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Probabilistic Valuation Engine

단일 노드 환경에서 고부하 확률 계산 API를 75배 이상 성능 개선한 백엔드 시스템

메이플스토리 장비의 확률적 기대비용을 계산하는 서비스. 캐릭터 이름 하나로 스타포스·큐브 기대비용을 3개 프리셋 기준으로 산출한다.


한 줄 요약

복잡한 인프라(Redis + MySQL + MongoDB)를 제거하고 PostgreSQL 단일 구조와 Micro-Batching을 적용하여
97 RPS → 7,347 RPS (약 75배 개선) 달성하고 운영 복잡도 단순화


My Role

  • 전체 시스템 아키텍처 설계 및 기술 의사결정
  • 성능 테스트 및 병목 분석 (wrk 기반)
  • PostgreSQL 단일화 구조 설계 및 구현
  • Micro-Batching 처리 구조 설계 및 적용
  • 캐시 전략(L1/L2) 및 분산 정합성 설계
  • 실패한 최적화 케이스 분석 및 롤백

Why This Project Matters

이 프로젝트는 기능구현 보다 성능 문제를 정의하고 → 측정하고 → 구조적으로 해결하는 능력과 의사결정의 기록을 보여준다.

  • 인프라를 추가할수록 복잡해졌고, 제거할수록 단순해지며 불필요한 기술 스택 대신 데이터 흐름과 병목을 개선하여 성능을 향상시킨 과정
  • 실패한 최적화(LocalSingleFlight -56%)와 그 원인 분석
  • 단일 PostgreSQL로 캐시·분산락·Pub/Sub·메시지큐를 대체한 과정
  • 좋아요 도메인 하나가 123일간 어떻게 진화했는지
  • 복잡한 인프라 대신 데이터 흐름과 병목을 해결하는 것이 성능 개선의 핵심임을 검증한 프로젝트.

숫자가 아니라 그 숫자가 나온 이유를 설명할 수 있는 코드베이스다.


핵심 수치

지표 시작 최종
RPS 97 7,347 (실데이터 200k rows 기준)
p99 latency 4,100ms 36ms
데이터베이스 Redis + MySQL + MongoDB PostgreSQL 단일
에러율 59.7% 0%
Scale-out 불가 선형 확장 준비 완료

Trade-off

  • 최대 성능: 10,997 RPS (단일 노드, Caffeine L1 캐시 기반)
  • 최종 성능: 7,347 RPS (LISTEN/NOTIFY 기반 캐시 정합성 확보)

→ 성능을 일부 희생하고
→ Scale-out 환경에서의 데이터 일관성을 선택


Key Insight

성능 문제는 인프라의 수가 아니라 데이터 흐름과 병목에서 발생한다.

성능 여정 (10주, 8단계)

97 → 555 → 674 → 965 → [325] → 940 → 7,347 RPS

1단계  97  →  223  Chaos baseline (Redis + MySQL + MongoDB)
2단계 223  →   97  Singleflight 도입 → -56% 회귀 → 즉시 롤백
3단계  97  →  555  L1 Fast Path: GZIP byte[] 직접 반환, Executor 우회
4단계 555  →  674  Write-Behind Buffer: DB 저장 150ms → 0.1ms
5단계 674  →  965  프리셋 병렬 계산: 전용 Executor 분리로 데드락 방지
6단계 965  →  325  V5 Stateless 전환: 정합성 확보, 속도 53% 감소 (의도된 트레이드오프)
7단계 325  →  940  Auto Warmup: Cold Start 227% 개선
8단계 940  → 7,347  Redis·MySQL·MongoDB 제거 → PostgreSQL 단일화 → Micro-Batching (DB 왕복 3~5회→1회)

각 단계의 상세 기록: docs/06_Performance_Journey/


핵심 아키텍처 결정

PostgreSQL만으로 충분하다

기능 이전 현재
캐시 Redis Caffeine L1 + PostgreSQL UNLOGGED
분산락 Redis Named Lock pg_advisory_lock
Pub/Sub Redis Pub/Sub PostgreSQL LISTEN/NOTIFY
메시지큐 Redis Stream PGMQ (PostgreSQL 익스텐션)
이벤트스토어 MongoDB PostgreSQL JSONB
영속성 MySQL PostgreSQL

Redis를 쓸 때보다 빨라진 이유:

  • Caffeine 히트율 99%+ → PostgreSQL이 실제로 맞는 요청이 거의 없음
  • 이미 연결된 DB 커넥션 재활용 → 추가 네트워크 홉 없음
  • 트랜잭션 내 pg_notify() → 롤백 시 무효화 이벤트도 사라짐 (원자성)

Micro-Batching — 940→7,347의 실제 원인

PostgreSQL 단일화는 전제조건이었고, 실제 성능 엔진은 Micro-Batching이었다.

Before (개별 쿼리):
Request 1 → SELECT WHERE id = 1  (DB 왕복 1)
Request 2 → SELECT WHERE id = 2  (DB 왕복 2)
Request 3 → SELECT WHERE id = 3  (DB 왕복 3)

After (Micro-Batching):
Request 1~3 → SELECT WHERE id IN (1, 2, 3)  (DB 왕복 1)

수 ms 시간 창 안에 들어온 요청을 모아 배치 쿼리로 처리한다.
캐시 미스 시 DB 왕복이 3~5회에서 1회로 줄었다. LISTEN/NOTIFY는 이 성능을 Scale-out 환경에서도 유지 가능하게 만든 보완재다.

L1 Fast Path

캐시 히트 시 스레드풀·직렬화·역직렬화를 전부 우회하고 GZIP byte[]를 그대로 반환한다.

캐시 히트 경로 (Before): Controller → Executor → L1.get() → Deserialize → GZIP → Response (200ms)
캐시 히트 경로 (After):  Controller → L1.getGzipDirect() → Response (4ms)

Write-Behind Buffer

계산 결과를 즉시 DB에 쓰지 않는다. 메모리 버퍼에 모았다가 배치로 처리한다.

DB 저장 지연: 150ms(동기) → 0.1ms(Buffer.offer)
셧다운 안전성: Phaser 기반 graceful flush
동시성 제어:  CAS + Exponential Backoff (lock-free)

PostgreSQL LISTEN/NOTIFY 기반 분산 캐시 정합성

Scale-out 환경에서 각 노드의 Caffeine L1 캐시가 달라지는 문제를 해결한다.

노드 A → UPDATE + NOTIFY (같은 트랜잭션)
노드 B, C → LISTEN → Caffeine evict → 다음 요청에서 재조회

트랜잭션 롤백 시 NOTIFY도 발생하지 않는다. Redis Pub/Sub과 달리 spurious invalidation이 없다.


좋아요 도메인 — 123일의 진화

좋아요 기능 하나가 123일간 어떻게 변했는지 기록한 별도 문서가 있다.

2025.11  비관적 락 → 낙관적 락 → Write-Behind Buffer + Graceful Shutdown
2026.01  Redis Lua Script 원자성 → 보상 트랜잭션 → Scale-out Pub/Sub
2026.02  Java → Kotlin 마이그레이션 → 멀티모듈 분리
2026.03  헥사고날 아키텍처 → Redis 제거 → DB Trigger 기반 정합성

시작: Controller → DB (동기, 원자성 없음)
최종: Controller → DB Transaction + Trigger → 완료

인프라를 교체할 때 module-core와 module-app 코드는 한 줄도 바뀌지 않았다. 헥사고날 아키텍처(Port/Adapter)가 실제로 작동한 사례다.

상세 기록: docs/22_Like_Refactoring_Journey/


배운 것

측정 없는 최적화는 미신이다
LocalSingleFlight는 이론적으로 완벽했다. 실제로는 캐시 히트마저 blocking해서 -56%가 됐다. 측정하지 않았으면 "좋은 최적화"로 남아있었을 것이다.

복잡도가 성능의 적이다
Redis, MySQL, MongoDB를 걷어낼 때 RPS가 올라갈 거라고 생각하지 못했다. 네트워크 홉과 인프라 오버헤드가 그만큼 비쌌다.

트레이드오프를 명시하라
V5 Stateless에서 속도를 53% 포기하고 정합성을 선택했다. 이 포기를 문서화하지 않으면 "왜 느리지?"로만 남는다.

현실 데이터와 시스템 제약을 함께 고려해야 한다
빈 DB 기준 10,997 RPS는 참고 수치일 뿐이며,
200k rows 환경과 LISTEN/NOTIFY 기반 캐시 정합성 비용을 포함한 7,347 RPS가 실제 운영에 가까운 지표다.
낙관적인 수치로 설계하면 실제 환경에서 문제를 만든다.


기술 스택

언어·프레임워크: Kotlin, Java 21 (Virtual Threads), Spring Boot 3
데이터베이스: PostgreSQL 17 (PGMQ 익스텐션 포함)
캐시: Caffeine (L1), PostgreSQL UNLOGGED TABLE (L2)
모니터링: Prometheus, Grafana, Micrometer
테스트: Testcontainers, JUnit 5, ArchUnit
부하테스트: wrk
인프라: Docker Compose, Vultr Seoul KR


문서 구조

docs/
├── 01_ADR/                        아키텍처 결정 기록 (86개)
├── 05_Reports/
│   └── 05_06_Load_Tests/          부하테스트 리포트 전체
├── 06_Performance_Journey/        97 → 7,347 RPS 여정 (11장)
└── 22_Like_Refactoring_Journey/   좋아요 도메인 123일 기록 (7장 + 부록)

빠른 시작

git clone https://github.com/zbnerd/probabilistic-valuation-engine
cd probabilistic-valuation-engine
cp .env.example .env  # NEXON_API_KEY 등 설정
docker compose up -d
./gradlew :module-app:bootRun --args='--spring.profiles.active=local'
# 부하테스트
./gradlew loadTest -Pscenario=patch-day

# 비교 리포트
./gradlew loadTestReport -PbeforeFile=baseline.json -PafterFile=current.json

Author: SeungJun | Stack: Spring Boot + Kotlin + PostgreSQL | 2026

About

75x 성능 개선을 달성한 확률 계산 백엔드 엔진 97 → 7,347 RPS (Micro-Batching + PostgreSQL 단일화) 복잡한 인프라 제거로 성능과 단순성 동시에 확보

Topics

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors