SERVICE · 솔루션 개발 · 공급
자체 구축 · 소스와 데이터를 고객사가 소유하는 푸시 플랫폼

FCM 위에 올리는
운영 가능한 발송 플랫폼

FCM 연동은 하루면 됩니다. 그다음부터가 문제입니다 — 만료 토큰 정리, 수만 건 발송 중 서버 재시작, 수신 동의와 야간 제한, 실패 원인 추적, 테넌트 격리. 이 플랫폼은 그 운영 문제들을 스키마와 파이프라인 수준에서 해결한 상태로 납품됩니다.

Spring Boot 4.0.7 · Java 21 · PostgreSQL 18 · React 19 — 온프레미스 / 고객사 클라우드 배포

push-admin / 대시보드
푸시 발송 현황 대시보드 — 당일 발송·성공·실패 KPI, 7일 추이, 플랫폼 분포, 실패 사유
data-only FCM payload 방식 notification 미사용 · 앱이 렌더링 전담
4계층 대량 발송 파이프라인 Campaign → SendJob → Item → SendLog
152개 통합 테스트 (실 DB) Testcontainers · mock DB 미사용
V22까지 Flyway 마이그레이션 additive-only · 기존 스크립트 불변
Capabilities

기능 목록이 아니라,
운영 중 터지는 지점에 대한 답입니다

아래 항목은 모두 구현 · 테스트가 끝난 동작입니다. 각 카드의 태그는 실제 클래스 · 컬럼 · 정책 이름이며 요구사항 추적표(RTM)의 기능 ID와 1:1로 연결됩니다.

단건 · 멀티캐스트 · 토픽 발송

기기 ID, 사용자 ID, 원문 토큰, 토픽 중 무엇으로도 대상을 지정합니다. 멀티캐스트는 요청당 500건 상한이고 그 이상은 대량 발송 잡으로 넘깁니다. 단건 발송은 PENDING 로그를 먼저 커밋한 뒤 @Async 스레드에서 FCM을 호출하므로 API 응답이 FCM 지연에 묶이지 않습니다.

deviceId · userId · topicMAX_RECIPIENTS 500PENDING → SUCCESS/FAILED

발송 로그 · 원인 추적

건별로 상태 · FCM 메시지 ID · 오류 코드 · 전체 payload를 남깁니다. 상세 화면에서는 토큰을 역조회해 어느 기기 · 어느 사용자였는지까지 보여주고, 오류 코드별 조치 안내를 함께 붙입니다.

payload 원본 보존토큰 → 기기 역조회코드별 조치 안내

토큰 수명 자동 관리

FCM이 UNREGISTERED · INVALID_ARGUMENT 를 반환하면 해당 토큰을 즉시 비활성 처리합니다. 판정 코드는 PushSendCore 한 곳에서만 관리해 발송 경로별로 기준이 갈리지 않습니다.

INVALID_TOKEN_CODES 중앙 관리재등록 시 자동 복구

카테고리 구독 · 수신 동의

알림을 카테고리로 나누고 사용자별 구독 상태를 관리합니다. 마케팅 카테고리는 옵트인 이력을 남기고 야간(21~08)에는 발송을 차단합니다. 거래 · 보안 알림은 카테고리 단위로 야간 예외를 지정합니다.

정보통신망법 §50quietHoursExempt 플래그

대량 발송 — 재시작 내구성

워커가 UPDATE … RETURNING + FOR UPDATE SKIP LOCKED 로 PENDING 아이템을 배치 선점합니다. 프로세스가 죽어도 선점되지 않은 아이템은 그대로 남아 다음 틱(5초)에 이어서 처리되고, 같은 아이템이 두 번 나가지 않습니다.

SKIP LOCKED 선점fixedDelay 5s 워커멱등키

메시지 저장 · 예약 발송

DRAFT로 저장해 두고 예약 시각에 스케줄러(매분)가 자동 발송합니다. 대상 목록 · 이미지 · 딥링크 · 커스텀 데이터를 메시지에 함께 저장하므로 같은 메시지를 재사용 · 재발송할 수 있습니다.

DRAFT · SCHEDULED · SENTcron 0 * * * * *

RBAC · 테넌트 격리

SUPER_ADMIN · TENANT_ADMIN · OPERATOR · VIEWER 4단계를 메서드 단위 @PreAuthorize 로 강제합니다. 조회 · 발송 범위는 owner_app_id 기준으로 잘려 타 테넌트 데이터가 쿼리 결과에 섞이지 않습니다.

