알림이 무시되기 시작하는 지점 — 발송 시스템 설계

운영을 시작하고 몇 달이 지나면 비슷한 이야기가 나옵니다.

"알림이 너무 많아서 안 봐요."

그리고 이 말이 나온 시점에는 이미 상당수 사용자가 알림을 꺼둔 상태입니다. 한 번 꺼진 알림은 거의 다시 켜지지 않습니다. 정작 중요한 공지를 보내야 할 때 닿지 않게 됩니다.

알림 시스템에서 어려운 부분은 보내는 기술이 아닙니다. 무엇을 보내지 않을지 정하고, 그 기준을 시스템으로 강제하는 것입니다.

이 글은 알림 기능을 설계할 때 정해야 하는 것들을 정리한 내용입니다. 사용자 대상 서비스 알림과 운영자 대상 장애 알림을 함께 다룹니다.


1. 무엇을 알림으로 보낼지 먼저 거릅니다

기능 목록에 "알림 발송"이라고 한 줄 적혀 있으면, 실제로는 수십 가지 상황이 숨어 있습니다.

보낼 이유를 분류합니다

유형 예시 성격
확인 주문 접수, 결제 완료 사용자 행동에 대한 응답
진행 배송 출발, 처리 상태 변경 진행 상황 공유
요청 승인 대기, 입력 필요 사용자 행동을 요구
예고 예약 하루 전, 만료 임박 시점 기반
공지 점검 안내, 정책 변경 전체 또는 대상 집단
홍보 이벤트, 추천 사용자 이익과 무관할 수 있음
운영 장애, 임계치 초과 내부 담당자 대상

이 분류가 필요한 이유는 유형마다 다르게 다뤄야 하기 때문입니다. 확인 알림은 즉시 보내야 하고, 홍보 알림은 시간과 빈도를 제한해야 하며, 공지는 수신 거부와 무관하게 전달해야 할 수도 있습니다.

홍보 유형을 따로 떼어 두는 데에는 법적인 이유도 있습니다. 홍보 알림은 정보통신망법이 말하는 "영리목적의 광고성 정보"에 해당하고, 사전 동의·표기·야간 전송 제한이 붙습니다. 주의할 점은 안내 메시지에 광고 문구를 한 줄 덧붙이면 메시지 전체가 광고성 정보로 취급된다는 것입니다. 방송미디어통신위원회·KISA 안내서는 결제 내역을 보내면서 하단에 광고를 넣은 이메일을 그 예로 듭니다.(불법스팸 방지를 위한 정보통신망법 안내서 제7차 개정본) 그래서 유형은 템플릿 단위로 고정하고, 확인·진행 유형의 템플릿에는 홍보 문구를 넣지 못하게 막아 두는 편이 안전합니다. 구체적인 요건은 4장에서 다룹니다.

보내지 않아도 되는 것을 걸러냅니다

판단 기준입니다.

  • 사용자가 지금 알아야 하는가, 나중에 확인해도 되는가
  • 사용자가 할 일이 있는가, 단순 정보인가
  • 이미 다른 경로로 알고 있지 않은가
  • 여러 건을 묶어서 보낼 수 있는가

사용자가 직접 한 행동의 결과는 알림 없이 화면에서 보여주면 충분한 경우가 많습니다. 버튼을 눌러 신청했는데 "신청되었습니다" 푸시가 오는 것은 불필요합니다.

반대로 사용자가 보고 있지 않을 때 일어난 변화, 특히 사용자가 무언가 해야 하는 상황은 알림이 필요합니다.

묶어서 보내는 방식을 검토합니다

같은 유형의 알림이 짧은 시간에 여러 번 발생하면 묶는 편이 낫습니다.

  • 5분 안에 발생한 같은 유형을 하나로 합침
  • "새 댓글 3개"처럼 요약해서 표시
  • 하루 단위로 모아 보내는 요약 알림

운영자 대상 장애 알림에서 특히 중요합니다. 서버 하나가 문제를 일으켜 알림이 수백 건 발송되면, 담당자는 알림을 꺼버립니다. 같은 원인의 알림은 묶고, 상황이 계속되면 일정 간격으로만 다시 알리는 구조가 필요합니다.


2. 채널마다 성격이 다릅니다

같은 내용이라도 어느 경로로 보내느냐에 따라 도달률과 비용이 다릅니다.

채널 특성

채널 도달 비용 적합한 내용
앱 내 알림함 접속해야 봄 없음 모든 알림의 기본 보관
푸시 앱 설치·권한 필요, 유실 가능 낮음 즉시성이 있는 안내
문자 도달률 높음 건당 과금 인증, 중요 공지
이메일 확인이 늦을 수 있음 낮음 상세 내용, 증빙
카카오 등 메신저 도달률 높음 건당 과금, 템플릿 심사 예약·결제 안내
전화·ARS 즉시성 높음 높음 긴급 상황

앱 내 알림함을 기본으로 두는 구성을 권합니다. 모든 알림이 여기 쌓이고, 그중 일부를 푸시나 문자로 밀어주는 구조입니다. 이러면 푸시를 놓친 사용자도 나중에 확인할 수 있습니다.

푸시는 도달을 보장하지 않습니다

푸시를 주 채널로 삼을 때 설계에 반영해야 하는 제약입니다. 아래 내용은 2026년 10월 기준 공식 문서에서 확인한 것입니다.

항목 동작 설계에 반영할 것
알림 권한 Android 13(API 33) 이상에서는 새로 설치한 앱의 알림이 기본으로 꺼져 있고, POST_NOTIFICATIONS 런타임 권한을 받아야 합니다 권한 요청 시점을 정하고, 권한이 없는 사용자에게 닿지 않는 알림 목록을 파악합니다
수락과 도달 FCM이 반환하는 메시지 ID는 FCM이 요청을 받았다는 뜻이고, 기기에 도달했다는 뜻이 아닙니다 발송 성공을 "확인됨"으로 취급하지 않습니다
오래 꺼진 기기 Android 기기가 한 달 넘게 FCM에 접속하지 않으면 FCM은 메시지를 받아들인 뒤 바로 버립니다. 270일 동안 비활성인 토큰은 만료 처리되어 발송이 거부됩니다 토큰별 마지막 갱신 시각을 저장하고 오래된 토큰을 정리합니다
보관 기간 FCM의 Android·웹 메시지는 기본 4주(최대 2,419,200초) 보관됩니다. APNs는 apns-expiration이 0이면 한 번만 시도하고 저장하지 않습니다 시한이 있는 알림은 만료 시각을 짧게 지정합니다
우선순위 Android의 기본은 normal이며 Doze 상태에서는 전달이 미뤄질 수 있습니다. high는 기기를 깨우지만, 사용자에게 보이는 알림으로 이어지지 않는 패턴이 감지되면 normal로 낮춰질 수 있습니다 high는 즉시 확인이 필요한 유형에만 씁니다

