EMV 3-Dセキュア対応で見えた、決済処理の実装時に考慮したいポイント

2025/06/06

こんにちは、グリーエックス株式会社の上田です。

以前、クレジットカード決済においてEMV 3-Dセキュアを導入する機会がありました。その中で、決済まわりの実装には考慮すべきポイントが非常に多いと感じたため、本記事ではそれらについて整理してみようと思います。

EMV 3-Dセキュア(3Dセキュア2.0)とは

EMV 3-Dセキュア(3Dセキュア2.0)は、オンラインでのクレジットカード不正利用を防ぐために、国際ブランドが推奨する本人認証サービスです。詳細はここでは割愛しますが、従来の3Dセキュア1.0では、すべての取引でIDやパスワードの入力が必要でした。一方、3Dセキュア2.0ではリスクベース認証が採用されており、取引のリスクレベルに応じて必要な場合のみ追加認証が行われます。この仕組みによって、多くの取引が追加認証なしで完了するため、カゴ落ちのリスクを軽減できるとされています。

以降では、EMV 3-Dセキュアに限らず、決済処理の実装時に考慮したいポイントを挙げていきます。

データの整合性の担保

これが最も重要なポイントと言っても過言ではありません。「決済は完了しているのに商品が提供されない」、あるいは「商品やサービスを提供したのに決済が処理されていない」といった状態が発生すると、金銭的な損失だけでなく、ユーザーの信頼を大きく損なう可能性があります。

トランザクション管理

決済処理が複数のステップにまたがる場合、すべての処理が成功しなければ一連の処理をロールバックさせる必要があります。たとえば、決済代行サービスへのAPIリクエスト、決済結果の保存、サービスの提供(例:コンテンツ付与やステータス変更など)が別々の処理単位になっていると、途中で失敗したときにデータの整合性が崩れるリスクがあります。ただし、外部APIとのやり取りは通常データベーストランザクションに含められないため、処理の順序やエラーハンドリング等の設計が特に重要です。詳細は後述します。

排他制御

同じリクエストが短時間に複数回送られた場合でも、処理が重複して実行されないようにする必要があります。ユーザーがページをリロードしたり、購入ボタンを連打したり、あるいはネットワーク遅延で再送されるケースなど、さまざまなパターンを想定して設計することが重要です。たとえば、「購入ボタンをクリックしたら即座にdisabledにする」といったUIレベルでの誤操作防止策も有効です。

外部APIを呼び出す際の処理順序

ここでは、決済代行サービスの提供する決済用のAPIを利用する場合を前提としています。

キャンセルできない/したくない処理を後回しにする

処理の途中でエラーが発生した場合に、アプリケーション側のDB操作は通常トランザクションでロールバック可能ですが、外部APIによる決済処理は一度成功してしまうと、別途キャンセル用のAPIを叩いて明示的に取り消す必要があります。ですが、そのキャンセル処理自体も失敗する可能性があるため、確実な整合性を担保するためになるべくキャンセル用のAPIは呼び出したくないです。

このような背景から、キャンセルできない/したくない処理はなるべく後に実行するという設計が有効な場合があります。

以下のコード例は、説明のために一部簡略化しています。実際の実装では、セキュリティや例外処理の観点からさらなる考慮が必要です。

Bad: 決済処理 → アイテム付与

def execute_payment_and_grant_items(payment_data, user)
  ActiveRecord::Base.transaction do
    # 1. 決済APIの呼び出し(外部処理)
    payment = execute_payment(payment_data)
 
    # 2. アイテムを付与(内部処理)
    grant_items(user, payment[:amount])
  end
rescue => e
  # 決済が済んでいれば決済のキャンセルを試みる
  cancel_payment(payment) if payment
 
  Rails.logger.error("Transaction failed: #{e.message}")
end

このような順序では、アイテム付与でエラーが発生した場合に「決済は済んでいるが、アイテムは付与されていない」という状態が起こりえます。そのため、外部の決済キャンセルAPIを呼び出す必要があります。

Good: アイテム付与 → 決済処理

def grant_items_and_execute_payment(payment_data, user)
  ActiveRecord::Base.transaction do
    # 1. アイテムを付与(内部処理)
    grant_items(user, payment_data[:amount])
 
    # 2. 決済APIの呼び出し(外部処理)
    execute_payment(payment_data)
  end
rescue => e
  Rails.logger.error("Transaction failed: #{e.message}")
end

この順序であれば、決済処理に入る前の段階でアプリケーション内部の処理(たとえばDB更新)を完了させておき、決済APIの呼び出しで失敗した場合には、トランザクションがロールバックされて内部状態も元に戻り、決済キャンセルAPIも呼び出す必要はなくなります。

データ不整合を検知・調査しやすくするためのアラート通知とログ設計

データ不整合を検知するためのアラート通知

たとえば、決済のキャンセル処理に失敗したようなケースでは、データ不整合が発生する可能性があるため、適切なアラート通知が必要です。ただし、すべてのエラーを通知してしまうと、運用者が重要な情報を見落とすリスクがあるため、通知の粒度やレベルをコントロールすることが重要です。

通知の設計例

  • 即時対応が必要なもの:サービス影響やユーザー影響が大きいと判断できるものは、リアルタイムに通知。

  • 定期確認でよいもの:月次集計やバッチ確認で十分なレベルのものは、日次・週次でサマリー通知し、詳細はログを確認。

  • 通知不要なもの:自動的にリトライされる処理や、最終的に整合性が取れると確信できるケースについては通知しない設計もあり。

調査しやすいログ出力

不整合や例外が発生した際には、調査時に必要な情報をログに残しておくことが重要です。特に以下のような情報が含まれていると、調査や後続の対応がスムーズになります。

  • 影響範囲を特定できる情報(例: ユーザーID、取引ID、タイムスタンプ など)
  • 処理のどの段階で失敗したのか(例: 決済API呼び出し時、DB保存時、Webhook受信時 など)
  • 外部APIのレスポンス内容やステータスコード(可能であれば)

特殊ケースの対応

可能な範囲で、特殊ケースを切り捨てることも選択肢の一つ

すべてのケースに対応しようとすると、開発や運用のコストが増大し、本来注力すべき部分にリソースを割けなくなる可能性があります。そのため、発生頻度が極めて低い、または発生しても影響が限定的なケースについては、仕様上切り捨てることも現実的な選択肢の一つかなと思います。

もちろん、どのケースを対応するか、対応しないかは、事業側の判断とエンジニア側の技術的判断のすり合わせが必要です。

データ不整合を検知するチェックバッチの導入

どれだけ丁寧にエラーハンドリングを設計しても、すべてのエラーケースを網羅するのは現実的には困難です。そのため、定期的にデータ整合性をチェックするバッチ処理を導入しておくことが有効な場合があります。

チェック内容の例:

  • DB上のステータスと決済代行サービス上のステータスが一致しているか
  • ステータスが「処理中」のまま一定時間以上経過しているデータがないか

このようなバッチ処理を定期実行することで、リアルタイムで検知できなかったデータ不整合を後からでも気づくことができ、クリティカルな障害が起きる前に対処することが可能になります。

おわりに

決済まわりの実装は、外部の決済代行サービスとの連携や多様なエラーパターンの考慮が必要になるため、どうしても複雑になってしまいます。できる限りのエラーケースを考慮して”内側”のロジックで守るのはもちろんですが、データの整合性をチェックするバッチ処理など”外側”からも不整合に気づいて対処できるような体制を整えておくことも重要だと思います。

2024年4月に新卒でグリーホールディングス株式会社に入社。