😶‍🌫️

gRPCスキーマを活用したDTO変換パターン:型安全性と保守性の向上

2025/03/06に公開

はじめに

gRPCは効率的な通信プロトコルとして広く採用されていますが、Protocol Buffersで定義されたメッセージ型と実際のアプリケーションドメインモデルの実装、もしくは変換を適切に行うことが重要です。
この記事では、Rustにおいて.protoファイルから生成されたgRPCスキーマを利用してDTO(Data Transfer Object)変換を実装する方法と、そのメリットについて解説します。
※クリーンアーキテクチャに関する知識があると理解しやすいと思います。

コードの概要

サンプルコードでは、gRPCで定義したメッセージ型と、アプリケーション内で使用するDTOモデルとの間の変換をFromトレイトを使って実装しています。特にchrono::NaiveDateTime型と文字列表現の相互変換に焦点を当てています。

gRPCスキーマとDTOパターンの関係

まず、Protocol Buffersで定義されたgRPCスキーマを見てみましょう:

test.proto
syntax = "proto3";
package test;

service TestService {
  rpc TestMethod (TestRequest) returns (TestResponse) {}
}

message TestRequest {
  string name = 1;
  string email = 2;
  string password = 3;
}

message TestResponse {
    string name = 1;
    string email = 2;
    string password = 3;
    string created_at = 4;
    string updated_at = 5;
}

対応するDTOモデル(アプリケーションドメインモデル)は以下のようになっています:

src/model.rs
use crate::proto::test_module::TestResponse;
use chrono::NaiveDateTime;

#[derive(Default, Debug)]
pub struct TestRequestDto {
    pub name: String,
    pub email: String,
    pub password: String,
}

#[derive(Default, Debug)]
pub struct TestResponseDto {
    pub name: String,
    pub email: String,
    pub password: String,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

gRPCスキーマベースのDTO変換のメリット

1. 型安全性の向上

gRPCスキーマを基にしたDTO変換の最大のメリットは型安全性です。以下のコードに注目してください:

src/model.rs
// gRPC struct => Dto
impl From<TestResponse> for TestResponseDto {
    fn from(response: TestResponse) -> Self {
        Self {
            name: response.name,
            email: response.email,
            password: response.password,
            created_at: NaiveDateTime::parse_from_str(&response.created_at, "%Y-%m-%d %H:%M:%S")
                .unwrap(),
            updated_at: NaiveDateTime::parse_from_str(&response.updated_at, "%Y-%m-%d %H:%M:%S")
                .unwrap(),
        }
    }
}

gRPCの文字列として扱われる日時情報をNaiveDateTime型に変換することで、アプリケーション内では日付操作に特化した型として扱えます。これにより、日付計算や比較などが型安全に行えます。

2. 関心事の分離

Protocol Buffersは通信のためのスキーマであり、アプリケーションのドメインモデルとは直接一致しない場合が多いです。※プリミティブ型しかないため、適切なRustの型にParseする必要がある。

DTOパターンを使うことで:

  • 通信層(gRPC)とドメイン層の責務を明確に分離できます
  • 各層に最適な型とバリデーションを適用できます
  • アプリケーション内部の実装詳細を隠蔽できます

3. バージョン互換性と進化の容易さ

gRPCのスキーマとアプリケーションのモデルを分離することで、片方を変更しても互換性を保ちやすくなります:

src/model.rs
// Dto => gRPC struct
impl From<TestResponseDto> for TestResponse {
    fn from(dto: TestResponseDto) -> Self {
        Self {
            name: dto.name,
            email: dto.email,
            password: dto.password,
            created_at: dto.created_at.format("%Y-%m-%d %H:%M:%S").to_string(),
            updated_at: dto.updated_at.format("%Y-%m-%d %H:%M:%S").to_string(),
        }
    }
}

この変換層があることで、内部モデルと外部APIの独立した進化が可能になります。

4. コードの再利用性と保守性

Fromトレイトを使用した変換パターンにより:

  • 変換ロジックが一元化され、重複が排除されます
  • テストが容易になります
  • 変換ロジックの変更が一箇所で完結します

5. エラーハンドリングの改善

実際のアプリケーションでは、サンプルコードのunwrap()の代わりに適切なエラーハンドリングを実装できます:

src/model.rs
impl TryFrom<TestResponse> for TestResponseDto {
    type Error = chrono::ParseError;

    fn try_from(response: TestResponse) -> Result<Self, Self::Error> {
        Ok(Self {
            name: response.name,
            email: response.email,
            password: response.password,
            created_at: NaiveDateTime::parse_from_str(&response.created_at, "%Y-%m-%d %H:%M:%S")?,
            updated_at: NaiveDateTime::parse_from_str(&response.updated_at, "%Y-%m-%d %H:%M:%S")?,
        })
    }
}

実装例の解説

サンプルのmain.rsでは、gRPCのレスポンス型をDTOに変換する例を示しています:

src/main.rs
fn main() {
    // テスト用のレスポンスを作成
    let test_response = TestResponse {
        name: "name".to_string(),
        email: "email".to_string(),
        password: "password".to_string(),
        created_at: "2021-01-01 00:00:00".to_string(),
        updated_at: "2021-01-01 00:00:00".to_string(),
    };

    // 型変換の明示的な方法を使用
    let test_response_dto: models::TestResponseDto = test_response.into();
    println!("{:?}", test_response_dto);
    println!(
        "TestResponseの型: {}",
        type_of(&test_response_dto.created_at)
    );
}

この例では、test_response.into()で自動的に型変換が行われ、文字列だった日時がNaiveDateTime型に変換されています。

まとめ

gRPCスキーマを活用したDTO変換パターンは、型安全性、関心事の分離、バージョン互換性の維持、コードの再利用性など、多くのメリットをもたらします。特にRustのような強い型システムを持つ言語では、このパターンの恩恵を最大限に受けることができます。

また、このパターンはマイクロサービスアーキテクチャにおいて特に有用で、サービス間の通信インターフェースと内部実装の分離を明確にし、システム全体の堅牢性と保守性を高めることができます。

💬 もし興味がある方は、ぜひコメントやフィードバックをお願いします!

Discussion