(Android Developers: Notification runtime permission, Firebase: Manage FCM registration tokens, Firebase: Set message lifespan, Firebase: Set message priority, Apple Developer: Sending notification requests to APNs)

Android 13 권한에는 함정이 하나 더 있습니다. 앱의 targetSdk가 32 이하이면 알림 채널을 만든 뒤 처음 화면이 뜰 때 시스템이 권한 창을 알아서 띄웁니다. 대개 앱을 처음 켠 순간이라 사용자는 이유를 모른 채 거부하기 쉽고, 이 경우 한 번 거부하면 앱을 다시 설치하거나 targetSdk를 올리기 전까지 다시 묻지 않습니다. targetSdk 33 이상으로 맞추고, 알림이 왜 필요한지 보여준 뒤 직접 요청하는 흐름을 권합니다.

예약 리마인드처럼 시한이 있고, 새 알림이 이전 알림을 대체해도 되는 경우의 FCM HTTP v1 요청 본문 예시입니다. 토큰과 값은 설명용입니다.

{
  "message": {
    "token": "기기_등록_토큰",
    "notification": {
      "title": "내일 14시 예약이 있습니다",
      "body": "변경이나 취소는 오늘까지 가능합니다"
    },
    "android": {
      "priority": "high",
      "ttl": "3600s",
      "collapse_key": "reservation-1201"
    },
    "apns": {
      "headers": {
        "apns-priority": "10",
        "apns-expiration": "1791878400",
        "apns-collapse-id": "reservation-1201"
      }
    }
  }
}
  • ttl은 초 단위 기간이고 apns-expiration은 UNIX 시각(초)입니다. 단위가 달라 한쪽만 맞추는 실수가 잦습니다. 위 예시의 시각 값은 발송 시점에 계산해 넣습니다
  • collapse_key가 같으면 FCM은 아직 전달되지 않고 대기 중인 이전 메시지를 버리고 새 메시지로 바꿉니다. apns-collapse-id가 같은 알림은 기기에서 하나로 합쳐지며, 값은 64바이트를 넘을 수 없습니다
  • 묶는 키는 사용자에게 보이는 알림을 정리하는 수단입니다. 서버가 같은 알림을 두 번 보내는 문제는 3장의 중복 방지 키로 따로 막아야 합니다

알림톡은 정보성 메시지만 보낼 수 있습니다

카카오 알림톡은 채널을 추가하지 않은 사용자에게도 보낼 수 있지만, 정보통신망법 안내서가 정한 "영리목적 광고성 정보의 예외"에 해당하는 정보성 메시지로 한정됩니다. 내용의 전부 또는 일부에 광고성 내용이 들어가면 발송할 수 없고, 카카오 심사에서 승인된 템플릿으로만 보낼 수 있습니다.(카카오비즈니스 가이드: 알림톡, 알림톡 심사 가이드)

일정에 영향을 주는 부분은 심사입니다.

  • 심사는 영업일 기준 2일 이내에 순차 처리된다고 안내되어 있습니다. 반려되면 수정해서 다시 요청해야 하므로, 오픈 일정에서 템플릿 확정 시점을 앞당겨 잡습니다
  • 변수에는 예시 텍스트를 넣어야 하고, 변수 40개 초과나 변수로만 이루어진 템플릿은 반려 사유입니다. "#{내용}" 하나로 모든 문구를 돌려 쓰는 설계는 통과하지 못합니다
  • 승인된 문구로만 나가므로 알림톡 템플릿은 코드 배포와 별개의 일정으로 관리하고, 5장의 템플릿 관리에 심사 상태를 함께 둡니다

여러 채널을 쓸 때의 규칙

  • 같은 내용을 여러 채널로 동시에 보내면 사용자는 중복으로 받습니다
  • 채널별 우선순위를 두고, 앞선 채널에서 확인되면 뒤를 취소하는 방식이 있습니다
  • 실패했을 때 다른 채널로 대체할지 정합니다

중요 알림의 단계적 발송이 실무에서 유용합니다. 먼저 푸시를 보내고, 일정 시간 안에 확인하지 않으면 문자를 보내는 방식입니다. 비용이 드는 채널을 필요한 경우에만 쓰게 됩니다.

대체 발송을 구현할 때 정해야 하는 것은 "언제 다음 채널로 넘어가는가"입니다.

상황 판단 근거 처리
보낼 주소가 없음 유효한 푸시 토큰이 없거나 알림 권한이 꺼져 있음 기다리지 않고 바로 다음 채널로
발송이 영구 실패 채널이 토큰 무효 등으로 거부 바로 다음 채널로
발송은 됐으나 반응 없음 정해 둔 시간 안에 열람 기록이 없음 대기 후 다음 채널로
이미 확인함 앱에서 해당 알림이나 대상 화면을 열람 다음 채널 발송을 취소

세 번째와 네 번째를 가르려면 앱이 열람 사실을 서버에 보고해야 합니다. 푸시 채널의 발송 성공 응답만으로는 사용자가 봤는지 알 수 없기 때문입니다. 열람 보고가 없는 구조에서 "푸시 실패 시 문자"만 구현하면, 푸시가 조용히 유실된 사용자에게는 아무것도 가지 않습니다.

대체 발송 대상은 정보성 알림으로 한정하는 것이 원칙입니다. 홍보 알림은 매체별로 받은 동의 범위 안에서만 보낼 수 있으므로, 푸시 광고에만 동의한 사용자에게 문자로 넘겨 보내면 안 됩니다.

비용이 드는 채널은 상한을 둡니다

문자나 메신저는 건당 비용이 발생합니다. 대상이 잘못 잡히면 금전적 피해로 이어집니다.

  • 1회 발송 대상 수의 상한
  • 기간별 총 발송량 상한
  • 상한 초과 시 차단하고 담당자에게 알림
  • 대량 발송 전 대상 수 확인과 승인 절차

대량 발송 전 승인 절차는 번거로워 보이지만 필요합니다. 조건을 잘못 걸어 전체 회원에게 발송되는 사고는 실제로 발생합니다.


3. 언제 보낼지가 절반입니다

내용이 같아도 시점에 따라 받아들여지는 방식이 달라집니다.

발송 시간을 제한합니다

  • 심야·이른 아침 발송을 막습니다
  • 제한 시간에 발생한 알림은 다음 날 아침에 보낼지, 버릴지 정합니다
  • 긴급한 유형은 예외로 둡니다. 어떤 것이 예외인지 명시합니다