메서드 레벨 권한owner_app_id 스코프API Key 스코프 분리

통계 · 데이터 내보내기

일별 집계는 서버에서 계산합니다(페이지 상한을 넘는 순간 클라이언트 집계는 조용히 왜곡됩니다). CSV는 화면에 그려진 페이지가 아니라 조건에 맞는 전체를 내보내고, 상한에서 잘리면 응답 헤더로 알립니다.

서버 사이드 집계UTF-8 BOM · 한글 헤더X-Export-Truncated
Screens

운영 중인 관리자 화면

React 19 + TypeScript 단일 페이지 앱입니다. 라이트 · 다크 테마, 한국어 · 영어, 모바일 대응이 기본 포함이며 컴포넌트 · 레이아웃 규칙이 문서로 고정돼 있어 화면을 늘려도 형태가 갈리지 않습니다. 이미지를 클릭하면 크게 볼 수 있습니다.

01 / 대시보드

당일 발송 상태와 실패 원인을 한 화면에

KPI 4종, 7일 추이, 플랫폼 분포, 당일 실패 사유 Top을 한 화면에 둡니다. 일별 집계는 서버가 기간을 좁혀 세므로 발송량이 늘어도 그래프가 왜곡되지 않습니다.

  • KPI — 당일 발송 / 성공 / 실패 / 활성 기기
  • 추이 · 분포 — 7일 발송 라인 · 플랫폼 도넛
  • 실패 사유 Top — 코드별 건수 + 조치 안내
push-admin / 대시보드
푸시 알림 플랫폼 — 당일 발송 상태와 실패 원인을 한 화면에
02 / 발송

대상 · 내용 · 옵션 세 구획으로 끝

대상은 등록 기기 검색 팝업에서 고르거나 원문 토큰을 직접 넣습니다. 딥링크 · 알림 이미지 · 커스텀 데이터는 한 구획으로 모았고, 발송 버튼 옆에 무엇이 비었는지가 항상 표시돼 비활성 이유를 찾을 필요가 없습니다.

  • 대상 지정 — 기기 검색 팝업 / 원문 토큰 입력
  • 알림 이미지 — URL 입력 시 즉시 미리보기 검증
  • payload 검증 — 표준 · 예약 key 충돌을 입력 단계에서 차단
push-admin / 발송
푸시 알림 플랫폼 — 대상 · 내용 · 옵션 세 구획으로 끝
대시보드 · 라이트
푸시 관리자 — 라이트 테마 대시보드
대시보드 · 다크
푸시 관리자 — 다크 테마 대시보드

CSS 커스텀 프로퍼티 기반 테마 — 차트 색까지 테마별로 지정해 다크에서 축 · 범례가 묻히지 않습니다

push-admin / 발송 로그
푸시 알림 플랫폼 — 발송 로그 — 서버 사이드 필터 · 정렬 · CSV
발송 로그 — 서버 사이드 필터 · 정렬 · CSV
push-admin / 기기
푸시 알림 플랫폼 — 기기 관리 — 토큰은 마스킹만 노출
기기 관리 — 토큰은 마스킹만 노출
push-admin / 카테고리 통계
푸시 알림 플랫폼 — 카테고리별 구독자 · 발송량 · 성공률
카테고리별 구독자 · 발송량 · 성공률
push-admin / 공지
푸시 알림 플랫폼 — 공지 — 범위(전체 · 테넌트 · 앱)별 게시 · 첨부
공지 — 범위(전체 · 테넌트 · 앱)별 게시 · 첨부
Architecture

발송 경로가 두 갈래로
명확히 나뉩니다

단건 · 소량은 지연을 줄이는 경로, 대량은 내구성을 확보하는 경로로 분리했습니다. 같은 코드로 둘 다 처리하려 하면 API가 FCM 지연에 묶이거나, 반대로 수만 건이 메모리에서 사라집니다.

단건 · 소량PENDING 커밋 → @Async 발송
멀티캐스트 상한요청당 500건
대량 발송DB 잡 테이블 + SKIP LOCKED
워커 주기fixedDelay 5s 배치 선점
예약 발송스케줄러 매분 도래분 조회
재시작 복구고아 PENDING → STALE_PENDING
보존 배치매일 03:00 로그 정리
멱등성잡 단위 idempotencyKey
01

Campaign → SendJob → SendJobItem → PushSendLog

