bitA Tech Blog
💬

「動けばOK」だった僕が2ヶ月のAPI実装研修を通して学んだこと

に公開

はじめに

心が折れかけたkonchan_dev

こんにちは!株式会社ビットエー所属 25卒エンジニアのkonchanです。

学生時代に数年間プログラミングを学び、「これで僕もWebアプリが作れるぜ!」と意気揚々と入社した僕を待っていたのは、4つのAPI実装研修課題でした。

研修で実装した内容は、グループ会社の社内報サービス『ourly』の既存APIをRuby on Railsで再現する、というものでした。

実装は、本番には影響しない安全な研修用環境で行い、テストケースを含む以下の4機能に取り組みました。

  • 【課題1】コメント投稿API
  • 【課題2】コメント編集API
  • 【課題3】コメント削除API
  • 【課題4】プロフィール設定編集API

あくまで課題ではありますが、実際の業務で行われる実装フロー(仕様書に基づき実装(テスト含む) -> テスト・静的コード解析の実行 -> プルリクエストを作成しレビューを受ける)と同じ流れで実装を進めました。

この直近2ヶ月間、僕はただひたすらに4つの課題に取り組み、コードを書き、レビューで指摘され、修正し、またレビューされる…という日々を過ごしました。正直、何度も心が折れかけました。

しかし、課題を終えた今、僕が見ている世界は入社前とは全く違います。この記事では、僕が課題を通じて学んだことを、単なる技術の羅列ではなく、思考がどう変化していったかというストーリーでお伝えします。

第1章:コードは「動けば良い」ではなかった

課題に取り組み始める前の僕は、「仕様通りに動くコードが書ければOK」だと思っていました。
しかし、その考えはコードのフィードバックをいただいていく中で変わりました。

僕の考えが変わったきっかけとなるエピソードの1つは、記事の権限に関する指摘でした。
要件定義書には、コメント内容の文字数チェックなどについては書かれていましたが、権限に関する記述は一切ありませんでした。

しかし、用意されたテストケースを実行すると、

  • 「記事の閲覧権限がない」
  • 「コメントの投稿権限がない」

…といった理由で失敗してしまいます。
当初の僕は、テストが通らないからという理由で、見よう見まねで権限を確認するコードを実装に追加しました。

# CommentsController 
def create
  article = Article.find(params[:article_id])
  
  unless current_user.can_read?(article) 
    # (閲覧権限がない場合の処理) 
  end 
  
  unless current_user.can_comment?(article) 
    # (コメント権限がない場合の処理) 
  end

  # コメント投稿処理
end

この実装のプルリクエストを作成したところ、先輩から「なぜこの実装を追加したのか」と確認されました。
私は「このコードがないとテストが通らなかったので…」と説明しました。
すると、先輩からこんなフィードバックが返ってきたのです。

「『テストを通すために実装する』という理由ではなく、『定義書や要件を満たすためにこのメソッドが必要かどうか?』の観点でメソッドの必要可否を考えるようにしましょう! ! ! !
前提として、テストは実装の保証をするための1つの手段でしかないので、手段が目的化していると思います!」

この時求められていたのは、すぐにコードを追加することではありませんでした。
まず、「そもそも、この権限確認は仕様として本当に正しいのか?」と考えること。そして、考慮できていなかった仕様を考慮して、あるべき形に作り直すことこそがゴールだったのです。
僕はテストが通ることだけを考えていて、そのコードがなぜ必要なのか、どうあるべきかを全く考えていなかったのです。
このようなフィードバックを通じて、ただ動くコードを書くのではなく、なぜそのコードが必要なのか、仕様の意図・背景を考えてコードを書くようになりました。

レビュワーの視点に立ってコードを書くこと

コードレビューでは、自分では気づきもしなかった点が次々と指摘されました。

マジックナンバーの意味: ある仕様に「コメント(content)は1000文字まで」というものがありました。僕が書いたのは、次のようなコードです。

def create 
  # (前略) 
  comment = Comment.new(content: params[:content]) 

  # contentが1001文字以上ならエラー 
  if comment.content.length > 1000 
    # エラー処理 
  end 
  # (後略) 
end

この 1000 という数字について、コードを書いた本人以外には、この数字がどこから来て、何を意味するのかさっぱり分からないという指摘をいただきました。
MAX_COMMENT_LENGTH というような意味のある名前を定数として定義することで、誰が読んでも「コメントの最大文字数が1000文字である」という仕様がコードから読み取れるようになります。
これにより、数字に「意味」を持たせることの重要性を学びました。

DRY原則の意識:当初、僕は同じようなコードをコピペしていました。これに対し、「同じ記述を何度も書くのはDRY原則に反する」という指摘を受けました。重複する処理を一つにまとめることで、コードの再利用性、保守性が上がることを実感しました。

