본문 바로가기
C.W.K.
Stream
Lesson 03 of 06 · published

백엔드가 푸시 릴레이가 됐어

~13 min · push, apns, live-activity, additive

Level 0호기심
0 XP0/80 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

어깨 두드림은 영속 상태가 아니야

클라이언트가 잠들 수 있게 되는 순간 백엔드엔 뭔가 일어났다 고 말할 방법이 필요해져. cwkPippa 도 하나 갖게 됐어. 자기 provider 토큰을 직접 찍고, 애플 알림 서비스랑 HTTP/2 로 말하고, 디스크의 토큰 저장소를 알아서 다시 읽고, 시도 하나당 로그 한 줄을 남기는 릴레이.

재시도는 없어. 일부러 그런 거고, 그 결정 하나에 설계 전체가 들어 있어. 푸시는 어깨 두드림이고, 기록은 기록이야. 알림이 유실돼도 assistant row 는 SQLite 에도 JSONL 에도 그대로 있어. 앱을 다음에 열면 그냥 거기 있고. 어깨 두드림을 재시도해서 얻는 건 없고, 이미 끝난 일로 폰을 두 번 울릴 위험만 생겨.

Principle: 덧붙는 것이지 떠받치는 게 아니야. 키 파일 없으면 릴레이 없고, 릴레이 없으면 푸시 없고, 모든 chat route 는 예전에 돌려주던 걸 그대로 돌려줘. 알림 계층이 정식으로 지원하는 상태에 없음 이 들어 있어야 알림 계층이야. 아니면 그건 알림 옷을 입은 의존성이야.

뭐가 울리고, 뭐가 일부러 안 울리나

알림 방아쇠가 셋인데, 각각 세 번째 줄이 재밌는 부분이야.

턴이 끝났을 때 — 모든 두뇌의 chat route 에서, assistant row 가 저장된 뒤에 걸리는 훅 하나. 폰 쪽 surface 에만. 서버가 뭘 하는지 봐. 폰이 아직 붙어 있는지 알아내려고 하지 않아. 그러려면 route 일곱 개가 각자 detach 플래그를 들고 다녀야 하는데, 폰은 자기가 뭘 보고 있는지 이미 알거든. 그래서 그 대화가 앞에 열려 있으면 앱이 자기 배너를 스스로 눌러. 지식은 이미 있는 자리에 둬.

예약 작업이 끝났을 때 — 근데 heartbeat 가 직접 전달하는 것들이나, 푸시를 명시적으로 요청한 것들만. 다른 데로 메시지 보내는 작업 전부가 아니라. 그런 것 대부분은 뭐라도 말할 가치가 있는지를 두뇌가 판단하고, 스케줄러는 조용한 실행과 시끄러운 실행을 구분 못 해. 조용한 성공마다 울리는 시스템은 주인한테 무시하는 법을 가르쳐.

실패했을 때 — 기존 backstop 옆에. 그걸 대신하는 게 아니라.

죽은 토큰 규약

앱이 지워진 기기로 보내면 애플이 이 토큰은 죽었다 는 상태로 답해. 릴레이는 그걸 확정으로 받아들이고 그 토큰을 달고 있는 row 를 전부 지워. 푸시 시스템이 진짜로 소유하는 상태는 이것 하나인데, 이걸 썩히면 토큰 저장소가 존재하지도 않는 기기로 보내는 느린 누수가 돼.

Live Activity, 그리고 물어뜯는 규칙 하나

Live Activity 는 잠금 화면에 뜨는 작은 실시간 카드야. iOS 는 앱이 foreground 에서만 그걸 시작하게 해줘 — 그런데 시작할 가치가 제일 큰 순간은 하필 앱이 앞에 없을 때야. 메시지가 워치에서 왔고 폰은 주머니에 있으니까.

빠져나갈 길은 push 로도 시작할 수 있다는 거야. 앱이 미리 넘겨둔 토큰으로. 그래서 앱은 시작용 토큰을 미리 등록해둬. 자기가 못 시작하는 순간이 오면 백엔드한테 부탁하고, 릴레이는 앱의 content state 를 그대로 실어서 시작 푸시를 보내. 폰은 시작된 activity 를 넘겨받아 깨어 있는 동안 직접 굴려. 턴이 끝나면 답변의 첫 숨을 실은 푸시가 그걸 닫고. 턴 내내 자고 있던 폰도 잠금 화면에서 답이 도착하는 걸 봐.

