포인트 소멸 알림 푸시 발송
목차
포인트 소멸 알림 기능을 추가한 건 사실 작은 작업이었는데, 막상 다 엮어놓고 보니 건드린 레이어가 꽤 많았다. FCM 발송 자체보다 "언제 쏘고, 실패하면 어떻게 하고, 앱에서 어떻게 받나"를 같이 다뤄야 했기 때문이다.
서비스 구조상 포인트 소멸 예정 알림은 결제 완료 알림과 달리 이벤트 트리거가 없다. 잔액이 N일 후 만료되는 시점을 미리 계산해서 배치로 쏴야 한다. 결제 완료는 트랜잭션이 끝나는 시점에 자연스럽게 붙이면 되지만, 소멸 예정 알림은 스케줄러가 대상자를 뽑아서 발송 큐에 넣는 흐름이다.
발송 구조 - 왜 비동기인가
FCM 호출을 메인 트랜잭션에 묶으면 발송이 느리거나 FCM 서버가 순간 응답을 못 할 때 결제 처리 자체가 타임아웃 난다. 알림 실패가 결제 실패로 번지면 안 된다. 그래서 발송은 트랜잭션이 커밋된 이후 비동기로 분리했다.
결제 요청
└─ DB 트랜잭션 커밋
└─ (커밋 후) FCM 발송 큐 적재 ← 여기서부터 비동기
└─ FCM API 호출
├─ 성공: 발송 로그 기록
└─ 실패: 재시도 큐 적재
이 구조에서 주의할 점은 "커밋 후에 큐를 넣어야 한다"는 것이다. 트랜잭션 안에서 큐를 먼저 적재하면 롤백 시 알림만 나가는 상황이 생긴다. 순서가 반대면 안 된다.
재시도는 별도 배치가 주기적으로 돌면서 실패 상태인 발송 건을 다시 시도하는 방식이다. 일시적 네트워크 오류나 FCM 서버 점검 같은 케이스를 커버하기 위해서다. 재시도 횟수 상한은 두고, 그 이상 넘어가면 실패로 확정하고 운영팀에 알람을 보낸다.
| 알림 유형 | 트리거 시점 | 발송 방식 |
|---|---|---|
| 결제 완료 | 결제 트랜잭션 커밋 직후 | 비동기 이벤트 |
| 잔액 소멸 예정 | 만료 N일 전 배치 실행 | 스케줄러 큐 |
| 운영 공지 | 관리자 직접 발송 | 즉시 또는 예약 |
Android 앱 쪽 이슈들
앱이 구글 플레이가 아니라 직접 APK 배포라서 자동 업데이트가 없다. 서버 API로 최신 버전 정보를 내려주고, 앱이 실행될 때마다 비교해서 업데이트를 안내하는 로직을 따로 구현해뒀다. 구글 플레이에서 처리해주던 걸 직접 관리해야 하니 유지 비용이 조금 있다.
NotificationListenerService는 시스템이 메모리 압박을 받으면 강제 종료할 수 있다. onStartCommand에서 START_STICKY를 반환해두면 시스템이 종료 후 재시작을 시도한다. 다만 재시작 타이밍은 보장이 안 되니, 중요한 실시간 처리를 여기에만 기대면 안 된다.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 포그라운드 서비스로 격상해서 생존율 높이기
startForeground(NOTIFICATION_ID, buildForegroundNotification())
return START_STICKY
}
포그라운드 서비스로 올리면 사용자에게 상태바 알림이 노출되는 트레이드오프가 있다. 백그라운드 생존율이냐 UX 노이즈냐 사이에서 선택해야 한다. 이번엔 안정성 쪽을 택했다.
Android 13부터 POST_NOTIFICATIONS 권한이 런타임 요청으로 바뀌었다. 기존 코드는 Manifest에만 선언하면 됐는데, 13+ 대응을 안 하면 알림이 조용히 안 나간다. 앱 최초 실행 시 권한 요청 다이얼로그를 띄우고, 거부하면 설정 유도 UI를 보여주는 흐름을 추가했다.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
버전 분기가 늘어날수록 코드가 지저분해지는데, 이 부분은 래핑해서 호출 측에서 버전을 신경 안 써도 되게 정리하는 게 낫다.
Android 14+ MediaProjection 크래시
같은 작업 배치에서 MediaProjection 관련 크래시도 수정했다. Android 14부터 기존에 생성한 MediaProjection 인스턴스를 재사용하면 크래시가 난다. 이전까지는 인스턴스를 들고 있다가 필요할 때 다시 쓰는 게 일반적인 패턴이었는데, 14부터 정책이 바뀌었다.
// 14+ 대응: 매번 새로 요청
fun startProjection(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// 기존 세션 명시적 종료
currentProjection?.stop()
currentProjection = null
}
// 새 캡처 요청
mediaProjectionManager.createScreenCaptureIntent().also {
activity.startActivityForResult(it, REQUEST_CODE)
}
}
버전 체크로 분기하는 방법도 있지만, 어차피 매번 새로 요청하는 방식으로 통일하면 분기 자체가 필요 없다. 인스턴스 재사용으로 얻는 성능 이득보다 호환성 유지가 더 중요한 상황이면 아예 재사용을 포기하는 쪽이 깔끔하다.
Android 버전 파편화 대응은 어느 시점부터 "대응"이 아니라 "추적"에 가까워진다. 주요 버전이 올라올 때마다 바뀌는 정책을 놓치지 않으려면 릴리즈 노트를 정기적으로 확인하는 루틴이 필요하다. 이번 크래시도 14 출시 당시 챙겼어야 했는데 뒤늦게 잡은 거라 아쉬움이 남는다.
전체적으로 작업 규모 자체는 크지 않았지만, FCM 비동기 발송 설계부터 Android 버전별 대응까지 레이어를 가로질러 챙겨야 할 게 분산돼 있었다. 이런 자잘한 안정성 작업들이 눈에 잘 안 띄지만, 쌓이지 않으면 운영 중에 한꺼번에 터진다.
댓글 0
첫 댓글 달아줘.