푸시알림 운영 정책¶
문서 역할¶
- 역할:
규범 - 문서 종류:
policy - 충돌 시 우선 문서: 이 문서
- 기준 성격:
as-is
목적¶
- 푸시 타입/발송 조건/장애 대응 기준을 표준화해 과발송, 누락 발송, 중복 발송 리스크를 줄인다.
적용 범위¶
coupler-api,coupler-mobile-app,coupler-admin-web- FCM 타입 관리, 발송 조건 로직, 알림 저장(
t_alarm), 운영 집계/모니터링
단일 SoT¶
- 알림 도메인 구조: 푸시알림 시스템
- 상위 기술 원칙: 엔지니어링 가드레일
- 리뷰/증빙 기준: 코드 리뷰 정책
- 로그 규칙: 로그 정책
필수 규칙¶
1) 타입 거버넌스¶
- FCM 타입 추가/변경/폐기는 정책 문서와 아키텍처 문서를 동시에 갱신한다.
- 신규 타입을 추가할 때는 서버 상수/문구, 모바일 상수/라우팅,
t_alarm저장/운영 집계 범위를 함께 갱신한다. - 타입 ID 재사용은 금지한다.
- 미정의 타입 수신 시 조용히 무시하지 않고 오류/경고 로그를 남긴다.
- 서버는 타입별
custom_data필드를 계약된 JSON 원시 타입으로 발행한다. 숫자 필드는 숫자로 보내며 문자열 숫자나 대체 필드를 함께 발행하지 않는다. - 모바일은 FCM JSON 진입점에서 타입별
custom_data를 한 번만 검증해 typed domain event로 변환한다. 이후 화면과 상태 소비자는 같은 event payload 타입을 직접 사용하며toNumber, 문자열 숫자 변환, 기본값 fallback 또는 반복 runtime guard로 wire shape를 보정하지 않는다. - 외부 payload가 계약을 위반하면 경고 로그를 남기고 해당 이벤트를 적용하지 않는다. 임의 기본값으로 계속 진행하지 않는다.
라운지 custom_data 계약¶
| FCM 타입 | 필수 payload | 선택 payload |
|---|---|---|
LOUNGE_NEW_COMMENT(38) |
target: positive integer |
count: non-negative integer, title: string |
LOUNGE_PINNED(40) |
target: positive integer, event_id: lounge-pinned:<outbox ID> |
title: string |
LOUNGE_LIKE(68) |
target: positive integer, count: non-negative integer |
없음 |
인연찾기 custom_data 계약¶
| FCM 타입 | 필수 payload | 선택 payload |
|---|---|---|
MATCH_FINDING_*(86~89) |
target: positive integer |
없음 |
2) 발송 조건 통제¶
- 사용자 알림 설정(
alarm_chat,alarm_match,alarm_event)과 그룹미팅 방별 새 메시지 알림 선택을 서버 발송 경로에서 일관되게 적용한다.
| 대상 | 적용 조건 | 결과 |
|---|---|---|
| 1:1 매칭 타입 12~30, 70, 71, 86~89 | alarm_match = NO |
FCM 전송과 t_alarm 저장을 모두 건너뜀 |
MATCH_NEW_CHAT(22) |
alarm_chat = NO 또는 alarm_match = NO |
FCM 전송과 t_alarm 저장을 모두 건너뜀 |
CONCIERGE_CHAT(67) |
alarm_chat = NO |
FCM 전송과 t_alarm 저장을 모두 건너뜀 |
| 그룹미팅 77~81, 83~85 | alarm_event = NO |
FCM 전송과 t_alarm 저장을 모두 건너뜀 |
GROUP_MEETING_CHAT_MESSAGE(82) |
alarm_chat = NO 또는 현재 방 message_notification_enabled = false |
FCM 전송과 t_alarm 저장을 모두 건너뜀 |
MATCH_VOICE_CALL(53) |
항상 | FCM 전송과 t_alarm 저장을 모두 건너뛰고 알림 목록에서 제외 |
| FCM 토큰 없음 | 다른 발송·저장 조건은 충족 | FCM만 건너뛰고 t_alarm은 저장 |
sendPush = false 또는 OFFLINE_MODE |
다른 발송·저장 조건은 충족 | FCM만 건너뛰고 t_alarm은 저장 |
CONCIERGE_CHAT(67)은 Admin이 회원에게 보낸 메시지의 Mobile 사용자 알림과 재진입 보조 수단이다. FCM 수신 여부를 메시지 저장 성공이나 읽음 상태의 기준으로 사용하지 않는다.- Mobile foreground에서는 WebSocket 연결 여부와 무관하게
CONCIERGE_CHAT(67)시스템 알림을 표시한다. WebSocket이 연결돼 있으면 같은 FCM으로 상태 갱신 이벤트를 중복 적용하지 않고, 연결이 끊긴 경우에만 FCM 이벤트를 화면 상태 갱신 보조 경로로 사용한다. GROUP_MEETING_CHAT_MESSAGE(82)도 foreground 시스템 알림은 WebSocket 연결 여부와 무관하게 표시한다. WebSocket이 연결돼 있으면 같은 FCM으로 그룹미팅 방·통합 채팅 목록 상태를 중복 갱신하지 않고, 연결이 끊긴 경우에만 FCM 이벤트를 HTTP snapshot 갱신 보조 경로로 사용한다. 메시지 원본과 연결·복구 기준은 채팅 시스템의 N:N 그룹미팅 채팅 절을 따른다.- 방별 새 메시지 알림 선택은 82의 FCM과
t_alarm만 통제한다. 메시지 저장·WebSocket·unread/read와 그룹미팅 77~81·83~85에는 적용하지 않으며, 전역alarm_chat과 방별 선택이 모두 켜진 수신자만 82를 받는다. - 토큰 없음/발송 비활성 조건은 명시적으로 기록하고 스킵 사유를 남긴다.
- 동일 이벤트의 다중 발송을 방지하기 위해 idempotency key 또는 중복 체크 키를 사용한다.
인연찾기 수신자와 이동 기준¶
| 타입 | 발송 조건 | 수신자 | Mobile 이동 |
|---|---|---|---|
MATCH_FINDING_NEW_MALE_ARRIVE(86) |
여성 → 남성 카드가 FEMALE_WANT_SEE로 생성됨 |
게시글 작성자 남성 | For You |
MATCH_FINDING_MALE_PROFILE_ACCEPTED(87) |
남성이 수락해 MALE_WANT_SEE로 전이됨 |
카드를 보낸 여성 | On:Going |
MATCH_FINDING_MALE_TO_FEMALE_CARD_RECEIVED(88) |
남성 → 여성 카드가 PENDING으로 생성됨 |
게시글 작성자 여성 | For You |
MATCH_FINDING_MALE_TO_FEMALE_ACCEPTED(89) |
여성이 수락해 FEMALE_WANT_SEE로 전이됨 |
카드를 보낸 남성 | For You |
- 네 타입의
target은 생성된 1:1 매칭 ID다. - 86과 88은 카드 생성 트랜잭션 커밋 뒤 발송하며 CMS 일괄 카드의 1분 억제를 적용하지 않는다.
- 네 타입은 모두
alarm_match판정을 사용하고 표시 문구가 있으므로, 토큰 부재나 전송 비활성 상태에서도 설정 판정을 통과하면t_alarm에 저장한다.
그룹미팅 채팅 개방 수신자 기준¶
- 행사 전날 KST 13시 개방 경계에서는 호스트와 그 시점의 현재
APPROVED참가자에게GROUP_MEETING_CHAT_OPENED(85)를 경계당 한 번 발송한다. - 현재 행사 일시와 상태로 계산한 채팅 개방 이후 Admin이 참가자를 새로 승인하면, 해당 참가자에게
GROUP_MEETING_APPLICATION_APPROVED(78)과GROUP_MEETING_CHAT_OPENED(85)를 같은 승인 결과로 발송한다. - 85의 중복 방지 기준은 채팅 구성원별로 처리한 개방 경계다. 개방 이후 승인은 새 구성원의 경계를 승인 transaction에서 선점하고, cron은 현재 경계가 아직 처리되지 않은 구성원만 선점한다. 따라서 다음 cron이 같은 새 승인자를 다시 포함하지 않는다. 채팅 개방 전 승인은 78만 발송한다.
- 행사 단위 개방 marker는 cron batch의 요약이며 수신자 중복 방지 기준으로 사용하지 않는다.
- 행사 일시 변경으로 전날 KST 13시 개방 경계가 달라지면 새 경계에서 호스트와 당시 현재
APPROVED참가자를 다시 계산한다. 모집 재개·재마감만으로 같은 경계의 기존 구성원에게 재발송하지 않는다.
라운지 댓글/대댓글 수신자 기준¶
LOUNGE_NEW_COMMENT(38)은 게시글에 최상위 댓글(parent = 0)이 달린 경우에만 게시글 작성자에게 발송한다.LOUNGE_NEW_CHILD_COMMENT(39)은 대댓글(parent > 0)이 달린 경우 직접 부모 댓글 작성자에게만 발송한다.- 대댓글 작성 시 최초 게시글 작성자에게
LOUNGE_NEW_COMMENT(38)을 함께 발송하지 않는다. - 내가 쓴 댓글에 내가 대댓글을 다는 경우에는
LOUNGE_NEW_CHILD_COMMENT(39)을 발송하지 않는다. - 댓글/대댓글 알림의
target은 라운지 게시글 ID로 고정한다.
라운지 고정글 알림 기준¶
LOUNGE_PINNED(40)은 목록의 전용/admin/lounge/pinned명령으로 게시글이 실제N → Y로 전이할 때 작성자에게 생성한다. 이미Y인 일반 중복 요청은 새 알림 세대를 만들지 않는다. 상세/admin/lounge/save의pinned저장은 이 알림의 발생 조건이 아니다.- 전이와 알림 outbox intent는 같은 transaction에 저장한다.
t_alarm이력은 intent row lock 안에서 한 번만 연결한 뒤 OS FCM을 전송하므로 FCM 실패 재시도가 알림함 이력을 중복 생성하지 않는다. - API 요청은 intent 저장 후 worker를 깨우고 응답하며 외부 FCM 성공 여부에 의존하지 않는다. 각 API instance의 bounded worker는 시작 직후와 30초마다 retry index를 읽는다. 실패 intent는 마지막 시도 1분 뒤, 처리 중 instance가 종료된 intent는 5분 뒤 재선점하며 최대 시도 횟수는 최초 시도를 포함해 3회다.
- type 40 worker는 provider rejection을 throw하는 strict FCM 경계를 사용한다. provider가 성공을 응답했지만
outbox 완료 기록 전에 process/DB가 실패한 경우에는 전달 여부를 원자적으로 판별할 수 없으므로
at-least-once를 선택해 재전송한다.t_alarm은 한 건만 유지하고 모든 재시도 payload에 같은event_id를 넣는다. 이미 설치된 Mobile은 이 additive 값을 무시할 수 있으며 이 드문 ambiguous 구간의 OS 알림 중복 가능성은 유실 방지를 위해 허용한다. Y → N은 알림을 만들지 않는다. 이후 다시N → Y가 되면 별도 intent를 생성한다.
3) 저장/전송 일관성¶
- 전송 시도 결과와 무관하게 알림 저장 규칙(
t_alarm)을 일관되게 적용한다. - 저장 필드는 최소
member,type,target,create_date를 보장한다. - 운영 지표 집계 조건(
type IN (...)) 변경 시 정책/쿼리를 함께 갱신한다.
4) 장애 대응¶
- FCM 전송 실패율 급증 시 fallback이 아닌 원인 분석과 재시도 정책으로 대응한다.
- 재시도는 제한 횟수/간격을 명시하며 무한 재시도를 금지한다.
- 장애 시 영향 타입/영향 사용자/복구 시점을 릴리스 노트 또는 장애 보고에 기록한다.
5) 배포/검증¶
- 타입 변경은 엔지니어링 가드레일의 API/DB 하위 호환과 contract cutover
판정을 먼저 적용한다. Additive 타입은 직전 Mobile이 안전하게 무시할 수 있어야 하며, 기존 타입의 의미
변경·제거는
API cutover: Yes로 다룬다. - 핵심 타입(가입/매칭/결제/운영 알림)은 배포 직후 샘플 검증한다.
- 제거 예정 과도기 분기는 cutover 범위, 제거 조건·담당자·목표 시점을 기록한다.
운영 절차¶
- 변경 제안: 타입/조건/목적/영향 범위를 문서화한다.
- 구현: 서버 발송 조건과 저장 로직을 함께 반영한다.
- 검증: 허용/스킵/실패 재시도 시나리오를 실행한다.
- 배포: 모니터링 지표(성공률/실패율/중복률)를 확인한다.
- 정리: 임시 분기 제거 여부와 문서 동기화를 완료한다.
증빙/추적¶
- PR 본문에 아래를 필수로 남긴다.
- 타입 변경 내역(추가/수정/폐기)
- 발송 조건 테스트 결과(허용/스킵/거부)
- 실패 재시도 로그 또는 모니터링 링크
- 운영 변경 시 집계 쿼리 변경 근거를 함께 남긴다.
체크리스트¶
- [ ] 타입 ID 충돌/재사용 없이 문서와 코드가 동기화됐는가?
- [ ]
alarm_chat/alarm_match/alarm_event, 토큰 부재, 환경 조건 스킵이 일관되게 동작하는가? - [ ] 1:1 매칭 12~30·70·71·86~89와 그룹미팅 77~85가 폐쇄형 설정 매핑과 일치하는가?
- [ ]
CONCIERGE_CHAT(67)은alarm_chat비활성 시 FCM과t_alarm을 모두 건너뛰고, WebSocket·FCM 상태 갱신을 중복 적용하지 않는가? - [ ]
GROUP_MEETING_CHAT_MESSAGE(82)는alarm_chat또는 현재 방 새 메시지 알림이 비활성일 때 FCM과t_alarm만 건너뛰고, 메시지·WebSocket·unread와 77~81·83~85를 유지하는가? - [ ]
GROUP_MEETING_CHAT_OPENED(85)는 개방 경계의 현재 구성원과 개방 이후 새 승인자를 구분하고, 새 승인자 외 기존 구성원에게 같은 경계 알림을 중복 발송하지 않는가? - [ ] 토큰 부재·
sendPush = false·OFFLINE_MODE는 FCM만 생략하고,MATCH_VOICE_CALL(53)은 FCM과t_alarm을 모두 생략하는가? - [ ] 중복 발송 방지 키 또는 동등한 통제가 있는가?
- [ ] 라운지 댓글/대댓글 알림은 최상위 댓글과 직접 부모 댓글 기준이 분리되어 있는가?
- [ ]
LOUNGE_PINNED(40)은/admin/lounge/pinned의 실제N → Y에서만 생성되고 상세 저장·해제·중복 설정에서는 생성되지 않는가? - [ ] 실패 재시도 제한 횟수와 종료 조건이 명시됐는가?
- [ ] PR/배포 기록에 타입 변경 및 운영 지표 근거가 포함됐는가?