캠페인(기획 단위) 아래 잡(실행 단위), 그 아래 아이템(대상 1건), 최종 결과는 발송 로그에 남습니다. 4계층이라 캠페인 전체 성과와 이 사람에게 왜 안 갔나를 같은 데이터로 답할 수 있습니다.

02

대상 확장은 발송 시점의 활성 기기 기준

아이템 생성 시 활성 기기만 전개합니다. 요청 120건 중 활성 96대라면 그 차이를 발송 전에 미리보기로 보여주고, 미등록 · 비활성은 SKIPPED로 남겨 나중에 원인을 셀 수 있게 합니다.

03

API 응답과 FCM 왕복을 분리

PENDING 로그를 먼저 커밋하고 응답합니다. FCM이 느려도 클라이언트는 기다리지 않고, 프로세스가 죽어도 PENDING이 남아 복구 대상이 됩니다 — 발송 요청이 흔적 없이 사라지지 않습니다.

04

payload는 한 곳에서 조립

표준 키(title · body · type · deeplink · imageUrl · messageId · campaignId · sentAt)와 커스텀 데이터를 PushService 한 곳에서 합칩니다. 예약 키 · 표준 키 충돌은 서버와 관리자 화면 양쪽에서 막습니다.

Integration

앱이 붙는 지점은 한 개,
나머지는 관리자 API입니다

모바일 앱은 토큰 등록 엔드포인트 하나만 호출하면 됩니다. 기존 앱과의 호환을 위해 이 경로는 계약을 고정해 유지하며, 응답은 전 구간 단일 포맷입니다.

POST/api/v1/mobile/push-token — 토큰 등록 · 갱신
POST/api/v1/push/send — 단건 발송
POST/api/v1/push/multicast — 멀티캐스트(≤500)
POST/api/v1/admin/bulk-jobs — 대량 발송 잡 생성
GET/api/v1/admin/push-logs — 로그 검색(상태 · 기간 · 앱)
GET…/push-logs/export.csv — CSV(≤10,000행)
GET/api/v1/admin/stats/summary — 당일 KPI
GET/api/v1/admin/stats/categories — 카테고리 통계
01

data-only payload — 앱이 렌더링을 전담

FCM notification 블록을 쓰지 않고 data 만 채워 보냅니다. 앱이 포그라운드 · 백그라운드 모두에서 같은 코드로 알림을 그리므로 OS · 런처에 따라 표시가 갈리지 않습니다. 이미지도 data.imageUrl 로 전달합니다.

02

플랫폼별 전송 옵션 분리

Android는 priority=HIGH, iOS는 content-available 백그라운드 푸시로 보냅니다. 앱이 깨어나 직접 알림을 구성하는 방식이라 iOS에서도 data-only 정책을 유지합니다.

03

목록 검색은 전부 서버에서

검색어 · 기간 · 상태는 쿼리 파라미터로 처리합니다. 클라이언트에서 현재 페이지만 걸러내면 2페이지의 일치 항목이 안 보여 없다고 오인하게 됩니다. 페이지 크기는 서버가 100으로 클램프합니다.

04

CSV는 전체를 내보내고, 잘리면 알린다

조건에 맞는 전체를 대상으로 하며 상한은 10,000행입니다. 초과 시 X-Export-Total · X-Export-Truncated 헤더로 알려 잘린 파일로 집계하는 사고를 막습니다. 헤더는 화면 열 이름과 같은 한글, UTF-8 BOM으로 Excel 호환입니다.

Engineering

배포 기준이 문서로 고정돼 있습니다

기능 ID · 상태 · 테스트 매핑을 담은 요구사항 추적표, API 계약서, 화면 레이아웃 규칙서를 함께 납품합니다. 담당자가 바뀌어도 같은 기준으로 화면과 API를 늘릴 수 있습니다.

서버Spring Boot 4.0.7
언어Java 21 (LTS)
DBPostgreSQL 18 · JSONB
마이그레이션Flyway V22 · additive-only
발송Firebase Admin SDK (FCM)
관리자React 19 · TypeScript · Vite
인증JWT · RBAC 4단계 · API Key
테스트152개 · Testcontainers
01

실 DB 통합 테스트만 신뢰한다

152개 테스트가 Testcontainers로 PostgreSQL 18을 띄워 검증합니다. mock DB는 쓰지 않습니다 — JSONB 매핑, SKIP LOCKED, 마이그레이션 순서처럼 실제로 깨지는 지점은 가짜 DB에서 드러나지 않습니다.

02

스키마는 더하기만 한다

