대규모 서비스 아키텍처의 주된 관심사는 확장성과 병목 관리라 해도 과언이 아니다. 이와 관련된 트레이드오프나 문제 상황을 경험해보고 핸들할 수 있는 판단력을 갖추는 것이 요즘 시대에 가장 필요한 개발 역량이 아닌가 싶다. 규모가 크지 않은 서비스라도 이와 비슷한 문제 상황을 자연스럽게 고민하게 되는 도메인이 있는데 개인적으로 알림 도메인이라고 생각한다. 어느정도 반복되는 구조를 답습하는 대부분의 도메인과 다르게 각자 서비스 성격에 따라 효율적인 시스템 디자인을 간접적으로 고민해 볼 수 있는 가장 현실적이고 자유도 높은 도메인이 아닐까한다.
기본적으로 알림은 서비스가 유저에게 먼저 request를 보내는 거의 유일한 상호작용이다. 개발자이기 이전에 사용자로서 그동안 수많은 애플리케이션의 알림을 경험하며 귀찮게 하는 기능이라는 인식이 굳어졌다. "유저 혜택, 알림 동의"의 항목도 웬만하면 동의 안하고 넘어간다. 그럼에도 서비스 입장에서는 알림 기능이 비즈니스 목적 실현을 유도하는 나름 핵심 도메인이라는 사실은 부인할 수 없다.
"유저가 떠나지 않게 하기 위한 서비스의 울부짖음" 정도의 인상으로 유저에게 도달하기까지, 소셜 미디어의 확산과 함께 급격히 고도화된 현재의 알림 시스템은 어떻게 디자인되고 어떤 매커니즘으로 구현되는지 호기심이 들었다.
특히나 대형 서비스에서의 알림 도메인은 사실상 확장성 문제를 최전선에서 마주한다. 단순히 메시지를 전달하는 기능을 넘어 대량의 이벤트 발생, 대량의 데이터 저장, 그리고 실시간 사용자 응답이라는 세 가지 축으로 동작하는 과정에서 수많은 문제와 트레이드오프를 자연스럽게 마주한다.
따라서 처리, 저장, 유저 인터렉션 각 과정에서 나타나는 고유한 문제 상황과 책임에 따라 알림 도메인을 Processing Layer, Storage Layer, Serving Layer 세 가지 레이어로 분리하여 각 시스템을 구성하는 핵심 개념에 대해 알아보자.
Processing layer
Processing Layer는 알림 데이터를 실제로 만들어내는 시작점으로 서비스 내에서 발생시킨 도메인 이벤트를 기반으로 알림을 생성하고 흐르게 하는 역할을 담당한다. 어떤 이벤트가 발생했는지 정의하고 해당 이벤트가 어떤 유저에게 전달되어야 하는지 결정하며, 이를 fanout하거나 aggregation하는 방식으로 분배 전략을 선택한다. 생성된 대량의 이벤트는 Kafka와 같은 메시지 브로커를 활용해 비동기적으로 처리하며 다음 레이어로 전달된다.
주요 트레이드오프
Write-time vs Read-time
핵심 개념
| 개념 | 설명 | 예시 |
| domain event | 시스템에서 의미있는 사건을 나타내는 객체 | domain event, fanout event, notification event |
| Apache Kafka | 이벤트를 비동기로 전달하는 스트림 시스템 | like 이벤트 → Kafka topic → consumer group |
| producer / consumer | 이벤트 생성자 / 처리자 | service → producer fanout worker → consumer |
| fanout on write | 이벤트 발생 시 미리 유저별 데이터 생성 | 글 작성 → 팔로워 100명에게 알림 100개 생성 |
| fanout on read | 조회 시점에 동적으로 데이터 생성 | 피드 요청 → 팔로잉 목록 기반 posts 조회 |
| celebrity problem | 팔로워 많은 유저 fanout 폭발 | 팔로워 1억 |
| hybrid fanout | 유저 규모에 따라 write/read 혼합 | 일반 유저=write, 셀럽=read |
| batch fanout | 여러 유저 insert를 묶어서 처리 | 1000명씩 insert |
| aggregation | 여러 이벤트를 하나의 알림으로 묶음 | “A, B, C liked your post” |
| aggregation window | 일정 시간 동안 이벤트를 모아서 처리 | 5분 동안 like 모으기 |
| outbox pattern | DB + 이벤트 발행을 원자적으로 보장 | comment insert + outbox insert |
| CDC / poller | outbox → Kafka로 이벤트 전송 | Debezium |
| idempotency | 동일 이벤트 중복 처리 방지 | UNIQUE(event_id) |
| duplicate 문제 | Kafka 재처리로 중복 발생 | 같은 알림 2번 생성 |
| event ordering | 이벤트 순서 보장 문제 | follow → unfollow 순서 뒤바뀜 |
| partition key | Kafka 순서 보장을 위한 key | user_id 기준 partition |
| candidate generation | 보여줄 후보 데이터 생성 | 피드 후보 500개 생성 |
Storage layer
Storage Layer는 Processing Layer로부터 전달받은 알림 데이터 저장을 담당하는 영역으로 확장성 문제를 가장 직접 마주하는 영역이다. 유저 수에 따라 데이터가 폭발적으로 증가하는 문제를 해결하기 위해 어떤 DB 구조로 어떻게 쌓을지를 정의한다. 템플릿과 실제 전달 데이터를 분리하거나, 파라미터 기반 구조를 사용하는 등 다양한 저장 전략이 고려된다. 결국 이 레이어의 핵심은 대용량의 알림 데이터를 효율적으로 저장하면서도 Serving Layer에서 조회 성능을 확보하도록 설계하는 것이다.
주요 트레이드오프
Space vs Join cost
핵심 개념
| 개념 | 설명 | 예시 |
| notification table 단일 모델 | 유저 알림 저장 테이블 | user_id, actor_id, type |
| template + delivery 전달 분리 모델 | 템플릿과 유저 전달 데이터를 분리 | Notice + memeberNotice |
| announcement | 공지 저장 테이블 | 점검 안내 |
| announcement_read | 공지 읽음 상태 | user_id, announcement_id |
| notification_template | 알림 메시지 템플릿 저장 | “{actor} liked your post” |
| notification_delivery | 유저별 알림 데이터 | user_id, template_id |
| event table | 이벤트 기록 테이블, 저장 구조지만 처리 목적 | outbox_event |
| param_json / metadata | 알림 파라미터 저장 | template_id + params ex) {order_id:123} |
| personal_msg | 개인화된 메시지 저장 | “철수님이 댓글” |
| is_read 컬럼 | 읽음 여부 inline 저장 | boolean |
| notification_read | 읽음 상태 분리 테이블 | user_id + notification_id |
| append-only log | 수정 없이 insert만 하는 구조 | notification log |
| sharding | DB를 여러 개로 분산 | user_id % 100 |
| hot shard 문제 | 특정 shard에 트래픽 집중 | 인기 유저 |
| NoSQL (Cassandra 등) | 대용량 write 최적화 DB | wide-column DB |
| TTL | 자동 만료 정책 | 30일 후 삭제 |
| cold storage | 오래된 데이터 외부 저장 | S3 |
| storage explosion | 데이터 폭증 문제 | 하루 10억 알림 |
| index 비용 | insert 시 index 업데이트 비용 | user_id index |
| denormalized view | 조회용 비정규화 테이블 | notification_view |
| timeline/feed table | 유저별 피드 저장 | user_feed |
Serving layer
Serving Layer는 저장된 알림 데이터를 사용자에게 실제로 제공하는 단계로 유저 인터랙션을 담당하는 영역이다. 사용자는 읽지 않은 알림 개수를 즉시 확인하고 빠르게 목록을 조회할 수 있어야 한다. 이를 위해 캐시를 활용한 unread count 관리, 읽음 상태 저장 방식, 페이지네이션 전략 등이 함께 고려되며 궁극적으로는 사용자 경험을 해치지 않는 빠른 응답 속도를 보장하는 것이 목표다.
주요 트레이드오프
Latency vs Accuracy
핵심 개념
| 개념 | 설명 | 예시 |
| notification API | 알림 목록 조회 API | GET /notifications |
| announcement + notification merge 전략 | 공지 + 알림 합쳐서 반환하는 전략 | API aggregation |
| unread count API | 안읽은 알림 수 반환 | 🔔 23 |
| Redis unread count | 빠른 unread count 캐시로 구현 | unread:123 → 5 |
| badge 응답 | UI 빨간 점 숫자 | 앱 상단 알림 |
| pagination | 페이지 단위 조회 | limit 20 |
| fanout on read 조회 최적화 | DB 조회 기반 피드 생성 | SELECT posts |
| cache | 빠른 응답을 위한 캐시 | feed cache |
| precomputed timeline | 미리 계산된 피드 | fanout on write 결과 |
| ranking / relevance | 보여줄 순서 결정, 후보 생성은 processing, 정렬은 serving |
like 확률 기반 |
| ordering/sorting | 정렬 방식 | 시간순 vs 추천순 |
| feed API | 피드 조회 | GET /feed |
| server rendering | 서버에서 메시지 생성 | API에서 문자열 생성 |
| client rendering | 클라이언트에서 메시지 생성 | params 기반 렌더링 |
알림 도메인은 단순한 DB 설계를 너머
이벤트 기반 처리(Kafka, fanout), 대규모 저장(sharding, NoSQL), 고성능 조회(Redis, ranking)를 총괄하여 설계하는 시스템 아키텍쳐 문제다.
'scalability' 카테고리의 다른 글
| 알림 아키텍처 (2) - Processing Layer (0) | 2026.03.31 |
|---|