15万件/年のライフライン基幹システムの信頼性設計

ClassLab Engineering の Dev チームメンバーが執筆しました。

*この記事は、大規模トランザクション処理やSOAP API連携、信頼性設計に興味があるバックエンド/インフラエンジニア向けです。*

目次

1. 背景

ClassLab は年間約30万人の新生活をサポートするライフラインプラットフォームを運営しています。引越しに伴う電気・ガス・水道の開始・停止・変更手続きを、数千社超のパートナー企業と連携して処理しています。

この手続きの中核を担うのが、電力広域的運営推進機関(OCCTO)のスイッチング支援システムとの連携基盤です。OCCTOは電力・ガスの小売自由化に伴い、供給地点の切替手続きを一元管理する機関です。当社のシステムは、このOCCTOのSOAP APIを介して、供給地点の照会・再点(通電開始)・廃止(停止)・需要者情報変更などの手続きを自動化しています。

「止まったら困る」ライフラインの申込処理において、年間15万件超のトランザクションを安全に処理し続けるために、どのような信頼性設計を行ったかを紹介します。

2. 課題

課題 影響 定量データ
OCCTO APIの複雑な仕様 SOAP/WSDL + SSL相互認証 + 独自エラーコード体系 8つのSOAP API、25以上のエンドポイント
エラー原因の特定困難 申込エラー時の原因が不明瞭 エラーコード数十種、原因がAPI横断的
一括処理の信頼性 バッチ処理中の部分失敗 1回の一括処理で数百〜数千件を処理
複数ライフラインの統合 電気・ガス・水道で異なるAPI仕様 電力会社10社超 + ガス事業者 + 水道局
個人情報の保護 需要者名・住所等のセンシティブデータ 全リクエスト/レスポンスのマスキング必須

特に難しかったのは、OCCTOのエラー原因の特定です。たとえば再点申込(IF_10410)がエラーになった場合、その原因が「供給地点特定番号の誤り」なのか「既に別事業者が申込済み」なのか「設備情報自体に問題がある」のかは、エラーメッセージだけでは判断できません。追加で設備情報照会(IF_10110)や事業者一覧取得(IS_30110)を呼び出して原因を切り分ける必要がありました。

3. 設計

全体アーキテクチャ

graph TB
    SF[Salesforce CRM] -->|REST API| APP[Laravel API Server]
    APP -->|SOAP / SSL相互認証| OCCTO[OCCTO スイッチング支援システム]
    APP -->|REST API| GAS[ガス事業者API]
    APP -->|Web自動化| WATER[水道局システム]
    APP --> DB[(MySQL / RDB)]
    APP --> LOG[監査ログ]
    
    subgraph "OCCTO SOAP APIs"
        IF10110[IF_10110 設備情報照会]
        IF10410[IF_10410 再点申込]
        IF10510[IF_10510 廃止申込]
        IF10420[IF_10420 再点状態照会]
        IF10520[IF_10520 廃止状態照会]
        IF10820[IF_10820 需要者情報変更]
        IF11110[IF_11110 申込一括照会]
        IS30110[IS_30110 事業者一覧取得]
    end
    
    OCCTO --- IF10110
    OCCTO --- IF10410
    OCCTO --- IF10510
    OCCTO --- IF10420
    OCCTO --- IF10520
    OCCTO --- IF10820
    OCCTO --- IF11110
    OCCTO --- IS30110

技術選定

項目 選定技術 選定理由
フレームワーク Laravel (PHP) SOAP拡張の成熟度、既存チームの習熟度
SOAP通信 カスタムCurlSoapClient PHP標準SoapClientのタイムアウト制御不足を補完
SSL認証 クライアント証明書 (.pem/.key) OCCTO仕様書で必須のSSL相互認証
テスト PHPUnit + モックレスポンス OCCTO APIのXMLレスポンスを再現
バリデーション DTO + FormRequest OCCTO仕様書の必須項目を型安全に検証

