migrationsoap-to-restapi-designtutorial

既存の SOAP を残したまま REST へ移行する実践ガイド

SOAPless Team8 min read

多くのチームは、既存の SOAP を置き換える権限を持っていません。

現実に必要なのは、「SOAP を消すこと」よりも、「利用側のチームが生の SOAP を直接扱わなくてよい状態を作ること」です。

その意味で、現実的な移行は全面リライトではなく、よりよい境界を前に置いて利用側を順番に移していく作業です。この記事では、その進め方を解説します。

なぜ移行するのか

移行を検討する前に、その理由を明確にしておきましょう。チーム全体の合意形成に不可欠です。

開発速度: REST + JSON は SOAP + XML と比べて、リクエストの構築・デバッグ・テストがはるかに高速です。新しい開発者のオンボーディングも短縮できます。

ツールの選択肢: REST API のツール(Postman、Swagger UI、各種クライアントライブラリ)は SOAP のツールと比べて圧倒的に充実しています。

フロントエンド連携: JavaScript / TypeScript のフロントエンドから直接呼び出せます。SOAP ではサーバーサイドのプロキシが必要なケースがほとんどです。

採用とサポート: SOAP 対応のライブラリは新規開発が減少しており、メンテナンスも縮小傾向にあります。

移行戦略の選択

ストラテジー 1: ビッグバン移行(非推奨)

既存の SOAP サービスを一度にすべて REST に書き換える方法です。

[クライアント] → [SOAP サービス]
        ↓ 一斉切り替え
[クライアント] → [新 REST サービス]

リスク: 移行期間中のダウンタイム、バグ混入時の広範な影響、ロールバックの困難さ。小規模なサービスを除いて推奨しません。

ストラテジー 2: Strangler Fig パターン(推奨)

既存の SOAP サービスの前にプロキシ層を置き、エンドポイントを一つずつ REST に移していく方法です。Martin Fowler が提唱した Strangler Fig パターンに基づいています。

[クライアント] → [API ゲートウェイ / プロキシ]
                    ├─→ [REST エンドポイント A] (移行済み)
                    ├─→ [REST エンドポイント B] (移行済み)
                    └─→ [SOAP サービス] (未移行のオペレーション)

ストラテジー 3: SOAP-to-REST プロキシ(最速)

SOAP サービスはそのまま稼働させ、前段に REST プロキシを置いて JSON ⇔ XML の変換を自動化する方法です。

[クライアント] → [REST プロキシ] → [SOAP サービス]
               JSON ⇔ XML 変換

ステップバイステップの移行手順

ステップ 1: 現状の棚卸し

まず、移行対象の SOAP サービスのオペレーションを一覧化します。

# WSDL からオペレーション一覧を抽出
curl -s "https://example.com/service?wsdl" | \
  grep -oP '<wsdl:operation name="\K[^"]+' | sort -u

# 出力例:
# CreateUser
# DeleteUser
# GetUser
# ListUsers
# UpdateUser

各オペレーションについて以下を記録します。

オペレーション呼び出し頻度依存クライアント数複雑度優先度
GetUser51
ListUsers32
CreateUser23
UpdateUser24
DeleteUser15

ステップ 2: REST API の設計

SOAP オペレーションを REST のリソース指向に変換します。

# SOAP オペレーション → REST エンドポイントの変換
GetUser(userId)     → GET    /api/v1/users/{userId}
ListUsers(filters)  → GET    /api/v1/users?department=engineering
CreateUser(data)    → POST   /api/v1/users
UpdateUser(data)    → PUT    /api/v1/users/{userId}
DeleteUser(userId)  → DELETE /api/v1/users/{userId}

設計のポイント:

  • SOAP のオペレーション名を HTTP メソッド + リソースパスに変換
  • リクエストボディは XML ではなく JSON に
  • エラーレスポンスは HTTP ステータスコードを適切に使用
// SOAP Fault → REST エラーレスポンスの変換例
// soap:Client Fault → 400 Bad Request
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "userId は正の整数である必要があります",
    "details": [
      {
        "field": "userId",
        "reason": "値が 0 以下です"
      }
    ]
  }
}

