ストラテジーパターンを活用した柔軟な決済モジュールの実装記

ストラテジーパターンを活用した柔軟な決済モジュールの実装記

決済モジュールの実装を始めるにあたって、最初に直面した課題は、単に「決済承認 APIをどのように呼び出すか」ではありませんでした。核心となるのは、「今後どのような決済代行会社(PG)が追加されても、サービスの決済フローを揺るがすことなく、一貫した決済体験を提供できるか」でした。

初期MVPの目標はト* ペイメンツ(T***Payments)との連携でしたが、今後、決済手段、精算条件、海外決済対応(Stripeなど)、顧客企業の要件に応じて、複数のPGを同時にサポートしなければならない状況が発生する可能性があります。そこで、単純なAPI連携を超えて、次のような目標を設定しました。

  • さまざまなPG連携コードを一つに集約し、断片化を防ぎます。

  • 新しいPGを容易に追加できる構造的な拡張性を確保します。

  • 決済データを内部DBに保存し、追跡およびデバッグを容易にします。

1. if-else分岐処理の限界とStrategy Patternの導入

さまざまなPGとの連携を決済サービスのロジック内に直接実装することもできます。しかし、Stripe、K****Pay、海外PGなどが追加されるたびに、決済フローは次のように条件文で埋め尽くされることになります。