## before
# ユーザーAのフルネームを作成
user_a_last_name = "山田"
user_a_first_name = "太郎"
user_a_full_name = user_a_last_name + " " + user_a_first_name

# ユーザーBのフルネームを作成
user_b_last_name = "鈴木"
user_b_first_name = "花子"
user_b_full_name = user_b_last_name + " " + user_b_first_name

## after
# フルネームを作成するメソッドを定義
def create_full_name(last_name, first_name)
  "#{last_name} #{first_name}"
end

# メソッドを使って各ユーザーのフルネームを作成
user_a_full_name = create_full_name("山田", "太郎")
user_b_full_name = create_full_name("鈴木", "花子")

可読性という名の「思いやり」: 当初の僕は、 unless を使い二重否定の条件文を作っていました。このコードに対し、「ド・モルガンの法則を使えば、肯定的なわかりやすいif文になる」というレビューをいただきました。このレビューを通じて、コードは未来の自分や他人のためにあるのだと感じました。

## before
# ユーザーがゲストではない、のでなければ (つまり、ゲストなら)
unless !user.is_guest? 
  puts "ゲストです"
end

## after
# 以下のif文にできます
if user.is_guest?
  puts "ゲストです"
end

これらはプログラミングの基本ですが、「なぜそう書くべきなのか」を、身をもって理解することができました。

スパゲティコードに絡まるkonchan_dev
スパゲティコードに絡まるkonchan_dev

第2章:テストは未来への投資 - なぜテストコードを書くのか

最初は、テスト特有の書き方に戸惑い、テスト用のデータ作成を楽にしてくれるツールの便利さに感動するレベルでした。

テストは「動作確認」くらいの認識でした。

しかし、先輩からの指摘・アドバイスを取り入れていく中で、テストの本質に気づいてきました。

なぜテストコードを書くのか、この課題を通して見つけた答えは、主に以下3点です。

1. 実装の考慮漏れを防ぐため

テストコードを書く目的の1つは、実装中に考慮できていなかった観点に気づき、プロダクトをより堅牢にすることです。
当初、僕が書いていた異常系のテストは、「記事につくコメントの本文が空の時には更新できない」といった、画面操作で起こりうる範囲に限られていました。自分の実装が想定通りに動くことを確認し、これだけで品質は保証できると思っていました。
しかし、コードレビューのときに先輩から

「もし、curlコマンドなどでAPIが直接叩かれ、他人のコメントIDで更新リクエストが送られてきたらどうなりますか?」

と問われました。
画面上では起こりえない操作だけど、APIは常にこのような予期しない形で利用される可能性がある。実装段階では完全に見落としていた視点でした。
この経験を通じて、テストコードを書くことが、実装の想定範囲を明確にする「きっかけ」になるのだと学びました。
網羅的なテストコードを書いていく中で、初めて実装の考慮漏れを炙り出すことができたのです。

2. 「未来の変更」に備えるため

以前の僕は、「このリファクタリングで、どこか壊してしまっていないかな…?」という漠然とした不安に駆られてコードを修正していました。

そんなある日、先輩からレビューで

「テストは、未来の変更で意図しない影響が出た時にそれを検知するための、未来への投資でもある」
という指摘を頂きました。

それ以来、テストを単なる「現在の動作保証」ではなく、未来の実装者を助けるための「防波堤」として役割を果たすという意識に変わりました。
この防波堤があるからこそ、「どこかが壊れるかもしれない」という不安に駆られることなく、自信をもってコードの改善ができるようになるのだと、今では確信しています。

3. 「仕様」を明確に伝えるため

初めてテストコードを書くときは、特にテスト観点の洗い出しに苦労していました。そこで、どのような視点でテスト観点を考えるかべきか聞いたときに、

「テストが仕様書の代わりになることを目標とするといい」

というアドバイスを頂きました。
このアドバイスは、テストコードを書くことが将来のコードの変更が仕様から離れることを守るための未来への投資であるという意識に変わるきっかけになりました。

第3章:個人からチームの一員に - 報連相の重要性

技術的な壁に衝突していく中で、僕はもう一つ、それ以上に重要な壁に直面しました。それは「チームの一員として働く」という壁です。そして、チームの一員として働く上で重要となるのが「報連相」でした。

「がんばります」は信頼を生まない

課題の進捗に遅れが出た時、僕はつい「頑張って巻き返します」と言ってしまっていました。しかし、先輩から、プロとして期待される振る舞いは全く違うという指摘を頂きました。

「エンジニアとして最も重要なのは、問題発生時に力技で解決するのではなく、適切な報連相(原因分析、影響報告、対策報告)ができること」

これまでの僕は、遅れを自分の「頑張り」で取り戻せば、それが最善だと思い込んでいました。
しかし、「頑張り」という根性論は、多くの問題を見えなくしてしまう行為でした。

