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
- Compose Multiplatform 公式: https://www.jetbrains.com/lp/compose-multiplatform/
- Apollo Kotlin: https://www.apollographql.com/docs/kotlin/
- Apollo Server – Automatic Persisted Queries: https://www.apollographql.com/docs/apollo-server/performance/apq/
- Relay – Persisted Queries: https://relay.dev/docs/guides/performance/
- GraphQL over HTTP: https://graphql.org/learn/serving-over-http/

