스터디 노트

Chapter 10

알림 시스템 설계

APNs·FCM·SMS·이메일 4채널의 통합 알림 시스템. 채널별 큐로 분리하고 알림 로그·사용자 설정·재시도로 신뢰성을 보강한다.

이 챕터의 답

알림 시스템의 핵심 부품 4가지

알림의 마지막 마일은 모두 외부 서비스(APNs·FCM·SMS·이메일)에 의존한다. 외부 통제 불가 채널별로 다른 처리율·인증·실패 모드 두 제약이 시스템 설계의 기준이 된다.

  1. 채널별 큐 분리 — APNs/FCM/SMS/Email 각각의 큐. 한 채널이 막혀도 다른 채널은 살아 있게.
  2. 알림 로그 + dedup 키 — 손실 방지 + 중복 방지.
  3. 사용자 설정 + 템플릿 — 보내기 전에 opt-in 체크, 메시지는 템플릿에서 파라미터만 치환.
  4. Rate limit + 재시도 — 사용자·외부 서비스 모두 보호.

1. 요구사항 정의

알림 시스템 설계는 채널 비대칭, 외부 의존, 대량 발송 세 가지 제약을 동시에 다뤄야 한다.

면접에서 확정할 질문

  • 지원 채널 — 푸시(iOS/Android)? SMS? 이메일? 셋 다?
  • 실시간성 — 즉시 발송이 기본? 예약 발송도?
  • 발송 주체 — 클라이언트 단말? 서버 측 트리거? 외부 서비스?
  • 사용자 수신 거부 옵션 — 채널별·카테고리별 opt-in/out?
  • 일일 발송량 — iOS·Android·SMS·이메일 각각
  • 다국어·다지역 지원 여부 (시간대·언어)

이 챕터의 표준 요구사항

채널일일 발송량비고
iOS push1,000만APNs
Android push1억FCM
SMS500만Twilio 같은 외부 서비스
Email5억SendGrid/Mailchimp

채널마다 두 자릿수 차이가 나는 트래픽이라 한 큐로 묶을 수 없다. 이메일이 폭주하면 푸시도 막힌다. 이것이 4번 섹션의 채널별 큐 분리의 1차 근거.

이 시스템이 어려운 3가지 구조적 이유

① 채널 비대칭

APNs/FCM/SMS/Email은 인증 방식·페이로드 크기·전송 지연·에러 코드가 전혀 다르다. 한 코드로 처리하면 if 폭탄.

② 외부 의존

APNs·FCM·Twilio·SendGrid 모두 외부 서비스. 우리가 장애를 통제할 수 없고, 응답 시간 변동·rate limit· 일시 거부에 대응해야 한다.

③ 신뢰성의 양면

안 보내는 것(loss)도 문제, 두 번 보내는 것(duplicate)도 문제. 사용자는 둘 다 불신·이탈로 응답한다.

2. 이메일 / SMS / 푸시 알림 구조

각 채널이 실제로 어떻게 작동하는지부터 본다. 모두 외부 서비스가 마지막 마일을 책임지지만, 흐름·전제 조건은 채널마다 다르다.

iOS Push — Provider → APNs → Device

Provider (우리)
APNs (Apple)
iOS device

필요한 입력: device token(앱이 디바이스 + 앱 식별), payload(alert/badge/sound 등 JSON), 인증서(APNs와의 TLS 인증).

Android Push — Provider → FCM → Device

Provider
FCM (Google)
Android device

APNs와 거의 동일한 구조. 입력은 FCM 등록 토큰 + payload. APNs 대비 인증서가 아닌 API 키 기반이라 운영이 좀 더 단순한 편.

SMS · Email — 외부 서비스 위탁

SMS
Provider → Twilio / Nexmo → 전화번호

자체 통신망 구축은 비현실 → 거의 100% 3rd party. 비용이 푸시 대비 훨씬 비싸므로 ‘본인 인증’ 같은 고가치 알림에 집중.

Email
Provider → SendGrid / Mailchimp → 이메일 주소

직접 SMTP를 돌리면 평판·스팸 필터 우회가 거의 불가능 → 평판 관리 잘하는 외부 서비스에 위탁. 도달률·오픈율 분석이 따라옴.

연락처 정보 수집 — 모든 채널의 출발선

어떤 채널이든 “누구한테 보낼지” 정보가 미리 DB에 있어야 한다. 사용자가 가입할 때·앱을 처음 켤 때 수집해 저장한다. 한 사용자는 디바이스 여러 개·이메일 여러 개를 가질 수 있으니 1:N 관계로 모델링.

테이블주요 컬럼메모
usersid, email, country, timezone, …1
devicesuser_id, device_token, os, last_seenN (사용자당 여러 단말)
토큰 만료 처리

토큰이 만료되거나 디바이스가 바뀌면 APNs/FCM이 invalid token을 반환한다. 워커는 이 응답을 받으면 devices에서 해당 토큰을 비활성·삭제한다. 앱이 다시 켜질 때 새 토큰이 등록된다. 이 cleanup이 빠지면 매번 같은 무효 토큰에 헛발송한다.

