Blog from May, 2025

액터모델활용 -오버엔지니어링 방지

불특정하게 발생하는 사용자 이벤트, 그리고 그 비용

불특정하게 발생하는 사용자로부터의 이벤트를 처리할 때, 흔히 "이벤트가 발생할 때마다 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월 기준)

MySQL에  Read성능을 늘리기 위해 Replica를 구성해  Read를 분리할수있으며 Nosql이 없었던 시절에는 유일한 전통적인 방법이며 

쿼리튜닝과 병행한다고 하면 가장 심플한 방법중 하나였습니다.

온디멘드형이면 커넥션수를 늘려 튜닝도 할수 있겠지만 클라우드 DB의 사악한 과금정책이 1이라도 조정해서 초과하면~ 정액이아닌 사용량 기준으로 전환됩니다.

조정은 할수 있으나~ 월정액이 풀리며 동일 가격으로 커넥션수만 올리지 못합니다. ( 맥을 구매하고 메모리만 16g늘리고 싶은데~ 1.5배 비싼 다음 스펙을 사야하는 그런 케이스와 유사)


✅ 비용이 증가하는 이유 (MySQL Replica)


AWS Aurora DB의 경우는 특이하게 복제구성에 따른 저장소 비용은 증가는 하지 않습니다. -24년 조사 기준

클라우드 비용이 달러와 함께 자연증가해 엑셀로 계산기를 워낙 두들겼더니 이제는 개발하고 배포한 전체 서버의 개수및 스펙이 머릿속에 엑셀처럼 있습니다.

인프라비용이 우리가 만든 프로덕트의 수익을 잡아먹지 않게 만드는것이 중요한 시기가 되었기 때문에 과거에 무관심했던 영역이 이제는 습관이 되었습니다. 


Kafka와 같은 장치를 도입하는것은 이제는 특별한 기술이 아닌 널리 알려진 기술입니다.

Kafka를 동일 클러스터내에 이미 도입한 상태면 이슈가 없지만 단지 커넥션수를 증가 시키는 한 지점의 기능때문에 

카프카를 전면 채택하는것은  "오버 엔지니어링"   이 될수 있으며 Kafka를 잘 활용하는 능력을 이미 가졌다고해도

최초 도입은  결코 공짜 장치가 아닙니다. 


📌 Kafka 클러스터 권장 구성

🔸 1. Kafka 구성 기본 요소


🔸 2. 권장 노드 수 (기본 구성)

➡️ 최소 EC2 수: 8대


📊 예상 EC2 월간 비용 (서울 리전 기준, 온디맨드)


💡 참고 사항

  • Kafka의 Broker 수가 많아질수록 Throughput 향상 및 Partition 분산이 가능하지만 비용 증가

  • Zookeeper는 Kafka 3.5 이후로 KRaft 모드로 대체 가능 (ZK 제거 → EC2 2~3대 절감 가능)

  • 앱 수가 늘어나면 Consumer Scaling 필요 → App용 EC2 추가 필요

  • 디스크 IOPS는 별도 요금 (EBS 스토리지)


우리는 다른 서비스에서 이미 카프카를 도입해 숙련도가 있는 상황이지만 지금 당장 카프카를 도입하기에는

인프라확보및 이것을 전반적으로 활용할 설계준비가 아직안된 상태로 기능검증이 끝내고 오픈베타중인 상황으로

바로 그랜드 오픈준비를  후속으로 해야하는 상황이였습니다.


서비리스 웹 서비스의 자원문제

최근의 웹서비스들은 아예 서버리스로 가거나 클라우드 장치들을 대부분 활용하면서, 값싼 어플리케이션이 돌아가는 EC2또는 POD는 CPU및 메모리가

놀기사작했습니다.  DB및 PasS 클라우드 장치에 비교하면 어플리케이션을 작동하는 EC2는 거의 놀고 있습니다. 

간혹 API어플리케이션을 스케일아웃 하는 전략으로 분산처리가 될것으로 기대하는 경우가 있는데 단일지점 병목을 무시하게되면 성능 임계치를 더 가속화할 뿐입니다.

이 문제를 해결하기 위해, EC2의 남아도는 자원을 활용하는 액터모델을 이용한 StateFull 방식을 빠르게 채택하기로 하였습니다.

오버엔지니어링에서 고민

여기서 개발기간이 아주 크게 증가한다고 해도  오버엔리지어링 수치가 높아질수 있습니다. - 아래는 재미삼아 GPT의뢰 만들어본 공식

📌 OverEngineering Index (OEI) 공식:

📊 변수 정의:


오버엔지니어리일 안되는 기간을 1주일이내로 정하고 다음과 같은 방법을 사용하기로 했습니다.

FSM Actor를 이용한 준 실시간 벌크처리

전략은 간단합니다. 인메모리에서 짧은기간 큐에보유하고 있다가 적절한 순간 모은데이터만큼만 벌크처리를 하는것입니다.

이 전략은 모은 개수만큼 커넥션수를 절약할수 있으며 이벤트를 적재하고 즉시 꺼내 사용안해도 되는 케이스에서 이용가능한 전략입니다.


위 와같은 장치를 채택 한다고 해도~ 메시지 전송수준을 어디까지 높일것인가? 도 비용으로 바라볼수 있습니다.

💡 선택 기준 요약표

전송 수준

중복

손실

처리 비용

사용 예시

At Most Once

💲 (낮음)