기존 Flyway 스크립트는 수정하지 않습니다. 컬럼 삭제 · 타입 변경은 승인 절차를 거친 별도 마이그레이션으로만 진행하므로, 운영 중 배포가 데이터를 되돌릴 수 없게 만들지 않습니다.

03

토큰은 원문을 내보내지 않는다

기기 조회 API는 마스킹된 토큰만 반환합니다. 관리자 화면 · CSV · 로그 어디에도 원문 FCM 토큰이 남지 않습니다. 서비스 계정 키는 환경변수 경로로만 참조하고 저장소에 넣지 않습니다.

04

화면 규칙도 코드처럼 관리한다

버튼 크기 6단계 · 색 5종, 팝업 크기 4단계, 표 텍스트 3계층, 필터 배치 순서를 규칙서로 고정했습니다. 목록 팝업은 단일 컴포넌트 하나(모드 3종)로 통일해 화면이 늘어도 조작 방식이 갈리지 않습니다.

Security & Compliance

감사에서 물어보는 항목을
먼저 맞춰 뒀습니다

수신 동의, 개인정보 노출, 권한 분리, 보존 기간 — 도입 심사에서 반드시 나오는 질문들입니다.

수신 동의 (정보통신망법 §50)

마케팅 알림은 옵트인 원칙입니다. 동의 · 철회 이력을 남기고 야간(21~08) 발송을 차단합니다. 거래 · 보안 알림은 카테고리 단위로 야간 예외를 지정해 법적 의무와 서비스 필요를 함께 만족시킵니다.

옵트인 이력야간 자동 차단카테고리별 예외

개인정보 · 민감정보

FCM 토큰은 마스킹 값만 노출합니다. 발송 로그의 오류 원문은 화면에 문장으로 두지 않고 코드별 조치 안내로 치환합니다. 공지 본문 HTML은 저장 · 표시 시 화이트리스트 기반으로 정화(sanitize)해 저장형 XSS를 차단합니다.

토큰 마스킹HTML sanitize서비스 키 환경변수 참조

권한 · 감사

4단계 역할을 메서드 단위로 강제하고 조회 범위를 owner_app_id 로 제한합니다. 발송 요청자 · 시각 · payload가 로그에 남아 누가 무엇을 언제 보냈는지를 사후에 확인할 수 있습니다. 보존 배치는 매일 03:00에 정책 기간을 넘긴 로그를 정리합니다.

@PreAuthorize테넌트 격리요청자 기록 · 보존 배치
Delivery

기존 앱이 있으면
토큰 등록 한 줄부터 시작합니다

플랫폼은 완성 상태입니다. 도입 작업은 고객사 환경에 맞추는 일에 집중됩니다.

01

요구사항 확인 · 환경 설계

발송 유형(거래 · 마케팅 · 공지), 예상 물량, 앱 구성(단일 · 멀티 브랜드), 배포 환경(온프레미스 · 클라우드)을 확정합니다. 테넌트 · 앱 구조와 카테고리 체계를 여기서 정합니다.

02

FCM 프로젝트 · 앱 연동

고객사 Firebase 프로젝트에 서버를 연결하고, 앱은 토큰 등록 엔드포인트를 호출하도록 수정합니다. 알림 렌더링은 data payload 계약서에 맞춰 앱 측에서 구현합니다(계약서 제공).

03

관리자 화면 · 권한 구성

담당자 계정과 4단계 역할, 앱별 접근 범위를 설정합니다. 브랜딩 · 용어는 화면 규칙서 범위 안에서 조정합니다.

04

검증 발송 · 인수

실제 기기로 단건 → 소량 → 대량 순으로 검증합니다. 발송 로그 · 통계로 결과를 확인하고, 운영 런북(실행 · 장애 확인 절차)과 문서 일체를 인수합니다.

앱 변경토큰 등록 1 API + 렌더러
서버 배포온프레미스 / 고객사 클라우드
외부 의존FCM 외 없음
월 사용료SaaS 구독 없음
데이터 소유고객사 DB에 직접
납품 문서RTM · API 계약 · 레이아웃 규칙서
운영 문서설치 가이드 · 장애 런북
확장토픽 · 인앱 인박스 · A/B

스키마와 코드를 열어 그대로 보여드립니다

슬라이드가 아니라 실제 테이블 구조, 발송 파이프라인, 테스트 결과로 검토하실 수 있습니다. 예상 물량과 앱 구성만 알려주시면 필요한 구성과 일정을 정리해 드립니다.