예외 목록을 명확히 하는 것이 중요합니다. 모든 알림이 긴급으로 분류되면 제한이 무의미해집니다.

홍보 알림의 야간 제한은 운영 방침이 아니라 법정 요건입니다. 정보통신망법 제50조 제3항은 오후 9시부터 다음 날 오전 8시까지 광고성 정보를 보내려면 일반 수신 동의와 별도의 사전 동의를 받도록 하고, 시행령 제61조 제2항은 전자우편만 예외로 둡니다.(정보통신망법 제50조, 같은 법 시행령 제61조) 앱 푸시도 이 조항의 적용 대상입니다. 안내서는 이 시간을 한국 표준시 기준으로 수신자에게 도달하는 시각으로 해석하고, 시스템 지연으로 야간에 도달한 경우에는 전송자가 소명해야 한다고 설명합니다.

따라서 큐가 밀려 늦게 나가는 경우까지 고려해야 합니다. 발송 요청을 만들 때 한 번, 처리기가 실제로 보내기 직전에 한 번 더 시각을 확인합니다. 야간 동의가 없는 홍보 알림의 다음 발송 가능 시각을 구하는 함수 예시입니다(TypeScript, 한국 표준시 고정 기준).

const KST_OFFSET_MS = 9 * 60 * 60 * 1000;

function nextAdSendTime(now: Date, hasNightConsent: boolean): Date {
  if (hasNightConsent) return now;

  const kst = new Date(now.getTime() + KST_OFFSET_MS);
  const hour = kst.getUTCHours();
  if (hour >= 8 && hour < 21) return now;

  const next = new Date(kst);
  if (hour >= 21) next.setUTCDate(next.getUTCDate() + 1);
  next.setUTCHours(8, 0, 0, 0);
  return new Date(next.getTime() - KST_OFFSET_MS);
}

법정 경계는 21시와 8시이지만 기준이 도달 시각이므로, 운영에서는 경계 앞뒤로 여유를 두는 편이 안전합니다. 예를 들어 20시 50분에 마감하고 8시 10분부터 시작하는 식이며, 이 값은 발송량과 채널 지연을 보고 정합니다.

즉시 발송과 예약 발송을 구분합니다

  • 즉시 — 사건이 발생한 시점에 보냅니다
  • 예약 — 특정 시각에 보냅니다. 예약 하루 전 안내 같은 것입니다
  • 반복 — 일정 주기로 보냅니다. 미처리 건 상기 같은 것입니다

예약·반복 발송에서 주의할 점이 있습니다. 발송 시점에 상황이 바뀌었을 수 있습니다. 어제 예약한 "내일 예약 안내"를 보내는 순간, 그 예약이 이미 취소되었을 수 있습니다. 발송 직전에 조건을 다시 확인하는 절차가 필요합니다.

같은 알림이 두 번 가지 않게 합니다

중복 발송은 사용자 신뢰를 빠르게 깎습니다. 발생 경로는 대체로 정해져 있습니다.

  • 발송 처리가 실패한 줄 알고 재시도했는데 실제로는 성공했던 경우
  • 배치가 두 번 실행된 경우
  • 같은 사건에 대해 여러 지점에서 발송을 호출하는 경우
  • 서버가 여러 대일 때 각각이 같은 대상을 처리한 경우

막는 방법은 발송 단위에 고유 키를 두는 것입니다. "이 사용자에게, 이 사건에 대해, 이 유형의 알림"을 하나의 키로 만들고, 이미 발송된 키는 다시 처리하지 않습니다.

키를 무엇으로 구성할지는 업무에 따라 다릅니다. 같은 예약에 대한 확정 알림은 한 번만 가야 하지만, 상태가 여러 번 바뀌면 그때마다 가야 할 수도 있습니다. "같다"의 기준을 유형별로 정해두어야 합니다.

유형 키 구성 예시 결과
예약 확정 reservation:1201:confirmed 예약당 한 번
처리 상태 변경 order:881:status:SHIPPED 상태값마다 한 번
예약 하루 전 안내 reservation:1201:remind:2026-10-14 예약·날짜당 한 번
미처리 건 상기 approval:55:nudge:2026-W42 주 단위로 한 번

"이미 발송된 키인지 조회하고, 없으면 발송한다"는 순서로 구현하면 막지 못합니다. 두 처리기가 동시에 조회하면 둘 다 "없음"을 보기 때문입니다. 키에 유일 제약을 걸고 기록을 먼저 넣는 데 성공한 쪽만 발송하게 해야 합니다. PostgreSQL 10 이상 기준의 발송 요청 테이블 예시입니다.

CREATE TABLE notification_outbox (
  id              bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  dedupe_key      text        NOT NULL,
  user_id         bigint      NOT NULL,
  type            text        NOT NULL,
  channel         text        NOT NULL,
  payload         jsonb       NOT NULL,
  status          text        NOT NULL DEFAULT 'PENDING'
                  CHECK (status IN ('PENDING', 'SENDING', 'SENT', 'FAILED', 'CANCELED')),
  attempts        int         NOT NULL DEFAULT 0,
  next_attempt_at timestamptz NOT NULL DEFAULT now(),
  last_error      text,
  created_at      timestamptz NOT NULL DEFAULT now(),
  sent_at         timestamptz,
  CONSTRAINT uq_notification_dedupe UNIQUE (dedupe_key, channel)
);

CREATE INDEX idx_outbox_due ON notification_outbox (next_attempt_at)
  WHERE status IN ('PENDING', 'SENDING');
CREATE INDEX idx_outbox_user ON notification_outbox (user_id, created_at);
INSERT INTO notification_outbox (dedupe_key, user_id, type, channel, payload)
VALUES ('reservation:1201:confirmed', 57, 'RESERVATION_CONFIRMED', 'PUSH',
        '{"reservationId": 1201}')
ON CONFLICT (dedupe_key, channel) DO NOTHING;

배치가 두 번 돌거나 여러 지점에서 같은 사건을 호출해도 두 번째 INSERT는 아무 행도 만들지 않습니다. 유일 제약에 채널을 포함한 것은 같은 사건을 푸시 다음에 문자로 대체 발송할 수 있게 하기 위해서입니다. 이 테이블은 5장의 발송 대기열로도 그대로 씁니다.

외부 시스템 호출에서 멱등 키를 다루는 일반 원칙은 외부 시스템 연동에서 장애가 생기는 자리 — 인터페이스 설계 원칙에서 따로 정리했습니다.

같은 사람에게 몰리지 않게 합니다

여러 알림이 동시에 발생하면 한 사람에게 연달아 도착합니다.

  • 사용자당 시간 단위 발송 횟수 상한
  • 상한을 넘으면 묶거나 지연
  • 유형별 우선순위를 두고 낮은 것부터 제외

