이 글은 Today I Learn 시리즈의 228번째 기록입니다. (총 230개)

화면을 끄면 멈추는 건 메트로놈 때 배운 문제였다

화면을 끄고 웹 서핑을 하면 서너 곡 뒤에 음악이 멈췄다. 다른 앱이 소리를 가져간 건가 싶어 게임 소리를 끄고, 전부 껐다가 다시 켜 봤는데도 세 곡쯤 뒤에 똑같이 멈췄다. 다른 앱 탓은 아니었다.

원인은 포그라운드 서비스였다. 안드로이드는 화면에 없는 앱이 계속 도는 것을 막는데, 계속 돌려면 「지금 뭔가 돌고 있다」는 알림을 띄우고 시스템에 신고해야 한다. 그 신고가 포그라운드 서비스다. 알림을 띄우는 의무를 지는 대신 시스템이 앱을 함부로 끄지 않는다.

이건 앞선 메트로놈 TIL 7에서 이미 배운 내용이고, 룸톤에서도 TIL 1에서 화면을 꺼도 재생되게 하려고 같은 장치를 달았다. 그때는 홈 버튼을 누르자마자 메트로놈이 멈춰서 바로 알아봤다. 이번엔 까먹고 있다가 원인을 듣고 다시 떠올렸다. 헷갈린 이유는 증상이 달라서다. 첫 곡은 멀쩡히 돌다가 몇 곡 뒤에야 멈췄다.

  • 룸톤은 신고를 제대로 해 두었지만, 곡과 곡 사이의 아주 짧은 멈춤을 재생 라이브러리(just_audio — Flutter 앱에서 소리를 내는 패키지)가 「일시정지」로 보고 신고를 거둬들였다.
  • 다음 곡이 시작돼도 신고를 다시 하지 않았고, 신고가 없는 앱은 몇 분 안에 시스템이 재웠다.
  • 일시정지해도 신고를 유지하게 해서 고쳤다. 대가는 음악을 멈춰 둔 동안에도 알림이 남는다는 것이다. 매장에서 하루 종일 트는 앱이라 받아들일 만했다.

한 번 배운 원리도 다른 모습으로 나타나면 못 알아본다. 「백그라운드에서 멈춘다」는 증상이 보이면 언제 멈추든 포그라운드 서비스부터 의심한다.


정기결제를 처음 붙여 보니 모든 단계가 처음이었다

룸톤은 정기결제가 있어야 굴러가는 첫 앱이다. 지금까지 만든 앱은 무료이거나 한 번 사면 끝이었다. 그래서 상품 등록부터 결제가 계정에 붙는 것까지 모든 단계에서 질문이 생겼다.

Creem에는 넘겨줄 물건이 없었다

Creem(해외 세금·정산을 대신 처리하는 결제 대행사)에 상품을 만들면서 두 가지를 물었다. 이게 SaaS인지, 그리고 라이선스 키를 파는 건지.

분류는 SaaS가 맞았다. 서버에서 돌아가는 서비스를 매달 쓰게 해 주는 것이기 때문이다. 하지만 라이선스 키는 아니었다. 라이선스 키는 사용자가 앱에 직접 입력해 잠금을 푸는 코드다. 설치형 프로그램을 팔 때 주로 쓴다. 룸톤은 로그인한 계정 자체가 이용권이라 따로 넘겨줄 키도 파일도 없다. 그래서 전달물 칸은 비우고, 결제 뒤에 보이는 안내에 「구독 관리·해지는 결제 확인 메일의 링크에서」만 적었다.

라이선스 키 없이 결제가 룸톤 계정에 붙는 길

그러자 궁금해졌다. 키도 없는데 Creem에서 낸 돈이 어느 룸톤 계정의 것인지 어떻게 아나. Creem에도 가입해야 해서 같은 계정을 써야 한다면, 차라리 구글 결제만 두면 되는 거 아닌가.

답은 결제를 누가 시작하느냐에 있었다.

  1. 매장이 판매 페이지에서 룸톤 계정으로 로그인한다.
  2. 「구독하기」를 누르면 룸톤 서버가 Creem에 결제 화면을 만들어 달라고 요청한다. 이때 「이 결제는 계정 몇 번의 것」이라는 메모(메타데이터)를 붙여 보낸다.
  3. 매장이 Creem 화면에서 결제한다. Creem 계정은 따로 만들지 않아도 된다.
  4. 결제가 끝나면 Creem이 룸톤 서버로 알림(웹훅, 일이 생기면 상대 서버가 우리 서버 주소로 보내는 알림)을 보낸다. 알림에는 2번에서 붙인 메모가 그대로 들어 있다.
  5. 서버가 메모의 계정 번호를 보고 그 계정의 구독을 켠다.

라이선스 키는 사용자가 결제와 계정을 이어 준다. 이 방식에서는 결제를 시작한 쪽이 메모로 둘을 이어 준다. 대신 Creem 상품 링크로 바로 가서 사면 메모가 없어서 누구 결제인지 알 수 없다. 그래서 결제 입구는 반드시 로그인 뒤의 판매 페이지여야 한다.

해 본 적이 없어서 설명만으로는 확 와닿지 않았다. 아직 실제로 결제를 한 바퀴 돌려 보지 않았으니, 그걸 해 봐야 확실히 이해할 것 같다.

가격 하나를 정하니 나라마다 현지 가격이 붙었다

플레이 콘솔에서 구독 상품의 가격을 하나 넣자, 콘솔이 판매하는 나라 전부에 그 나라 통화로 된 가격을 채워 넣었다. 환율만 곱한 숫자가 아니라 각 나라에서 자연스러운 끝자리로 맞춘 가격이었다. 신기했다.

이 기능을 보니 플레이 구독이 달리 보였다. Creem은 달러로만 받아서 월 $8로 정했고, 매장은 결제할 때 자기 카드사 환율로 달러를 낸다. 플레이에서는 한국 매장은 원으로, 일본 매장은 엔으로 익숙한 숫자를 본다. 나라별 세금도 플레이가 알아서 붙인다.

  • 나라마다 가격을 따로 정할 수도 있다. 자동으로 채워진 가격은 시작점일 뿐이고, 물가가 다른 시장에 맞춰 손볼 수 있다.
  • 환율이 크게 바뀌면 자동 가격과 웹의 달러 가격이 벌어질 수 있다. 두 경로를 같이 운영하면 가끔 맞춰 봐야 한다.

결제 경로를 고를 때 수수료만 봤는데, 가격을 현지화하고 세금을 붙여 주는 일도 경로가 대신해 주는 일이다. 이걸 보고 웹 결제를 꼭 따로 둬야 하는지 다시 따져 보기로 했다.

플레이 서버에 물어볼 자격을 따로 줘야 했다

플레이 구독은 앱 안에서 결제가 끝나도 그걸로 끝이 아니다. 앱이 영수증을 서버에 보내면, 룸톤 서버가 구글에 「이 구매 진짜 맞아?」라고 다시 묻고 나서 구독을 켠다. 앱이 보낸 영수증만 믿으면 조작된 영수증도 통과하기 때문이다.

그러려면 룸톤 서버가 구글에 물어볼 자격이 필요하다. 사람이 아닌 프로그램용 계정인 서비스 계정을 만들고, 그 열쇠 파일(JSON 키)을 서버 비밀값으로 넣는다. 처음 「서비스 계정을 만들고 JSON 키를 내려받으라」는 안내를 봤을 때는 무슨 말인지 몰랐다. 서버에 줄 신분증이라고 이해하니 순서가 보였다.

그런데 테스트 앱에서 구독하자 「구매를 확인하지 못했다」가 떴다. 앱에 보이는 건 502라는 숫자뿐이었고, 실패 이유를 로그에 남기게 한 뒤 다시 해 보니 구글이 「권한 없음」으로 거절하고 있었다. 서비스 계정을 플레이 콘솔에 초대만 했고, 룸톤 앱의 재무 데이터 보기·주문과 구독 관리 권한은 주지 않았던 것이다. 권한을 준 뒤에도 한동안 반영되지 않다가, 구독 상품을 한 번 고쳐 저장하자 통과했다.

  • 초대는 들어올 자격이고, 앱별 권한은 무엇을 할 수 있는지다. 둘은 따로 줘야 한다.
  • 실패가 숫자 하나로만 보이면 원인을 추측하게 된다. 실패 이유를 남겨 두니 한 번에 짚였다.

확인하지 않은 구매는 환불된다

확인이 실패하던 중에 플레이에서 「개발자가 첫 구매를 확인하기 전에는 요금제 기간을 연장할 수 없다」는 문구를 봤다. 무슨 소리인지 물었다.

플레이에서는 결제가 됐다고 끝이 아니다. 판매자 서버가 「이 구매를 받았다」고 승인(acknowledge)해야 한다. 정해진 기간 안에 승인이 없으면 플레이가 구매를 취소하고 돈을 돌려준다. 앱이 멈췄거나 서버가 죽어 사용자가 돈만 내고 아무것도 못 받는 일을 막기 위해서다.

그래서 구매 확인은 있으면 좋은 기능이 아니라 매출이 남기 위한 조건이다. 서버의 확인 과정이 고장 나면 화면에서 에러가 나는 데서 끝나지 않고, 받은 돈이 되돌아간다.

테스트 구독은 공짜고 5분이면 한 달이 지난다

라이선스 테스터로 등록한 계정은 실제 결제 화면을 거치지만 돈이 나가지 않는다. 신기했다. 실제 카드로 결제하고 환불하는 번거로움 없이 구독 흐름을 끝까지 돌려 볼 수 있다.

게다가 테스트 구독은 시간이 빨리 간다. 월 구독이 5분마다 갱신되고, 해지하면 5분 뒤에 끝난다. 그래서 「구독이 끝나면 30초 미리듣기로 돌아가는가」를 한 달 기다리지 않고 그날 밤 바로 확인했다. 제대로 돌아가긴 했지만 바뀌는 사이 로딩이 한참 걸리는 것도 이때 보였다.

구독 앱에서 버그가 숨기 쉬운 곳은 결제 순간보다 한 달 뒤, 갱신과 만료 때다. 시간을 줄여 주는 테스트 환경이 있으니, 출시 전에 만료까지 한 바퀴 돌려 봐야 한다.


앱에서 웹 결제로 가는 길에도 정식 창구가 있었다

앱 설정에서 웹 판매 페이지로 가는 길을 만들고 싶었다. 그런데 판매 페이지에 구독 버튼이 있어서, 앱에서 거기로 보내면 플레이 밖 결제로 이끄는 것이 된다. 그럼 요금을 탭 뒤에 숨기면 되지 않을까 생각했지만, 몇 번 눌러서 결제에 닿으면 결국 돌려서 이끄는 것이라 위험은 같았다. 그래서 앱에서 넘어가는 곳은 결제로 가는 길이 아예 없는 계정 화면으로 정했다.

돌아가는 길 말고 정면으로 가는 길도 있었다. 한국에서는 플레이의 외부 결제 프로그램에 등록하면 앱에서 웹 결제로 공식적으로 보낼 수 있다. 그래서 등록하기로 했다.

메트로놈 때 배운 태도와 같은 모양이다. 플랫폼이 막아 둔 것을 하고 싶으면, 대개 우회로보다 정식 창구가 있다. 창구는 공짜가 아니다. 포그라운드 서비스는 알림을 띄우는 게 대가였고, 외부 결제 프로그램은 수수료가 없어지지 않고 줄어들 뿐이며 거래를 보고해야 한다(앞선 TIL 12의 표). 우회로는 걸리면 앱이 내려가지만, 창구의 대가는 미리 계산할 수 있다.


