🐶

共通基盤で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