위 테이블을 쓰면 상한 검사는 조회 한 번입니다. 최근 1시간 동안 한 사용자에게 잡힌 푸시 건수를 세는 예시입니다.

SELECT count(*)
FROM notification_outbox
WHERE user_id = 57
  AND channel = 'PUSH'
  AND status IN ('PENDING', 'SENDING', 'SENT')
  AND created_at > now() - interval '1 hour';

상한을 넘었을 때 버릴지 미룰지는 유형마다 다르게 정합니다. 인증번호나 결제 확인 같은 유형은 상한 계산에서 제외하고, 홍보 유형부터 버리며, 진행 알림은 next_attempt_at을 뒤로 미뤄 묶음 발송 대상으로 돌리는 식입니다. 이 검사는 동시에 들어온 요청 사이에서 한두 건 어긋날 수 있습니다. 빈도 제한은 정확히 맞아야 하는 값이 아니므로 그 정도 오차는 받아들이고, 반드시 한 번만 가야 하는 것은 중복 방지 키로 보장합니다.


4. 수신 설정과 동의를 관리합니다

사용자가 통제권을 가진다고 느껴야 알림을 꺼두지 않습니다.

유형별로 나눠 설정하게 합니다

전체 켜기·끄기만 제공하면, 홍보 알림이 싫은 사용자가 모든 알림을 끕니다. 결과적으로 중요한 안내도 닿지 않습니다.

  • 유형별 수신 여부
  • 채널별 수신 여부
  • 시간대 설정
  • 전체 일시 정지

유형과 채널의 조합으로 관리하는 것이 정확합니다. "주문 알림은 푸시로, 홍보는 받지 않음" 같은 설정이 가능해야 합니다.

꺼도 되는 것과 아닌 것을 구분합니다

거래 관련 필수 고지처럼 끌 수 없는 알림이 있습니다. 이 구분을 설정 화면에서 분명히 보여주어야 합니다.

  • 필수 알림은 설정에서 끌 수 없음을 표시
  • 광고성 정보는 별도 동의를 받고, 언제든 철회할 수 있게 함
  • 동의·철회 이력을 기록

광고성 정보 전송에는 법령상 요건이 적용되므로, 해당 여부를 먼저 확인하고 동의 방식과 표기를 맞춰야 합니다.

광고성 알림에 적용되는 법정 요건

정보통신망법 제50조와 시행령이 정한 요건을 시스템 기능으로 옮기면 다음과 같습니다. 2026년 10월 2일 시행 법령(법률 제21988호, 대통령령 제36728호)과 방송미디어통신위원회·KISA 안내서 제7차 개정본(2025년 12월 발간)을 기준으로 정리했습니다. 안내서는 2026년 개정 전 법률을 기준으로 쓰였으므로, 과태료 조항 번호처럼 이후 바뀐 부분은 현행 조문을 따랐습니다.(정보통신망법 제50조, 시행령 제61조~제62조의3, 불법스팸 방지를 위한 정보통신망법 안내서 제7차 개정본)

요건 근거 시스템에 필요한 것
명시적 사전 동의 법 제50조 제1항 광고 수신 동의를 약관·개인정보 동의와 분리된 항목으로 받고, 동의 시각과 경로를 기록
거래관계 예외 법 제50조 제1항 제1호, 시행령 제61조 제1항 동의 없이 보낼 수 있는 범위는 거래 종료일부터 6개월, 거래한 것과 같은 종류의 재화·서비스로 한정. 거래 종료일을 발송 조건에 포함
수신 거부·철회 시 전송 금지 법 제50조 제2항 철회 즉시 발송 대상에서 제외. 이미 대기열에 들어간 건도 발송 직전에 다시 확인
야간 전송 별도 동의 법 제50조 제3항, 시행령 제61조 제2항 21시~다음 날 8시용 동의 항목을 따로 두고, 없으면 발송 보류(전자우편 제외)
표기 법 제50조 제4항, 시행령 별표 6 시작 부분에 "(광고)"와 전송자 명칭·연락처, 끝 부분에 무료 수신 거부 방법. 홍보 유형 템플릿에서 자동으로 붙이고 누락 시 발송 차단
처리 결과 통지 법 제50조 제7항, 시행령 제62조의2 동의·거부·철회 의사를 받은 날부터 14일 이내에 전송자 명칭, 의사표시 사실과 날짜, 처리 결과를 통지
수신 동의 여부 확인 법 제50조 제8항, 시행령 제62조의3 동의받은 날부터 2년마다 전송자 명칭, 동의 사실과 날짜, 유지·철회 방법을 안내

위반 시 제재도 가볍지 않습니다. 제50조 제1항부터 제4항, 제8항 위반은 3천만 원 이하, 제7항의 처리 결과 미통지는 1천만 원 이하의 과태료 대상이고(법 제76조 제2항·제4항), 2026년 3월 개정으로 관련 매출액의 100분의 6 이내에서 과징금을 부과할 수 있는 조항(법 제50조의9)이 추가되었습니다. 이 영역은 개정이 이어지고 있으므로 적용 시점의 현행 조문을 다시 확인하시기 바랍니다.

안내서에서 설계에 바로 영향을 주는 해석을 몇 가지 골랐습니다.

  • 동의 항목의 이름과 기본값 — "혜택 알림 동의", "마케팅 동의"처럼 광고임을 알 수 없는 이름으로 받거나, 체크박스를 동의로 미리 선택해 두거나, 이용약관 동의로 갈음하면 명시적 사전 동의로 보지 않습니다. 항목 이름은 "광고성 정보 수신 동의"로 씁니다
  • 매체별 동의 — 문자, 앱 푸시, 이메일, 메신저를 각각 선택하게 하는 구성을 권장합니다. 매체별로 받았다면 동의한 매체로만 보낼 수 있습니다. 앞서 말한 유형·채널 조합 구조와 그대로 맞습니다
  • 앱 푸시 광고의 수신 거부 — 푸시에서 수신 거부 화면으로 바로 이동해야 합니다. "설정 > 알림에서 해제"라고 경로만 안내하거나 수신 거부에 로그인을 요구하는 것은 수신 거부를 어렵게 하는 행위로 봅니다. 로그아웃 상태에서도 열리는 수신 거부 화면과 딥링크가 필요합니다
  • 앱 푸시 광고의 표기 — "(광고)" 표기는 푸시에도 적용됩니다. 전송자 명칭은 앱 이름과 같을 때에만 생략할 수 있습니다
  • 처리 결과 통지 — 앱이나 웹에서 의사를 표시한 경우 팝업으로 알리는 것도 인정됩니다. 통지에는 의사를 표시한 날짜를 "오늘"이 아닌 실제 날짜로 적고, 광고 내용을 섞지 않습니다
  • 2년 주기의 기산 — 최초 동의일부터 2년마다 셉니다. 안내한 날부터 새로 세는 것이 아닙니다. 철회 후 다시 동의했다면 마지막 동의일이 기준입니다
  • 무엇이 광고가 아닌가 — 수신자가 요청한 정보, 체결한 계약의 이행에 관한 정보(확인, 변경, 안전·보안 고지 등)는 광고성 정보가 아닙니다. 반면 수신자 의사와 무관하게 지급한 쿠폰·마일리지의 발급·소멸 안내는 광고성 정보로 봅니다