3. 단일 서버 → 확장형 아키텍처

“이벤트가 들어오면 외부 서비스에 보낸다.” 가장 단순한 형태에서 시작해, 어떤 문제가 생기는지 보면서 확장형 아키텍처로 진화시킨다.

1차 — 단일 서버 직결

이벤트 소스
Notification Server (단일)
APNs · FCM · SMS · Email

요청을 받자마자 서버가 외부 API를 직접 호출한다. 트래픽이 적을 때는 동작하지만 곧 한계가 온다.

이 구조에서 무엇이 깨지는가:

  • 단일 장애점 — 서버가 죽으면 알림 전체 정지
  • 외부 지연이 입구로 역류 — APNs가 느리면 이벤트 소스의 요청도 같이 늘어진다
  • 채널 간 간섭 — 이메일 폭주가 푸시 처리를 막는다
  • 실패 시 복구 불가 — 워커가 죽는 순간 미발송 메시지가 사라진다

2차 — 확장형 아키텍처

위 문제를 한꺼번에 푸는 구조: 다중 인스턴스 + 채널별 큐 + 워커의 3단 파이프라인. 큐가 입구와 외부 호출 사이를 끊어준다.

이벤트 소스
주문 서비스
결제 서비스
마케팅
보안 알림
...
Stage 1 · Notification Servers (다중 인스턴스)
검증 + 사용자 정보 조회 + 큐로 분기
  • API 인증(appKey/appSecret), 페이로드 검증
  • users/devices/settings 조회 (DB + 캐시)
  • 사용자 opt-out·rate limit 체크 후 채널별 큐로
Stage 2 · 채널별 큐 (4번 섹션에서 상세)
iOS queue
Android queue
SMS queue
Email queue
Stage 3 · Workers (채널별)
큐에서 꺼내 외부 서비스로 전송 + 결과 기록
  • 각 채널 워커는 그 채널 SDK·인증만 안다 → 단순
  • 실패 시 재시도 큐로(지수 백오프)
  • 성공/실패를 알림 로그에 기록
외부 서비스
APNs
FCM
Twilio
SendGrid

큐가 한가운데 끼어 있어 생산(서버)과 소비(워커)가 완전히 분리된다. 워커 장애·외부 지연이 입구 API로 역류하지 않는다.

4. 메시지 큐 기반 비동기 처리

확장형 아키텍처의 핵심이 채널별 큐다. 왜 4개의 큐인지, 큐가 가져다주는 효과가 무엇인지 정리한다.

채널별로 다른 3가지

차원iOSAndroidSMSEmail
처리량(일일)1,000만1억500만5억
평균 지연수백 ms수백 ms초~수십 초초~분
실패 모드invalid tokeninvalid token번호 거부/지역바운스/스팸

한 큐로 묶으면 일어나는 일

  • 가장 느린 채널이 전체 처리량을 결정 — 이메일이 막히면 푸시도 같이 늦어진다
  • 장애 격리 불가 — SendGrid가 다운되면 APNs도 함께 대기열 폭주
  • 워커 코드가 if 폭탄 — 채널마다 인증·페이로드가 달라 한 워커에서 분기하면 복잡도 폭증

큐가 가져다주는 3가지 효과

버퍼링

순간 트래픽 스파이크를 평탄화. 입구 API 응답 시간이 외부 서비스 지연과 분리된다.

백프레셔

큐 깊이를 모니터링해 한 채널이 막힐 때 다른 채널·서비스에 영향 없이 격리.

재시도 거점

실패한 메시지를 다시 큐에 넣어 워커가 떠난 뒤에도 시도가 이어진다. 워커 무상태화 가능.

5. 알림 저장 및 전달

“보내야 한다 / 보내지 말아야 한다”를 결정하고, 보낼 내용을 만들고, 이력을 남기는 부분. 사용자 설정·템플릿·알림 로그가 한 묶음으로 움직인다.

알림 로그(DB) — 손실 방지의 기준선

알림 요청을 받자마자 DB에 한 줄 기록한 뒤 큐에 넣는다. 큐가 메시지를 잃거나 워커가 죽어도, 로그를 보고 미발송 항목을 재시도할 수 있다.

  • 상태 전이: PENDING → IN_FLIGHT → SENT / FAILED
  • FAILED는 재시도 큐 또는 사람 검수 큐로
  • DB가 진실의 원천 — 큐는 작업 배포 채널일 뿐

중복 방지 — Dedup key (멱등성)

분산 시스템에서는 at-least-once가 기본값이다. 같은 알림 요청이 두 번 들어오면? 클라이언트가 매 요청에 유일한 dedup key(idempotency key)를 붙이고, 서버는 그 키로 중복 체크.

function sendNotification(req):
if log.exists(req.dedupKey):
return "already processed" # 멱등
log.insert(req, status=PENDING)
queue.enqueue(req.channel, req)

dedup key 검사 + 로그 삽입을 한 트랜잭션으로 처리해야 중복 enqueue를 막을 수 있다. 키 저장은 보통 짧은 TTL의 인메모리(Redis) + 영구 DB 이중화.