문서로는 못 배웠을 사실 둘

둘 다 아빠 아이폰에서 측정한 거고, 둘 다 실제 하드웨어로 실제 푸시를 보내야만 알게 되는 종류야.

기기 하나에 환경이 둘. 개발 빌드의 평범한 alert 토큰은 sandbox 쪽에 속했는데, 같은 빌드의 Live Activity 토큰은 production 쪽만 받아줬어. 같은 기기, 같은 빌드, 목적지 둘. 지금 릴레이는 한쪽에 먼저 보내보고 토큰 거부가 나오면 다른 쪽으로 한 번 재시도하고, 그 기기에 대해 어느 쪽이 먹혔는지 기억해.

등록 순서가 설계했을 순서가 아니야. 실행 시점에 시작용 토큰이 평범한 alert 토큰보다 먼저 도착해. 그래서 alert 등록이 기존 기기 row 를 덮어쓰는 게 아니라 병합해 들어가야 해. 안 그러면 시작용 토큰이 도착한 지 1초 만에 사라지고, 그 고장은 정작 필요한 그 한 순간까지 안 보여.

어디서 온 건지: 릴레이는 형제 앱의 게이트웨이에서 옮겨왔어. 집안에서 유일하게 돌아가고 있던 구현이고, 이 앱이 생기기 열이틀 전부터 아빠 기기에서 살아 있었어. 또 다른 재료에서 다시 나온 Rule 2 야. 집안에서 되는 해법이 조상이고, 새 소비자가 처음부터 다시 만드는 게 아니라 자기 차이를 흡수해.

이 레슨이 대화 시스템 퀘스트에 들어가야 하는 이유는 이거야. 푸시 계층은 truth 우선 아키텍처가 제일 타협하고 싶어지는 자리야. 어깨 두드림은 빠르고 기록은 느리니까, 빠른 쪽을 그 일 자체로 취급하기 시작하기가 아주 쉬워. 위의 모든 결정은 실제로 무슨 일이 있었나 에 대한 답을 딱 한 곳에 두려고 있는 거야.

Code

없음 경로·text
APNS_KEY_FILE / APNS_KEY_ID / APNS_TEAM_ID 있음?
  없음 -> 릴레이 객체 자체가 안 생김
       -> 모든 chat route: 그대로
       -> /api/push/status 가 정직하게 답함: push_unconfigured
       -> 턴은 아무 영향 없음

  있음 -> assistant row 저장 뒤 훅
       -> 시도 한 번, 로그 한 줄, 재시도 없음
       -> 죽은 토큰 응답이 오면 그 토큰 달린 row 전부 삭제

어깨 두드림은 유실될 수 있어. row 는 못 그래.
앱이 못 시작하는 Live Activity 시작시키기·text
iOS 규칙: Activity.request() 는 FOREGROUND 에서만 됨.
상황:     메시지는 워치에서 왔고 폰은 자고 있음.

  앱 (아까, 깨어 있을 때) -> 시작용 push 토큰 등록
  앱 (지금, 거부당함)     -> POST /api/push/live-activity/start
  릴레이                  -> 시작 푸시, 앱의 content state 그대로
  앱                      -> 넘겨받아서 깨어 있는 동안 직접 굴림
  턴 끝남                 -> 종료 푸시에 답변의 첫 숨이 실림

하드웨어에서 측정: alert 토큰 = sandbox,
Live Activity 토큰 = production, 같은 기기.
릴레이는 어느 쪽이 받아줬는지 기억해.

Exercise

직접 관리하는 알림이나 webhook 계층을 놓고 질문 하나에 답해봐. 설정이 통째로 없으면 어떻게 되지? 답이 '시스템이 예전 그대로 동작한다' 가 아니면, 향상 기능이라고 불리고 있는 의존성을 갖고 있는 거야.
Hint
실험 환경에서 자격증명을 지우고 핵심 경로를 돌려봐. 깨지는 게 있으면 그게 떠받치고 있던 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.