이 요건을 지키려면 동의의 현재 상태와 이력을 따로 관리해야 합니다. 최소 구성 예시입니다(PostgreSQL 10 이상).

CREATE TABLE ad_consent (
  user_id            bigint      NOT NULL,
  channel            text        NOT NULL,
  is_agreed          boolean     NOT NULL,
  is_night_agreed    boolean     NOT NULL DEFAULT false,
  agreed_at          timestamptz,
  next_reconfirm_due date,
  PRIMARY KEY (user_id, channel)
);

CREATE TABLE ad_consent_log (
  id                 bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id            bigint      NOT NULL,
  channel            text        NOT NULL,
  action             text        NOT NULL
                     CHECK (action IN ('AGREE', 'WITHDRAW', 'NIGHT_AGREE', 'NIGHT_WITHDRAW')),
  source             text        NOT NULL,
  acted_at           timestamptz NOT NULL DEFAULT now(),
  result_notified_at timestamptz
);

source에는 가입 화면, 설정 화면, 푸시 수신 거부 링크, 080 수신 거부처럼 의사를 받은 경로를 남깁니다. 동의를 받았다는 사실은 전송자가 증명해야 하기 때문입니다. next_reconfirm_due는 동의 시점에 agreed_at의 2년 뒤 날짜로 넣고, 안내를 보낼 때마다 2년씩 더합니다.

두 가지 기한은 매일 도는 점검 작업으로 지킵니다.

-- 처리 결과를 아직 통지하지 않은 건 (법정 기한 14일, 예시는 10일째에 경보)
SELECT id, user_id, channel, action, acted_at
FROM ad_consent_log
WHERE result_notified_at IS NULL
  AND acted_at < now() - interval '10 days';

-- 30일 안에 수신 동의 여부 확인 기한이 돌아오는 대상
SELECT user_id, channel, agreed_at, next_reconfirm_due
FROM ad_consent
WHERE is_agreed
  AND next_reconfirm_due <= current_date + 30;

10일과 30일은 설명용 값입니다. 화면에서 동의를 바꾸는 즉시 팝업으로 결과를 알리고 result_notified_at을 채우는 것이 가장 단순하며, 첫 번째 조회는 그 경로를 타지 않은 건(080 수신 거부, 고객센터 접수 등)을 잡아내는 용도입니다.

법률 해석이 걸린 사안, 예를 들어 특정 메시지가 광고성 정보에 해당하는지는 안내서의 사례와 질의응답을 먼저 확인하고, 판단이 어려우면 법무 검토를 받으시기 바랍니다.

기본값을 신중하게 정합니다

대부분의 사용자는 기본 설정을 바꾸지 않습니다. 기본값이 곧 실제 설정입니다.

  • 처음부터 모든 알림을 켜두면 초기 이탈이 생깁니다
  • 너무 적게 켜두면 필요한 안내도 닿지 않습니다
  • 서비스 성격에 따라 판단하되, 운영하면서 조정할 수 있게 해둡니다

5. 보낸 뒤를 관리합니다

발송은 시작이고, 그 뒤에 확인할 것이 남습니다.

본 서비스와 발송을 분리합니다

주문 처리 중에 알림 발송을 같은 흐름에서 하면, 발송 채널이 느려질 때 주문 처리까지 느려집니다. 발송에 실패하면 주문까지 실패할 수도 있습니다.

일반적인 구조는 이렇습니다.

① 업무 처리 완료 → ② 발송 요청을 큐에 적재 → ③ 별도 처리기가 실제 발송

이 구조에서 얻는 것들입니다.

  • 발송 채널의 지연이 본 서비스에 전파되지 않습니다
  • 실패한 건을 재시도하기 쉽습니다
  • 발송량이 몰려도 처리 속도를 조절할 수 있습니다
  • 채널별로 처리기를 나눠 서로 영향을 주지 않게 할 수 있습니다

주의할 점은 업무 처리와 발송 요청 적재가 함께 성공하거나 함께 실패해야 한다는 것입니다. 주문은 저장됐는데 발송 요청이 유실되면 알림이 가지 않고, 그 사실을 아무도 모릅니다. 두 작업을 같은 트랜잭션으로 묶는 구조가 필요합니다.

아웃박스로 적재하고 재시도합니다

메시지 큐를 따로 쓰면 "DB 커밋은 됐는데 큐 적재는 실패"하는 틈이 남습니다. 이 틈을 없애는 흔한 방법이 발송 요청을 업무 데이터와 같은 DB의 테이블에 넣는 아웃박스 방식입니다. 3장에서 만든 notification_outbox 테이블을 그대로 씁니다.

BEGIN;

UPDATE reservations
SET status = 'CONFIRMED'
WHERE id = 1201 AND status = 'REQUESTED';

-- 3장의 INSERT ... ON CONFLICT DO NOTHING 문을 여기서 실행

COMMIT;

처리기는 보낼 차례가 된 행을 가져가면서 상태를 바꿉니다. 처리기를 여러 대 띄워도 같은 행을 잡지 않도록 FOR UPDATE SKIP LOCKED를 씁니다(PostgreSQL 9.5 이상).

WITH due AS (
  SELECT id
  FROM notification_outbox
  WHERE status IN ('PENDING', 'SENDING')
    AND next_attempt_at <= now()
  ORDER BY next_attempt_at
  LIMIT 100
  FOR UPDATE SKIP LOCKED
)
UPDATE notification_outbox o
SET status = 'SENDING',
    attempts = o.attempts + 1,
    next_attempt_at = now() + interval '5 minutes'
FROM due
WHERE o.id = due.id
RETURNING o.id, o.user_id, o.type, o.channel, o.payload, o.attempts;

가져갈 때 next_attempt_at을 5분 뒤로 미루는 것이 핵심입니다. 처리기가 발송 도중 죽어도 그 행은 5분 뒤 다시 대상이 됩니다. 이 처리가 없으면 SENDING 상태로 영원히 남는 행이 생깁니다. 5분과 100건은 설명용 값이며, 채널 호출 제한 시간보다 충분히 길게 잡습니다.

결과에 따라 행을 마무리합니다.