個人の力技に頼る方法は再現性がかなり低く、毎回うまくいくとは限りません。仮に無理やり間に合わせたとしても、焦りの中で書かれたコードは、将来の技術的負債につながる可能性が高くなる。
それよりも、遅延の原因を正直に伝え、対策を考えるプロセスこそが、プロダクトの品質を守り、本当の意味での信頼につながるのだと痛感しました。

このアドバイスを基に、根拠のない「がんばります」を封印して、たとえ勇気がいることでも、事実を伝えるようにしようという意識に変わりました。
そして、一人で抱え込まず情報を共有・相談するという当たり前が、いかに重要であるかを学びました。
報連相は、単なる報告ではなく、チームでリスクを管理し、プロダクトを成功に導くための最も重要なスキルでした。

第4章:コードの先にある目的 - 最適解を判断する視点

チーム開発に慣れてきた頃、僕は新たな壁にぶつかりました。

それは「何が本当に正しいコードなのか?」という問いです。

実装を行っていく中で、これまで学んできた「効率的なコード」が、必ずしも最適とは限らない場面に直面しました。
このジレンマを通して僕が学んだのは、エンジニアの仕事は単にコードを動かすことではなく、パフォーマンスの最適化と将来の保守性という視点から、常に最適解を判断し続けることだ、という視点でした。

「実装完了」がゴールではなかった

以前の僕は、自分に割り当てられた実装のタスクを「仕様通りに動くコードを実装すること」としか捉えていませんでした。
チケットを消化し、Pull Requestを作成する。それが僕にとっての「実装完了」でした。

つまり、期待された動作をすれば、それで自分の役割は果たしたことになると考えていました。
コードの裏側で何が起きているのか、それがユーザーの体験にどう影響するか、という視点は完全に欠けていました。

1行のコードが教えてくれたこと

その意識が大きく変わるきっかけとなったのが、あるコードレビューでの出来事でした。

APIを実装し、僕なりの動作確認を済ませ、自信を持ってレビューを依頼しました。

そこで、先輩から頂いたレビュー中に、たった一行に関するとある指摘がありました。

「ここの処理、UPDATEクエリを発行した直後に、同じレコードに対してSELECTクエリを発行しているけど、何か意図あります?」

当時の僕には、その指摘の意味がすぐには分からなかったです。
僕の書いたコードは、レコードを更新した直後に、念のため最新の状態をデータベースから取得し直すというもので、動作自体に問題はなかったからです。

コードの処理を詳しく調べて、指摘の意味にようやく気付きました。
僕が使っていた更新処理の命令は、データベースを更新した後、その更新結果をプログラムに返してくれていたのです。
僕はその返り値を使わず、わざわざもう一度データベースに「今のデータをください」とSELECTクエリを発行して問い合わせていたのでした。
つまり、たった一つの更新処理のために、データベースに二重の負荷をかけていました。

# user = User.find(1) のようなオブジェクトを想定

## before
if user.update(name: "新しい名前") 
  # ここで再度クエリを発行している
  updated_user = user.find(1)
  render json: {name: updated_user.name }
end

## after
if user.update(name: "新しい名前") 
  # 更新されたuserをそのまま使えば、追加のクエリは発行されない 
  render json: {name: user.name }
end

この経験により、コードが動くだけでなく、その裏側で発行されるSQLクエリまで意識することの重要性を痛感しました。
たった一行の違いがパフォーマンスに大きく影響する可能性を知り、より効率的な実装をする意識が身に付きました。

トレードオフを判断する

この一件により、僕の「正しさ」の基準が揺らぎました。
僕が追い求めていた「動くコード」はパフォーマンスを犠牲にした「遅いコード」だったのかもしれない。
そして、単純にパフォーマンスを追求することだけが正解ではなく、状況によって最適解は変わるんだと知りました。

パフォーマンスの最適化やコードの保守性は、手段の一つに過ぎませんでした。
どの手段を選ぶべきか、その時々の状況に応じてトレードオフを判断することこそ、エンジニアの本当の役割なのだと気づかされたのです。

さいごに:2ヶ月の課題を終えて

2ヶ月前、「動けばOK」と思っていた僕はもういません。
今の僕は、コードの可読性や保守性を考え、未来を想定したテストコードを書き、チーム開発における報連相を徹底し、そして何よりユーザーに届ける価値を想像しながらコードを書いています。

もちろん、まだまだ半人前です。
しかし、この課題は、僕にプロのエンジニアとしての「土台」を叩き込んでくれました。
レビューで僕をボコボコにしてくれた(愛のある)先輩方には、本当に感謝しかありません。

これから実務に入ります。
この2ヶ月で得た知見を元に、価値あるプロダクト開発に貢献していきたいと思います。

最後まで読んでいただき、ありがとうございました。

bitA Tech Blog
bitA Tech Blog

Discussion