こんにちは、プロダクト開発部コアグループの井上です。
コアグループでは、次世代ECの開発を行っています。
www.makeshop.jp
私は2022年6月に入社してから現在まで、主に新管理画面のバックエンド(Go)を担当しています。チーム内では一番Goの経験期間が長かったこともあり、いろいろなことをやらせていただく機会に恵まれました。
この記事では、私が新管理画面のバックエンド開発において、自分主導で行った改善について振り返ってみようと思います。
- 1. ログを追えるようにした
- 2. 必然性のないreflectをやめた
- 3. DIにwireを導入した
- 4. protoからgraphqlを生成できるようにした
- 5. private linterをCIに組み込んだ
- 終わりに
1. ログを追えるようにした
Before
logパッケージ配下に置いた*zap.SugaredLogger
を使っていましたが、
リクエスト単位でログを追ったり、どのショップのログなのかがわからない状態でした。
func DoSomething() { log.L.Debug("Hello world") }
{ "msg": "Hello world", "level": "debug", "ts": "2023-12-20 14:20:23.394801", "caller": "do_something_usecase.go:29", }
After
gRPCを使用しているので、欲しい情報を詰めたロガーを interceptorでcontext.Context
に入れておくことで、
それ以降はcontext.Context
からロガーをとるだけで済むようにしました。
contextにロガー自体を入れてしまう実装はzerologを参考にしました。
github.com
func UnaryInterceptor(idService id.IdService) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if info.FullMethod == healthpb.Health_Check_FullMethodName { return handler(ctx, req) } return interceptUnary(ctx, req, info, handler, idService) } } func interceptUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, idService id.IdService) (any, error) { // gRPCの前にいるBFFからリクエストの一意なIDなどをmetadataで引き渡しています rd := claim.GetRequestDataByMetadata(ctx) ctx = log.WithContext(ctx, "unknown", "unknown", "unknown", rd.RequestId, rd.EndUserRemoteAddr) // ... ctx = log.WithContext(ctx, shopId, host.IdType, userId, rd.RequestId, rd.EndUserRemoteAddr) resp, err := handler(ctx, req) if err != nil { return nil, err } return resp, nil }
func DoSomething(ctx context.Context) { log.FromContext(ctx).Debug("Hello world") }
{ "userId": "xxxx", "IP": "0.0.0.0", "msg": "Hello world", "level": "debug", "ts": "2023-12-20 14:20:23.394801", "caller": "do_something_usecase.go:29", "shopId": "makeshop", "container_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxx", "shopHost": "xxx", "traceId": "dd6de545-1f18-492e-8e38-927c22164b1a" }
traceIdを付与したことで、cloudwatchやAthenaでリクエストごとのログを時系列で閲覧できるようになり便利になりました。
また、別コンテナのログも同じtraceIdで検索できるのでバッチ開発時にかなり助かりました。
2. 必然性のないreflectをやめた
「とにかく遅いので、使わざるを得ない場合以外では使わない」
というのが、自分がそれまでreflectに抱いていた認識でしたが、
かなりカジュアルに使用されていてカルチャーショックを受けたような記憶があります。
2つ例をあげます。
Case1
なんとなくDeepEqual
var a int64 var b int64 ... if reflect.DeepEqual(a, b) { return }
Case2
再構築のプロジェクトである為、基本的に既存のテーブルを使用しています。
SQLアンチパターンに載っているようなテーブルが多々存在するため、
扱いやすい形に変換をしますが、そこをスマートに書きたかったのかよく使われていました。
// DBの値をマッピングする構造体 type Dto struct{ Id int64 Value1 string Value2 string Value3 string Value4 string Value5 string } func (d *Dto) GetValues() []string { val := reflect.ValueOf(d) result := make([]string, 0, 5) for i := 1; i < val.NumField(); i++ { result = append(result, val.Field(i).String()) } return result }
どちらもレビューと、リファクタリングで潰していきました。 といいつつまだ若干残っているので、隙をみて潰していきたいです。
3. DIにwireを導入した
DIを楽にする為に導入しました。
リリースが近くなってくるとかなり頻繁にコンフリクトが発生していたので、
コマンドですぐ修正できるのが助かりました。
導入時に思いのほか難しさを感じたのでREADMEを整理しておいたおかげか、 新卒のメンバーもすぐに使いこなしてくれて嬉しかった記憶があります。
4. protoからgraphqlを生成できるようにした
次世代ECの管理画面のバックエンドでは、gRPCとGraphQLを採用しています。
GraphQLのバックエンドはgqlgenで生成しているのですが、開発していく中でいくつかの課題を感じていました。
@goModel
ディレクティブを使用しているが、フィールド名や型を間違えると、gqlgenが正しく生成してくれず、警告なども表示されないので、フィールド数が多い場合しらみつぶしに確認しなければならず、タイポが原因で30分以上無駄にしてしまうことがあった- protoファイルとgraphqlのコメントが同期できておらず、両方に違うことが書いてある場合も
- 必須チェックや最大値などのリクエストのバリデーションがGraphQL側にしかなく、gRPCのAPIはGraphQLを通らない利用を想定できていなかった
- 同じような定義を書くのがシンプルに面倒くさい
上記の課題を解決するために、
- protoc-gen-validateの導入
- protoファイルをパースしてgraphqlのスキーマに生成するツールをGoで作成
を行いました。
できあがったツールは苦労したかいあって、チームのメンバーにもかなり好評で、
9月末にリリースした注文管理画面の開発効率にもかなり寄与してくれました。
生成ツールの詳細などについては、別の記事でいずれ紹介できればと思います。
5. private linterをCIに組み込んだ
次世代ECの管理画面開発ではCIをgithub actionsで走らせており、golangci-lintを使用しています。
追加でいくつかプロジェクト特有のルールを効かせたかったので、
analysis.Analyzer
を使用してprivate linterを作成しました。
github actionsでは go vet -vettool
で実行しています。
現在は3種類のlinterがあり、下記のような内容をチェックしています。
- logging
- メソッドの開始、終了ログのフォーマットチェック
- ログメソッドの間違いをチェック
- e.g. Debugfを使うべきところでDebugになっていないか?
- imports
- variadic
- 特定のパッケージで可変長引数の渡し忘れがないかのチェック
自分で作っておきながら自分のPullRequestが引っかかると若干のストレスがありますが(笑)、 レビュワーは、本質的でない箇所を気にすることなくレビューに集中でき、 レビュイーは、レビュー待ち時間や修正や再レビューのやり取りを減らせていいことづくめでした。
終わりに
以上が私が新管理画面のバックエンド開発で行ってきた主な改善点です。
振り返ってみて、こういうことをやってみたい、こうしたら便利になるんじゃないかという提案を否定せずにポジティブに受け入れてくれる、積極的に動きやすい環境だなと改めて感じました。
今後も新たな課題に取り組みつつ、より良いプロダクト開発を目指していければと思います。