-- 성공
UPDATE notification_outbox
SET status = 'SENT', sent_at = now()
WHERE id = 3310;

-- 일시적 실패: 2분, 4분, 8분... 간격으로 다시 시도하고 5회째에 포기
UPDATE notification_outbox
SET status = CASE WHEN attempts >= 5 THEN 'FAILED' ELSE 'PENDING' END,
    next_attempt_at = now() + interval '1 minute' * power(2, attempts),
    last_error = 'UNAVAILABLE'
WHERE id = 3310;

이 구조가 보장하는 것은 "적어도 한 번"입니다. 채널에는 보냈는데 SENT로 기록하기 전에 처리기가 죽으면 같은 알림이 한 번 더 나갑니다. 이 경우를 줄이는 방법은 세 가지입니다.

  • 채널이 중복 제거 수단을 제공하면 함께 넘깁니다. APNs는 apns-collapse-id가 같은 알림을 하나로 합칩니다
  • 발송 직전에 조건을 다시 확인합니다. 예약이 취소되었거나 수신 동의가 철회되었다면 CANCELED로 끝냅니다
  • 오래된 요청은 보내지 않습니다. 생성 후 일정 시간이 지난 진행 알림은 재시도보다 폐기가 낫습니다

아웃박스 테이블은 계속 쌓이므로 완료된 행을 주기적으로 이력 테이블로 옮기거나 파티션으로 잘라냅니다. 발송량이 DB로 감당하기 어려운 규모가 되면 처리기 앞에 메시지 큐를 두되, 큐로 옮기는 역할을 아웃박스 처리기가 맡게 하면 같은 보장을 유지할 수 있습니다.

발송 이력을 남깁니다

기록할 항목입니다.

구분 항목
대상 수신자, 수신 주소·토큰
내용 알림 유형, 발송된 내용, 연결 대상
경로 채널, 발송 시각
결과 성공·실패, 실패 사유
반응 열람 여부, 클릭 여부

발송된 실제 내용을 남기는 것이 중요합니다. "어떤 내용을 받았다"는 문의가 들어왔을 때, 당시 템플릿이 바뀌었으면 재현할 수 없습니다.

개인정보가 포함되므로 보관 기간과 마스킹 규칙을 함께 정해야 합니다.

실패를 처리합니다

  • 일시적 실패는 재시도합니다
  • 영구적 실패는 재시도하지 않습니다. 없는 주소, 차단된 수신자가 여기 해당합니다
  • 반복 실패하는 대상은 발송 목록에서 제외합니다
  • 실패율이 급증하면 알림을 보냅니다

반복 실패 대상 정리를 하지 않으면 비용과 평판이 함께 나빠집니다. 없는 주소로 계속 보내면 발송 채널 자체가 제재를 받을 수 있습니다.

일시적 실패와 영구적 실패는 채널이 돌려주는 응답으로 구분합니다. 푸시 두 채널의 기준입니다.

채널 응답 의미 처리
FCM 404 UNREGISTERED 앱 인스턴스가 등록 해제됨 토큰 삭제, 재시도 안 함
FCM 400 INVALID_ARGUMENT 요청 값 오류. 토큰이 잘못된 경우와 본문이 잘못된 경우가 섞여 있음 재시도 안 함. 본문이 정상임이 확실할 때에만 토큰 삭제
FCM 429 QUOTA_EXCEEDED 발송 한도 초과 retry-after 헤더만큼 대기 후 재시도. 헤더가 없으면 60초
FCM 500 INTERNAL, 503 UNAVAILABLE 서버 측 일시 오류 지수 백오프로 재시도
APNs 410 Unregistered 해당 토큰이 더 이상 유효하지 않음 토큰 삭제
APNs 400 BadDeviceToken 토큰이 잘못되었거나 환경(개발·운영)이 맞지 않음 재시도 안 함. 환경 설정부터 확인
APNs 429 TooManyRequests 같은 기기 토큰으로 연속 요청이 너무 많음 간격을 두고 재시도
APNs 500, 503 서버 측 오류 또는 종료 중 재시도

(Firebase: FCM error codes, Firebase: Best practices when sending FCM messages at scale, Apple Developer: Handling notification responses from APNs)

Firebase는 400·401·403·404는 재시도하지 말고, 재시도할 때에는 최소 10초를 기다린 뒤 지터를 섞은 지수 백오프를 쓰며, 시의성을 잃은 요청은 무한히 재시도하지 말고 버리도록 안내합니다. 이 분류를 코드로 옮기면 다음과 같습니다(TypeScript).

type SendOutcome = 'RETRY' | 'DROP_TOKEN' | 'FAIL';

function classifyFcmError(httpStatus: number, errorCode: string): SendOutcome {
  if (errorCode === 'UNREGISTERED') return 'DROP_TOKEN';
  if (httpStatus === 429 || httpStatus >= 500) return 'RETRY';
  return 'FAIL';
}

INVALID_ARGUMENT를 DROP_TOKEN으로 분류하지 않은 것은 의도한 것입니다. 템플릿 변수가 잘못되어 본문이 깨진 날, 멀쩡한 토큰이 전부 삭제되는 사고를 막기 위해서입니다.

토큰 정리는 실패 응답에만 기대지 않습니다. Firebase는 토큰을 서버에 저장할 때 시각을 함께 기록하고 앱이 주기적으로 갱신하게 한 뒤, 일정 기간 갱신되지 않은 토큰을 오래된 것으로 보고 정리하도록 권장합니다. FCM이 기본으로 보는 기준은 한 달이지만, 기준 기간은 서비스 사용 주기에 맞춰 정합니다. 한 달에 한 번 쓰는 앱에 30일 기준을 적용하면 정상 사용자의 토큰이 지워집니다. 또한 한 사용자가 여러 기기를 쓰거나 한 기기에서 계정을 바꿔 로그인하는 경우가 있으므로, 토큰은 사용자와 일대다로 저장하고 로그아웃 시 그 기기의 토큰을 계정에서 분리합니다. 분리하지 않으면 이전 사용자의 알림이 다음 사용자에게 표시됩니다.

자주 나는 오류와 원인