사용자 알림 설정

가입 시·설정 화면에서 사용자가 채널·카테고리별 opt-in/out을 지정. 알림 서버는 보내기 전에 이 설정을 조회한다.

컬럼예시설명
user_id42사용자 ID
channelpush / sms / email채널 식별
categorymarketing / security / order카테고리(보안은 강제 발송 가능)
opt_intrue / false수신 여부

알림 템플릿

“주문이 발송되었습니다” 같은 메시지를 매번 새로 작성하지 않고, 템플릿에 파라미터를 끼워넣는다. 캠페인·다국어 관리에 필수.

# template: order_shipped
제목: 주문 {{order_id}} 발송 완료
본문: {{user_name}}님, 주문하신 상품이 {{date}}에 도착합니다.
  • 일관성 — 같은 알림은 항상 같은 톤·문구
  • 관리 용이 — 문구 수정 한 번이면 전 캠페인 반영
  • 다국어 — 같은 키로 언어별 템플릿 다중 보관

처리 순서 — 값싼 검사부터

비싼 외부 호출(템플릿 렌더링·DB·외부 서비스)을 하기 전에 opt-out 체크가 가장 먼저. 거부된 메시지를 큐까지 보내면 큐가 의미 없이 부풀고 비용이 든다.

  1. opt-out 체크 (값쌈)
  2. rate limit 체크
  3. 템플릿 렌더링
  4. 큐 enqueue
  5. 워커의 외부 서비스 호출 (값비쌈)

6. 장애 처리 및 안정성

외부 서비스는 언제든 5xx를 내고, 워커는 언제든 죽는다. 신뢰성은 재시도·격리·정리 세 메커니즘의 조합으로 만든다.

재시도 — 지수 백오프 + 상한

  • 일시 실패(5xx) → 1s · 2s · 4s · 8s · 16s … 지수 백오프로 재큐
  • 영구 실패(invalid token·번호 거부 등) → 재시도 안 함, 연락처 비활성화
  • 최대 재시도 횟수 초과 → Dead Letter Queue로 보내 운영자 알림
큐의 durability ≠ 시스템 신뢰성

큐의 durability는 큐 안에 있는 동안만 보장된다. 워커가 ack 직후 죽으면 알림은 발송되었는데 시스템이 그걸 모를 수 있다. 반대로 발송 전 워커가 죽어 메시지가 재배달되면 중복. 모두 알림 로그(DB)로 풀어야 한다.

Invalid token cleanup

APNs/FCM이 invalid token을 반환하면 워커는 devices에서 해당 토큰을 비활성·삭제한다. 이 cleanup이 빠지면 영구 실패 토큰에 매번 재시도하며 외부 서비스 rate limit만 갉아먹는다.

가용성

  • 알림 서버·워커 모두 stateless → 다중 인스턴스 + 로드밸런서
  • 큐는 다중 노드 복제(Kafka·SQS·RabbitMQ 등)
  • 외부 서비스 장애에 대비해 대체 공급자를 둘 수도 있음 (이메일: SendGrid + Mailgun 등)

일관성 수준

  • 알림 로그·dedup 키 검사 → 강한 일관성
  • 오픈/클릭 추적 데이터 → 결과적 일관성으로 충분

7. 확장성과 최적화 고려사항

기본 시스템이 동작한 뒤 단계적으로 더하는 것들. 사용자 보호, 외부 서비스 보호, 운영 가시성·효과 측정이 이 단계에서 들어간다.

사용자별 Rate limit

  • 사용자에게 단시간 다량 알림을 보내면 스팸으로 인지 → 알림 끄기·앱 삭제로 응답
  • “1시간에 N개 이하” 같은 한도를 사용자별로 (4장 rate limiter와 연결)
  • 보안·결제 같은 고우선 카테고리는 예외로 두는 게 보통

외부 서비스 Rate limit

APNs·FCM·Twilio·SendGrid 모두 자체 rate limit이 있다. 우리가 한 번에 폭주시키면 일시 차단되고, 그 사이 메시지는 큐에 쌓인다. 워커는 외부 limit을 인지하고 발송 속도를 조절(token bucket 등).

보안 — appKey / appSecret

  • 알림 API는 누구나 호출 가능하면 안 됨 — 인증된 클라이언트(내부 서비스 또는 파트너)만
  • appKey/appSecret 발급 + 요청마다 HMAC 서명
  • 서명 검증 + IP 화이트리스트로 이중 방어

모니터링

  • 큐 깊이 — 채널별 큐가 늘어나면 외부 서비스 지연·장애 신호
  • 발송 성공률·실패 코드 분포 — invalid token이 급증하면 디바이스 cleanup 누락
  • 워커 처리량·재시도율·DLQ 적재량 등 운영 지표

이벤트 트래킹 — 효과 측정

  • 오픈율·클릭율·전환율 — 캠페인 효과 측정, 사용자 행동 데이터 축적
  • 추적 이벤트는 분석 서비스(데이터 웨어하우스)로 흘려보낸다