본문 바로가기
카테고리 없음

알림 시스템 설계 알아보기

by SuldenLion 2026. 2. 27.
반응형
알림 시스템 설계 | 소프트웨어 아키텍처 & 시스템
소프트웨어 아키텍처 & 시스템

알림 시스템
설계 완전 분석

푸시 알림, 이메일, SMS, 인앱 알림 — 수억 건의 알림을 지연 없이, 빠짐없이, 중복 없이 전달하는 시스템은 어떻게 설계될까? 채널별 아키텍처부터 우선순위 큐, 멱등성, 사용자 설정 관리, 장애 대응까지 실무 수준으로 파고든다.

시스템 디자인 인터뷰 핵심 주제 읽기 시간 약 18분 난이도 중급~고급
01

알림 시스템이란 무엇인가?

알림 시스템(Notification System)은 다양한 채널을 통해 사용자에게 정보를 전달하는 인프라다. 주문 완료, 친구 요청, 보안 경고, 마케팅 메시지 등 현대 서비스에서 발생하는 모든 알림을 처리한다. 단순한 문자 전송처럼 보이지만, 수억 명의 사용자에게 수십억 건의 알림을 신뢰성 있게, 지연 없이, 중복 없이 전달하는 것은 복잡한 분산 시스템 문제다.

~5B
Facebook이 하루에 전송하는
알림 수 (추정)
4종
주요 알림 채널
(Push / Email / SMS / In-App)
<1s
Critical 알림
목표 전달 지연

알림 시스템은 세 가지 핵심 원칙을 동시에 만족해야 한다. At-Least-Once Delivery(최소 한 번 전달, 유실 방지), 중복 제거(같은 알림이 여러 번 도착하지 않음), 순서 보장(인과 관계가 있는 알림의 순서 유지)이 그것이다. CAP 정리처럼 이 세 가지를 동시에 완벽히 달성하기는 어려우므로, 서비스 특성에 맞게 트레이드오프를 결정해야 한다.

🔔

트랜잭션 알림

주문 완료, 결제 성공, 비밀번호 변경 등 사용자 행동에 즉각 반응하는 알림. 신뢰성과 속도가 최우선.

📣

마케팅 알림

프로모션, 재참여 유도, 이벤트 안내 등 대량 발송 알림. 정확한 타겟팅과 전달률 측정이 핵심.

🚨

시스템 알림

서비스 장애, 보안 이상 감지, SLA 위반 경고 등 운영 관련 알림. 지연이 절대 허용되지 않음.

👥

소셜 알림

좋아요, 댓글, 팔로우 등 사용자 간 인터랙션 알림. 높은 발생 빈도를 효율적으로 집계해야 함.

02

요구사항 분석

기능 요구사항

시스템은 Push 알림(iOS/Android), 이메일, SMS, 인앱 알림 채널을 지원해야 한다. 알림은 즉시(Real-time)와 예약(Scheduled) 두 가지 발송 방식을 모두 지원한다. 사용자는 채널별로 수신 동의 여부와 수신 시간대(방해 금지)를 설정할 수 있어야 한다.

비기능 요구사항

항목 목표 비고
처리량 초당 100만 건 알림 생성, 전달 대형 마케팅 발송 시 피크 트래픽 고려
지연 Critical: < 1초 / 일반: < 10초 / 마케팅: < 5분 알림 유형별 SLA를 다르게 설정
신뢰성 At-Least-Once Delivery, 유실률 0%에 수렴 전달 실패 시 재시도 필수
중복 방지 동일 알림이 2회 이상 전달되지 않아야 함 재시도 시에도 멱등성 보장
가용성 99.9% 이상 외부 서비스(APNs, FCM) 장애에도 큐로 버퍼링
확장성 수평 확장으로 트래픽 대응 상태 없는(Stateless) 워커 설계

💡 규모 추정: DAU 1억 명, 사용자당 하루 평균 30건 알림 수신 → 하루 30억 건 → 초당 약 35,000 건(평균). 마케팅 대량 발송(5천만 명 동시)은 순간 초당 수십만 건까지 치솟을 수 있다. 이 피크 트래픽을 메시지 큐로 버퍼링하는 것이 아키텍처의 핵심이다.

