プロダクト開発におけるエラー定義について

こんにちわ。GMOメイクショップ コアグループの小野です。

次世代EC開発で主にバックエンドを担当しています。

次世代EC開発とは? www.makeshop.jp

1. はじめに

いきなりですが、エラーはプログラミングやアプリからは切っても切れず、プログラマーはまず考えないことはないかと思います。

また開発の度に追加・変更されたり、いざ保守しようと思ったらよく分からないナニコレとなったりと、頭を悩ませる事も多いかと思います。

今回は次世代EC開発に伴いエラー定義を見直したことについて記していこうと思います。

この記事に登場する利用技術は以下の通りです。

  • Vue
  • Go
  • GORM
  • GraphQL

2. 事の発端

ある機能を実装した際、下記状況を確認しました。

  • GORM のメソッドにより返却される情報が一定ではないこと
  • フロンドエンドへ返却するエラーがDBのエラー内容そのままの状態であったこと

当時は開発全体から見たらまだまだ初期であったり、自身の言語把握もやっとだったり、先駆者も参画して間もない状況だったりと、今考えれば当たり前のことと思ってても、そこまで考慮できずに(後回しにして)開発を進めていました。

そこで上記課題に対して、

  • フロント・バック双方に関係し、後にすればするほどエラーハンドリングを適用していくのが難しくなる可能性があること
  • DBのことをプロダクト全体が考慮するのは後々辛いであろうこと
    • バックエンド構成がクリーンアーキテクチャを模したものになっていたことから、DBの関心事をrepository層に閉じておき、domain層からは別の情報で管理した方が良さそうなこと
  • ある程度一定した情報を返却した方がプロダクト全体の揺れが少ないであろうこと

といった見解から、優先度を上げてエラー定義やエラーハンドリングを検討・実装することとなりました。

3. Go言語におけるエラーハンドリング

ということで検討を始めたものの、次世代EC開発参画まではJavaメインでしたので、what's type error interface というところから始まりまして...

  • GoにはException等例外の概念がなく、エラーは error が担っている
  • 単純にエラーを返すだけであれば errors.New を使えば良い。フォーマットもしたいなら fmt.Errorf が使える
    • ただこれだけだとエラーメッセージは確認できても、エラー種別等の情報は一切ない(Javaでいう EOFException とか IOException とか)
  • 構造体に Error() メソッドを実装すれば error 型と判定される独自のエラー構造体を用意できる(これにより上述のエラー種別を解消することができ得る)
    • また構造体なのでメッセージ以外の情報も持つことができる

Java脳的に、インターフェースの概念を押さえるだけでも結構悩んだりとなかなか把握が大変でしたが、、、

ここらのプログラム的な詳細については、また別の機会にご紹介できればと思います。

結論、独自の情報を後述する エラーコードとメッセージは対にして、ある程度一元的に管理する にあたり、 エラーコードとメッセージ等を持つ独自のエラー構造体を用意することとしました。

4. エラー情報の管理

ちなみに現行システムでアラート表示する際、一部を除いてメッセージしか表示してない状況がありました。

ですので、何かあった該当するエラーメッセージから原因を特定する必要があり、その文字列によっては特定が大変そうな印象を持ちました。

例えば、現行システムでは、「登録に失敗しました。」とエラーの事象のみをメッセージとして表示して、エラーの原因を表示していない箇所があります。 このような箇所では、エラー原因を特定するためにプログラムを注意深く調べる必要がありました。

それもあり次世代ECでは下記方針で管理していくのが良いのでは、と検討しました。

  • エラーコードとメッセージは対にして、ある程度一元的に管理する
  • コードは短めにかつ端的には意味の分からない文字列(ただし法則性はあり)
    • ユーザーやカスタマーサポートからの問い合わせに対して、伝言ゲームの要素が少なくなることを期待
    • 調査に対しては一意で検索・特定できることを期待
    • 万一エラー内容に変更があっても、コードが意味を持たなければメッセージを差し替えられることも考慮(欠番でも構わない)
  • メッセージは長すぎず過不足が無い程度に
    • 法則性があるとはいえ、コードだけでは一次把握が難しいので、補う程度の内容も付加
    • 国際化も考慮してバックエンドでは英語表記で定義
      • ただしフロントエンドで日本語化もしくは差し替えを行い、適宜表示できるように

まあまあよくある形式かと思いますが、それが故にこの形に落ち着いたとも言えるかと思います。

5. エラー表示例

早速ですがデータ通信におけるエラー情報は下記の状況となりました。(抜粋と、一部具体的な内容は伏せさせていただいてます)

例1. record not found
  • 変更前
  "errors": [
    {
      "message": "record not found"
    }
  ],
  • 変更後
  "errors": [
    {
      "message": "recode not found",
      "extensions": {
        "code": "XXXX0001",
      }
    }
  ],
例2. table not found
  • 変更前
  "errors": [
    {
      "message": "Error 1146 (42S02): Table 'xxx.yyy' doesn't exist"
    }
  ],
  • 変更後
  "errors": [
    {
      "message": "table not found",
      "extensions": {
        "code": "XXXX0002",
      }
    }
  ],
  • errors.message: エラーメッセージ
  • errors.extensions.code: エラーコード

record not found の場合は変更前後でmessageの差異はありませんが、 table not found の場合の変更前ではDBのエラーコードもそのまま出てしまっています。

これに対し、変更後はシンプルなmessageに変えています。

またGraphQLの標準エラー形式ではエラーコードに該当するフィールドが存在しませんが、拡張フィールドを利用してどちらの場合もcodeも返却するようにしました。

これによって統一性のあるエラー情報になり、またコードが分かればエラーを特定できる状況が作れたかと思います。

実際にGraphQLレスポンスとしてこの形式に落とし込むためにはいくつか段階があるのですが、それはまた別の機会にご紹介できればと思います。

6. 課題

  1. 4.において エラーコードとメッセージは対にして、ある程度一元的に管理する 方針にしてますが、一方 ただしフロントエンドで日本語化もしくは差し替えを行い、適宜表示できるように という多少の柔軟さを残した結果、完全に一元管理することはできず、フロント/バックでの二重管理は避けられていません。現状は人力でフロント/バックそれぞれにエラー定義を行っておりますが、これに対してはツールを作成して何らかのタイミングで同期を取るなど、人力での管理を省ければ無視できる範疇になるかと考えています。
  2. データ通信上はエラーコードを確認できるのですが、現時点で画面上に表示はできていません。これについてはフロントエンドでの汎用的なエラーハンドリング待ちになってしまっています。

7. まとめ

今回は次世代EC開発で検討・採用したエラー定義について、簡単ではありますがご紹介いたしました。

プロダクトやチームの特性・方針によって内容が多岐に渡る議題だと思いますので、これが正解というものは無いかなと考えています。

今回草案は私が考えましたが、とはいえネットに転がってる情報や以前利用したツールなど参考にさせていただいておりますし、また最終的にはチーム内で議論をした内容で進めており、合意形成していくことも大切だと思います。

今後同じような検討を行うどなたかの一助になれば幸いです。