多言語ドライバ開発の難題:MongoDBの挑戦

MongoDB、多言語ドライバの「ポリグロットのジレンマ」をYAMLテストで解決した開発秘話のイメージ画像

MongoDBでは、Python、JavaScript、Rustなど、約12種類のプログラミング言語でクライアントライブラリ(ドライバ)を開発しています。これらのドライバのほとんどは、それぞれの言語でネイティブに実装されており、C言語で書かれたコアを薄くラッピングする一般的なアプローチとは一線を画しています。ネイティブドライバは、インストールが容易で、各言語のイディオムに沿ったAPIを提供し、スレッドモデルや非同期ループ、ガベージコレクションといったランタイムに自然に統合できるという利点があります(Lobsters)。

各言語のランタイムに深く統合されたネイティブドライバは、開発者体験を大きく向上させる一方で、その一貫性を保つことが非常に難しい課題となります。

しかし、このアプローチは「ポリグロットのジレンマ」とでも言うべき課題を生み出します。すなわち、多くの異なる言語で書かれたドライバ間で、APIと振る舞いの整合性をどのように保証するかという問題です。例えば、Pythonのメソッドは snake_case、Javaのメソッドは camelCase といった言語固有の規約がありますが、基本的なAPIセットや、因果一貫性(causal consistency)のようなコアな概念は、言語を問わず同じ意味を持つ必要があります。

統一テスト言語の模索:CucumberからYAMLへ

MongoDB、多言語ドライバの「ポリグロットのジレンマ」をYAMLテストで解決した開発秘話のイメージ画像

MongoDBのエンジニアたちは、ドライバの標準化について長年議論を重ねてきました。記事の筆者がMongoDBに2011年11月に参加した当初、JavaドライバのテストにはBehavior-Driven Development (BDD) のフレームワークである「Cucumber」が使われていました。Cucumberは、以下のような人間が読める形式の構文でテストケースを記述できます。

Scenario: Exclude a field from query results
  Given collection "users" contains documents
    """
    {"_id": 1, "name": "alice", "password": "hunter2"}
    {"_id": 2, "name": "bob",   "password": "swordfish"}
    """
  When find documents by
    """
    {"name": "alice"}
    """
  And return fields
    """
    {"password": false}
    """
  Then the result should be
    """
    {"_id": 1, "name": "alice"}
    """

筆者は当初、PythonやRubyなど、他の言語でもCucumberテストランナーを開発することを模索しましたが、同僚のエンジニアから「Cucumberの構文は受け入れられない」という強い反発を受け、このアプローチは断念されました。創設者のEliot Horowitz氏もCucumberを「滑稽だ」と評したといいます(Lobsters)。

数年後、ネットワーク遅延の測定と応答に関する新機能開発で、再び共通テストスイートの必要性が浮上しました。今度はCucumberを避け、YAML上にカスタムのテスト言語を開発するというアプローチが採用されました。このYAMLテストは、MongoDBの歴史上最も成功したプロジェクトの一つとなり、11年以上にわたりドライバの動作の一貫性を確保してきました。

YAMLの驚くべき効果とUnified Test Format

YAMLベースのテスト言語は、MongoDBのドライバチームにとって「非常に効果的」でした。このアプローチが導入されてから11年以上が経過し、すべてのMongoDBドライバが共通のAPIと振る舞いに収束しています。各言語のイディオムや慣習に合わせて意識的に選択された差異は残しつつも、必要な範囲での整合性が実現されています。

現在では、YAMLをベースとした「Unified Test Format」が開発され、12種類もの異なる仕様に準拠したドライバのテストが可能となっています。これらの仕様は、ドキュメントの読み書き、コマンドの再試行、トランザクションの開始と終了など、ドライバの多岐にわたる機能をカバーしています。この標準化されたテストプロセスは、開発効率を向上させ、各ドライバの品質を高いレベルで維持するために不可欠な要素となっています。

項目 Cucumber (BDD) カスタムYAMLテスト
テスト記述形式 自然言語に近い構文 YAML形式
採用状況 MongoDBでは反発が多く断念 MongoDBで広く採用され成功
メリット (BDD) ビジネスロジックの可読性 簡潔性、機械可読性、柔軟性
デメリット (BDD) 構文への反発、表現の曖昧さ 初期設計の手間
特徴 行動駆動開発 特定ドメインに特化可能

エンジニア目線で見ると:ドメイン特化言語とテストの重要性

多言語での開発において、共通の振る舞いを保証するためのテストは本当に悩ましい課題だと感じます。特に、特定のドメイン(この場合はデータベースドライバの動作)に特化したテストフレームワークを構築するというアプローチは、非常に理にかなっているのではないでしょうか。一般的なBDDフレームワークがエンジニアから反発を受けたのは、その表現が時に冗長であったり、プログラマティックなテスト記述に慣れた開発者にとって「しっくりこない」と感じられたりしたためではないかと思います。

YAMLは簡潔で構造化されたデータ記述言語であり、カスタムのテスト言語として利用することで、必要な情報だけを効率的に表現できます。PythonやTypeScriptを日常的に使う私にとって、このようなドメイン固有言語(DSL)的なアプローチは、複雑なロジックや振る舞いを簡潔に定義する際に非常に有効だと感じます。

例えば、私の個人プロダクトで、複数のマイクロサービス間でのデータ整合性を保証するためのテストを設計する際にも、YAMLベースの設定ファイルでテストシナリオを記述することは有力な選択肢になりそうです。特定のインタフェースを持つサービスに対して、入力と期待される出力をYAMLで定義し、それを各言語のテストランナーで解釈させることで、テストコード自体の重複を減らしつつ、一貫したテスト環境を構築できる可能性があります。

また、テストデータとテストロジックの分離は、テストのメンテナンス性を大きく向上させます。テストケースの追加や変更が容易になり、仕様変更への対応も迅速に行えるようになります。

これは、Agile開発やスタートアップのような迅速なイテレーションが求められる環境において、非常に重要な要素だと考えます。私のチームでも、CI/CDパイプラインにこのような共通テストフォーマットを組み込むことで、リリース前の品質保証をより効率的かつ確実に行えるのではないかと期待が膨らみます。