Composeマルチプラットフォームと金融API

0. はじめに

金融アプリでは、PII(個人情報)を最小限に扱うことと、モバイル回線下でも高速に動かすことがトレードオフになりがち。本記事は、Kotlin/ComposeでマルチOSに展開できるCompose Multiplatform(CMP)と、GraphQLのPersisted Queries(永続化クエリ, PQ)を組み合わせ、安全×速さを両立する設計をコードと意図で分解する。
以降は (1) 要件整理 → (2) クライアント実装(CMP+Apollo Kotlin) → (3) サーバ&CDN設計(APQ/Safelist/キャッシュ) → (4) PII最小化と権限・監査 → (5) 運用・計測・テスト の順で進める。

1. 背景・要件:PII最小化と高速化の二兎を追う

1-1. PQ(永続化クエリ)で帯域と攻撃面を同時に削る

Persisted Queriesはクエリ本文の代わりにハッシュ(通常SHA-256)だけを送る。初回だけ本文を登録し、以後はハッシュで呼び出す(APQ: Automatic Persisted Queries)。これでペイロードが激減し、CDNキャッシュと相性が良い。
金融ではさらにSafelist(許可リスト)で「登録済みハッシュ以外は拒否」。未知の重いクエリやスキーマ探索を抑止し、攻撃面を小さくする。

1-2. Compose Multiplatformで共通UI/ロジックを担保

CMPはKotlin+ComposeでAndroid/iOS/デスクトップ/Webへ展開でき、UI・状態管理・ドメインロジックを共有しやすい。通信層はApollo Kotlin(マルチプラットフォーム対応)を採用し、クエリ定義→型生成→呼び出しまでを共通化。
OS固有のセキュアストレージ(Keychain/Keystore)やネットワーク設定はexpect/actualで橋渡しし、多くの安全策と最適化を共通コードへ集約する。

1-3. 金融要件の前提(最低限おさえること)

ノーキャッシュ(PII)最小権限監査ログ(誰が・何を・いつ)短寿命トークン二重実行防止(冪等)。本稿は特にクエリ設計・キャッシュ・ログにフォーカス。

2. クライアント実装:CMP+Apollo KotlinでPQを使う

2-1. Apollo Kotlin導入とコード生成(型安全)

Apollo Kotlinはスキーマから型安全なKotlinを生成。CMPのcommonMainに依存を置けば、Android/iOS/デスクトップから共通利用できる。

// build.gradle.kts(sharedモジュール)
plugins {
  kotlin("multiplatform")
  id("com.apollographql.apollo3") version "3.9.0"
}
kotlin {
  androidTarget()
  ios()
  jvm("desktop")
  sourceSets {
    val commonMain by getting {
      dependencies { implementation("com.apollographql.apollo3:apollo-runtime:3.9.0") }
    }
  }
}
apollo {
  service("api") {
    packageName.set("com.example.bank.api")
    generateKotlinModels.set(true)
  }
}

意図:スキーマと.graphqlからモデルを生成。PII(phone, address等)をデフォルトのクエリに含めない方針を徹底し、必要時のみレビューのうえ追加。

2-2. APQ(自動永続化)を有効化

Apollo KotlinはAPQをサポート。初回=本文送信、2回目以降はハッシュのみで呼び出す。

import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.network.okhttp.OkHttpEngine // Android例(iOSはNSURLSession)
import com.apollographql.apollo3.network.apolloHttpCache.ApolloHttpCache
import okio.Path.Companion.toPath

fun apolloClient(baseUrl: String, cacheDir: String): ApolloClient =
  ApolloClient.Builder()
    .serverUrl("$baseUrl/graphql")
    .httpEngine(OkHttpEngine())
    .enableAutoPersistedQueries() // ★ APQを有効化
    .httpCache(
      ApolloHttpCache(
        directory = "$cacheDir/apolloCache".toPath(),
        // 公開情報のみキャッシュ。PII系はサーバの Cache-Control: no-store で除外
      )
    )
    .build()

意図enableAutoPersistedQueries()で帯域/レイテンシを削減。PIIはキャッシュ不可をサーバ指示で統制。

2-3. Compose UIと結線(状態管理+エラーハンドリング)

@Composable
fun AccountSummaryScreen(vm: AccountViewModel) {
  val uiState by vm.uiState.collectAsState()
  when (uiState) {
    is UiState.Loading -> CircularProgressIndicator()
    is UiState.Error   -> ErrorView((uiState as UiState.Error).message)
    is UiState.Success -> SummaryView((uiState as UiState.Success).data)
  }
}

class AccountViewModel(private val client: ApolloClient): ViewModel() {
  private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
  val uiState: StateFlow<UiState> = _uiState

  fun load(accountId: String) = viewModelScope.launch {
    runCatching { client.query(AccountSummaryQuery(accountId)).execute() }
      .onSuccess { res ->
        if (!res.errors.isNullOrEmpty()) {
          _uiState.value = UiState.Error(res.errors!!.first().message)
        } else {
          // ★ PIIはクエリに含めない。必要時のみ限定クエリで取得し短期保持
          _uiState.value = UiState.Success(res.dataAssertNoErrors)
        }
      }
      .onFailure {
        // APQ未登録はクライアントが自動再送。ここではネットワーク系を案内
        _uiState.value = UiState.Error("通信が不安定です。再試行してください")
      }
  }
}

