例外クラスのリファクタリングで考慮したこと:多数のサービスから依存されるクラス設計の事例と学び

1. はじめに

こんにちは。アソビュー!でバックエンドエンジニアをやっている島田です。

アソビューではマイクロサービスアーキテクチャを採用していて、複数のアプリケーション間での通信が頻繁に発生します。 これらのアプリケーション間の通信のために、高効率で安定した通信プロトコルであるgRPCを採用しています。そして、各アプリケーションが提供するインターフェースやデータ構造は、Protocol Buffersを用いて定義されており、これにより統一性を保ちつつ、拡張性やメンテナンス性も確保しています。

また、各サービスのエラーレスポンスの型を共通化するために、汎用的な例外クラスを定義しています。 今回はこの例外クラスを修正した際に良い設計と悪い設計について学びを得たので、本記事でそれを紹介したいと思います。

2. 修正対象のクラス

まずはじめにどのようなクラスを修正していくのかを見ていきます。 以下は、Protobufのエラーを扱いやすくするためにSpring FrameworkのResponseStatusExceptionクラスを参考に、汎用的なExceptionクラスを独自で定義したResponseStatusExceptionクラスです。 当該クラスはProtocol Buffersで生成されていて多数のアプリケーションから依存されるクラスとなっています。

シンプルなコンストラクタが用意されており、messageは開発者向けのメッセージとなっています。

gist.github.com

今回こちらのResponseStatusExceptionクラスにユーザ向けメッセージを渡せるように修正を行います。 弊社ではGoogle Cloud APIの設計指針をベースに開発を行っています。 ユーザ向けエラーメッセージが必要な場合に設定する、google.rpc.LocalizedMessage を詳細フィールドとして使用します。

cloud.google.com

また、メッセージ自体は messages.properties で管理するため、メッセージキーと可変文字を受取るように修正します。

3. 悪い設計

コンストラクタを一つ追加しています。

一番単純でやりがちな修正かと思います。 利用される依存箇所や利用方法が限定されている場合は良いかもしれませんが、多数のサービスに依存していて利用方法も多様な場合は良くない設計と言えます。

gist.github.com

冗長なデータ構造

LinkedHashMap<String, Object[]> という構造は、キーと値のペアを保持する一方で、そのキーや値が何を示しているのか、コードを読むだけでは明確ではありません。 さらに、このような構造は、扱いにくく、誤解を招く可能性が高まります。例えば、Object[] の要素がどのような順番で格納されているか、その意味合いは何かが読み取りづらいです。

拡張性の制限

LinkedHashMapでは、メッセージのフォーマットや構造を変更することが困難です。新しいタイプのメッセージや属性を追加するための余地がほとんどありません。

nullの多用

detailsフィールドは、初期値としてnullが多く用いられています。これは、特定のフィールドが未設定の場合のデフォルト値としての役割を果たしている一方、 nullの使用は、NullPointerExceptionのリスクを増やし、コードの安全性を低下させます。

また、nullの値がどのような意味を持つのか(例えば、データが未設定であることを示すのか、エラー状態を示すのかなど)が一貫していない場合、コードの読解性やメンテナンス性にも影響を与える可能性があります。

さらに、今後コンストラクタが増えるにつれてフィールドが増える上にnullが設定されることが予想されます。

4. 良い設計

以下の設計を行いました。

  • データ構造の最適化
  • 抽象化の導入
  • ユーティリティクラスとBuilderの採用
  • メッセージ解決の仕組み

データ構造の最適化

detailsで利用していたLinkedHashMapの代わりに、Setを採用しました。これにより、データの取り扱いがシンプルになり、コードの可読性が向上しました。

また、emptySet にすることでnullを利用せず状態を表現できました。

gist.github.com

抽象化の導入

以下のパターンを想定し、抽象化されたインターフェイスを扱う形としました。

  • テンプレート化されたメッセージを扱うパターン(TemplateMessage)
  • そのままProtobufのMessageを例外に渡すパターン(RawMessage)

エラーの詳細メッセージを扱う方法としてDetailMessageというインターフェイスを導入しました。

これにより、DetailMessageインターフェイスを通じて、異なる種類のメッセージを一貫した方法で取り扱えるようになり、 TemplateMessageRawMessageという二つの具体的な実装を通じて、多様なメッセージ形式に対応することが可能になりました。

  • TemplateMessage: テンプレート化されたメッセージを表現するためのインターフェイス。具体的な実装としてLocalizedMessageTemplateMessageがあり、これは今回追加したいローカライズされたエラーメッセージをサポートしています。
  • RawMessage: ProtobufのMessageを直接扱うためのクラス。エラーメッセージをそのまま表現したい場合に利用します。

gist.github.com

ユーティリティクラスとBuilderの採用

DetailMessagesのユーティリティクラスを導入することで、冗長なコードを排除し、また、エラーメッセージの生成を簡潔で分かりやすくしました。特に、Builderパターンを採用することで、メッセージの生成が直感的になりました。

gist.github.com

メッセージ解決の仕組み

MessageResolverインターフェイスを導入することで、ローカライズされたメッセージの生成やテンプレートメッセージの解決を明確かつ柔軟に行うことができるようになり、 将来的に異なる方法でメッセージを解決するための拡張が容易になりました。

gist.github.com

まとめ

今後の拡張や変更が容易になるだけでなく、コードの可読性やメンテナンス性も向上しました。 これらの変更を通じて、構造化や抽象化によって拡張・保守性が大事であることを学んだので今後の設計に活かしていきたいです。

最後に

アソビューではより良いプロダクトを世の中に届けられるよう共に挑戦していくエンジニアを募集しています。 カジュアル面談もやっておりますので、お気軽にエントリーください! お待ちしております。

www.asoview.com