
この記事は、Goodpatch Advent Calendar 2025 19日目の記事になります。
こんにちは! ReDesigner Divisionでバックエンドエンジニアとして働いているsenriです。
2024年11月に入社してから、あっという間に1年が経ちました。この1年でバックエンドエンジニアとして色々な変化があったので、節目としてざっくり振り返ってみます。
コーディング環境の変化
AI搭載IDEが混沌とした中でコーディング環境が目まぐるしく変化した一年でした。
一年前までは、VSCode + Copilotでコーディングしていたのですが、CursorやKiro、Google Antigravityなども試しつつ、現状はCursor + Claude Codeに落ち着いています。 学生時代と利用している言語が異なるので何とも言えないのですが、Visual StudioやEclipse, Atomなども試行錯誤して落ち着いてきた背景の中で、この一年は怒涛の変化点だったかと思います。
(今期のReDesigner Divisionでは、AI活用は一つの目標となっており、本来は手段ですが、まずは使い方を知ることが大事ということで、色々と触りながら自身に合っているものを探していた形でした)
バックエンドテスト再考
ReDesigner Divisionのバックエンドチームにはもともとテストはありましたが、個々の管理が中心で属人的且つ実行環境に依存していた状態でした。 プロダクトの既存機能を守り、今後の改修によるdegradeを防ぐという目的のもと、テスト基盤を再考し、残すようになりました。
テストの構成
具体的な取り組みとしては以下のとおりです。
- テスト自体はHTTPメソッドに合わせてYAMLで記述
- テストカテゴリごとにファイルを切り分けてレビュー・参照しやすく整理
- テストデータはmodelベースで用意し、デフォルト値を設定しつつ必要なデータのみを指定する形に
- テストの粒度はコントローラ単位(複数のコントローラを跨がない。無理せずに継続できるように。)
YAMLでテストを記述するイメージとしては、以下のような形式です。
- test_function: "テストカテゴリ名" description: "xxx" test_cases: - name: "テストケース名" account: email: "xxx" password: "xxx" expected: http_status: 200 status: 100 error_message: "" multiple_output: - field: "xxx" value: xxx match_type: "FULL_MATCH"
このようにYAMLで記述することで、テストケースの可読性が高まり、「何をテストしているのか」が把握しやすくなります。 また、構造化データなので、テストケースの追加や修正も容易で、AIによるコード生成サポートも部分的に受けやすい印象です。
テストデータの管理
テストデータはmodelベースで用意しています。デフォルト値をあらかじめ設定しておけば、各テストケースでは必要な差分だけを指定するだけで済むため、テストコードの肥大化を防げます。 (たとえば「メールアドレスが不正なユーザー」をテストしたい場合は、Emailだけを変更すれば良いので、テストコードがシンプルになります。)
当初は生SQLでデータを準備していたのですが、クエリ量が膨れ上がり保守性も低くなってしまったため、途中で切り替えました。modelベースにしてからはテストの追加・修正、DBカラムの追加対応などが格段に楽になっています。
テストケースとプロダクト施策の紐付け
テストは、新規実装したコントローラについては必ずテストを記述し、既存コントローラについてはプロダクト内のインパクトに応じて優先度を決め、順次テストを追加しました。 また、実装したコントローラのテストケースはプロダクト施策と紐づけて残すようにしています。これにより、具体的にどこが担保されているのかをチェックできるようになりました。 この紐付けがあることで、「この施策に関連するテスト」「このテストが落ちたらどの機能に影響があるか」が以前よりは明確になったかなと思います。また、振り返り時に「テストでどこまでカバーできているか」を確認することもできるのがチーム内で共有ができるので大きなメリットだと感じます。
開発仕様の見直しと習慣化
プロダクトチームとしてNotionで様々なドキュメントが共有されています。
自分が入社した際、「この機能の仕様はなぜこの作りなのか?」という疑問が多々ありました。機能の仕様はコードを確認すればわかりますが、そうなった経緯は当時の意思決定者への確認が必要です。そこで、今後のサービスのためにもバックエンドとしての開発仕様を残すようになりました。
開発仕様の構成
開発仕様には、以下のような内容を記載しています。
- プロダクトメンバー: 関わったメンバー(デザイナー、エンジニア等)
- 目的・背景: なぜこの機能を実装するのか、どのような課題を解決するのか
- アーキテクチャ: 機能のSequence DiagramsとしてMermaidで記述
- 処理フロー: データの流れや処理の順序
- 既存との相違点: 既存の類似機能との違い
- 要求仕様へのリンク: プロダクト側の要求仕様ドキュメントへの相互リンク
- 等々
こうすることで、実装後も参照されるリファレンスとして機能しています。新しいメンバーが入ってきた際にも、「なぜこうなっているのか」を自己解決できる環境が整いつつあります。
特に「目的・背景」は重要だと考えています。コードを見れば「何をしているか」はわかりますが、「なぜそうしたか」はコードからは読み取れません。当時は妥当だった判断も、時間が経つと「なぜこんな実装になっているのか」と疑問に思うことがあります。そういったときに背景が残っていると、過去の意思決定を尊重しつつ、今の状況に合わせた判断ができます。
ドキュメントを書くタイミング
タイミングは特に決めていませんが、開発仕様は実装後に書く場合が多いです。要求仕様や実装を整理して、開発仕様に起こす形になっています。 当然、大きな機能の場合は実装前に設計ドキュメントを別途作成し、レビューする形です。この場合は、あとから設計ドキュメントが追えるように開発仕様にリンクを張る形で対応したりしています。
開発サイクルの変化
徐々にですが、開発サイクルとして「仕様確認 → 開発 → テスト → ドキュメント」という流れが定着してきました。
もともとは、Figmaのデザイン案をベースにして議論及び仕様の確認が行われ、タスク化、そして開発という流れでしたが、プロダクトの成長を見据えて、自動テストとドキュメント作成という工程が追加されました。
ReDesignerはまだ複雑な作り・密な関係性ではありませんが、今後プロダクトへの機能追加によって複雑度が増した場合、機能改修に伴うdegradeチェックの方が大変、なんてことになりかねません。テストを実施してドキュメントを残すことは、将来的な技術的負債を防ぐための布石だと考えています。
また、生成AI開発時代において構造化データはコンテキストとして重要です。テストケースやドキュメントが整備されていると、AIにコンテキストとして渡した際の精度も上がります。AIと共に成長するという観点からも、これらの取り組みは大切なのかなと思います。
このサイクルが定着してきたことで、個人的には安心感を持って開発に臨めるようになりました。テストがあることで「この修正で既存機能が壊れていないか」を確認でき、ドキュメントがあることで「後から見返したときに理解できる」という安心感があります。
Go言語に慣れてきた
一番はこれかもしれません。
前職ではPHPをメインで扱っていたので、Go言語(Gin)への頭の切り替えは新鮮でした。 一言でいうとSimpleで扱いやすいところに魅力に感じています。
具体的にどのような点でシンプルさを感じたのか、いくつか例を紹介します。
form / modelのstructへのbind
リクエストのパラメータをstructにbindする処理がシンプルに書けます。bindingタグを使えばバリデーションも同時に行えるので、リクエストの受け取りからバリデーションまでを数行で完結できるのが嬉しいポイントです。
type CreateUserRequestForm struct { Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required,email"` } var inputForm CreateUserRequestForm if err := c.BindJSON(&inputForm); err != nil { // エラー処理 }
errorを呼び出し側で扱う設計
Goではerrorを呼び出し側の関数で処理するのが原則です。呼び出し側で適切にハンドリングすることが求められるため、関数自体は「処理して結果を返す」という責務に集中できます。エラーの発生箇所と対処箇所が明確になるので、デバッグもしやすいと感じました。
func GetUser(id int) (*User, error) { // ユーザー情報取得処理 if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } return user, nil } // 呼び出し側 user, err := GetUser(1) if err != nil { // エラー処理 }
変数のゼロ値
Goでは変数を宣言すると自動的にゼロ値で初期化されます。未定義の変数という状態がそもそも存在しないため、初期化し忘れによるバグを心配する必要がありません。基礎の基礎ですが、安心してコードを書ける要因の一つです。
var s string // ""(空文字) var n int // 0 var b bool // false var p *User // nil
defer指定
deferを使うことで、関数終了時に実行される処理を宣言できます。deferが宣言された後の処理では、正常終了でもエラーで早期リターンしても実行されるため、リソースのクローズ処理に最適です。オープン直後にdeferで閉じる処理を書いておけば、後続の処理に集中できます。
func GetUser(id int) (*User, error) { // DB接続 db, err := DBConnection() if err != nil { // エラー処理 } // 関数終了時にDB接続を閉じる defer db.Close() // 後続処理 }
まとめ
1年前と比較すると、テストの整備やAIツールの活用など、これまで手が回らなかった部分に取り組めるようになりました。「こうあった方がいい」と思った改善を少しずつ形にできており、バックエンドエンジニアとしてプロダクトの安定的なグロースに貢献できている実感が出てきました。もちろん、まだまだ取り組みたい箇所はあるので、引き続き着実に進めていきたいと思います。
また、この記事を書きながら気づいた(思い出した)のですが、GoodpatchにJoinするまでReDesigner Divisionとしてどのような取り組みがされているのか、外からはなかなか見えませんでした。この記事でそのような箇所が少しでもカバーできていると嬉しいです。
最後に、1年間しっかりと業務に向き合えているのは、共に働くプロダクトチームとReDesigner Divisionのメンバーのおかげです。感謝の気持ちを忘れずに、次の1年も挑戦を続けていきます!
Goodpatchではデザイン好きなエンジニアの仲間を募集しています。 少しでもご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう!