端末・Apple・サーバー、3者の決済状態をどう同期させるか — Voicy+ iOSアプリ内課金の裏側

左からいく、まさ、ささん、Sammy

はじめに

 Voicyには「Voicy+」という有料プランがあります。広告が非表示になったり、お気に入りのパーソナリティをグルーピングして聴きやすくする「フォローグループ」機能が使えたりと、リスナー体験をより快適にするためのプランです。

 これまでVoicy+への加入は、Webでクレジットカードを登録し、決済するフローしかありませんでした。アプリでVoicyを聴いているのに、加入のためだけにブラウザへ移動してカード情報を入力する——この一手間が加入のハードルになっていました。この課題を解消するため、私たちは約3ヶ月半をかけて「iOSアプリ内課金」を実装し、リリースしました。

 この記事では、次の3つをお話しします。

  • 技術: 端末・Apple・Voicyサーバーの3者で決済状態の整合性をどう取ったか
  • チーム: バックエンドは今回が初挑戦のメンバーを含む4人チームが「自然言語から始める設計」でどう進めたか
  • AI : 学習・設計・実装・QAの各局面で、DevinとClaudeをどう活用したか

 なお本記事は、プロジェクトメンバー4人による社内トークセッション(音声プラットフォームの会社らしく、ちゃんと収録しました)の文字起こしをもとに執筆しています。 声はこちらから👇

voicy.jp

プロジェクト概要

 体制はエンジニア4名。プロダクトエンジニアが3名——バックエンドエンジニアだったまさ(チームリード)、iOSエンジニアだったささん、Androidエンジニアだったいく——と、QAとプロジェクト管理を兼務したSammyという役割分担です。期間は約3ヶ月半です。

 Web決済ではアプリからWebに移動して、クレジットカードを登録した上で加入する必要がありました。アプリ内課金なら、購入ボタンを押してFace IDなどの認証が通ればそれだけで加入が完了します。有料放送やプレミアムリスナー機能ですでに提供していたアプリ内課金の体験を、Voicy+にも広げた形です。

最大の難所は「整合性」だった

 今回の実装で最も苦労したのは、Apple側の決済状態とVoicyサーバー側の状態をどう同期させるか、という一点に尽きます。

 ユーザーが決済を行うと、まずユーザーの端末とAppleの間で通信が行われ、それが成功して初めてVoicyサーバーへの連携が走ります。ところが、VoicyサーバーからはApple側の決済状態を常に100%把握できるわけではありません。決済自体はApple側で成功しているのに、その情報がVoicyサーバーへうまく伝わらず、失敗したかのように見えてしまうケースがあります。

 Web決済では状態を管理する対象は「決済代行サービス」と「Voicyサーバー」の2者でした。それがアプリ内課金では「ユーザーの端末」「Apple」「Voicyサーバー」の3者に増え、それぞれがずれる可能性がある。正常系の流れをそのまま書くだけでは、不整合パターンを取りこぼします。

 この問題に対して私たちが採った方針は、「起こった出来事を、完了した時点で記録していく」 という設計です。処理の途中状態を推測で管理するのではなく、確定した事実をイベントとして記録し、それをもとに現在の状態を判定・復元できるようにしました。

ささん「正直、正常系の流れを作るだけなら簡単で、多分自分ひとりでもいけたんですよ。難しいのは、どこで失敗しているかを知れるようにすること。だから『起こった出来事を、完了したら記録していく』方法に落ち着きました」

Event Sourcingで「起きたこと」をすべて記録する

 具体的には、Event Sourcingの考え方を取り入れ、Appleからの通知(App Store Server Notifications)をすべてDBに記録しています。発生したイベントを順番に組み立てれば「いま、どういう状態なのか」が分かり、何かあったときには「いつ、何が起きたのか」を遡って追跡できる仕組みです。

 イベントの取りこぼしを防ぐため、通知の処理は次の2段構えにしました。

 通知を受け取るとまずSQSにメッセージを流し、それをポーリングするワーカーがイベントの記録とイベントごとに必要な処理を行います。この構成のポイントは、どこで失敗してもイベントを失わないことです。SQSへの投入に失敗した場合はAppleにエラーを返せば最大5回まで通知を再送してくれますし、ワーカー側の処理に失敗した場合はメッセージがDLQ(デッドレターキュー)に入るので、あとから手動または自動でDBに記録を補い、正しい状態を作り直せます。

 あわせて、処理全体を冪等に作りました。処理が途中で止まってしまっても、ユーザーがもう一度同じ操作をすれば正しい状態に収束します。

「Appleのサブスクリプション」という概念を分ける

 この設計にたどり着くまでには、採用しなかった案もあります。開発当初は「Voicyのサブスクリプション」という概念しか考えておらず、そこに紐づく「加入プロセスを管理するモデル」を置く想定でした。加入の流れを少しずつ記録していき、完了したら初めて加入状態になる、というイメージです。

 しかしこの案には課題がありました。課金の完了はApple側に依存していてVoicy側では制御できず、プロセス完了の判断が難しい。「通知が届いたら完了」とする案も考えましたが、通知は遅れて届く可能性があり、プロセスが完了するまで機能を提供できないとなると「課金したのに使えない」状態が生まれかねません。加入を試みるたびに新しいプロセスが作られる流れになるため冪等性の担保も難しく、さらにiOSのアプリ内課金にはVoicy側で制御できない加入導線(設定アプリからの更新や再加入など)もあり、それらを加味し始めるとモデルがどんどん複雑で曖昧なものになっていきました。

 そこで「Appleのサブスクリプション」を独立した概念として導入し、Voicyのサブスクリプションとは分けて扱うことにしました。Appleのサブスクリプションの状態をもとにVoicyのサブスクリプションの状態を決める——この一方向の依存に整理したことで、シンプルな作りに落とし込めました。

 加えて、検証環境であるAppleのサンドボックスでは本番と同じように通知が届かないことがあり、動作確認自体が難しいという別の壁もありました。ドキュメントを読み込みながら進める中で「こんなパターンもあるのか」という発見が都度出てきて、その都度、既存の設計で耐えられるかを見直す。変更しやすいコードを保っていたからこそ吸収できた、地道な検証の積み重ねでした。

自然言語から始める設計で、認識のズレを防ぐ

 今回のチームは、バックエンド実装の経験期間が短いメンバーが開発者3人のうち2人という編成でした。だからこそ意識したのが「まず日本語で処理の流れを書き、それをチームの共通認識にしてからコードに落とす」という進め方です。

 いきなり実装に入ると、理解の速い人だけが前に進み、認識のズレがどんどん大きくなります。自然言語でロジックを一度言語化することで、「分かっている人」と「これから理解しようとしている人」のギャップを小さく保ったまま進められる。今回のプロジェクトで得た、チームとしての大きな学びです。

ささん「まささんの解説を聞きながら、最初は『何を言ってるんだろう』と思ってたんですけど(笑)、実装が終わる頃には『確かに』となるくらい理解できていた。コミュニケーションを取るのが大事だと実感しました」

 設計の議論も、実装方針が固まってから共有するのではなく、ホワイトボードを囲んで4人全員で「どう作るか」をゼロから一緒に考えるスタイルを取りました。QAエンジニアの視点も交えてその場で疑問点をぶつけられたことで、実装理解が深まっただけでなく、「確かにそこも考慮しないといけないですね」という気づきがその場で生まれる場面も多くありました。

 ただ、うまくできなかったこともあります。データモデリングのような抽象度の高い工程は、結局ささんといくにはほとんど触れてもらえませんでした。「もっと小さくタスクを切って、一部だけでも任せていれば、実装のイメージもつかみやすかったし、バックエンドの力も付いたはず」——チームリードのまさからは、そんな反省も出ています。

AIをどう使ったか:DevinとClaudeの活用

 このプロジェクトのもうひとつの大きなテーマがAI活用です。コーディング支援にとどまらず、学習・設計・実装・QAの各局面で役割を持たせました。

学習の壁を越える相棒として。 バックエンド経験の期間が短いメンバーにとって、既存コードを一人で読み解くのは簡単ではありません。「このファイルはこういう流れでこういう処理をしている」という前提をまずAIに教えてもらうことで、コードへの入り方が大きく変わりました。

いく「最初は分からない言葉だらけで、一般知識もVoicyドメイン固有の知識も、一つずつ細かく質問して、分かるまでずっとやり取りしていました。細かい疑問はみんなの時間を取って聞くようなものでもないので、本当に助かりました」

設計の壁打ち相手として。 メンバーに設計相談を持ちかける前に、AIと対話して自分の考えを整理する。準備なしに行くと議論が発散しますが、事前に壁打ちしてから臨むことでディスカッションがスムーズになり、お互いの時間の無駄が減りました。

タスクの並列実行として。 アウトプットのイメージが固まっているタスクはDevinに任せ、自分は別のタスクを並行して進める。

まさ「AIがなかったら『頭の中にはあるけど、まだコードにできていない』という待ち時間が生まれていたはず。それをなくせるのはすごく大きい」

QAのテスト分析として。 要件や設計メモをAIに読み込ませ、既存機能へのリグレッション観点と新規実装で必要になるテスト項目の両方を洗い出してもらいました。これまで画面一覧に手作業でチェックを付けていた工程が、AIの出力を人が最終確認するだけの形に変わり、体感で1時間半かかっていた作業が30分程度になりました。

生産性への効果。 あくまでチームの感覚値ですが、AI活用による生産性向上は1.5倍から、専門分野外の作業では2倍以上。コーディングの手入力が不要になったことに加え、専門外の技術のキャッチアップコストが大きく下がったことが、実感として大きかったポイントです。

これから

 iOSアプリ内課金によって、Voicy+への加入のハードルは確実に下がりました。ここからは機能の拡充に加えて、加入者数や解約率といったビジネス指標を意識した開発に、チームとしてより主体的に向き合っていきます。どうやったらVoicy+をもっと使ってもらえるのか。それをチーム自身が考えてPDMに相談・提案し、機能を早く作って触ってもらい、「入ってよかった」と思えるものに育てていく。次のフェーズではそこに挑みます。

 メンバー個人としても、iOSとバックエンドの両方に触れた今回の経験を、一気通貫で機能を作れるプロダクトエンジニアとしての力に変えていきたいという声が上がりました。

おわりに

 この記事は、社内トークセッションの音声を文字起こしした内容をもとに執筆しました。音声プラットフォームを作る私たちらしく、自分たちの声で開発を振り返り、それを記事としてお届けする。技術やAI活用の話だけでなく、そうした形式そのものにもVoicyらしさを感じていただけたら嬉しいです。

 Voicyでは、音声×技術に挑戦したいエンジニアを募集しています。今回のような開発の進め方に興味を持った方は、ぜひカジュアル面談でお話ししましょう。

📮 Voicy採用ページ 💬 カジュアル面談のお申し込みはこちら