$30 off During Our Annual Pro Sale. View Details »

外部システム連携先が10を超えるシステムでのアーキテクチャ設計・実装事例

 外部システム連携先が10を超えるシステムでのアーキテクチャ設計・実装事例

JJUG CCC 2024 fall の発表資料です。

Kiyoshi Iwasaki

October 27, 2024
Tweet

More Decks by Kiyoshi Iwasaki

Other Decks in Programming

Transcript

  1. Ⓒ2024 Dai Nippon Printing Co., Ltd. All Rights Reserved. 2024年10月27日

    大日本印刷株式会社 情報イノベーション事業部 岩崎 清 外部システム連携先が10を超えるシステムでの アーキテクチャ設計・実装事例
  2. 2 自己紹介 ◼ 名前 : 岩崎 清 ◼ 所属 :

    大日本印刷株式会社 情報イノベーション事業部 ICTセンター ICTDX本部 ◼ 経歴 ◦ 2000年から Java 利用開始 ◦ 2003年から Webアプリケーション開発に従事 ◦ 2013年から社内向け Webアプリケーションフレームワークの開発を開始 ◦ 現在はフレームワークの開発・保守、実案件でのアーキテクトに従事
  3. 4 概要 ◼ 外部システムの REST API を呼び出す外部接続モジュールの アーキテクチャ設計・実装の事例紹介 ◦ クラス設計等の実装よりのアーキテクチャ設計事例

    ◦ 外部システム連携先は10カ所を超える ◦ 結合テスト、システムテストでも利用するスタブも含むアーキテクチャ
  4. 5 システム概要 スマホアプリ 管理画面 API サーバー 管理画面 サーバー 外部連携先 サーバー

    外部連携先 サーバー 外部連携先 サーバー ・・・・・・ 連携先は10以上 連携APIは50以上
  5. 8 外部接続モジュールの要件:通信設定、キュー設定 ◼ 通信設定、キュー設定のリロード ◦ 再起動なしに一定間隔で設定をリロード可能にする 外部連携先 接続 タイムアウト リード

    タイムアウト リトライ 回数 … X 10,000 10,000 2 … Y 5,000 10,000 3 … …… …… …… …… … 外部連携先 同時実行数 サイズ 最大待機時間 … X 5 10 10,000 … Y 10 15 5,000 … …… …… …… …… … キュー設定 通信設定 外部接続 モジュールX 外部接続 モジュールY
  6. 9 外部接続モジュールの要件:同時実行数のキュー制御 実行中 最大待機数 最大実行数 API サーバー 外部連携先 サーバー API

    サーバー API サーバー • 最大実行数以下は即時実行可能 • 実行終了時にキューから削除 • システム全体で同時実行数の制御が必要 接続要求
  7. 10 外部接続モジュールの要件:同時実行数のキュー制御 実行中 実行中 実行待ち 最大待機数 最大実行数 API サーバー 外部連携先

    サーバー API サーバー API サーバー • 最大実行数を超えると待機 • 待機中に最大待機時間を超えると、 待機を中断してエラー 接続要求
  8. 11 外部接続モジュールの要件:同時実行数のキュー制御 実行中 実行中 実行待ち 実行待ち 最大待機数 最大実行数 API サーバー

    外部連携先 サーバー API サーバー API サーバー • 最大待機数を超えると即時エラー 接続要求 実行待ち
  9. 外部 連携先 API種別 外部API 種別 HTTP ステータス 条件1 … 条件N

    エラー コード ログ レベル X - - 200 000 … - ー INFO X API_001 API_X01 400 001 … .*error.* E0101 ERROR X API_002 - 400 - … - E0200 WARN X - API_X03 - 102 … - E0304 WORN X - - - - … - E9999 ERROR …… …… …… …… …… … …… …… …… 12 外部接続モジュールの要件:エラーマッピング ◼ エラーマッピング表によるエラーコード変換 ◦ 以下のような表に従い、レスポンスからエラーコードに変換する ◦ 設定は再起動なしにリロード可能にする
  10. 13 外部接続モジュールの要件:エラーマッピング 外部 連携先 API種別 外部API 種別 HTTP ステータス 条件1

    … 条件N エラー コード ログ レベル X - - 200 000 … - ー INFO X API_001 API_X01 400 001 … .*error.* E0101 ERROR X API_002 - 400 - … - E0200 WARN X - API_X03 - 102 … - E0304 WORN X - - - - … - E9999 ERROR …… …… …… …… …… … …… …… …… • 共通のマッピング条件 • 呼び出し元のAPI、外部API、レスポンスの HTTPステータスにマッピングする
  11. 14 外部接続モジュールの要件:エラーマッピング 外部 連携先 API種別 外部API 種別 HTTP ステータス 条件1

    … 条件N エラー コード ログ レベル X - - 200 000 … - ー INFO X API_001 API_X01 400 001 … .*error.* E0101 ERROR X API_002 - 400 - … - E0200 WARN X - API_X03 - 102 … - E0304 WORN X - - - - … - E9999 ERROR …… …… …… …… …… … …… …… …… • 外部連携先ごとのマッピング条件 • レスポンスの項目と各条件Nをひもづける • 項目と条件のマッピングは外部連携先ごとに 個別に定義する { "result" : "001", "message" : "xxx error.", …… } レスポンス例
  12. 15 外部接続モジュールの要件:エラーマッピング 外部 連携先 API種別 外部API 種別 HTTP ステータス 条件1

    … 条件N エラー コード ログ レベル X - - 200 000 … - ー INFO X API_001 API_X01 400 001 … .*error.* E0101 ERROR X API_002 - 400 - … - E0200 WARN X - API_X03 - 102 … - E0304 WORN X - - - - … - E9999 ERROR …… …… …… …… …… … …… …… …… • 各条件に対するエラーコードとログレベル • ERROR は発報対象 • エラーコードの “ー” は成功を表す • 成功の条件 • 必ず条件なしの 行を用意する
  13. スマホアプリ API サーバー 外部連携先 サーバー 管理画面 参照可能 17 外部接続モジュールの要件:リクエスト・レスポンスの記録 ◼

    スマホアプリのリクエストにひもづけて、リクエスト・レスポンスを記録 ◦ 管理画面から検索・参照可能 【余談】 • 性能面を危惧してAPI単位でオフする機能を用意したが、 性能テストで問題になることはなかった • 逆に外部結合テスト、システムテストでのメリットが非常 に大きかった • テストチームが自ら調査して迅速に問題解決でき、開発 チームへの問合せは極端に少なかった
  14. 21 ロジックと外部接続モジュールの責務の分離 ◼ 方法1)モジュールを、ドメイン等から完全に独立した再利用可能なモ ジュールにする ◦ モジュールの独立性は高く、他の案件でも再利用可能 ◦ モジュール単体での保守性も高い ◦

    全体としての実装コストは高くなる • 連携先ごとの設定(APIキー、パスフレーズ等)や、通信設定は、外部から与える か、専用のプロパティファイルを用意する • 通信設定はデータベースに保存しており、リロードする必要があるが、モジュール の利用者側で管理するか、コールバックを用意する必要がある。 • エラーコードへの依存もできないため、レスポンスからのエラーマッピングも利用 者側に実装する必要がある • リクエスト・レスポンスをデータベースに記録する仕組みを入れづらい
  15. 23 ロジックと外部接続モジュールの責務の分離 ◼ 方法2)モジュールインターフェイスの、ドメインへの依存を許可し、利 用者側が簡単に利用できるようにする ◦ 案件固有の業務ロジックも含めるため、利用者側は使いやすい • 例)会員を渡し、外部接続モジュール側で会員からトークンを取得して通信する ◦

    全体としての実装コストは最小化できる • 初期化や設定値のリロードもモジュール内で実装しやすい。 • エラーマッピングもモジュール内で実施可能 • リクエスト・レスポンスのデータベースへの記録も実装可能 ◦ モジュールの独立性、再利用性、保守性はかなり低い • ドメインの修正がモジュールに影響する
  16. 25 ロジックと外部接続モジュールの責務の分離 ◼ 方法3)モジュールインターフェイスの、業務に依存する ドメインへの依存は許可しない ◦ 方法1と方法2の中間 ◦ 会員等の業務に直結するドメインへの依存は許可しないが、通信設定、エ ラーコード等の、システムよりのドメインへの依存は許可する

    ◦ 全体としての実装コストは、方法1>方法3>方法2 • 設定のリロード、エラーマッピング、リクエスト・レスポンスの記録等を、モジュール 側の共通実装にできる ◦ モジュールの独立性、再利用性、保守性は、方法1<方法3<方法2 • ただし、システムよりのドメインは安定しているので影響は受けづらい 採用
  17. 29 対策1:外部接続モジュールの分離 ◼ 通信を行う部分をさらにインターフェイス化して分離 ◦ (以降、前段を Client、後段を Connector と表記する) ◦

    RestTemplate を利用した実際の通信処理は Connector 側で行う ◦ それ以外の処理は Client 側で行う Client 側のコードは単体テスト時も実行可能になる
  18. 30 Client の責務 ◼ Client の主な責務 ◦ リクエストの準備と Connector の呼び出し

    ◦ (外部連携先により)同時実行数のキュー制御 ◦ (外部連携先により)特定の条件(流量制限エラー等)でのリトライ ◦ リクエスト・レスポンスの記録
  19. 31 Connector の責務 ◼ Connector の主な責務 ◦ 通信設定、エラーマッピングの読み込みおよびリロード ◦ RestTemplate

    を利用した外部連携APIの実行 ◦ タイムアウトによるリトライ ◦ レスポンスからのエラーマッピング
  20. 33 対策2:RestTemplate のインターセプタの利用 ◼ ConnectorStubImpl は ConnectorImpl を継承し以下の処理を行う ◦ RestTemplate

    のインターセプタを設定する ◦ スタブ固有の処理を行う ◼ 単体テストで ConnectorImpl のほぼすべてのコードが実行可能
  21. 34 対策2:RestTemplate のインターセプタの利用 ◼ ConnectorStubImpl 利用時(単体テスト時)の挙動 ◦ RestTemplate のインターセプタを設定し、実際に通信を行う直前でスタブの 処理を行い、返却したいレスポンスのバイト列を返す

    ⇒ MessageConverter でレスポンスのデシリアライズ処理も実行される XxxClientImpl 外部連携先 サーバー XxxConnector Impl RestTemplate ※RestTemplate の インターセプタ XxxConnector StubImpl 実際に通信は行わず、 レスポンスのバイト列 を返す
  22. 38 Client の基底クラスとサブクラスの責務 ◼ サブクラス(XxxClientImpl) ◦ Connector に渡すリクエストの準備 ◦ Connector

    の呼び出し ◼ 基底クラス(BaseClient) ◦ リクエスト・レスポンスのデータベースへの保存
  23. 39 XxxClientImpl の実装イメージ public class XxxClientImpl extends BaseExtComClient implements XxxClient

    { @Autowired private XxxConnector xxxConnector; @Override public SomeApiResult someApi(String memberId, String senderId) { SomeApiRequestImpl request = new SomeApiRequestImpl(); // request のセットアップ …… XxxConnectorResult<SomeApiResponseImpl> connectorResult = callConnector(() -> xxxConnector.someApi(request)); return new SomeApiResult(request, connectorResult); } …… 1.リクエストを用意 2.Connector を 呼び出す 3.結果を返却する
  24. 40 Connector の基底クラスとサブクラスの責務 ◼ サブクラス(XxxConnectorImpl) ◦ RestTemplate に設定する MessageConverter の生成(初期化時)

    ◦ APIごとの RequestEntity の生成 ◼ 基底クラス(BaseConnector) ◦ RestTemplate の生成・キャッシュ ◦ 通信設定が更新された場合の RestTemplate の再作成 ◦ 実際の通信と、通信設定に応じたリードタイムアウト時の リトライ処理 ◦ レスポンスからのエラーマッピング
  25. 41 XxxConnectorImpl の実装イメージ ◼ RestTemplate に設定する MessageConverter の生成(初期化用) public class

    XxxConnectorImpl extends BaseExtComConnector implements XxxConnector { …… @Override protected List<HttpMessageConverter<?>> createMessageConverters( SyExtApiType syExtApiType) { List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(); …… // JSON 用の MessageConverter を設定 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper); messageConverters.add(converter); return messageConverters; }
  26. 42 XxxConnectorImpl の実装イメージ ◼ 個々のAPIの実装は、RequestEntity を生成するラムダ等を渡し、 基底クラスのメソッドを呼び出す …… @Override public

    XxxConnectorResult<SomeApiResponseImpl> someApi(SomeApiRequestImpl request) { SyExtApiType extApiType = SyExtApiType.XXX_001; return execute( extApiType, () -> createSomeApiRequest(request, XXX_001_PATH), SomeApiResponseImpl.class, createResultCreator(extApiType)); } ……
  27. 43 XxxConnectorImpl の実装イメージ ◼ RequestEntity を生成するメソッドでは、RequestEntity を生成して、 リクエストとヘッダ等を設定して返す …… private

    RequestEntity<MultiValueMap<String, String>> createSomeApiRequest(SomeApiRequestImpl request, String path) { // ヘッダ設定 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setAccept(List.of(MediaType.APPLICATION_JSON)); return new RequestEntity<>(toMultiValueMap(request), headers, HttpMethod.POST, getUri(path)); } ……
  28. 47 スタブの設計要因:スタブ設定 ◼ スタブ設定:スタブが返却するレスポンスの設定 ◦ 返却するレスポンスのタイプ • 固定レスポンス • 自動レスポンス

    • 指定レスポンス ◦ (指定レスポンスの場合)レスポンスの指定方法 • レスポンスの登録順 • 登録時に指定したキー
  29. 51 スタブの設計要因:スタブ設定 – レスポンスタイプ:指定レスポンス ◼ 指定方法:登録順 ◦ レスポンスを登録した順に返却する(FIFOキュー) ◦ 連続した同一APIの呼び出しで異なるレスポンスを返すことが可能だが、

    複数人での同時テストは困難 APIサーバー スタブ スマホアプリ レスポンス1 レスポンス2 …… ・登録したレスポンスは上に積まれる ・スタブは下から利用していく
  30. 52 スタブの設計要因:スタブ設定 – レスポンスタイプ:指定レスポンス ◼ 指定方法:キー ◦ APIの入力項目からキーを決める(※キー項目の詳細は後述) ◦ キーを指定してレスポンスを登録する

    ◦ 入力項目に該当するレスポンスを返却する ◦ 複数人で同時テスト可能だが、連続した同一APIの呼び出しで異なるレスポン スを返すことはできない APIサーバー スタブ スマホアプリ zzz ⇒ レスポンス3 yyy ⇒ レスポンス2 xxx ⇒ レスポンス1 xxx
  31. 53 スタブの設計要因:スタブ設定 – レスポンスタイプ:指定レスポンス ◼ 指定方法:キー+登録順 ◦ 登録順とキーの組合せ ◦ キーごとにキューを持つ

    ◦ 連続した同一APIの呼び出しで異なるレスポンスを返すことも、 複数人での同時テストも可能 APIサーバー スタブ スマホアプリ キー:xxx レスポンスx1 レスポンスx2 …… キー:yyy レスポンスy1 レスポンスy2 …… キー:zzz レスポンスz1 レスポンスz2 …… xxx
  32. 54 スタブの設計要因:折り返しスタブとスタブサーバー ◼ 折り返しスタブ ◦ 外部通信は行わず、 プロセス内で折り返す ◼ スタブサーバー ◦

    実モジュールを利用 ◦ 実際に通信を行う ◦ 外部連携先相当の スタブサーバーを用意する APIサーバー ClientImpl ConnectorImpl ConnectorStubImpl スタブ サーバー APIサーバー ClientImpl ConnectorImpl ConnectorStubImpl
  33. 56 スタブの各ユースケースと制約 ユースケース 設定の保存場所 複数設定の制御 設定手段 単体テスト • メモリも可 •

    1テストずつの実行と なるため、登録順で も問題ない • プログラム (テストコード) 自動結合 テスト • メモリは不可 • DB, KVS等に 保存する必要 あり 手動テスト • 複数人でテストする ため、複数の設定か ら特定する必要あり • 手動 (設定ツールが 必要) 今回のターゲット
  34. 57 スタブの要件 ◼ スタブ設定は「指定レスポンス」を「キー+登録順」で管理する ◦ 設定は KeyValueStore に保存する ◦ 単体テストでは不要だが、単体テストも同じ仕組みにする

    ◼ スタブ設定用管理画面を用意する ◦ テスター(≠開発者)が利用するため、GUIツールが必要 ◦ 専用のWebアプリを用意する ◼ 外部APIと同じ動作をするスタブサーバーも用意する ◦ 外部結合テスト前に、外部接続モジュールの通信テストを行えるようにする ◦ 性能テスト時に実際に通信する状態で計測できるようにする
  35. APIサーバー 59 自動結合テスト実行時 ClientImpl ConnectorImpl ConnectorStubImpl Redis スタブ設定 データ 結合テスト

    (WebDriver) 1.テスト準備で、スタブ設定を投入する 2.投入されたスタブ設定に従い レスポンスを返す
  36. スタブサーバー 60 手動テスト実行時 APIサーバー ClientImpl ConnectorImpl ConnectorStubImpl スマホアプリ Redis スタブ設定

    データ 外部APIスタブ スタブ設定 管理画面 スタブ設定 1.テスト実行前に、スタブ設定を行う 2.スタブサーバーがスタブ設定に 従ってレスポンスを返す
  37. 61 スタブ設定 ◼ 共通のスタブ設定項目 ◦ 特殊エラー • 接続タイムアウト、リードタイムアウト等 ※外部APIスタブは設定できない ◦

    利用回数 • 指定した回数利用すると削除 • 無制限も指定可能 ◦ 遅延秒数 • リードタイムアウトを発生させることができる ◦ HTTPステータス • レスポンスのHTTPステータスコード
  38. 62 スタブ設定のキー項目 ◼ スタブ設定のキーは外部APIのリクエスト項目でなくてはならない ◼ キー項目はスマホアプリからの入力値で制御可能でなくてはならい ◦ キー項目を直接指定できないが制御可能である例 • スタブ設定のキーはカードID

    • スマホからのAPIにカードIDは含まれないが、会員IDが含まれる • サーバーで会員IDからカードIDを引いてリクエストに利用している スマホアプリ API サーバー 外部API スタブ リクエスト項目の中から スタブ設定のキーを設定する スタブ設定のキーは、入力値で 制御可能でなくてはならない
  39. APIサーバー 64 キー設定が難しい事例① ◼ 問題点:キー項目値を外部から参照できない スマホアプリ IDを採番 IDはDBに保存 外部API1 口座番号

    口座番号, ID DBからIDを 取得 外部API2 ID ID IDは外部からは参照不可 外部API1のスタブキーは 口座番号を利用可能 スタブキーとして利用 できるのはIDのみ
  40. APIサーバー 65 キー設定が難しい事例① ◼ 解決策:スタブ側でキー項目へ変換するためのデータを管理する スマホアプリ IDを採番 IDはDBに保存 外部API1 口座番号

    口座番号, ID DBからIDを 取得 外部API2 ID IDをキーに口座番号を保存 IDから口座番号を取得。 取得した口座番号をキー としてスタブ設定を取得 ID Redis スタブ設定 キー:口座番号 Redis ID⇒口座番号
  41. APIサーバー 66 キー設定が難しい事例② ◼ 問題点:スタブ設定を行うタイミングがない スマホアプリ IDはDBに保存 外部API1 ID、口座番号 ID,

    口座番号 DBからIDを 取得 外部API2 ID 外部API1のスタブキーは 口座番号を利用可能 ID IDは管理画面から参照可能 二つのAPI呼び出しは連続 して行われるため、スタブ設 定を行うタイミングがない IDはスマホ内で採番 スタブキーとして利用 できるのはIDのみ
  42. 67 キー設定が難しい事例② ◼ 解決策:テスト用にスマホアプリ側に一時停止機能を追加する APIサーバー スマホアプリ IDはDBに保存 外部API1 ID、口座番号 ID,

    口座番号 DBからIDを 取得 外部API2 ID ID 二つのAPI呼び出しの間 に、テスト用の確認ダイア ログを表示し、一時停止 できるようにして対応
  43. 68 スタブ設計・実装のまとめ ◼ スタブのユースケースによって求められる機能も変わる ◦ 単体テスト<自動結合テスト<手動テストの順で高機能なものが求められる ◦ どこまでの機能が必要か見極め、早めに対応した方がよい • 今回の仕組みは、外部連携モジュール自体の設計時から一緒に検討していた

    ◼ スタブ設定のキー項目をうまく見つける ◦ キー項目は「外部APIのリクエスト項目」かつ「APIの入力値で制御できるも の」でなければならない ◦ 直接キー項目になるものがない場合も、合わせ技で制御できる場合もある (事例参照)
  44. 70 仕様書に未記載の JSON プロパティ ◼ 仕様書に未記載の JSON プロパティが返却され例外が発生した ◦ Jackson

    の ObjectMapper のデフォルト動作では、マッピングしていないプロ パティがあると、例外を送出する ◦ ObjectMapper に以下の設定を行うことで例外の発生を抑止できるが、マッピ ングを誤っても気づきづらい ◦ DeserializationProblemHandler を設定することで挙動をカスタマイズできる • 未マッピングでも無視するプロパティを設定でき、不明な JSON プロパティが見 つかった場合に、サーバー起動後初回のみエラーレベルのログを出力する Handler を作成して対応 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 不明なJSONプロパティはエラーレベルのログを出力して処理を継続する objectMapper.addHandler(XxxUnknownJsonPropertyLogHandler.INSTANCE);
  45. 71 Content-Type が異なるレスポンス ◼ レスポンスボディーは XML だが、Content-Type が “text/plain” に

    なっており、変換できずに例外が発生した ◦ RestTemplate に設定した HttpMessageConverter は getSupportedMediaTypes() が返すメディアタイプに一致する場合にのみ適 用される ◦ 以下の様に SupportedMediaType を変更することで対応可能 (以下はすべての Content-Type に適用する例) MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter(xmlMapper); converter.setSupportedMediaTypes(List.of(MediaType.ALL));
  46. 72 自動応答を実装しておくと手動テストの効率が上がる ◼ スタブ実装時は単体テストから開始しているため、未設定時はエラー となるように実装していた ◦ そのため手動テスト時に、テスト対象にたどり着くまでに呼ばれるAPIにもスタ ブ設定が必要となっていた ◼ そのようなケースでは、正常系のレスポンスが返って次に進めれば

    よいので、 自動で正常系の応答を返せる API には自動応答の機能 を追加した ◦ これにより手動テストの効率がかなり向上した ◦ (後で対応したため、 スタブサーバー側のみ対応したが、折り返しスタブにも あった方がよい機能だった)
  47. 73 OpenID Connect の認証ページ ◼ スタブの範囲外だが OpenID Connect の認証ページも動作検証に 必要となる

    ◼ 認証ページでスタブ設定を行えるようにしておくと効率的 ◦ 入力フォームには、デフォルトで正常系のレスポンスをプリセットしておくと さらにテストを効率的に実施できる
  48. 76 最後に ◼ 今回の事例は連携先が多く、要件も多かったため、基盤開発にコス トをかけるメリットが大きかった ◦ 連携先が少なければ、それぞれ愚直に実装する方がコードの可読性も上がる ◦ 共通化することで構造が複雑になり理解しづらい部分もある ◼

    外部連携モジュールの実装コストを抑えつつ、モジュールに起因する 不具合はほとんど発生しなかった ◦ 開発の効率化、品質向上を図ることができた ◦ スタブ管理画面まで用意したことで、手動テストも効率的に行えた