03

알림 채널 4종 — 특성과 제약

채널마다 기술 스택, 전달 보장 수준, 사용자 경험이 완전히 다르다. 각 채널의 특성을 이해해야 채널별 워커를 올바르게 설계할 수 있다.

📱
모바일 Push 알림
APNs (iOS) / FCM (Android)

iOS는 Apple Push Notification service(APNs), Android는 Firebase Cloud Messaging(FCM)을 통해 전달된다. 서버가 직접 디바이스에 전달하지 않고 반드시 제조사의 게이트웨이를 경유해야 한다.

앱이 설치되어 있고 사용자가 권한을 허용한 경우에만 전달된다. APNs는 최근 전달된 1개의 알림만 보관하므로(Coalescing), 디바이스가 오프라인인 동안 여러 알림이 발송되면 마지막 것만 도달한다.

APNs / FCM 경유 필수 권한 동의 필요 즉시 전달 오프라인 시 소실 가능
📧
이메일
SendGrid / AWS SES / Mailgun

전달 신뢰성이 높고 긴 내용을 전달하기 적합하다. 마케팅, 영수증, 상세 공지 등에 사용된다. SendGrid, AWS SES, Mailgun 같은 이메일 서비스 제공자(ESP)를 통해 전송하는 것이 표준이다.

스팸 필터링, 도메인 평판(IP Reputation), SPF/DKIM/DMARC 설정이 전달률에 결정적 영향을 미친다. 대량 발송은 발신 IP 워밍업(Warming Up)이 필요하다.

높은 신뢰성 HTML 템플릿 지원 스팸 필터 위험 전달 지연 가능
💬
SMS
Twilio / AWS SNS / 국내 통신사 API

앱 설치나 인터넷 연결이 없어도 전달된다. OTP, 보안 인증, 긴급 알림에 사용된다. Twilio, AWS SNS 등의 SMS 게이트웨이를 통해 전 세계 통신사 네트워크로 전달된다.

건당 비용이 발생하므로 대량 발송에 비용 최적화가 중요하다. 국가별로 규제가 상이하고, 일부 국가에서는 발신 번호 등록이 필수다. 160자 제한(영문 기준)이 있다.

앱 없이 전달 높은 열람률 (~98%) 건당 비용 발생 160자 제한
🔔
인앱 알림 (In-App)
WebSocket / SSE / Polling

앱이나 웹사이트를 사용 중인 사용자에게 실시간으로 표시되는 알림이다. 알림 벨 아이콘의 숫자 뱃지, 토스트 메시지, 인앱 피드 등이 이에 해당한다.

WebSocket을 통한 양방향 통신 또는 SSE(Server-Sent Events)로 실시간 푸시를 구현한다. 외부 서비스에 의존하지 않으므로 제어권이 온전히 서비스에 있다. 오프라인 사용자에게는 읽지 않은 알림으로 DB에 보관되어 다음 접속 시 표시된다.

외부 의존 없음 완전한 제어권 앱 실행 중에만 실시간 WebSocket 연결 관리 필요
04

전체 시스템 아키텍처

알림 시스템은 이벤트를 수신하는 Notification Service, 처리를 분배하는 Message Queue, 채널별로 실제 전송을 담당하는 Channel Worker 세 계층으로 구성된다.

이벤트 소스
주문 서비스
인증 서비스
마케팅 시스템
모니터링
수신 계층
Notification API (수신 & 검증)
Scheduler (예약 발송)
처리 계층
사용자 설정 조회
템플릿 렌더링
중복 제거 (Dedup)
우선순위 분류
메시지 큐
Push Queue
Email Queue
SMS Queue
InApp Queue
채널 워커
Push Worker
Email Worker
SMS Worker
InApp Worker
외부 서비스
APNs / FCM
SendGrid / SES
Twilio / SNS
WebSocket Server
저장소
알림 DB (이력)
Redis (Dedup / 설정 캐시)
Analytics DB
05