요약

  • 백그라운드에서 멈추면 언제 멈추든 포그라운드 서비스부터 본다. 한 번 배운 원리도 증상이 다르면 못 알아본다.
  • 계정이 곧 이용권인 서비스는 라이선스 키가 필요 없다. 결제를 시작하는 쪽이 계정 번호를 메모로 붙이고, 결제사의 알림이 그 메모를 돌려준다. 그래서 결제 입구는 로그인 뒤여야 한다.
  • 플레이는 가격 하나로 나라별 현지 가격과 세금을 채워 준다. 결제 경로가 대신해 주는 일은 수수료 말고도 있다.
  • 플레이 구독은 서버가 구글에 다시 물어서 확인한다. 서비스 계정은 초대와 앱별 권한을 따로 줘야 한다.
  • 플레이는 승인하지 않은 구매를 환불한다. 구매 확인은 매출의 조건이다.
  • 테스트 구독은 공짜이고 5분 주기라서, 갱신과 만료까지 출시 전에 돌려 볼 수 있다.
  • 플랫폼이 막은 길은 우회하지 말고 정식 창구의 대가를 계산한다.

Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 습관만들기 Pawbit TIL 1 27 insertOnConflictUpdate vs DoUpdate(target) 28 세션 필터 29 아코디언(Accordiaon) UI를 펼친상태로 만들기 30 input의 step 31 습관만들기 Pawbit TIL 2 32 습관만들기 Pawbit TIL 3 33 Word Cloud 34 Google Sheets를 데이터 버스로(with AppSheet) 35 Django 모델 텍스트 필드 자동 수집 패턴 36 localStorage로 섹션 토글 상태 유지 37 순차 ID 생성(`select_for_update()` + `max()` 조합) 38 역참조 검색과 distinct() 39 xlsx 다운로드와 로딩 오버레이 충돌 40 Android 파일 공유 MIME 타입 41 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 42 크레센도 Phaser + TypeScript TIL 1 43 크레센도 Phaser + TypeScript TIL 2 44 크레센도 Phaser + TypeScript TIL 3 45 크레센도 Phaser + TypeScript TIL 4 46 크레센도 Phaser + TypeScript TIL 5 47 여둘까 Office Layout TIL 1 48 습관만들기 Pawbit TIL 4 49 여둘까 Office Layout TIL 2 50 여둘까 Office Layout TIL 3 51 크레센도 Phaser + TypeScript TIL 6 52 UTF-8 BOM과 PowerShell 파일 쓰기 53 소리꽃 KeyBloom TIL 1 54 습관만들기 Pawbit TIL 5 55 여둘까 Office Layout TIL 4 56 소리꽃 KeyBloom TIL 2 57 소리꽃 KeyBloom TIL 3 58 메트로놈 Simple Metronome TIL 1 59 소리꽃 KeyBloom TIL 4 60 메트로놈 Simple Metronome TIL 2 61 소리꽃 KeyBloom TIL 5 62 습관만들기 Pawbit TIL 6 63 크레센도 Phaser + TypeScript TIL 7 64 메트로놈 Simple Metronome TIL 3 65 소설 타임라인 관리 도구 Lumior TIL 1 66 습관만들기 Pawbit TIL 7 67 정적 블로그 SEO 정비와 Pagefind 검색 도입 68 크레센도 Phaser + TypeScript TIL 8 69 메트로놈 Simple Metronome TIL 4 70 습관만들기 Pawbit TIL 8 71 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 72 메트로놈 Simple Metronome TIL 5 73 모래게임 Sandrop TIL 1 74 습관만들기 Pawbit TIL 9 75 메트로놈 Simple Metronome TIL 6 76 모래게임 Sandrop TIL 2 77 모래게임 Sandrop TIL 3 78 습관만들기 Pawbit TIL 11 79 습관만들기 Pawbit TIL 10 80 메트로놈 Simple Metronome TIL 7 81 모래게임 Sandrop TIL 4 82 모래게임 Sandrop TIL 5 83 습관만들기 Pawbit TIL 12 84 모래게임 Sandrop TIL 6 85 소리꽃 KeyBloom TIL 6 86 모래게임 Sandrop TIL 7 87 모래게임 Sandrop TIL 8 88 소리꽃 KeyBloom TIL 7 89 모래게임 Sandrop TIL 9 90 소리꽃 KeyBloom TIL 8 91 모래게임 Sandrop TIL 10 92 소리꽃 KeyBloom TIL 9 93 콘티온 ContiOn TIL 1 94 소리꽃 KeyBloom TIL 10 95 콘티온 ContiOn TIL 2 96 습관만들기 Pawbit TIL 13 97 콘티온 ContiOn TIL 3 98 룸톤 Roomtone TIL 1 99 모래게임 Sandrop TIL 11 100 소리꽃 KeyBloom TIL 11 101 여둘까 Office Layout TIL 5 102 모래게임 Sandrop TIL 12 103 모래게임 Sandrop TIL 13 104 콘티온 ContiOn TIL 4 105 모래게임 Sandrop TIL 14 106 소리꽃 KeyBloom TIL 12 107 습관만들기 Pawbit TIL 14 108 모래게임 Sandrop TIL 15 109 모래게임 Sandrop TIL 16 110 소리꽃 KeyBloom TIL 13 111 온실 GreenHouse TIL 1 112 콘티온 ContiOn TIL 5 113 모래게임 Sandrop TIL 17 114 소리꽃 KeyBloom TIL 14 115 온실 GreenHouse TIL 2 116 콘티온 ContiOn TIL 6 117 모래게임 Sandrop TIL 18 118 콘티온 ContiOn TIL 7 119 모래게임 Sandrop TIL 19 120 소리꽃 KeyBloom TIL 15 121 룸톤 Roomtone TIL 2 122 모래게임 Sandrop TIL 20 123 모래게임 Sandrop TIL 21 124 소리꽃 KeyBloom TIL 16 125 소리꽃 KeyBloom TIL 17 126 소리꽃 KeyBloom TIL 18 127 여둘까 Office Layout TIL 6 128 소리꽃 KeyBloom TIL 19 129 소리꽃 KeyBloom TIL 20 130 콘티온 ContiOn TIL 8 131 콘티온 ContiOn TIL 9 132 로그스톤 상표 셀프 출원 TIL 1 133 소리꽃 KeyBloom TIL 21 134 로그스톤 샵 LogStone Shop TIL 1 135 소리꽃 KeyBloom TIL 22 136 로그스톤 샵 LogStone Shop TIL 2 137 룸톤 Roomtone TIL 3 138 소리꽃 KeyBloom TIL 23 139 모래게임 Sandrop TIL 22 140 모래게임 Sandrop TIL 23 141 소리꽃 KeyBloom TIL 24 142 로그스톤 샵 LogStone Shop TIL 3 143 모래게임 Sandrop TIL 24 144 소리꽃 KeyBloom TIL 25 145 모래게임 Sandrop TIL 25 146 MiniMacro TIL 1 147 룸톤 Roomtone TIL 4 148 모래게임 Sandrop TIL 26 149 콘티온 ContiOn TIL 10 150 Astro가 무엇인지, 왜 옮기는지 151 룸톤 Roomtone TIL 5 152 룸톤 Roomtone TIL 6 153 모래게임 Sandrop TIL 27 154 소리꽃 KeyBloom TIL 26 155 MiniMacro TIL 2 156 로그스톤 샵 LogStone Shop TIL 4 157 메트로놈 Simple Metronome TIL 8 158 모래게임 Sandrop TIL 28 159 여둘까 Office Layout TIL 7 160 룸톤 Roomtone TIL 7 161 메트로놈 Simple Metronome TIL 9 162 블로그 Astro 이관 TIL 163 여둘까 Office Layout TIL 8 164 로그스톤 샵 LogStone Shop TIL 5 165 모래게임 Sandrop TIL 29 166 오픈데이 Openday TIL 1 167 로그스톤 샵 LogStone Shop TIL 6 168 모래게임 Sandrop TIL 30 169 오픈데이 Openday TIL 2 170 모래게임 Sandrop TIL 31 171 Unreal Engine MCP TIL 1 172 모래게임 Sandrop TIL 32 173 소리꽃 KeyBloom TIL 27 174 콘티온 ContiOn TIL 11 175 오픈데이 Openday TIL 3 176 로그스톤 샵 LogStone Shop TIL 7 177 모래게임 Sandrop TIL 33 178 소리꽃 KeyBloom TIL 28 179 오픈데이 Openday TIL 4 180 여둘까 Office Layout TIL 9 181 오픈데이 Openday TIL 5 182 ScorePlayer TIL 1 183 모래게임 Sandrop TIL 34 184 BuildingHub TIL 1 185 ScorePlayer TIL 2 186 여둘까 Office Layout TIL 10 187 BuildingHub TIL 2 188 ScorePlayer TIL 3 189 Unreal Engine MCP TIL 2 190 궁 미로 PalaceMaze TIL 1 191 메트로놈 Simple Metronome TIL 10 192 메트로놈 Simple Metronome TIL 11 193 오픈데이 Openday TIL 6 194 BuildingHub TIL 3 195 Obsidian vault에서 코드만 빼기 — directory junction 196 ScorePlayer TIL 4 197 여둘까 Office Layout TIL 11 198 오픈데이 Openday TIL 7 199 URL은 바뀔 수 있는 것에 묶지 않는다 200 돌이키기 힘든 변경은 작게 먼저 확인한다 201 도구는 기능이 아니라 내 일에 맞는지로 고른다 202 궁 미로 PalaceMaze TIL 2 203 궁 미로 PalaceMaze TIL 3 204 소리꽃 KeyBloom TIL 29 205 오픈데이 Openday TIL 8 206 룸톤 Roomtone TIL 8 207 BuildingHub TIL 4 208 ScorePlayer TIL 5 209 로그스톤 샵 LogStone Shop TIL 8 210 여둘까 Office Layout TIL 12 211 오픈데이 Openday TIL 9 212 BuildingHub TIL 5 213 MiniMacro TIL 3 214 룸톤 Roomtone TIL 10 215 룸톤 Roomtone TIL 9 216 소리꽃 KeyBloom TIL 30 217 오픈데이 Openday TIL 10 218 여둘까 Office Layout TIL 13 219 룸톤 Roomtone TIL 11 220 소리꽃 KeyBloom TIL 31 221 궁 미로 PalaceMaze TIL 4 222 룸톤 Roomtone TIL 12 223 로그스톤 샵 LogStone Shop TIL 9 224 룸톤 Roomtone TIL 13 225 로그스톤 샵 LogStone Shop TIL 10 226 룸톤 Roomtone TIL 14 227 소리꽃 KeyBloom TIL 32 228 룸톤 Roomtone TIL 15 읽는 중 229 룸톤 Roomtone TIL 16 230 룸톤 Roomtone TIL 17