하루 N억 건의 알림 시스템 구축기 (1): 카카오뱅크는 어떻게 발송 플랫폼의 새로운 뼈대를 세웠나
Blog

하루 N억 건의 알림 시스템 구축기 (1): 카카오뱅크는 어떻게 발송 플랫폼의 새로운 뼈대를 세웠나

KakaoBank
2026.09.15
·Web·by igor
#Architecture#Developer Experience#Messaging System#Performance Optimization#Scalability

핵심 포인트

  • 1카카오뱅크는 레거시 발송 시스템의 구조적 한계와 성능 병목을 해결하기 위해 Service Layer와 Core Layer를 분리하는 계층화 아키텍처를 도입했습니다.
  • 2개발자가 비즈니스 로직에만 집중할 수 있도록 복잡한 성능 최적화 요소를 Core Layer 내부로 캡슐화하고 단순한 인터페이스를 제공하여 멘탈 모델을 단순화했습니다.
  • 3Lock-Free 방식의 Work-Stealing 알고리즘과 GC 최적화를 적용하여, 초당 1만 건 이상의 처리량(TPS)과 낮은 지연시간을 동시에 확보하며 시스템의 확장성을 극대화했습니다.

카카오뱅크 알림경험엔지니어링팀은 하루 N억 건의 메시지를 처리하는 UMS(Unified Messaging System) 발송기를 재구축하며, 시스템의 성능 최적화와 개발자 경험(DX) 개선이라는 두 가지 목표를 동시에 달성했습니다. 본 시스템의 핵심 철학은 "개발자의 멘탈 모델을 단순하게 유지하는 것"이며, 이를 위해 아키텍처를 Service Layer와 Core Layer로 계층 분리하였습니다.

1. 설계 전략: 계층 분리 아키텍처

플랫폼 개발자는 성능과 복잡성 사이의 Trade-off를 극복하기 위해 기술적 복잡성을 내부로 캡슐화했습니다.

  • Service Layer (단순 인터페이스 제공): 개발자는 RequestHandler와 Sender라는 두 가지 핵심 인터페이스만 구현합니다. 비즈니스 로직(메시지 조립, 전송, 결과 처리)에만 집중할 수 있도록 동시성 제어나 큐 분배 로직을 배제하여 선언적(Declarative) API 구조를 갖추었습니다.
  • Core Layer (복잡성 및 최적화 캡슐화): 고성능 달성을 위한 Lock-Free, Work-Stealing, GC(Garbage Collection) 최적화 등 고도화된 기술이 구현되어 있습니다.

2. 핵심 기술: Age-Based Work-Stealing

레거시 시스템의 병목 원인이었던 '단일 큐에 의한 Tail Latency' 문제를 해결하기 위해 채널별로 큐를 분리하고, 부하 분산을 위한 Work-Stealing 알고리즘을 도입했습니다.

  • 동작 원리: 중앙 관리자가 100ms마다 각 큐를 스캔하여, 큐 생성 시각을 기준으로 일정 시간(Age > 500ms) 이상 대기한 큐를 발견하면 이를 유휴 상태인 Worker에게 분산시킵니다.
  • Lock-Free 구현: AtomicReferenceArray와 CAS(Compare-And-Set) 연산을 통해 Lock 획득/해제 오버헤드를 제거했습니다. 또한, Barrier를 활용하여 큐를 나눌 때 배열을 복사하지 않는 Zero-Copy 방식을 채택하여 메모리 할당을 최소화했습니다.
  • 성능 최적화 의사코드:
kotlinfun steal(): List<Task>? { val mid = (h + t) / 2 if (!barrier.compareAndSet(-1, mid)) return null val stolen = (mid until t).mapNotNull { idx -> tasks.getAndSet(idx % 1024, null) } tail.set(mid) barrier.set(-1) return stolen }

3. 추가 성능 최적화 기법

  • GC 최적화: 고정 크기 Array를 사용하여 CPU 캐시 효율을 높이고, 처리된 요청을 즉시 null로 설정하여 객체가 Old Generation으로 승격되는 것을 방지했습니다.
  • Kotlin Inline Class: 불필요한 Wrapper 객체 생성을 줄여 primitive 타입과 유사한 성능을 확보했습니다.
  • 템플릿 캐싱: 메시지 조립 시 매번 파싱하는 대신 전처리(Preprocessing)를 통해 변수 위치를 미리 캐싱함으로써, 알고리즘 복잡도를 O(M×N)O(M \times N)에서 O(N)O(N)으로 개선했습니다. (단, MM: 치환 변수 개수, NN: 템플릿 본문 길이)

결론

본 시스템은 복잡한 성능 로직을 Core Layer 내부에 숨김으로써, 새로운 채널 추가 시 개발자가 시스템 전체를 이해해야 하는 인지 부하를 획기적으로 줄였습니다. 이를 통해 10,000 TPS 이상의 고성능을 유지하면서도 확장 가능한 아키텍처를 구축하였습니다.