Evolution of Notification & Push Delivery System
알림 · 푸시 시스템 진화 기록
알림 · 푸시 시스템 진화 기록
이 글은 알림(Notification)·푸시(Push) 시스템을 설계하고 운영하면서 어떤 문제를 만났고, 어떤 선택을 했으며, 그 선택이 어떤 결과를 만들었는지를 정리한 내용입니다. (사내 보안 규정에 따라 실제 서비스·테이블·코드 식별자는 모두 일반화되어 있습니다)
문서 정보
목적: 알림/푸시 시스템을 처음 설계하거나, 기존 시스템을 개선하려는 개발자에게 의사결정 기준과 인사이트 제공
작성 시점: 2025-12
기간 범위: 약 1개월간의 점진적 개선
1. Executive Summary — 이 글에서 얻어가야 할 것
이 글의 핵심은 "어떻게 만들었는가"가 아니라 다음 질문에 대한 답입니다.
❓ 알림 시스템은 언제 복잡해지고, 우리는 어떤 신호를 보고 구조를 바꿔야 하는가?
초기 시스템은 다음과 같은 상태였습니다.
알림은 서비스마다 흩어져 있었고
푸시는 일부 기능에서만 임시로 사용되었으며
실패·중복·재시도·추적이라는 개념이 없었습니다
한 달 동안의 개선을 통해 시스템은 다음과 같이 진화했습니다.
기록이 없는 이벤트 → 기록 가능한 알림
동기 처리 → 이벤트 기반 비동기 처리
소량 기준 설계 → 대량 발송 기준 설계
즉시 노출만 가능 → 예약 노출 가능
이 글은 그 변화의 과정 전체를 그대로 보여줍니다.
Table of Contents
Phase 0 - 문제를 문제로 인식하기
Phase 1 - MVP. 완벽함보다 존재가 먼저다.
Phase 2 - 잘못된 도메인 언어는 기술 부채보다 더 위험하다.
Phase 3 - 푸시는 이벤트의 결과다.
Phase 4 - 대량 발송은 언젠가 반드시 온다.
Phase 5 - 이벤트로 분리하지 않으면 언젠가 무너진다.
Phase 6 - 모든 알림은 같지 않다.
Phase 7 - 중복은 기술 문제가 아니라 신뢰 문제다.
Phase 8 - 데이터 구조가 곧 가능성이다.
Phase 9 - 생성과 노출을 분리하다.
Phase 10 - 즉시 알림과 대량 알림을 하나의 파이프라인으로 통합하다.
3. Phase 0 — 문제를 문제로 인식하기
상황
플랫폼 성장과 함께 알림에 대한 요구는 늘어났지만, 현 시스템 구조는 이를 따라가지 못했습니다.
알림이 갔는지 안 갔는지 알 수 없었고
푸시 메시지는 서비스, 성격마다 제각각이었습니다.
판단
이 시점의 핵심 인식은 단순했습니다.
"알림은 기능이 아니라 인프라다."
행동
알림/푸시를 공통 파이프라인 문제로 정의: 서비스별로 흩어진 구현으로는 실패·중복·재처리 기준을 통일할 수 없다고 판단했기 때문에, 알림을 개별 기능이 아닌 인프라 레벨 문제로 재정의했습니다.
결과
이후 모든 설계에서 “이 기능은 알림 파이프라인에 어떻게 연결되는가?”라는 공통 기준이 생겼고, 부분 최적화가 아닌 전체 흐름 관점에서 큰 숲을 볼 수 있었습니다.
Insight
알림 시스템을 늦게 만들수록, 나중에 더 큰 비용을 치른다.
4. Phase 1 — MVP: 완벽함보다 존재가 먼저다
왜 이 선택을 했는가
가장 큰 문제 "이벤트는 발생했는데, 남아 있는 기록이 없다." 였습니다.
행동
최소한의 알림 엔티티와 조회 API 도입: 우선 알림이 실제로 생성·저장되고 있음을 사용자가 확인할 수 있도록, 확장성보다 가시성을 우선했습니다.
도메인 서비스에서 직접 알림 생성: 이벤트 분리나 비동기 처리를 도입하기엔 요구사항과 시간이 충분하지 않았기 때문에, 가장 단순한 방식으로 빠르게 연결했습니다. ("일단 빠르게 만드는게 좋다"라는 시니어 피드백을 수용했습니다.)
결과
알림 센터 MVP가 단기간에 완성되었고, 실제 사용자 피드백을 수집할 수 있는 기반이 마련되었습니다.
트레이드오프
서비스 간 결합도가 증가했고, 관심사 분리가 제대로 되지 않아 기술 부채로 남아있게 됐습니다.
동작 원리
이벤트 발생 → 즉시 알림 데이터 저장 → 클라이언트 조회
5. Phase 2 — 잘못된 도메인 언어는 기술 부채보다 더 위험하다.
왜 이 선택을 했는가
유저라는 테이블이 있고, 유저의 채널이라는 테이블이 분리가 되어있었습니다. 이는 초기에 잘못된 설계였다고 듣게되었고, 결국엔 "사용자 알림"이라는 용어는 실제 비즈니스를 반영하지 못했습니다.
행동
알림의 기준을 사용자 → 채널로 전환: 실제 비즈니스에서 알림의 수신 단위는 사용자 계정이 아니라 채널이었기 때문에, 코드와 데이터 모델이 현실을 그대로 반영하도록 수정했습니다.
결과
이후 기능 추가 시 알림의 주체에 대한 혼란이 사라졌고, 도메인 설명과 API 명세가 훨씬 명확해졌습니다.
트레이드오프
이미 작성된 코드와 스키마를 수정해야 했고, 단기적으로는 작업량이 증가했습니다.
6. Phase 3 — 푸시는 이벤트의 결과다.
왜 이 선택을 했는가
푸시는 "보내는 기능"이 아니라 알림 이벤트의 또 다른 소비자여야 했습니다.
행동
푸시 전용 데이터 구조 도입: 푸시를 단순 외부 호출이 아니라, 알림과 동일하게 추적 가능한 데이터로 남기기 위해 별도의 저장 구조를 설계했습니다.
알림 이벤트 기반 생성: 푸시가 독립적으로 생성되는 것을 막고, 반드시 알림 이벤트의 결과로만 생성되도록 흐름을 제한했습니다.
결과
알림 센터와 푸시의 메시지와 타이밍이 일관되게 유지되었고, 발송 여부를 사후에 검증할 수 있게 되었습니다.
7. Phase 4 — 대량 발송은 언젠가 반드시 온다
왜 이 선택을 했는가
소량 기준 설계는 어느 순간 반드시 한계를 드러내게 되었고, 쌓인 기술 부채가 터지게 되었습니다.
행동
Facade 도입: 특정 서비스 계층의 코드가 과도하게 길어지고 책임이 불명확해졌기 때문에, 알림 생성이라는 행위를 하나의 진입점으로 분리했다. 이를 통해 외부 서비스는 세부 구현을 몰라도 되도록 했습니다.
대량 알림은 JDBC Batch로 분리: ORM 기반 저장이 대량 데이터 처리에 적합하지 않다는 점이 드러나, 성능이 중요한 구간을 명시적으로 배치 처리와 Bulk Insert로 전환했습니다.
결과
알림 생성 로직의 중복이 크게 줄었고, 대량 발송 시 성능 병목이 제거되었습니다.
8. Phase 5 — 이벤트로 분리하지 않으면 언젠가 무너진다
왜 이 선택을 했는가
알림 실패가 본 요청 실패로 이어지기 시작했다.
행동
도메인 이벤트 발행: 알림 처리 실패가 본 트랜잭션에 영향을 주지 않도록, 도메인 로직과 알림 로직의 실행 경로를 분리했습니다.
알림/푸시 비동기 구독: 전용 스레드 풀을 사용해 알림 처리가 서비스 응답 시간에 영향을 주지 않도록 했습니다.
결과
API 응답 시간이 안정되었고, 알림 장애가 서비스 장애로 전파되지 않게 되었습니다.
트레이드오프
디버깅 난이도 상승
9. Phase 6 — 모든 알림은 같지 않다
왜 이 선택을 했는가
추가 요구사항인, 전체 대상, 지역 대상, 예약 알림은 서로 다른 문제였습니다.
행동
세분화된 조회 전략 도입: 전체 대상, 지역 대상, 특정 조건 대상 알림을 동일한 방식으로 처리할 수 없다고 판단해 조회 전략을 분리했습니다.
배치 기반 처리: 메모리 폭주와 타임아웃을 방지하기 위해, 대량 알림은 일정 단위로 나누어 처리했습니다.
결과
대량 알림 생성 시 시스템 자원 사용이 안정되었고, 운영자가 실행 시점을 제어할 수 있게 되었습니다.
트레이드오프
배치 스케줄 관리와 모니터링에 대한 운영 부담이 증가했습니다.
10. Phase 7 — 중복은 기술 문제가 아니라 신뢰 문제다
문제
일부 사용자가 팔로잉, 취소를 반복하여 알림이 계속 생성되는 문제가 발생했습니다.
행동
캐시 기반 레이트 리미트 도입: 동일 유형의 알림이 짧은 시간에 반복 발송되는 것을 막기 위해, 빠른 만료와 조회가 가능한 캐시를 선택했습니다.
(알림 시스템을 완성하고나서는, 빠른 알림 생성과 발송으로 인해 기능을 하지못하게된 작업입니다.)
결과
중복 알림 제거
11. Phase 8 — 데이터 구조가 곧 가능성이다
행동
푸시 1행 = 1대상 구조로 변경: 다수 대상이 한 레코드에 묶여 있으면 실패 재처리와 통계 집계가 어렵기 때문에, 실패 단위를 명확히 하기 위해 구조를 변경했습니다.
노출 시점 분리: 생성 시점과 노출 시점을 구분함으로써 예약 알림 요구사항을 자연스럽게 수용했습니다.
결과
푸시 실패 단위 추적이 가능해졌고, 예약 기능 확장이 쉬워졌습니다.
트레이드오프
레코드 수 증가로 인해 인덱스와 저장소 관리 부담이 늘어났습니다.
12. Phase 9 — 생성과 노출을 분리하다
왜 이 선택을 했는가
알림 요구사항이 늘어나면서, 다음과 같은 모순이 드러났다.
어떤 알림은 즉시 보여야 했고
어떤 알림은 특정 시점에만 노출돼야 했으며
생성 시점과 노출 시점이 같다고 가정한 설계는 더 이상 유지될 수 없었습니다
특히 예약 알림과 대량 알림이 등장하면서, 생성(created)과 노출(display)을 같은 개념으로 취급하는 것은 운영과 추적 측면에서 위험해졌습니다.
행동
알림에 노출 시점(display time) 개념을 명시적으로 도입
즉시 알림과 예약 알림을 동일한 데이터 모델로 표현
동작 원리
알림은 생성 즉시 저장되지만
실제 조회 시점에는
노출 시점 <= 현재 시간조건을 만족하는 경우에만 노출
결과
예약 알림, 특정 시각 노출 알림을 무리 없이 표현 가능
생성 이력과 노출 이력의 의미가 명확히 분리됨
트레이드오프
조회 쿼리 복잡도 증가
인덱스 설계 필요성 증가
13. Phase 10 — 즉시 알림과 대량 알림을 하나의 파이프라인으로 통합하다
상황
알림 요구사항이 계속 확장되면서, 시스템 내부에는 서로 다른 흐름이 공존하게 되었습니다.
도메인 이벤트 발생 시 즉시 생성되는 알림
콘솔이나 스케줄러를 통해 대량·예약으로 생성되는 알림
알림 센터와 푸시가 각기 다른 진입점과 처리 방식을 가짐
이 구조는 기능은 동작했지만, 다음과 같은 문제가 있었습니다.
처리 방식이 제각각이라 전체 흐름을 한 번에 이해하기 어려움
즉시 알림과 대량 알림의 구현이 달라 유지보수 비용 증가
실패·중단·재시작을 일관되게 다루기 어려움
왜 이 결정을 내렸는가
핵심 질문은 이것이었습니다. "알림이 언제 생성되든, 결국 같은 문제를 풀고 있지 않은가?"`
즉시 알림과 대량 알림은 트리거만 다를 뿐,
대상 채널을 결정하고
차단/수신 설정을 적용하고
알림 또는 푸시 데이터를 저장한다는 본질은 같았습니다.
행동
알림 생성 큐(queue)를 중심으로 흐름을 통일
즉시 알림은 바로 처리되는 이벤트 기반 생성
대량/예약 알림은 큐에 적재된 뒤 배치로 처리
알림 센터와 푸시는 동일한 이벤트 모델을 공유
동작 원리
알림 트리거 발생
도메인 이벤트(게시글, 댓글, 팔로우 등)
또는 콘솔/스케줄러를 통한 대량 등록
생성 방식 결정
즉시 처리 대상 → 이벤트 리스너에서 바로 생성
대량/예약 대상 → 생성 큐에 적재
큐 기반 처리
커서 기반으로 대상 채널을 순회
처리 진행 상황을 커서로 기록
중단/재시작 가능
동일한 생성 로직 재사용
알림 센터
푸시
결과
즉시 알림과 대량 알림이 하나의 개념적 파이프라인으로 정리되었습니다.
운영 관점에서 “알림 생성”이라는 행위를 단일 모델로 설명 가능해졌고,
실패 복구, 재처리, 모니터링 기준이 통일됬습니다.
트레이드오프
초기 설계 대비 구조 복잡도 증가
큐·배치·이벤트를 함께 이해해야 함
14. 마무리
알림은 늦게 설계할수록 비싸다.
처음에 피드백을 들은대로 빠른 구현에 대한 댓가였고, 시간을 들여 더 고민했었다면 좋았을 경험이었습니다.
Last updated