ステップ 3: プロキシ層の構築

最もシンプルなアプローチとして、Express.js でプロキシを構築する例を示します。

const express = require('express');
const soap = require('soap');

const app = express();
app.use(express.json());

let soapClient;

// 起動時に SOAP クライアントを初期化
async function initSoapClient() {
  soapClient = await soap.createClientAsync(
    'https://example.com/service?wsdl'
  );
  soapClient.setSecurity(
    new soap.WSSecurity('user', 'pass')
  );
}

// REST エンドポイント: GET /api/v1/users/:id
app.get('/api/v1/users/:id', async (req, res) => {
  try {
    const [result] = await soapClient.GetUserAsync({
      userId: parseInt(req.params.id)
    });

    // SOAP レスポンスを JSON に変換
    res.json({
      id: result.user.id,
      name: result.user.name,
      email: result.user.email,
      department: result.user.department || null
    });
  } catch (error) {
    // SOAP Fault を HTTP エラーに変換
    if (error.root?.Envelope?.Body?.Fault) {
      const fault = error.root.Envelope.Body.Fault;
      const isClientFault = fault.faultcode?.includes('Client');
      res.status(isClientFault ? 400 : 500).json({
        error: {
          code: fault.faultcode,
          message: fault.faultstring
        }
      });
    } else {
      res.status(500).json({
        error: { message: 'Internal server error' }
      });
    }
  }
});

// REST エンドポイント: POST /api/v1/users
app.post('/api/v1/users', async (req, res) => {
  try {
    const [result] = await soapClient.CreateUserAsync({
      name: req.body.name,
      email: req.body.email,
      department: req.body.department
    });

    res.status(201).json(result.user);
  } catch (error) {
    res.status(500).json({ error: { message: error.message } });
  }
});

initSoapClient().then(() => {
  app.listen(3000, () => console.log('REST proxy running on port 3000'));
});

ステップ 4: テストの整備

移行前後でレスポンスの一貫性を検証するテストを作成します。

const assert = require('assert');

// SOAP と REST の結果を比較するテスト
async function verifyMigration(userId) {
  // SOAP で直接呼び出し
  const [soapResult] = await soapClient.GetUserAsync({ userId });

  // REST プロキシ経由で呼び出し
  const restResponse = await fetch(`http://localhost:3000/api/v1/users/${userId}`);
  const restResult = await restResponse.json();

  // 結果が一致することを確認
  assert.strictEqual(restResult.id, soapResult.user.id);
  assert.strictEqual(restResult.name, soapResult.user.name);
  assert.strictEqual(restResult.email, soapResult.user.email);

  console.log(`ユーザー ${userId} の検証: OK`);
}

ステップ 5: 段階的な切り替え

クライアントを一つずつ REST エンドポイントに切り替えます。

Week 1: 内部管理ツール → REST に切り替え(影響範囲小)
Week 2: モバイルアプリ → REST に切り替え
Week 3: パートナー連携 → REST に切り替え(事前通知必要)
Week 4: 全クライアント移行完了 → SOAP エンドポイント廃止予告

ステップ 6: SOAP サービスの段階的廃止

すべてのクライアントが REST に移行した後、SOAP エンドポイントの廃止を進めます。

  1. SOAP エンドポイントに非推奨ヘッダーを追加
  2. 一定期間(3-6 ヶ月)の並行稼働
  3. アクセスログで残存クライアントがないか確認
  4. SOAP エンドポイントの停止

移行を加速する方法

自前のプロキシ構築は柔軟性がありますが、SOAP オペレーションごとにエンドポイント定義、型変換、エラーハンドリングを実装する必要があり、数十のオペレーションがある場合は膨大な作業量になります。

SOAPless は WSDL を自動解析して REST エンドポイントを生成するため、プロキシ層のコーディングが不要です。WSDL の URL を入力するだけで、約 30 秒ですべてのオペレーションに対応する REST API が利用可能になります。既存の SOAP サービスには一切変更を加えず、クライアント側だけを REST に切り替えられるため、移行リスクを最小限に抑えられます。

段階的な移行でも、一括移行でも、SOAPless がプロキシ層の構築コストをゼロにします。