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)를 중심으로 흐름을 통일

  • 즉시 알림은 바로 처리되는 이벤트 기반 생성

  • 대량/예약 알림은 큐에 적재된 뒤 배치로 처리

  • 알림 센터와 푸시는 동일한 이벤트 모델을 공유

동작 원리

  1. 알림 트리거 발생

    • 도메인 이벤트(게시글, 댓글, 팔로우 등)

    • 또는 콘솔/스케줄러를 통한 대량 등록

  2. 생성 방식 결정

    • 즉시 처리 대상 → 이벤트 리스너에서 바로 생성

    • 대량/예약 대상 → 생성 큐에 적재

  3. 큐 기반 처리

    • 커서 기반으로 대상 채널을 순회

    • 처리 진행 상황을 커서로 기록

    • 중단/재시작 가능

  4. 동일한 생성 로직 재사용

    • 알림 센터

    • 푸시

결과

  • 즉시 알림과 대량 알림이 하나의 개념적 파이프라인으로 정리되었습니다.

  • 운영 관점에서 “알림 생성”이라는 행위를 단일 모델로 설명 가능해졌고,

  • 실패 복구, 재처리, 모니터링 기준이 통일됬습니다.

트레이드오프

  • 초기 설계 대비 구조 복잡도 증가

  • 큐·배치·이벤트를 함께 이해해야 함


14. 마무리

  • 알림은 늦게 설계할수록 비싸다.

  • 처음에 피드백을 들은대로 빠른 구현에 대한 댓가였고, 시간을 들여 더 고민했었다면 좋았을 경험이었습니다.


Last updated