if (pgProvider.equals("T***Payments")) {
    // T*** 승인 로직
} else if (pgProvider.equals("Stripe")) {
    // Stripe 승인 로직
} else if (pgProvider.equals("SomeOtherPg")) {
    // 다른 PG 승인 로직}

この方式には、PGが追加されるほど、決済の中核となるビジネスフローと外部連携の詳細が強く結合されるという致命的な欠点があります。決済フローは「金額検証 → PG承認 → 成功/失敗処理」という大きな流れだけに集中すべきであり、各PGの認証ヘッダーやレスポンスの解析方式まで知る必要はありません。これらを分離するために、Strategy Patternを適用しました。

2. 共通インターフェース(PgProxy)と実装クラスの設計

まず、すべてのPG事業者が共通して実装すべきPgProxyインターフェースを定義しました。決済フロー側では、PG事業者の種類に関係なく、「決済を承認してください」という同じメッセージを渡すだけで済みます。

public interface PgProxy {
    String getPgProviderType();
    WebClient getWebClient();
    PgPaymentResponse approvePayment(String paymentKey, String orderId, Money amount);
    PgPaymentResponse getPgPaymentResponse(String orderId);
}

ト* ペイメンツの実装クラスは、上記のインターフェースを継承して次のように実装します。

@Configuration
@ConditionalOnProperty(value = "pg.t***.enabled", havingValue = "true")
public class T***PgProxy implements PgProxy {
    @Value("${pg.t***.secret-key}")
    private String secretKey;
    @Override
    public PgPaymentResponse approvePayment(String paymentKey, String orderId, Money amount) {
        T***Payment response = confirmPayment(paymentKey, orderId, amount);
        return new PgPaymentResponse(response);
    }
    @Override
    public String getPgProviderType() {
        return "T***Payments";
    }
}

ここでは、@ConditionalOnPropertyアノテーションを活用し、設定値(pg.t***.enabled=true)に応じて、環境ごとに必要なPG実装クラスだけがSpring Bean(bean)として動的に登録されるよう制御しました。今後追加されるStripePgProxyも同じインターフェースを実装して、あらかじめ準備しておくことができます。

3. 適切なStrategyを見つけるルーティングファクトリー(PgProxyFactory)

次に、リクエストされたPGタイプに対応する実装クラスを見つけるPgProxyFactoryを構成します。

@Component
public class PgProxyFactory {
    private final Map<String, PgProxy> pgProxies;

    public PgProxyFactory(List<PgProxy> pgProxies) {
      this.pgProxies = pgProxies.stream()
                .collect(Collectors.toMap(PgProxy::getPgProviderType, Function.identity()));
    }
    public PgProxy getPgProxy(String pgProvider) {
      PgProxy pgProxy = pgProxies.get(pgProvider);
      if (pgProxy == null) {
          throw new NoSuchElementException("지원하지 않는 결제 대행사입니다: " + pgProvider);        }
      return pgProxy;
    }
}

SpringコンテナがPgProxy型のすべてのBeanをリストとして注入すると、ファクトリーはそれをMap形式に変換してメモリ上に保持します。その後、ビジネスロジックからPG識別子(Enumなど)を渡すと、適切な実装クラスを即座に返します。クラス名はFactorykakaですが、ここではオブジェクトの「生成」よりも、実行時環境に応じた「Strategyオブジェクト」を選択してルーティングするStrategy Managerとしての役割を担います。

4. 中核ビジネスロジックの簡素化

Strategy Patternを適用した実際の決済サービスロジックは、外部通信の詳細から完全に解放されます。

@Service
@Transactional
@RequiredArgsConstructor
public class PaymentNeutFlow {
    private final PaymentTask paymentTask;
    private final PgProxyFactory pgProxyFactory;
    private final PaymentLogic paymentLogic;

    public String requestPayment(String paymentId, BigDecimal amount, String pgPaymentKey) {
      paymentTask.updatePgPaymentKey(paymentId, pgPaymentKey);
      try {
          Payment payment = paymentLogic.findPayment(paymentId);
          paymentTask.validateAmount(paymentId, amount);
          // 외부 PG 통신: 팩토리에서 구현체를 가져와 승인 요청만 위임
          PgPaymentResponse pgPaymentResponse = pgProxyFactory
               .getPgProxy(payment.getPgProvider())
               .approvePayment(pgPaymentKey, payment.getId(), payment.getAmount());
          paymentTask.markSuccess(pgPaymentResponse.getOrderId());
          return pgPaymentResponse.getOrderId();
      } catch (Exception e) {
          paymentTask.markFailed(paymentId);
          throw e;
      }
   }
}

これでPaymentNeutFlowは、特定のPGのAPI URLやレスポンスオブジェクトの形式を知る必要がありません。決済キーの保存、金額検証、ファクトリーを通じた承認の委譲、成功/失敗状態の処理という、本来のビジネス責任だけに集中します。

5. 拡張に開かれた構造(OCP原則の達成)

この構造の真価は、新しいPG(例:N***Pay)を追加するときに発揮されます。必要な作業はわずか2つです。

  1. PgProviderEnumにN***Pay定数を追加します。

  2. PgProxyを継承するN***PgProxyクラスを新規作成します。

既存の決済フロー(PaymentNeutFlow)やファクトリー(PgProxyFactory)のコードは、1行も修正する必要がありません。Springが新しい実装クラスを自動的に検出して注入するためです。変更範囲が極めて限定されるため、既存機能にバグが発生する可能性(リグレッションバグ)も大幅に低減されます。

6. 実装効果とまとめ

決済モジュールに戦略パターンとアダプターの概念を組み合わせることで得られるメリットは、次のとおりです。

  • 決済フローの簡素化: サービス層はPG会社ごとの詳細を把握する必要がなく、決済ビジネスフローそのものに集中できます。

  • 高い凝集度: 通信タイムアウト、ヘッダー設定、レスポンス解析など、特定のPGに依存するコードは、それぞれのProxyオブジェクト内部に安全に隔離されます。

  • 柔軟な運用設定: @ConditionalOnProperty により、ローカル、開発、本番などの環境に応じて、特定のPG実装を自由に有効化・無効化できます。

  • テスト容易性の確保: 外部通信部分をMockingしやすくなるため、決済フローそのものに対する単体テストの作成が非常に簡単になります。

決済システムは、必然的に失敗や変更の多い外部世界と接しています。堅牢な決済モジュールには、外部の変化がシステム内部の中核ロジックに波及しないように防ぐ防波堤が必要です。戦略パターンは、その強固な境界を構築するための、最も効果的かつ実用的な設計でした。

chnsik

Site footer