🐶
共通基盤でClientパターンをつかうかどうかの個人的基準
概要
このドキュメントでは、アプリケーションの共通基盤においてClientパターンを使うかどうかの個人的な基準をまとめています。
Client パターンとは?
Client パターンは、外部サービスやリソースへの接続を抽象化し、接続管理や設定の一元化を行うデザインパターンです。
Client パターンが有効なケース
// DB接続の例
const dbClient = new DatabaseClient(config);
await dbClient.connect();
await dbClient.query("SELECT ...");
await dbClient.disconnect();
メリット:
- 接続管理(connect/disconnect)
- トランザクション管理
- 接続プール
- リトライロジック
- 共通設定の一元管理
Client パターンの必要性が低いケース
// シンプルな関数呼び出し
const result = validateEmail(email);
const encrypted = encrypt(text, key);
特徴:
- 状態を持たない
- 1回の呼び出しで完結
- 接続管理が不要
- 依存関係がない
✅ Client パターンの必要性が高い共通基盤
1. 認証 (Auth)
必要度: ✅ 高い
const authClient = new AuthClient({
serviceUrl: process.env.AUTH_SERVICE_URL,
timeout: 5000,
retryConfig: { maxRetries: 3 }
});
// 接続の再利用
await authClient.verifyToken(token);
await authClient.refreshToken(refreshToken);
await authClient.getUserPermissions(userId);
必要な理由:
- 接続の再利用: HTTP クライアント(axios/fetch)の接続プール
- 設定の一元管理: タイムアウト、リトライ、エンドポイント
- 利用箇所が多い: API ミドルウェア、バックグラウンドジョブ、CLI ツール
- 状態管理: トークンキャッシュ、レート制限
2. ロギング (Logger)
必要度: ✅ 高い
const logger = new LoggerClient({
service: 'user-service',
environment: process.env.NODE_ENV,
logLevel: 'info',
transports: [
new ConsoleTransport(),
new FileTransport({ path: './logs' }),
new CloudWatchTransport({ region: 'ap-northeast-1' })
]
});
// コンテキスト付きロギング
logger.info('User logged in', { userId, sessionId });
logger.error('Payment failed', { orderId, error });
必要な理由:
- 共通フォーマット: JSON 構造化ログ、トレース ID 付与
- 複数出力先管理: Console + File + CloudWatch
- バッファリング: バッチでログ送信(パフォーマンス)
- コンテキスト保持: リクエストスコープの情報を自動付与
3. メトリクス (Metrics)
必要度: ✅ 高い
const metricsClient = new MetricsClient({
namespace: 'MyApp',
provider: 'CloudWatch', // or 'Datadog', 'Prometheus'
flushInterval: 60000 // 1分ごとにバッチ送信
});
// カウンター
metricsClient.increment('api.requests', { endpoint: '/users', status: 200 });
// ゲージ(現在値)
metricsClient.gauge('queue.size', queueLength);
// ヒストグラム(分布)
metricsClient.histogram('api.latency', duration, { endpoint: '/users' });
必要な理由:
- バッチ送信: 個別送信だと API 呼び出し過多(コスト・パフォーマンス問題)
- 接続プール: CloudWatch/Datadog への HTTP 接続を再利用
- バッファリング: メモリ内で集約してから送信
- 自動フラッシュ: 定期的 or プロセス終了時に送信
4. Audit ログ
必要度: ⚠️ 場合による
Client が不要なケース(シンプル)
// DB に直接書き込むだけ
export async function logAudit(event: AuditEvent) {
await db.audit.create(event);
}
Client が必要なケース(複雑)
const auditClient = new AuditClient({
retentionPolicy: '90d',
encryption: true,
destinations: ['database', 's3-archive', 'siem-system']
});
await auditClient.log({
action: 'USER_DELETED',
actor: adminId,
target: userId,
metadata: { reason: 'GDPR request' }
});
Client が必要になる条件:
- 複数の送信先(DB + S3 アーカイブ + SIEM システム)
- 暗号化(センシティブデータの自動暗号化)
- バッチ処理(大量の Audit ログをバッファリング)
- 接続確認(SIEM システムへの接続ヘルスチェック)
5. キャッシュ (Cache)
必要度: ✅ 高い
const cacheClient = new CacheClient({
provider: 'redis',
host: process.env.REDIS_HOST,
connectionPool: { min: 2, max: 10 },
defaultTTL: 3600
});
await cacheClient.set('user:123', userData, { ttl: 1800 });
const user = await cacheClient.get('user:123');
await cacheClient.invalidate('user:*'); // パターンマッチ削除
必要な理由:
- 接続プール: Redis 接続の管理
- フェイルオーバー: プライマリ障害時にレプリカに切り替え
- シリアライゼーション: 自動で JSON ⇔ Object 変換
- TTL 管理: デフォルト有効期限の設定
6. メール送信 (Email)
必要度: ✅ 高い
const emailClient = new EmailClient({
provider: 'ses', // or 'sendgrid', 'mailgun'
from: 'noreply@example.com',
templates: './email-templates',
rateLimits: { maxPerSecond: 14 } // SES の制限
});
await emailClient.send({
to: user.email,
template: 'welcome',
data: { userName: user.name }
});
必要な理由:
- 接続管理: SMTP/API接続の再利用
- レート制限: SESなど利用サービスの送信制限を超えないよう制御
- リトライ: 失敗時の自動リトライ
- テンプレート管理: HTML/テキストメールの生成
7. ストレージ (Storage)
必要度: ✅ 高い
const storageClient = new StorageClient({
provider: 's3',
bucket: process.env.S3_BUCKET,
region: 'ap-northeast-1',
presignedUrlExpiry: 3600
});
// ファイルアップロード
await storageClient.upload('avatars/user-123.jpg', fileBuffer);
// 署名付き URL 生成
const downloadUrl = await storageClient.getSignedUrl('avatars/user-123.jpg');
必要な理由:
- マルチプロバイダー対応: S3、GCSなどのストレージサービスを抽象化
- 接続プール: HTTPなどの通信接続の再利用
- 署名管理: 認証情報の自動付与
- ストリーム処理: 大きなファイルのメモリ効率的な処理
8. メッセージキュー (Queue)
必要度: ✅ 高い
const queueClient = new QueueClient({
provider: 'sqs',
queueUrl: process.env.SQS_QUEUE_URL,
batchSize: 10,
visibilityTimeout: 30
});
// メッセージ送信
await queueClient.send({ type: 'EMAIL', data: { userId: 123 } });
// バッチ受信
const messages = await queueClient.receive();
for (const msg of messages) {
await processMessage(msg);
await queueClient.delete(msg); // 処理完了後に削除
}
必要な理由:
- バッチ処理: 複数メッセージを一度に送受信(コスト削減)
- 可視性タイムアウト管理: 処理中のメッセージを他の worker から隠す
- デッドレターキュー: 失敗メッセージの自動転送
- 接続管理: 長時間接続(long polling)
❌ Client パターンの必要性が低い共通基盤
9. バリデーション
// シンプルな関数で十分
export function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export function validatePassword(password: string): boolean {
return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password);
}
理由:
- 状態を持たない
- 接続管理が不要
- 純粋関数で実装可能
10. 暗号化(単純な場合)
// 状態不要なら関数で十分
export function encrypt(text: string, key: string): string {
return crypto.createCipher('aes-256-cbc', key).update(text, 'utf8', 'hex');
}
export function decrypt(encrypted: string, key: string): string {
return crypto.createDecipher('aes-256-cbc', key).update(encrypted, 'hex', 'utf8');
}
理由:
- 鍵の管理が外部で行われる場合
- 状態を持たない単純な暗号化/復号化
注意: 鍵のローテーション、マルチバージョン対応が必要な場合は Client パターンを検討
11. フォーマッター / Reporter
// インスタンス化して即実行なら Client 不要
new HtmlFormatter().generate();
new AllureFormatter().generate();
理由:
- 1回実行で完結
- 状態管理が不要
- 前の処理結果に依存しない
Client が必要になるケース:
- 複数のレポート生成が依存関係を持つ
- 共通の設定・状態が必要(例: 全レポートを S3 にアップロード)
- トランザクション的な操作が必要
📊 まとめ表
| 機能 | Clientの必要性 | 主な理由 |
|---|---|---|
| 認証 (Auth) | ✅ 高い | 接続再利用、設定管理、トークンキャッシュ |
| ロギング (Logger) | ✅ 高い | 複数出力先、バッファリング、コンテキスト保持 |
| メトリクス (Metrics) | ✅ 高い | バッチ送信、接続プール、自動フラッシュ |
| Audit ログ | ⚠️ 場合による | 複数送信先・暗号化が必要なら ✅、DB のみなら ❌ |
| キャッシュ (Cache) | ✅ 高い | 接続プール、フェイルオーバー、シリアライゼーション |
| メール (Email) | ✅ 高い | レート制限、リトライ、テンプレート管理 |
| ストレージ (Storage) | ✅ 高い | マルチプロバイダー、署名管理、ストリーム処理 |
| キュー (Queue) | ✅ 高い | バッチ処理、可視性管理、デッドレターキュー |
| バリデーション | ❌ 低い | 状態不要、シンプルな関数で十分 |
| 暗号化(単純) | ❌ 低い | 状態不要、関数で十分 |
| Reporter / Formatter | ❌ 低い | 1回実行、状態不要、依存関係なし |
🎯 判断基準チェックリスト
Client パターンを検討する際の基準をまとめました。
✅ Client が必要な可能性が高い
- 接続管理が必要(connect/disconnect、接続プール)
- 複数回の呼び出しで状態を共有する
- 外部サービスへの接続がある(HTTP、TCP、など)
- バッファリングやバッチ処理が必要
- リトライロジックが必要
- タイムアウト管理が必要
- レート制限の制御が必要
- 設定の一元管理が有効
- 複数の出力先/送信先がある
- トランザクション的な操作が必要
❌ Client が必要な可能性が低い
- 純粋関数で実装できる(入力が同じなら出力も同じ)
- 状態を持たない
- 外部接続がない
- 1回の呼び出しで完結する
- 依存関係がない
- 設定がほぼ固定
💡 Clientパターンの導入手順
1. 小さく始める
最初から Client パターンを導入せず、必要になったタイミングでリファクタリングする。
// Phase 1: シンプルな関数
export async function sendEmail(to: string, subject: string, body: string) {
await ses.sendEmail({ to, subject, body });
}
// Phase 2: 要件が増えたら Client 化
export class EmailClient {
async send(to: string, template: string, data: any) {
// レート制限、リトライ、テンプレート管理
}
}
2. インターフェースを明確にする
Client の責務を明確にし、単一責任の原則を守る。
interface CacheClient {
get(key: string): Promise<any>;
set(key: string, value: any, options?: CacheOptions): Promise<void>;
delete(key: string): Promise<void>;
// ❌ キャッシュの責務を超えている
// sendEmail(to: string): Promise<void>;
}
3. テスタビリティを考慮
Client はモック/スタブに置き換えやすい設計にする。
// ✅ インターフェースを定義
interface AuthClient {
verifyToken(token: string): Promise<User>;
}
// 本番実装
class Auth0Client implements AuthClient { ... }
// テスト用モック
class MockAuthClient implements AuthClient {
async verifyToken(token: string): Promise<User> {
return { id: '123', email: 'test@example.com' };
}
}
🔄 将来的な拡張を見据えた設計例
【テストフェーズ】
テスト結果のレポート機能で、将来 Client が必要になるケース:
// 現在: Client 不要
new HtmlFormatter().generate();
new AllureFormatter().generate();
// 将来: 以下のような要件が出たら Client を検討
const reportClient = new ReportClient({
outputDir: './reports',
compression: true,
uploadToS3: true, // ← 全レポートで共通の処理
notifySlack: true // ← 全レポート生成後に通知
});
await reportClient.generateAll(['html', 'allure', 'json']);
拡張が必要になるサイン:
- 共通の前処理/後処理が必要
- レポート間で依存関係が発生
- 複数のレポートをトランザクション的に扱いたい
- 外部サービスへのアップロードが必要
Discussion