증상 흔한 원인 해결
같은 알림이 두 번 옴 조회 후 발송 순서로 중복을 검사함. 또는 발송 성공 후 기록 전에 처리기가 재시작됨 유일 제약으로 선점. 채널의 묶음 키를 함께 전달
발송 성공인데 사용자는 못 받음 알림 권한 꺼짐, 오래 접속하지 않은 기기, 기기의 절전 상태 성공을 도달로 해석하지 않음. 앱 내 알림함과 열람 보고로 보완
운영 전환 직후 iOS만 전부 실패 개발 환경 토큰을 운영 APNs로 보냄(BadDeviceToken) 토큰에 환경을 함께 저장하고 환경별 엔드포인트로 발송
특정 일자부터 실패율 급증 APNs 인증서나 인증 키가 만료·누락됨(FCM 401 THIRD_PARTY_AUTH_ERROR, APNs 403) 만료일을 운영 일정에 등록, 인증 오류 응답에 즉시 경보
취소된 예약의 안내가 발송됨 예약 발송 건을 발송 직전에 재검증하지 않음 처리기가 보내기 전에 원본 상태 확인 후 CANCELED 처리
홍보 알림이 밤에 도착 큐 지연으로 21시 이후에 처리됨 발송 직전 시각 재확인, 마감 시각에 여유
수신 거부한 사용자에게 발송됨 대상 추출 시점과 발송 시점 사이에 철회함 대량 발송도 건별로 발송 직전에 동의 상태 확인
"안녕하세요 님"으로 발송됨 템플릿 변수 누락 변수 검증 실패 시 발송하지 않고 FAILED 처리
정각마다 발송이 밀림 예약 발송이 매시 정각에 몰림 발송 시각 분산. Firebase는 매시 0·15·30·45분 전후 2분을 피하도록 권장

효과를 측정합니다

발송 건수만 보면 판단할 수 없습니다.

  • 도달률 — 실제로 전달된 비율
  • 열람률 — 사용자가 확인한 비율
  • 행동률 — 알림을 통해 실제 행동으로 이어진 비율
  • 수신 거부율 — 이 알림을 받은 뒤 설정을 끈 비율

마지막 지표가 가장 중요합니다. 수신 거부율이 높은 알림은 보내지 말아야 할 알림입니다. 열람률이 높아도 거부율이 함께 높으면 장기적으로는 손해입니다.

템플릿을 관리합니다

알림 문구를 코드에 직접 넣으면 수정할 때마다 배포해야 합니다.

  • 문구를 템플릿으로 분리합니다
  • 변수 부분을 정의합니다
  • 다국어를 지원하면 언어별로 관리합니다
  • 변경 이력을 남깁니다
  • 승인 절차가 필요한 채널은 그 상태도 함께 관리합니다

변수 누락이 흔한 사고입니다. 템플릿에 넣기로 한 값이 비어 "안녕하세요 님"처럼 발송되는 경우입니다. 발송 전 변수가 모두 채워졌는지 확인하는 절차를 두면 막을 수 있습니다.


6. 문구가 알림의 절반입니다

기술적으로 잘 전달되어도 내용이 불명확하면 사용자는 행동하지 않습니다.

짧은 공간에 필요한 것만 담습니다

푸시는 화면에 보이는 글자 수가 제한됩니다. 길게 쓰면 잘립니다.

  • 앞부분에 핵심을 둡니다. 뒤는 잘릴 수 있습니다
  • 무엇이 일어났는지를 먼저 쓰고, 부연은 뒤에 둡니다
  • 서비스명을 문구 앞에 반복하지 않습니다. 발신자로 이미 표시됩니다
나쁜 예: [○○서비스] 안녕하세요 고객님, 고객님께서 신청하신 건에 대하여...
좋은 예: 예약이 확정되었습니다 · 3월 12일 14시

홍보 알림은 예외입니다. 4장에서 본 것처럼 시작 부분에 "(광고)"와 전송자 명칭을 표기해야 하므로, 이 표기를 뺀 나머지 글자 수 안에서 문구를 씁니다.

무엇을 해야 하는지 알려줍니다

행동이 필요한 알림이라면 그 행동을 명시합니다.

  • "승인 대기 중" 보다 "승인이 필요한 건이 2건 있습니다"
  • 기한이 있으면 기한을 함께 표시합니다
  • 눌렀을 때 바로 그 화면으로 이동하게 합니다

개인정보를 잠금화면에 노출하지 않습니다

푸시 내용은 잠금화면에 표시될 수 있습니다. 옆 사람이 볼 수 있다는 뜻입니다.

  • 진료 내용, 금액, 상대방 이름 같은 정보를 본문에 넣지 않습니다
  • "새 메시지가 도착했습니다"처럼 최소한만 표시하고, 상세는 앱에서 확인하게 합니다
  • 민감한 유형은 미리보기 숨김을 기본값으로 둘지 검토합니다

이 판단이 누락되어 사고가 되는 경우가 있습니다. 설계 단계에서 유형별로 잠금화면 노출 가능 여부를 정해두는 편이 안전합니다.

여러 언어를 지원한다면

  • 사용자별 언어 설정을 따릅니다. 설정이 없으면 기기 언어를 씁니다
  • 템플릿을 언어별로 관리합니다
  • 언어에 따라 문장 길이가 크게 달라지므로 잘림을 각각 확인합니다
  • 날짜·시간·통화 표기 형식도 언어에 맞춥니다

7. 운영자 알림은 기준이 다릅니다

사용자 알림과 목적이 다르므로 설계도 달라집니다.

무엇을 알릴지 기준을 정합니다

  • 즉시 조치가 필요한 상황만 즉시 알립니다
  • 확인만 하면 되는 것은 모아서 보고합니다
  • 지표는 임계치를 넘을 때만 알립니다

모든 오류를 알리면 아무것도 알리지 않는 것과 같아집니다. 담당자가 알림을 무시하기 시작하면 정작 중요한 신호를 놓칩니다.

알림에 판단 정보를 담습니다

받는 사람이 바로 움직일 수 있어야 합니다.

  • 무엇이 언제부터 문제인지
  • 영향 범위 — 전체인지 일부인지
  • 관련 지표의 현재 값과 평소 값
  • 확인할 화면이나 로그 위치

"오류 발생"만 적힌 알림은 받은 사람이 처음부터 조사해야 합니다.

전달 경로를 이중화합니다

알림 시스템 자체가 죽으면 알림이 오지 않습니다. 그리고 그 사실도 알 수 없습니다.

  • 주 경로와 보조 경로를 둡니다
  • 정기적으로 시험 알림을 보내 경로가 살아 있는지 확인합니다
  • 야간·휴일 담당자와 연락 순서를 정합니다

8. 자주 나오는 질문

알림 기능은 어느 정도 공수가 듭니까

"푸시 보내기"만 보면 작지만, 실제 범위는 넓습니다. 산정에 포함할 요소입니다.

  • 알림 유형 수와 각각의 발생 조건
  • 연동할 채널 수
  • 수신 설정 화면과 관리 로직
  • 발송 이력 저장과 조회 화면
  • 템플릿 관리 기능
  • 실패 처리와 재시도
  • 운영자용 발송 도구