Go や Python ではなく PHP/Laravel を選んだのは、SOAPの取り扱いやすさが決定的な理由でした。OCCTOのAPIはWSDLベースのSOAPインターフェースで、PHPのSoapClientはWSDLの自動パース・型マッピングをネイティブでサポートしています。

エラー自動調査アーキテクチャ

本システムの設計上の最大の特徴は、エラー発生時に追加のAPI呼び出しで原因を自動調査する仕組みです。

flowchart TD
    REQ[再点/廃止申込] --> CALL[OCCTO API呼び出し]
    CALL --> OK{成功?}
    OK -->|Yes| SUCCESS[SF更新: 成功]
    OK -->|No| INVESTIGATE[エラー調査開始]
    
    INVESTIGATE --> EQ_CALL[設備情報照会 IF_10110]
    EQ_CALL --> EQ_OK{照会成功?}
    
    EQ_OK -->|失敗| EQ_FAIL[equipment_inquiry_failed<br/>設備情報も取得不可]
    EQ_OK -->|エラーあり| EQ_ERR[equipment_error<br/>供給地点に問題あり]
    EQ_OK -->|正常| BIZ_CALL[事業者一覧 IS_30110]
    
    BIZ_CALL --> BIZ_CHECK{他事業者の<br/>申込あり?}
    BIZ_CHECK -->|Yes| CONFLICT[already_applied<br/>他社申込済み]
    BIZ_CHECK -->|No| UNKNOWN[original_error<br/>OCCTO原文を返却]
    
    EQ_FAIL --> SF_UPDATE[Salesforce更新: エラー + 調査結果]
    EQ_ERR --> SF_UPDATE
    CONFLICT --> SF_UPDATE
    UNKNOWN --> SF_UPDATE

この設計により、オペレーターは「なぜエラーになったか」の調査を手作業で行う必要がなくなりました。

4. 実装

SOAP通信の基盤クラス

OCCTOのSOAP APIは8つのサービスIDを持ち、すべてSSL相互認証が必要です。基盤クラスでこれらを一元管理しています。

class OCCTOBaseService
{
    protected const SERVICE_ID_MAP = [
        'requestSetsubiYokyuT'        => 'IF_10110', // 設備情報照会
        'requestSaitenYokyuT'         => 'IF_10410', // 再点申込
        'requestHaishiYokyuT'         => 'IF_10510', // 廃止申込
        'requestSaitenIdoYokyuT'      => 'IF_10420', // 再点状態照会
        'requestHaishiIdoYokyuT'      => 'IF_10520', // 廃止状態照会
        'requestJuyoshaHenkoYokyuT'   => 'IF_10820', // 需要者情報変更
        'requestIdoIkkatsuYokyuT'     => 'IF_11110', // 申込一括照会
        'requestJigyoshaIchiranYokyu' => 'IS_30110', // 事業者一覧
    ];

    // 抜粋 — 実際は住所・連絡先等を含む十数項目
    protected const SENSITIVE_KEYS = [
        'juyoshaShimeiKanji',   // 需要者氏名(漢字)
        'juyoshaShimeiKana',    // 需要者氏名(カナ)
        'moshikomiRenrakuTel',  // 連絡先電話番号
    ];
}

エラー自動調査の実装

エラー発生時に追加APIを呼び出して原因を特定する OcctoErrorInvestigator の設計です。

class OcctoErrorInvestigator
{
    public function __construct(
        private EquipmentInformationService $equipmentService,
        private BusinessOperatorInformationService $businessService,
    ) {}

