알림 아키텍처 (1)

2026. 3. 27. 19:28·scalability

대규모 서비스 아키텍처의 주된 관심사는 확장성과 병목 관리라 해도 과언이 아니다. 이와 관련된 트레이드오프나 문제 상황을 경험해보고 핸들할 수 있는 판단력을 갖추는 것이 요즘 시대에 가장 필요한 개발 역량이 아닌가 싶다. 규모가 크지 않은 서비스라도 이와 비슷한 문제 상황을 자연스럽게 고민하게 되는 도메인이 있는데 개인적으로 알림 도메인이라고 생각한다. 어느정도 반복되는 구조를 답습하는 대부분의 도메인과 다르게 각자 서비스 성격에 따라 효율적인 시스템 디자인을 간접적으로 고민해 볼 수 있는 가장 현실적이고 자유도 높은 도메인이 아닐까한다.

 

기본적으로 알림은 서비스가 유저에게 먼저 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
'scalability' 카테고리의 다른 글
  • 알림 아키텍처 (2) - Processing Layer
cusum26
cusum26
  • cusum26
    CUSUMlog
    cusum26
  • 전체
    오늘
    어제
    • 분류 전체보기 (18)
      • dev (15)
        • blockchain (1)
        • ai (6)
        • web (0)
        • infra (4)
        • app (4)
      • cs (1)
        • blockchain (1)
      • scalability (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    acks
    fanout-on-read
    Kafka
    도메인 이벤트
    bccprm
    msa
    Merkle Trie
    비동기
    KafkaConfig
    group metadata
    컨슈머 오프셋
    codex skill
    __consumer_offsets
    min.insync.replicas
    kafka ui
    fanout-on-write
    FanoutTask
    consumer offset
    kafkaListenerContainerFactory
    lazy fanout
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
cusum26
알림 아키텍처 (1)
상단으로

티스토리툴바