알림 생성 — 이벤트 드리븐 설계

알림은 각 서비스에서 직접 전송 로직을 실행하는 것이 아니라, 이벤트를 발행하고 Notification Service가 이를 구독해 처리하는 이벤트 드리븐 방식이 권장된다. 서비스 간 결합도를 낮추고 알림 채널 추가 시 발신 서비스를 수정할 필요가 없다.

알림 이벤트 스키마 (JSON)
{ "notification_id" : "ntf_a1b2c3d4e5f6", // 전역 고유 ID (멱등성 키) "type" : "ORDER_COMPLETED", "priority" : "HIGH", // CRITICAL / HIGH / NORMAL / LOW "user_id" : "usr_12345", "channels" : ["push", "email"], // 원하는 채널 목록 (설정에 따라 필터링됨) "template_id" : "tmpl_order_done_v2", "template_vars" : { "order_id" : "ORD-2024-98765", "total_amount" : 35800, "delivery_eta" : "2024-01-20" }, "send_at" : "2024-01-15T10:23:00Z", // null이면 즉시 발송 "ttl" : 86400, // 초 단위 유효 기간 (만료 시 폐기) "idempotency_key" : "order:98765:completed" // 중복 방지용 }

Notification API 처리 흐름

이벤트 수신
유효성 검증
사용자 설정 조회
Dedup 체크
통과 → 채널 큐에 적재
우선순위 분류
템플릿 렌더링
중복 / TTL 만료 → 폐기
모든 단계는 Redis 캐시를 활용해 DB 조회 최소화
06

메시지 큐와 워커 — 비동기 파이프라인

Notification API는 이벤트를 수신하고 검증한 뒤 즉시 큐에 적재하고 응답을 반환한다. 실제 전송은 큐를 구독하는 Channel Worker가 비동기로 처리한다. 이 분리가 시스템 탄력성(Resilience)의 핵심이다 — 외부 서비스(FCM, SendGrid)가 느려지거나 다운되어도 API는 영향을 받지 않고, 큐가 버퍼 역할을 한다.

Push Worker — Python 의사코드
class PushWorker: def process(self, message): notification = deserialize(message) # 1. TTL 만료 여부 확인 if is_expired(notification): log_discarded(notification.id, "TTL_EXPIRED") return # 2. 디바이스 토큰 조회 tokens = device_store.get_tokens(notification.user_id) if not tokens: log_skipped(notification.id, "NO_DEVICE_TOKEN") return for token in tokens: try: # 3. 플랫폼별 게이트웨이 선택 gateway = apns if token.platform == "ios" else fcm result = gateway.send(token.value, notification.payload) log_delivered(notification.id, token.device_id) except InvalidTokenError: # 무효 토큰 → DB에서 삭제 device_store.delete_token(token.value) except RateLimitError: # 게이트웨이 Rate Limit → 메시지 재큐 queue.requeue(message, delay=5)
⚙️

채널별 큐를 분리해야 하는 이유

SMS 큐와 이메일 큐를 분리하지 않으면, 대량 이메일 발송이 시작될 때 긴급 SMS 전달이 수 분씩 지연될 수 있다. 채널별 독립 큐는 채널 간 격리를 보장하고, 각 채널의 처리 속도와 우선순위를 독립적으로 제어할 수 있게 해준다. Kafka 기반이라면 토픽을 채널별로 분리하고, 파티션 수를 채널의 처리량에 맞게 조정한다.

07

알림 우선순위 — Priority Queue

모든 알림이 동등하지 않다. 계정 보안 경고와 프로모션 쿠폰 발송이 같은 큐에서 같은 속도로 처리되어선 안 된다. 알림 우선순위를 명시적으로 정의하고, 우선순위별 큐를 운용해 높은 우선순위 알림이 항상 먼저 처리되도록 해야 한다.

CRITICAL
즉시 전달, 재시도 무제한, TTL 없음
보안 경고, 결제 오류, 시스템 장애
HIGH
1초 이내, 재시도 5회, TTL 1시간
주문 완료, OTP, 배송 도착
NORMAL
10초 이내, 재시도 3회, TTL 24시간
댓글 알림, 친구 요청, 리뷰 좋아요
LOW
5분 이내, 재시도 1회, TTL 7일
주간 리포트, 마케팅, 뉴스레터
Kafka 토픽 구조 설계
# 채널 × 우선순위 토픽 매트릭스 # 파티션 수는 처리량과 비례해 설정 "notification.push.critical" # 파티션 16개, 전용 워커 클러스터 "notification.push.high" # 파티션 32개 "notification.push.normal" # 파티션 64개 "notification.push.low" # 파티션 16개 "notification.email.high" # 트랜잭션 이메일 "notification.email.low" # 마케팅 이메일 (대량 발송) "notification.sms.critical" # OTP, 보안 SMS (비용 절감 불가) "notification.sms.normal" "notification.inapp" # 인앱은 단일 토픽 (비용 없음) "notification.deadletter" # 재시도 초과 → DLQ로 이동
08

중복 방지 — 멱등성 보장

분산 시스템에서 재시도와 중복 발송은 불가피하다. 네트워크 지연으로 타임아웃이 발생해도 외부 서비스에 이미 전달되었을 수 있고, 워커가 처리 도중 크래시하면 메시지를 재처리한다. 사용자가 동일한 알림을 두 번 받는 것은 신뢰도 하락으로 이어진다.

해결책: Idempotency Key + Redis 중복 필터

멱등성 처리 — Python 의사코드
def send_notification(notification): idempotency_key = notification.idempotency_key dedup_key = f"sent:{idempotency_key}" # Redis SET NX: 키가 없을 때만 세팅 (원자적) # 성공이면 이번이 첫 처리, 실패면 이미 처리됨 is_first = redis.set(dedup_key, 1, nx=True, ex=86400) if not is_first: log(f"Duplicate dropped: {idempotency_key}") return # 중복 → 즉시 폐기 try: # 실제 전송 result = gateway.deliver(notification) db.save_log(notification.id, status="DELIVERED", result=result) except Exception as e: # 전송 실패 → Redis 키 삭제해 재시도 가능하게 복구 redis.delete(dedup_key) raise e

💡 Idempotency Key 설계 원칙: 비즈니스 의미가 담긴 키를 사용한다. 단순 UUID보다 "order:98765:completed", "user:12345:login_otp:2024011510" 처럼 이벤트의 의미와 시간 윈도우를 포함하면, 같은 이벤트의 재시도는 걸러내되 다음 로그인 시도는 새로운 알림으로 처리할 수 있다.

09

재시도 전략 — 신뢰할 수 있는 전달

외부 알림 게이트웨이(FCM, APNs, SendGrid)는 일시적으로 장애가 발생하거나 요청을 거부할 수 있다. 단순히 실패를 버리면 알림 유실이 발생한다. 체계적인 재시도 전략이 필수다.

Exponential Backoff with Jitter

재시도 전략 — Python 의사코드
def send_with_retry(notification, attempt=0): max_attempts = RETRY_CONFIG[notification.priority] # 우선순위별 상이 try: return gateway.deliver(notification) except TransientError as e: # 일시적 오류 → 재시도 if attempt >= max_attempts: move_to_dlq(notification) # Dead Letter Queue로 이동 return # Exponential Backoff: 2^attempt 초 대기 + 랜덤 지터 base_delay = 2 ** attempt jitter = random.uniform(0, base_delay * 0.3) delay = min(base_delay + jitter, 300) # 최대 5분 queue.requeue(notification, delay=delay, attempt=attempt + 1) except PermanentError as e: # 영구 오류 → 재시도 금지 # e.g. InvalidTokenError, UserUnsubscribed log_permanent_failure(notification.id, e) handle_permanent_error(notification, e)

Dead Letter Queue (DLQ)

재시도 횟수를 모두 소진한 알림은 Dead Letter Queue로 이동시킨다. DLQ는 유실을 방지하고, 운영자가 실패 원인을 분석하거나 수동으로 재처리할 수 있는 안전망이다. DLQ에 쌓인 메시지가 임계값을 초과하면 즉시 알람을 발송해야 한다.

오류 유형 예시 재시도 여부 처리 방법
일시적 오류 5xx, 타임아웃, Rate Limit 재시도 Exponential Backoff 후 재시도
영구 오류 무효 토큰, 구독 해제, 4xx 재시도 금지 토큰 삭제 / 수신 거부 처리
TTL 만료 유효 기간 초과 폐기 미전달 로그 기록 후 폐기
재시도 소진 일시적 오류 max 횟수 초과 수동 검토 DLQ로 이동, 운영자 알림
10

사용자 설정 — 알림 수신 제어

사용자가 원하지 않는 알림을 수신하면 앱 삭제, 이메일 수신 거부, 법적 문제(CAN-SPAM, GDPR)로 이어진다. 세밀하고 직관적인 알림 수신 설정은 사용자 신뢰와 직결된다.

사용자 알림 설정 스키마 (JSON)
{ "user_id": "usr_12345", // 채널별 전역 수신 동의 "channels": { "push" : true, "email" : true, "sms" : false, // SMS 수신 거부 "in_app" : true }, // 알림 유형별 세밀한 설정 "types": { "ORDER_UPDATE" : {"push": true, "email": true}, "SOCIAL_LIKE" : {"push": false, "in_app": true}, // Push 끄고 인앱만 "MARKETING" : {"push": false, "email": false} // 마케팅 전체 거부 }, // 방해 금지 시간대 (서버 측에서 시간대 변환 필요) "quiet_hours": { "enabled" : true, "start" : "22:00", "end" : "08:00", "timezone" : "Asia/Seoul", "override_critical": true // CRITICAL 알림은 방해 금지 무시 } }

⚠️ 방해 금지 시간대와 Critical 알림: 보안 경고나 결제 오류 같은 CRITICAL 알림은 방해 금지 시간대에도 반드시 전달해야 한다. 사용자 설정 조회 시 override_critical: true 플래그를 확인하고, CRITICAL 우선순위 알림은 quiet_hours를 무시하는 로직을 명시적으로 구현해야 한다.

사용자 설정은 Redis에 캐싱해 매 알림마다 DB를 조회하는 부하를 없애야 한다. 설정 변경 시에는 캐시를 즉시 무효화(Invalidation)해야 한다. GDPR 등 데이터 규정에 따라 사용자가 언제든 모든 알림을 철회할 수 있어야 하며, 이 설정 변경은 즉시 반영되어야 한다.

11

알림 템플릿과 개인화

각 채널은 콘텐츠 형식이 다르다. Push 알림은 제목+본문 100자 이내, 이메일은 HTML 전체 페이지, SMS는 160자 내외다. 동일한 이벤트를 채널별로 적절한 형식으로 렌더링하는 템플릿 시스템이 필요하다.

템플릿 정의 (YAML)
template_id: "ORDER_COMPLETED_v2" version: 2 locale: "ko" channels: push: title: "주문이 완료됐어요 🎉" body: "{{order_id}} 주문이 접수됐습니다. {{delivery_eta}} 도착 예정" deep_link: "myapp://orders/{{order_id}}" email: subject: "[{{brand_name}}] {{order_id}} 주문 완료 안내" html_template: "order_completed_email.html" from: "noreply@example.com" "reply_to": "support@example.com" sms: body: "[주문완료] {{order_id}} 접수. 배송 예정일 {{delivery_eta}}. 문의: 1588-0000"
🌍

다국어 지원과 개인화

글로벌 서비스라면 동일 템플릿을 언어(locale)별로 관리해야 한다. 사용자의 언어 설정, 이름, 구매 이력 등을 활용한 개인화는 클릭률을 크게 높인다. 단, 개인화 데이터는 렌더링 시점에 실시간으로 주입하되 민감 정보는 절대 포함하지 않는다. 템플릿 변경 시에는 버전 관리로 A/B 테스트와 롤백이 가능해야 한다.

12

모니터링과 분석

알림 시스템의 건강도는 정량적으로 측정되어야 한다. 전달 성공률, 지연 분포, 열람률, 재시도율, DLQ 적재량은 반드시 실시간으로 추적해야 하는 지표다.

지표 설명 임계값(예시) 대응
전달 성공률 전송 시도 대비 성공 비율 < 99% 시 알람 채널별 게이트웨이 상태 확인
전달 지연 (p99) 이벤트 발생부터 전달까지 시간 CRITICAL > 2s 시 알람 큐 적체 여부, 워커 수 조정
열람률 (Open Rate) 전달된 알림 중 실제 열람 비율 Push < 5% 시 검토 템플릿, 타겟팅, 발송 시간 최적화
재시도율 전체 전송 중 재시도 발생 비율 > 5% 시 알람 외부 게이트웨이 이상 여부 확인
DLQ 적재량 Dead Letter Queue 메시지 수 > 1,000건 시 알람 즉시 운영자 개입, 원인 분석
수신 거부율 이메일 Unsubscribe / Push 권한 해제 이메일 > 0.5% 시 검토 발송 빈도, 콘텐츠 관련성 재검토
전달 이벤트 로그 스키마
{ "notification_id" : "ntf_a1b2c3d4e5f6", "user_id" : "usr_12345", "channel" : "push", "type" : "ORDER_COMPLETED", "priority" : "HIGH", "status" : "DELIVERED", // QUEUED|SENT|DELIVERED|FAILED|DISCARDED "attempt" : 1, "queued_at" : "2024-01-15T10:23:00.123Z", "sent_at" : "2024-01-15T10:23:00.891Z", "delivered_at" : "2024-01-15T10:23:01.234Z", "latency_ms" : 1111, "gateway_response": "fcm:message_id:abc123" }
13

정리 및 핵심 교훈

알림 시스템은 단순한 메시지 전송 서비스가 아니다. 신뢰성, 확장성, 사용자 경험이 교차하는 복잡한 분산 시스템이다. 잘 설계된 알림 시스템은 사용자가 필요한 정보를 정확한 순간에 받게 해주고, 나쁜 알림 시스템은 사용자를 짜증나게 해 앱 삭제로 이어진다.

✉️

채널은 독립적으로 격리하라

채널별 큐와 워커를 분리하면 마케팅 이메일 대량 발송이 긴급 OTP 전달을 지연시키는 일이 없다. 격리가 신뢰성의 기반이다.

🔁

멱등성은 설계 초기부터

재시도를 허용하면서도 중복 전달을 막으려면 Idempotency Key와 Redis 중복 필터를 처음부터 설계에 포함해야 한다. 나중에 추가하는 것은 훨씬 어렵다.

🎚️

우선순위가 SLA를 결정한다

모든 알림을 동등하게 처리하면 결국 아무것도 제때 도착하지 않는다. 우선순위별 큐와 워커를 명확히 분리해야 한다.

🙅

사용자 설정을 존중하라

GDPR, CAN-SPAM 같은 법적 요건은 차치하더라도, 원치 않는 알림을 보내는 것은 사용자 신뢰를 잃는 가장 빠른 방법이다.

📊

전달률을 실시간으로 측정하라

DLQ 적체와 전달 지연은 문제가 이미 심각해진 뒤에야 알게 된다. 실시간 대시보드와 임계값 알람이 필수다.

🔌

외부 게이트웨이 장애를 가정하라

APNs, FCM, SendGrid는 언제든 다운될 수 있다. 큐로 버퍼링하고, 서킷 브레이커와 폴백 채널을 사전에 설계해야 한다.

💬 설계 인터뷰 팁: 알림 시스템 문제에서 면접관이 가장 듣고 싶어하는 것은 세 가지다. 첫째, 채널을 어떻게 분리하고 확장할 것인가. 둘째, 중복 전달과 유실을 동시에 어떻게 방지할 것인가. 셋째, 외부 서비스(APNs/FCM) 장애 시 시스템이 어떻게 동작할 것인가. 이 세 가지에 명확한 답변을 준비하면 핵심은 모두 커버된다.

소프트웨어 아키텍처 & 시스템 시리즈  |  알림 시스템 설계

반응형

댓글