    public function investigate(
        string $supplyPointId,
        string $originalError
    ): array {
        // Step 1: 設備情報照会で供給地点の状態を確認
        $equipment = $this->equipmentService
            ->inquire($supplyPointId);

        if ($equipment['status'] === 'failed') {
            return [
                'reason' => 'equipment_inquiry_failed',
                'message' => "設備情報の確認もできませんでした。"
                    . "OCCTO原文: {$originalError}",
            ];
        }

        if ($equipment['hasError']) {
            return [
                'reason' => 'equipment_error',
                'message' => "この供給地点の設備情報に問題があります。"
                    . "OCCTO原文: {$originalError}",
            ];
        }

        // Step 2: 事業者一覧で他社申込の有無を確認
        $operators = $this->businessService
            ->searchBySupplyPoint($supplyPointId);

        if ($operators['hasConflict']) {
            return [
                'reason' => 'already_applied',
                'message' => "他の事業者が申込済みです。",
            ];
        }

        // 原因不明:OCCTO原文をそのまま返却
        return [
            'reason' => 'original_error',
            'message' => $originalError,
        ];
    }
}

個人情報のマスキング

全SOAP通信のリクエスト/レスポンスをログに記録しますが、個人情報は自動マスキングしています。

protected function maskSensitiveData(array $data): array
{
    $masked = $data;
    foreach (self::SENSITIVE_KEYS as $key) {
        if (isset($masked[$key]) && is_string($masked[$key])) {
            // 先頭1文字のみ残す(ログ照合用。完全値はDB側で暗号化保持)
            $masked[$key] = mb_substr($masked[$key], 0, 1)
                . str_repeat('*', mb_strlen($masked[$key]) - 1);
        }
    }
    return $masked;
}
// 入力: "田中太郎" --> 出力: "田***"

一括処理のバッチ設計

数百件の再点/廃止申込を一括処理する際、部分失敗に対応するバッチ設計を採用しています。

public function sfRelightBatch(Request $request): JsonResponse
{
    $validated = $request->validate([
        'records'   => 'required|array|max:5000',
        'records.*' => 'required|array',
    ]);
    $results = [];
    foreach ($validated['records'] as $record) {
        try {
            $result = $this->processRelight($record);
            $results[] = ['status' => 'success', ...$result];
        } catch (SoapFault $e) {
            // 個別失敗はスキップし、残りを継続処理
            $investigation = $this->errorInvestigator
                ->investigate(
                    $record['supplyPointId'],
                    $e->getMessage()
                );
            $results[] = [
                'status' => 'error',
                'investigation' => $investigation,
            ];
        }
    }
    return response()->json([
        'total' => count($results),
        'success' => count(array_filter(
            $results, fn($r) => $r['status'] === 'success'
        )),
        'errors' => count(array_filter(
            $results, fn($r) => $r['status'] === 'error'
        )),
        'details' => $results,
    ]);
}

5. 結果(数値)

指標 Before After 改善率
申込処理時間(1件あたり) 約15分(手動入力) 数秒(API自動化) -99%超
エラー原因特定時間 約30分(手動調査) 即時(自動調査) 大幅短縮
月間処理件数 数千件(人手上限) 1万件超(自動化後) 数倍
入力ミスによるエラー率 数%(手動起因) 0.1%未満(バリデーション通過後) -97%超
OCCTO API応答の成功率 99%超(月間平均)

特筆すべきはエラー自動調査の効果です。従来はOCCTOからエラーが返ると、オペレーターが手動で設備情報を照会し、他社の申込状況を確認し、原因を推測するという作業に30分以上かかっていました。自動調査により、エラーの原因がレスポンスに即座に含まれるようになり、オペレーターは「何をすべきか」にすぐ集中できるようになりました。

6. 展望

次に取り組む課題

  • SSL証明書ローテーションの自動化: 現在は手動更新。証明書切替のブルーグリーン方式を一緒に設計してくれるエンジニアを探しています
  • Salesforce Apex バッチとの統合強化: Laravel側とSalesforce側のバッチ処理の二重管理を解消し、ステータス同期を一元化する設計が必要です
  • プロアクティブな監視基盤: APIレスポンスタイムの推移や、特定供給地点でのエラー集中を検知する監視を構築予定です
  • 関連記事

  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次