로그, 알림

At Least Once

💲💲 (중간)

주문, 결제

Exactly Once

💲💲💲 (높음)

금융, 정산


🔍 인프라 비용 고려 팁

  • 로그나 비동기 알림 At Most Once로 충분 → 비용 절감

  • 결제, 주문처리 At Least Once + 중복제거 로직 추가로 타협 가능

  • 회계, 정산 Exactly Once 필수지만, 서비스 경계를 좁히면 필요한 범위를 최소화할 수 있음


여기까지가 우리가 함께 고민한 내용이며  , 업그레이드해 메시지 전송 수준(Message Delivery Semantics) 까지

고려한 로컬 액터모드로 문제를 해결한 테크를 소개합니다. 

코드완성후 PR 과정

우리는 AI를 통한 PR을 프로덕트레벨에 이미 적용해 사용중에 있습니다. AI가 이컨텐츠의 주인공은아니지만 짧게 소개



제목 : Akka를 이용한 대규모 데이터 처리 로직 최적화

작성자 :  BlumnAI  Dev - 데니아

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

enqueueOrFlushOnThreshold

FLUSH_BUFFER_SIZE = 100일 때,
즉시 Flush 실행

3 초 무입력

enqueueWithTimerTimerScheduler.startSingleTimer

100건이 안 돼도,
3 초 동안 새 로그가 없으면 자동 Flush

이득

  • Redshift 커밋 횟수↓, 네트워크 RTT↓

  • 실시간성 + Bulk 처리


2-B. 비동기 + Fail-Soft 구조

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 장애

TimeoutException·DataAccessResourceFailureException → 단순 로깅 후 조용히 무시

시스템 폭주 방지 (트래픽 백프레셔 역할)

일반 예외

데이터 n > 1분할 / 재시도(splitAndRetry),
n = 1 → 실패 로그를 남긴 후 종료

원인 레코드 정확히 추적, 최대 log₂ N 회 재귀


2-D. SQL Batch 최적화

  • place-holder 자동 생성


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 오류

onErrorResume에서 배치 분할 후,
두 개의 Child Actor (Worker)로 위임

대량 오류 → 폭발 가능성 최소화

Worker 타임아웃

TimeoutException이면 재시도 X, 단순 로깅

과부하 폭주 방지

Worker 일반 오류

절반으로 Split + 재전송 (재귀)

이분 탐색으로 오염 레코드 격리


3. 핵심 클래스 구조 & 동작 개요

Akka (Pekko) 경험이 없는 독자용 요약


계층

클래스로 보는 역할

설명

Application

EventTracerController → Service

HTTP 요청을 Actor 메시지로 변환해 비동기 파이프라인 시작

Actor Root

ActorManager

“스프링 빈 대신 ActorRef” 를 동적 생성해주는 ActorManager. CreateChildActor 메시지를 수신하면 자식 Actorcontext.spawnreplyTo 로 돌려준다

Wiring

AkkaConfiguration

ActorSystem 부트스트랩
② Spring Bean bulkManagerActor 생성 시 Manager에게 Ask-Pattern(미래)로 요청 → ActorRef<BulkManagerActor>를 주입

Domain Actor

BulkManagerActor

버퍼·타이머·에러 위임 담당 상태 머신

  • idle()active() 전이

  • 타이머/버퍼 기준 Flush

  • 예외 시 자식 Worker Actor로 분배

Worker Actor

BulkWorkerActor

실제 DB 호출 + 분할 재시도 담당

  • pipeToSelf로 Mono -> Actor 메시지 변환

  • 이분할 재귀 + 타임아웃 무재시도 정책

💡 Actor 동작 흐름 (텍스트 버전)

  1. ControllerBulkManagerActor.ReceiveData(Data)

  2. Manager : 버퍼 적재 → (Timer or Threshold) → Flush

  3. Flush 성공 : 로그 카운트 기록 후 Idle

  4. Flush 실패 : worker1, worker2에게 절반씩 ProcessBatch

  5. Worker : Mono 결과를 Success or Failure자기 자신에게 전송 → 이분할 재시도 혹은 로그 후 종료

deciduous tree Actor 클래스 구조

ActorSystem
 └─ ActorManager
     └─ BulkManagerActor (부모)
         ├─ BulkWorkerActor#1 (자식)
         └─ BulkWorkerActor#2 (자식)


4. 데이터 처리 순서


마치며

오픈한지 얼마 안되었으나 이미 하루에 수십만건이상의 데이터를 저비용장치모드로 처리하고 있으며 , 이러한 안전장치를 빠르게 마련하지 못했다면 허용 최대 커넥션풀이 초과해 데이터의 대량유실로 이어지거나 거의 최소스펙에 가까운 저장장치를 스케일업을 해야하는 상황이 금방 도래했을것으로 예상하며

AKKA는 KAFKA의 대체 장치가 아니며, 오히려 ReactiveStream의 구현체를 주도하고 그 기반으로  KAFKA를 잘 이해해는 장치중 하나입니다.  넥스트 플랜에서는 이것을 활용하는 가치있는 기능을 추가하여 더 많은 트래픽을 준비하기위해 도입예정에 있습니다.

추가 참고 자료

깨알광고 

  • https://hey-there.io/ - (주)블룸에이아이
    • 여기서 소개되는 기술은 최근 개발한 오픈한 온사이트 마케팅툴인 헤이데어의 일부로 포함되어 있습니다.