유형 수가 공수를 결정합니다. 알림 3종과 30종은 완전히 다른 작업입니다. 요구사항에 "알림 기능"이라고만 적혀 있으면 유형 목록부터 확정해야 합니다.

실시간으로 보내야 합니까

대부분은 그럴 필요가 없습니다. 몇 초에서 몇 분의 지연은 대체로 허용됩니다.

즉시성이 필요한 경우는 제한적입니다. 인증번호, 결제 확인, 긴급 상황 같은 것입니다. 나머지는 큐를 거쳐 처리하는 편이 안정적입니다. 발송 채널이 느려져도 본 서비스가 영향받지 않습니다.

알림을 외부 서비스에 맡겨도 됩니까

발송 자체를 대행하는 서비스를 쓰는 것은 일반적입니다. 검토할 점입니다.

  • 수신자 정보가 외부로 전달되는 것에 대한 검토
  • 장애 시 대체 경로
  • 발송 이력을 우리 쪽에도 남길 것인지
  • 비용 구조와 상한

발송 이력은 우리 쪽에도 남기는 편이 좋습니다. 외부 서비스를 바꾸면 그쪽 기록은 가져오기 어렵습니다.

알림을 보내야 하는데 연락처가 없으면 어떻게 합니까

가입 시 연락처를 필수로 받지 않는 서비스에서 생기는 문제입니다.

  • 앱 내 알림함은 계정만 있으면 가능하므로 기본 경로로 둡니다
  • 외부 채널이 필요한 알림은 그 시점에 연락처를 요청합니다
  • 연락처가 없는 사용자에게는 어떤 알림이 닿지 않는지 목록으로 파악해둡니다

결제나 계약처럼 통지가 필요한 업무라면, 해당 기능을 쓰기 전에 연락처를 받는 흐름으로 설계하는 편이 낫습니다. 나중에 "알림을 못 받았다"는 문제가 생겼을 때 대응할 근거가 없습니다.

이미 알림이 너무 많은 상태라면 어떻게 정리합니까

순서를 두고 접근합니다.

  1. 현재 발송 중인 알림 유형을 전부 목록화합니다
  2. 유형별 발송량, 열람률, 수신 거부율을 확인합니다
  3. 거부율이 높고 열람률이 낮은 것부터 중단을 검토합니다
  4. 비슷한 유형을 묶습니다
  5. 수신 설정을 세분화해 사용자가 고를 수 있게 합니다

목록화 단계에서 놀라는 경우가 많습니다. 기능이 추가되면서 알림도 하나씩 붙어, 아무도 전체를 파악하지 못하는 상태가 되어 있습니다.


9. 착수 전 점검 목록

대상 정의

  • 알림 유형 목록이 확정되어 있는가
  • 유형별 발생 조건이 명확한가
  • 보내지 않아도 되는 것을 걸러냈는가
  • 묶어서 보낼 대상을 정했는가

채널

  • 유형별로 사용할 채널이 정해져 있는가
  • 앱 내 알림함에 모든 알림이 남는가
  • 채널 실패 시 대체 경로가 있는가
  • 비용이 드는 채널에 발송 상한이 있는가
  • 대량 발송 전 승인 절차가 있는가
  • 푸시 권한 요청 시점과 권한이 없는 사용자의 대체 경로를 정했는가
  • 알림톡 템플릿 심사 기간이 일정에 반영되어 있는가
  • 대체 발송으로 넘어가는 조건(주소 없음, 영구 실패, 미열람)이 정의되어 있는가

시점

  • 발송 제한 시간과 예외 유형이 정해져 있는가
  • 예약 발송 직전 조건을 다시 확인하는가
  • 사용자당 발송 빈도 상한이 있는가
  • 유형별 중복 방지 키가 정의되어 있고 유일 제약으로 강제되는가

수신 설정

  • 유형·채널별로 설정할 수 있는가
  • 끌 수 없는 알림을 구분해 표시하는가
  • 광고성 정보의 동의·철회 이력을 남기는가
  • 기본값을 검토했는가

광고성 알림

  • 광고 수신 동의가 약관 동의와 분리되어 있고 기본 선택이 해제되어 있는가
  • 야간(21시~8시) 전송 동의를 별도로 받고, 없으면 발송이 보류되는가
  • "(광고)", 전송자 명칭, 무료 수신 거부 방법이 템플릿에서 자동으로 붙는가
  • 푸시에서 로그인 없이 수신 거부 화면으로 바로 이동하는가
  • 동의·거부·철회 처리 결과를 14일 이내에 통지하는가
  • 2년 주기의 수신 동의 여부 확인 안내가 자동화되어 있는가
  • 정보성 템플릿에 홍보 문구가 섞이지 않도록 막고 있는가

운영

  • 발송 이력에 실제 내용이 남는가
  • 업무 처리와 발송 요청 적재가 같은 트랜잭션에 있는가
  • 처리기가 중간에 죽은 건을 다시 가져가는가
  • 실패 유형별 재시도 정책이 있는가
  • 반복 실패 대상과 오래된 푸시 토큰을 정리하는가
  • 수신 거부율을 지표로 보는가
  • 템플릿이 코드와 분리되어 있는가
  • 변수 누락을 발송 전에 검사하는가

운영자 알림

  • 즉시 알릴 것과 모아 보고할 것을 구분했는가
  • 같은 원인의 알림을 묶는가
  • 알림에 판단에 필요한 정보가 담기는가
  • 알림 경로가 살아 있는지 정기적으로 확인하는가

마치며

알림 설계에서 중심이 되는 질문은 "어떻게 보낼까"가 아니라 **"이것을 꼭 보내야 하는가"**입니다.

보낼 수 있는 모든 상황에 알림을 붙이면, 처음에는 친절해 보이지만 결국 전부 무시됩니다. 그 상태가 되면 정말 필요한 순간에 쓸 수 있는 수단이 없어집니다.

유형을 나누고, 보내지 않아도 되는 것을 걸러내고, 사용자가 고를 수 있게 하고, 수신 거부율을 지켜보는 것. 이 네 가지가 알림을 살아 있는 채널로 유지하는 방법입니다.

저희는 성격이 다른 알림 체계를 구축해왔습니다. 광고 리워드 앱에서는 Firebase Cloud Messaging 기반 푸시를 관리자 콘솔에서 운영하고 발송 이력을 대상과 출처별로 남기도록 했고, 건물 에너지 절감 솔루션 B-Saver에서는 알림 종류와 채널(이메일·푸시·SMS·웹)을 사용자별로 설정하고 채널별 발송 이력을 저장하는 알림 센터를 만들었습니다. 알림 기능을 설계하고 계시다면 유형 목록을 정리하는 단계부터 함께 보시면 좋겠습니다. 문의는 편하게 남겨주세요.


함께 보면 좋은 글

참고 자료