결제 알림 캡처 안정화
목차
결제 알림을 받아서 파싱하고 서버로 전송하는 Android 앱인데, v2.3 작업이 생각보다 길어졌다. 작업 목록만 보면 안정화 패치 수준이라 금방 끝날 것 같았는데, Android 14/15 대응이랑 주문 매칭 버그가 겹치면서 하루 반을 잡아먹었음.
앱의 기본 흐름은 단순하다. NotificationListenerService가 결제 알림을 캡처하면 정규식으로 금액과 은행 정보를 뽑아내고, 파싱 결과를 서버로 전송함. 파싱 실패 시엔 AI 폴백으로 재시도하는 구조가 붙어 있음.
| 항목 | 내용 |
|---|---|
| Android 14/15 대응 | MediaProjection API 변경사항 반영 |
| 원격 지원 안정화 | WebSocket 연결 유지 및 재연결 로직 보강 |
| FCM 처리 | 알림 수신 서비스 로직 개선 |
Android 버전 호환성 - 결국 권한 문제
Android 13부터 알림 권한이 런타임 요청 방식으로 바뀌었다. POST_NOTIFICATIONS 퍼미션을 사용자가 직접 허용해야 하는 구조인데, 기존 코드는 매니페스트 선언만으로 처리하고 있었음. 그래서 Android 13 이상 기기에서 알림을 아예 못 받는 케이스가 발생했고, 이번에 런타임 요청 흐름을 추가했음.
Android 14/15는 거기서 한 발 더 나갔다. MediaProjection 관련 API 시그니처가 일부 바뀌었고, 포그라운드 서비스 타입을 명시적으로 선언해야 하는 요건이 생겼음. 타입 없이 서비스를 시작하면 SecurityException으로 바로 터진다.
<!-- AndroidManifest.xml -->
<service
android:name=".NotificationService"
android:foregroundServiceType="dataSync"
android:exported="false"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
서비스 자체는 START_STICKY로 유지한다. 시스템이 서비스를 죽여도 재시작을 시도하는 모드인데, 메모리 부족 환경에서는 재시작이 지연되는 경우가 있어서 FCM 핑을 활용한 보조 웨이크업 로직도 같이 붙여뒀음.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
WebSocket 재연결도 이번에 손봤다. 원격 지원 기능이 WebSocket으로 연결을 유지하는데, 네트워크 끊김 후 재연결 타이밍이 불안정해서 지수 백오프 방식으로 교체했음. 고정 인터벌로 계속 때리면 서버 부하가 생기고, 아예 안 하면 연결이 죽은 채로 방치되니까. 재연결이 필요한 서비스라면 지수 백오프에 최대 대기 상한을 걸어두는 게 무난한 구조다.
결제 파싱 - 정규식과 AI 폴백
알림 텍스트에서 금액과 은행명을 뽑는 건 정규식으로 처리한다. 은행마다 알림 포맷이 제각각이라 패턴을 여러 개 두고 순서대로 시도하는 방식임.
문제는 은행들이 앱 업데이트마다 알림 문구를 슬그머니 바꾼다는 거다. 분명히 잘 되던 파싱이 어느 날 갑자기 실패하기 시작함. 그래서 파싱 실패 시 AI 폴백을 달아놨는데, 자연어 텍스트에서 금액·은행 정보를 뽑아달라고 요청하는 식이다. 정규식 실패율이 올라가면 패턴을 추가 보완하고, AI 폴백이 남아 있으니 당장 서비스가 끊기진 않는 구조.
파싱 결과를 신뢰하기 위한 검증 레이어도 있다. 금액이 0이거나 음수면 거부하고, 알림 발생 시각이 일정 범위를 벗어난 경우도 필터링함. 파싱은 성공했는데 결과가 이상한 케이스를 걸러내는 용도임.
주문 매칭 - 비회원 누락 버그
입금 알림이 들어오면 금액, 발신자 정보, 시간 조건으로 주문을 매칭한다. 동일 금액 주문이 여러 건이면 가장 최근 주문을 우선함.
여기서 버그가 하나 있었는데, 매칭 쿼리가 회원 주문만 보고 있었음. 비회원 주문은 검색 대상에서 빠져 있어서, 입금이 들어와도 주문이 매칭되지 않고 그냥 지나쳐버리는 상황이었다. 주문 수가 적을 때는 눈에 안 띄다가 비회원 주문이 쌓이면서 발견됐음.
쿼리 조건에서 회원 여부 필터를 제거하고 전체 주문 대상으로 돌리는 걸로 수정했다. 비슷한 매칭 로직을 짤 때는 초기 단계부터 "매칭 대상의 범위가 정말 맞는가"를 명시적으로 확인하는 게 맞다. 당연히 전체를 보겠지 하고 넘어가다가 이런 식으로 조용히 누락이 생긴다. 특히 사용자 유형이 두 가지 이상 공존하는 시스템에선, 쿼리 작성 시점에 어떤 유형이 대상에서 빠지는지를 한 번은 짚고 가야 함.
로그 레벨도 이번에 같이 정리했다. 개발 중에 verbose하게 찍어두던 로그들이 production 빌드에도 그대로 올라가 있었는데, 결제 관련 정보가 포함될 여지가 있어서 BuildConfig 플래그로 debug/release 빌드에서 출력 레벨이 달라지게 분리했음.
배포 쪽은 keystore 서명 설정을 local.properties에 분리해서 레포에 올라가지 않게 관리 중임. 지금 구조에선 이걸로 충분하고, 팀 환경으로 확장될 경우엔 secrets 관리 도구를 붙이면 됨.
작업 규모 자체는 크지 않았는데, Android 버전 파편화 대응은 생각보다 손이 많이 간다. 한 버전 올라갈 때마다 권한 모델이나 서비스 동작 방식이 조금씩 바뀌니까, 신규 버전 릴리즈 타이밍에 맞춰 미리 체크하는 습관을 들이는 게 나을 것 같다는 생각이 남았음.
댓글 0
첫 댓글 달아줘.