意図:UI層にPIIを持ち込まず、必要最小限・短期保持を徹底。

3. サーバ&CDN:APQ→Safelist固定・キャッシュキー設計

3-1. サーバでAPQ(自動登録)を有効にする

初期はAPQで慣らし、その後本番はSafelist固定へ。

import { ApolloServer } from '@apollo/server';
import { createPersistedQueryPlugin } from '@apollo/server-plugin-persisted-queries';
import crypto from 'crypto';

const server = new ApolloServer({
  typeDefs, resolvers,
  plugins: [
    createPersistedQueryPlugin({
      hash: (s: string) => crypto.createHash('sha256').update(s).digest('hex')
    })
  ],
});

意図:横展開時は外部キャッシュ(Redis等)で忘却を防ぐ。

3-2. Safelist固定運用(本番)

CIでhash→queryのマニフェストを作成・配布し、未登録ハッシュは拒否(本文送信も拒否)。

// 擬似例:unknown hash 拒否
const safelist = new Set(JSON.parse(fs.readFileSync('pq-manifest.json','utf8')));
app.use('/graphql', (req, res, next) => {
  const hash = req.body?.extensions?.persistedQuery?.sha256Hash;
  if (hash && safelist.has(hash)) return next();
  return res.status(400).json({ errors: [{ message: 'PersistedQueryNotAllowed' }]});
});

意図:検証=APQ許可、本番=Safelist固定の二段構え。

3-3. CDNキャッシュとHTTP制御(PIIはno-store)

公開情報(ニュース/金利等)はpublic, max-age, ETag積極キャッシュ。PIIや残高はCache-Control: no-store一切キャッシュしない

# 例:NGINXでPOSTボディからsha256Hashをキーに含める(概念)
map $request_body $pqkey {
  "~sha256Hash\":\"([a-f0-9]{64})\"" $1;
  default "";
}
proxy_cache_key "$scheme$request_method$host$request_uri$pqkey";

意図:ハッシュをキー化しエッジヒット率を向上。ただしAuthorization付きは共有キャッシュ不可。公開/非公開でパス分離が王道。

4. PII最小化・権限制御・監査:金融品質の必須論点

4-1. スキーマ設計(PIIを外へ出さない)

type AccountSummary {
  accountId: ID!
  balanceMasked: String!   # "¥***,***"
  lastUpdateIso: String!
  # PIIは含めない
}

type AccountPII {          # 分離
  ownerNameMasked: String!
  phoneTail: String!
}

type Query {
  accountSummary(id: ID!): AccountSummary!
  accountPII(id: ID!): AccountPII! @requiresScope(scope: "pii:read")
}

意図:基本はマスキング済み。PIIはスコープ付の専用クエリに分離。

4-2. フィールドレベル認可とSafelistの相乗効果

@requiresScope等でフィールド単位の権限を確認。Safelistにより「そのフィールドを含むクエリ」自体が事前登録され、意図しないPII取得を構造的に抑止。
監査はハッシュ値operationNameを記録し、追跡可能に。

4-3. ログ/監査の実務(PII非記録+トレース)

保存するのは匿名ユーザーID / traceId / operationName / hash / 所要時間 / 結果など。PII混入の可能性があるヘッダー/変数はマスク/除外。重要操作は冪等キー必須。

5. 運用・テスト・ロールアウト

5-1. マニフェスト生成(CIで自動化)

ビルド時に.graphqlをハッシュ化してpq-manifest.jsonを出力・配布。検証=APQ、本番=Safelist固定で段階導入。

// build-scripts/persist.js(概念)
const fs = require('fs'); const crypto = require('crypto'); const globby = require('globby');
(async () => {
  const files = await globby('src/**/*.graphql');
  const map = {};
  for (const f of files) {
    const q = fs.readFileSync(f, 'utf8').trim();
    const h = crypto.createHash('sha256').update(q).digest('hex');
    map[h] = { operationName: /query\s+(\w+)/.exec(q)?.[1] ?? 'Unknown' };
  }
  fs.writeFileSync('dist/pq-manifest.json', JSON.stringify(map, null, 2));
})();

意図ハッシュ+operationNameで監査の可読性を高める。

5-2. KPI/計測(高速化の見える化)

モバイルで平均ペイロードサイズ / RTT / APQヒット率 / CDNヒット率を可視化。PIIクエリは件数/所要時間/利用者割合を監視し、不要アクセス削減を継続。

5-3. フェールセーフとロールバック

新スキーマ時は青/緑デプロイで旧新マニフェストを同時許可し、即時ロールバック可能に。クライアントはFeature Flagでクエリ切替。CDN/WAF変更も段階適用+メトリクス監視。

まとめ

Compose MultiplatformでUI/ロジックを共通化し、Apollo KotlinのPersisted Queries(APQ→Safelist)を組み合わせれば、PII最小化高速化を同時に実現できる。
スキーマでPIIを分離し、フィールド権限と監査で締める。公開情報はハッシュをキーにエッジ高速化、PIIはno-storeで痕跡を残さない——これが金融アプリの現実解。

参考URL

採用情報 長谷川 横バージョン
SHARE
PHP Code Snippets Powered By : XYZScripts.com
お問い合わせ