Page History
불특정하게 발생하는 사용자 이벤트, 그리고 그 비용
불특정하게 발생하는 사용자로부터의 이벤트를 처리할 때, 흔히 "이벤트가 발생할 때마다 DB 인서트를 수행"하는 구조를 사용하게 됩니다. 단순한 구조지만, 매 요청마다 커넥션을 생성하고 인서트를 수행하는 방식은 곧 다음과 같은 문제를 야기합니다.
"커넥션 폭주, 리소스 고갈, 그리고 예기치 못한 비용 증가"
해결 방법은 의외로 간단합니다. DB의 커넥션 수를 넉넉하게 잡아두면 됩니다. 하지만 중요한 사실은,
"DB 커넥션은 공짜가 아닙니다."
클라우드 DB 커넥션이 왜 비용을 발생시키는가?
각 클라우드 벤더의 매니지드 DB 서비스(Aurora, Azure SQL, Cloud SQL 등) 는 다음 리소스를 기준으로 비용을 책정합니다:
vCPU 및 메모리 용량: 커넥션 수가 많을수록 더 큰 인스턴스 필요
I/O 사용량 증가: 커넥션이 많으면 lock 경합, 대기시간 증가
백업, 모니터링, 리플리카 비용: 연결된 워크로드 증가에 따른 연쇄 비용
...
가정 조건
단일 Region, Standard Tier 기준
초당 1,000건 요청이 지속될 경우를 기준으로
커넥션 풀 설정 (100 → 1,000 → 10,000 → 100,000 커넥션 단위 시나리오)
DB 커넥션 수 증가에 따른 월간 예상 비용 비교 (2025년 5월 기준)
| 플랫폼 | DB 종류 | 커넥션 수 | 예상 인스턴스 | 비용 (월) | 메모 |
|---|---|---|---|---|---|
| AWS | Aurora MySQL | 100 | db.r6g.large (2vCPU/16GB) | 약 $120 | 기본 커넥션 수 약 500 |
| 1,000 | db.r6g.xlarge (4vCPU/32GB) | 약 $240 | 커넥션 처리 여유 확보 | ||
| 10,000 | db.r6g.4xlarge (16vCPU/128GB) | 약 $900 | 대량 커넥션용 | ||
| Azure | SQL Database (PaaS) | 100 | DTU 100 (Basic Tier) | 약 $30 | 커넥션 제한 큼 |
| 1,000 | vCore: 4 vCPU / 20GB | 약 $330 | Business Critical | ||
| 10,000 | vCore: 16 vCPU / 80GB | 약 $1,100 | 확장성 제한 존재 | ||
| GCP | Cloud SQL (MySQL) | 100 | db-custom-2-8 (2vCPU/8GB) | 약 $100 | 초과 연결 시 제한 |
| 1,000 | db-custom-4-16 (4vCPU/16GB) | 약 $200 | 적정 성능 | ||
| 10,000 | db-custom-16-64 (16vCPU/64GB) | 약 $800 | 고부하 처리용 |
MySQL에 Read성능을 늘리기 위해 Replica를 구성해 Read를 분리할수있으며 Nosql이 없었던 시절에는 유일한 전통적인 방법이며
쿼리튜닝과 병행한다고 하면 가장 심플한 방법중 하나였습니다.
하지만 이러한 전략은 더 많아진 트래픽처리로
✅ 비용이 증가하는 이유 (MySQL Replica)
| 항목 | 설명 |
|---|---|
| 복제 인스턴스 자체의 비용 | Primary와 거의 동일한 사양의 DB 인스턴스를 추가 운영해야 함 |
| 스토리지 비용 | 데이터를 동기화하므로, Replica도 동일한 스토리지 사용량을 가짐 |
| 네트워크 비용 | 지역 간 복제(다중 리전 시)일 경우 전송 트래픽 비용 발생 |
| 백업/모니터링 | Replica 인스턴스도 개별 백업/모니터링 설정시 비용 추가 발생 |
| Info |
|---|
AWS Aurora DB의 경우는 특이하게 복제구성에 따른 저장소 비용은 증가는 하지 않습니다. -24년 조사 기준 클라우드 비용이 달러와 함께 자연증가해 엑셀로 계산기를 워낙 두들겼더니 이제는 개발하고 배포한 전체 서버의 개수및 스펙이 머릿속에 엑셀처럼 있습니다. 인프라비용이 우리가 만든 프로덕트의 수익을 잡아먹지 않게 만드는것이 중요한 시기가 되었기 때문에 과거에 무관심했던 영역이 이제는 습관이 되었습니다. |
📌 Kafka 클러스터 권장 구성
🔸 1. Kafka 구성 기본 요소
| 구성 요소 | 설명 |
|---|---|
| Broker | 메시지를 저장하고 전달하는 핵심 노드. 일반적으로 3개 이상 구성 |
| Zookeeper | 클러스터 메타데이터 관리. Kafka 3.x 이후로는 KRaft 도입 가능 |
| Producer App | 메시지를 Kafka로 전송하는 앱 |
| Consumer App | Kafka에서 메시지를 읽는 앱 |
...
🔸 2. 권장 노드 수 (기본 구성)
| 구성 요소 | 권장 수량 | EC2 사양 기준 (t3.medium: 2vCPU, 4GB) |
|---|---|---|
| Kafka Broker | 3대 | 각 4GB RAM EC2 |
| Zookeeper | 3대 | 최소 사양 가능 (t3.micro 또는 t3.small 가능) |
| Producer 앱 | 2개 | 1개 EC2 가능 |
| Consumer 앱 | 2개 | 1개 EC2 가능 |
➡️ 최소 EC2 수: 8대
...
📊 예상 EC2 월간 비용 (서울 리전 기준, 온디맨드)
| EC2 타입 | 개수 | 시간당 비용 | 월 비용 (약 730시간) | 용도 |
|---|---|---|---|---|
| t3.medium | 5대 | $0.0416 | 약 $152 | 3 Broker + 2 App (Producer+Consumer) |
| t3.micro | 3대 | $0.0104 | 약 $23 | Zookeeper |
| 합계 | 약 $175 / 월 | 단일 AZ, 스토리지 미포함 |
...
💡 참고 사항
Kafka의 Broker 수가 많아질수록 Throughput 향상 및 Partition 분산이 가능하지만 비용 증가
Zookeeper는 Kafka 3.5 이후로 KRaft 모드로 대체 가능 (ZK 제거 → EC2 2~3대 절감 가능)
앱 수가 늘어나면 Consumer Scaling 필요 → App용 EC2 추가 필요
디스크 IOPS는 별도 요금 (EBS 스토리지)
1. Pekko(Akka)란?
Pekko(Akka의 Apache 포크)는 Actor 모델에 기반한 고성능 비동기 메시지 기반 시스템을 개발하기 위한 툴킷입니다.
주로 동시성, 분산 처리, 장애 복원성이 필요한 시스템에서 사용됩니다.
전통적인 Java/Kotlin 쓰레드 기반 프로그래밍은 공유 상태와 동기화 문제로 인해 복잡해지기 쉽습니다.
Pekko는 이를 해결하기 위해 다음과 같은 방식을 취합니다:
Actor 단위로 상태와 동작을 캡슐화하고, 오직 비동기 메시지로만 통신합니다. 이로 인해 경합 조건(Race Condition) 없이 안전한 상태 관리를 할 수 있습니다.
Actor는 경량 프로세스처럼 동작하며 수천, 수만 개도 가볍게 생성할 수 있습니다.
Typed API를 통해 메시지 타입을 컴파일 타임에 고정할 수 있어 안정성이 높아지고, 가독성도 향상됩니다.
Supervisor 전략을 통해 각 Actor의 오류를 상위 Actor가 감지하고, 재시작 또는 중단 등의 정책으로 시스템 전체를 무너지지 않게 보호합니다.
예를 들어, “은행 계좌”를 하나의 Actor로 보고, 입출금 요청을 메시지로 전달하는 방식으로 동작을 구현할 수 있습니다. 이런 방식은 이벤트 기반 시스템, 채팅 서버, 실시간 알림, IoT 메시지 처리 시스템 등에 효과적으로 적용됩니다.
☑️ Pekko는 Akka의 공식 포크이며, 현재는 Apache 재단에서 관리되고 있습니다.
2. 데이터 처리 로직 최적화 전략
2-A. “100 건 or 3 초” Buffer Flush
트리거 | 구현 위치 | 설명 |
|---|---|---|
버퍼 크기 ≥ 100 |
|
|
3 초 무입력 |
| 100건이 안 돼도, |
이득
Redshift 커밋 횟수↓, 네트워크 RTT↓
실시간성 + Bulk 처리
...
2-B. 비동기 + Fail-Soft 구조
| Code Block | ||
|---|---|---|
| ||
handler.process(batch) // R2DBC Batch INSERT
.subscribeOn(Schedulers.boundedElastic()) // JDBC 블로킹 격리
.timeout(Duration.ofSeconds(5)) // 지연 차단
.onErrorResume { err -> delegateBatchToChildActors(err, batch) }
.subscribe() |
BoundedElastic : Reactor I/O 스레드를 막지 않고 별도 풀에서 DB 호출 수행.
5 초 Timeout : 미응답 배치를 에러로 간주해 Child Actor에게 작업을 위임하여 처리 재시도.
(응답이 늦어지는 것을 방지)onErrorResume : 예외(에러) 발생시 Child Actor에게 동일 배치를 절반씩 위임해 문제 레코드를 격리/재시도. (최종적으로 문제 레코드 파악 가능)
...
2-C. Child Worker 2-계층 Retry
단계 | Worker 내부 로직 | 결과 |
|---|---|---|
타임아웃·DB 장애 |
| 시스템 폭주 방지 (트래픽 백프레셔 역할) |
일반 예외 | 데이터 | 원인 레코드 정확히 추적, 최대 log₂ N 회 재귀 |
...
2-D. SQL Batch 최적화
place-holder 자동 생성
| Code Block | ||
|---|---|---|
| ||
val allValuesPlaceholders = requests.joinToString(", ") {
(1..columnNames.size).joinToString(" , ", "(", ")") { "\$${placeholderIndex++}" }
} |
N 행 × 15 컬럼SQL을 한 줄로 생성 → 서버 ↔ DB Round Trip 1회로 단축 .R2DBC bind : 파라미터 바인딩으로 SQL Injection 방어 및 타입 안전성을 확보.
...
2-E. 장애 전파 ≠ 스레드 차단
Manager-Actor에서 Mono.subscribe() 후 결과를Fire-and-Forget→ 호출 스레드(HTTP) 즉시 반환.실패해도 Worker가 독립 처리하므로 컨트롤러 타임아웃에 영향을 주지 않고 느슨한 연결을 유지.
...
2-F 예외·타임아웃 대응 전략 정리
단계 | 로직 | 설명 |
|---|---|---|
Manager 오류 |
| 대량 오류 → 폭발 가능성 최소화 |
Worker 타임아웃 |
| 과부하 폭주 방지 |
Worker 일반 오류 | 절반으로 Split + 재전송 (재귀) |
|
...
3. 핵심 클래스 구조 & 동작 개요
Akka (Pekko) 경험이 없는 독자용 요약
계층 | 클래스로 보는 역할 | 설명 |
|---|---|---|
Application |
| HTTP 요청을 Actor 메시지로 변환해 비동기 파이프라인 시작 |
Actor Root |
| “스프링 빈 대신 ActorRef” 를 동적 생성해주는 |
Wiring |
| ① |
Domain Actor |
| 버퍼·타이머·에러 위임 담당 상태 머신
|
Worker Actor |
| 실제 DB 호출 + 분할 재시도 담당
|
💡 Actor 동작 흐름 (텍스트 버전)
Controller →
BulkManagerActor.ReceiveData(Data)Manager : 버퍼 적재 → (Timer or Threshold) → Flush
Flush 성공 : 로그 카운트 기록 후 Idle
Flush 실패 :
worker1,worker2에게 절반씩ProcessBatchWorker : Mono 결과를
SuccessorFailure로 자기 자신에게 전송 → 이분할 재시도 혹은 로그 후 종료
Actor 클래스 구조
| Code Block | ||
|---|---|---|
| ||
ActorSystem
└─ ActorManager
└─ BulkManagerActor (부모)
├─ BulkWorkerActor#1 (자식)
└─ BulkWorkerActor#2 (자식) |

