はじめに
9月2日から9月13日まで、LayerXさんの2週間のサマーインターンに参加しました。この記事では、インターンの内容や学んだことについて紹介します。バクラクサマーインターンが本当に実りある楽しい内容だったことを全力でお伝えしていきます。
インターン期間は前述の通り、9月2日から9月13日の2週間でした。全日オフィス開催の予定でしたが、台風 ⛈️ の影響で初日と2日目は急遽オンライン開催となり、3日目以降からオフィスでの開催となりました。
オフライン初日はとても緊張しましたが、伝言ゲームなどのアイスブレイクを通じて、チームメンバーとすぐに打ち解けることができました。その後の日程も、チームで食事に行ったり、開発中にわからないことはすぐに質問できたりと、コミュニケーションが取りやすく、とても充実した時間を過ごすことができました。
インターンの内容
前半:講義パート
インターン最初の3日間は、技術に関する講義がメインでした。LayerXで実際に使用されていて、インターンでも使用する技術を学びました。インターン生は事前に「must(必須)」「should(推奨)」「want(望ましい)」のレベルで課題(課題というよりはキャッチアップ、予習といった感じでした!)が用意されていて、バックエンドからフロントエンドの技術を各自キャッチアップして臨みました。
参考までに事前課題として「must(この知識が無いとインターン中の課題を進めるのが困難)」に含まれていた内容は以下の通りです:
- フロントエンド: TypeScript基礎、React基礎
- バックエンド: Go基礎、GraphQL基礎
- その他: Git基礎
これらに加え、「should」や「want」のレベルでNext.jsのAppルーターやGORMといったフロントエンドおよびバックエンドのフレームワーク・ライブラリの知識も求められました。
スキーマ駆動開発の学習
今回のインターンで学んだ新しい概念として「スキーマ駆動開発」があります。スキーマ駆動開発とは、データ構造を定義するスキーマをもとにシステム全体を設計・実装する手法です。このアプローチにより、以下のような利点があります。
- 開発効率の向上: スキーマから自動生成されるコードにより、開発者は重複した作業を減らすことができます。
- バグの減少: データの形式やバリデーションがスキーマで一貫して管理されるため、データに起因するバグを減少させることができます。
- スムーズなチーム間の協業: スキーマが開発の共通の基盤となるため、チーム間のコミュニケーションが円滑になります。
今回のインターンでは、以下の2種類のスキーマ駆動開発を学びました。
1. GraphQL スキーマ駆動開発
- GraphQLスキーマファイル(.graphql)をもとに、gqlgenを使用してAPIのリゾルバやモデルを自動生成しました。gqlgenは、スキーマからGoのコードを生成するライブラリで、スキーマとコードの整合性を保ちながら効率的な開発を支援します。
- 生成されたリゾルバはスケルトンコード(ひな形)であり、これをもとにサービス層でビジネスロジックを実装することで、クライアントからのリクエストに対応しました。
2. データベース スキーマ駆動開発
データベースのスキーマ定義をもとに、自動でコードを生成する方法を学びました。xo
というツールを使用するコマンドがMakefile
に組み込まれており、このコマンドを実行することで、データベースのテーブル構造に対応するGoの構造体が自動的に作成されました。
スキーマファーストとコードファーストのアプローチ
データベース設計には「スキーマファースト」と「コードファースト」という2つのアプローチがあります。(この考え方もインターンで初めて学びました👀)
-
スキーマファースト: 先にデータベースのスキーマを定義し、そのスキーマをもとにコードを生成する方法です。今回のプロジェクトでは、データベーススキーマからGoの構造体を自動生成するために
xo
を使用していました。 -
コードファースト: コードを先に書き、それにもとづいてデータベースのスキーマを生成する方法です。Go の
GORM
の Auto Migration や、Ruby on Rails の Active Record マイグレーションがこの方法に該当します。
今回のインターンでは、スキーマファーストのアプローチが採用されていました。以下、講義スライドより引用させていただきました。
データベーススキーマの方がアプリケーションより、ライフサイクル(生存期間)が長い。
アプリケーションは、インターフェースやエンドポイントさえ変えなければ、まるっと差し替えることも可能。
しかし、データベースは状態を持つので、サービスが存続する限りはデータ移行が必要。
スキーマファーストのアプローチを知り、データベース設計の重要性を改めて理解しました👀
データベースのマイグレーション
インターンを通じて、データベースのマイグレーションについて学びました。マイグレーションとは、データベースの構造(スキーマ)をバージョン管理しながら変更する手法です。例えば、新しいテーブルを追加したり、既存のカラムを変更する際に、専用のマイグレーションファイルを作成し、これを適用することでデータベースを更新します。
これにより、変更履歴を追跡し、必要に応じて過去の状態に戻す(ロールバック)ことも可能です。直接データベースを手動で操作するのではなく、コマンドを用いて変更を管理するため、チーム開発において一貫性が保たれ、データベースの状態を安全に維持できます。
GORMの論理削除
データベースのデータ復旧方法として、以下の3つが紹介されました。
- データ変更のログを残す
- 履歴テーブルを設ける
- 論理削除のフラグを設定する
講義では、シンプルな論理削除について紹介がありました。論理削除(ソフトデリート)は、データベースのレコードを物理的に削除するのではなく、削除されたように「見せる」ための手法です。データは保持され、DeletedAt
カラムに削除日時を記録することで削除された状態として扱われ、通常のクエリでは見えなくなります。具体的には、レコードを削除する代わりに、UPDATE
文が実行され DeletedAt
カラムに現在の日時が設定されます。
GORMの論理削除の仕組み
以下は公式ドキュメントからの引用です。
gorm.Modelにも含まれているgorm.DeletedAtフィールドがモデルに含まれている場合、そのモデルは自動的に論理削除されるようになります。Deleteメソッドを実行した際、レコードはデータベースから物理削除されません。代わりに、
UPDATE
文が実行され、DeletedAtフィールドに現在の時刻が設定され、そのレコードは通常のクエリ系のメソッドでは検索できなくなります。
GORMの機能の充実さを実感しました👀
さらに、DeletedAt
を考慮した UNIQUE
制約や、Generated Columns
の活用についても学びました。とても興味深いトピックでした👀⭐️
論理削除を考慮したユニーク制約
論理削除を考慮する際に注意が必要なのは、DeletedAt
カラムが NULL
(レコードが削除されていない有効な状態)の場合のみユニーク制約が適用されるように設計しないと、論理削除済みのデータと同じユニークキーを持つ新しいデータを挿入できなくなることです。
この問題を解決する方法として、DeletedAt
カラムの状態に応じて「有効・無効」を示すカラムを追加し、UNIQUE
制約に組み合わせることで一意性を保つ設計が紹介されました。
また、NULL
を許可するカラムを使ってユニーク制約を回避する方法もありますが、これは一般的に推奨されないそうです。
その他の学びについて(盛り沢山な講義でした!)
エラー処理についても学びました。これまでは、エラー発生時に log.Fatal などを使用して単に処理を終了させていましたが、エラーには「予期されるエラー」と「予期しないエラー」があり、予期されるエラーの場合は、正常な動作の一部として処理を続行できることを学びました。例えば、データベースにレコードが存在しない場合には、新規にレコードを作成する処理を行うなど、あらかじめ想定された挙動を実装できます。
その際、エラーの判別には errors.Is を使用して、特定のエラーかどうかを確認できます。これにより、エラーメッセージの内容が変わっても正しく判別でき、エラーメッセージに依存した実装がもたらす不具合を防ぐことができます。また、errors.As を用いて、特定のエラー型をキャッチし、エラー情報の詳細を取得する方法も学びました。
他にも、GraphQLにおけるエラーハンドリングや、ログについては構造化ログを活用することで、トラブルシューティングやログ分析に役立てることができることを学びました。さらに、ブートローダーやトランザクション、ロックの概念についても学び、個人開発ではあまり考慮していなかった内容について新しい知見をたくさん得ることができました。ここでは触れませんでしたが、フロントエンドの講義も盛りだくさんな内容が用意されていました!
感動したこと
余談ですが、Makefile
に実装されていた help
コマンドに感動しました。ターミナルで make help
を実行すると、コマンドとその説明を表示する Makefile
のコマンドです。シェルが大好きなので、「このワンライナーはなんだ!」と感動しました。
@grep -E '^[/a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
このコマンドは、grep
で Makefile から特定のパターンの行を抽出し、awk
でそれらの行を整形して出力しています。awk
の部分では、フィールドセパレータ(FS)を設定し、コマンド名を色付きで表示するように整形しています。これにより、コマンド名は青色、色で表示され、見やすくなります。
helpコマンドいいなと思ったことと、出力されたコマンド名が色付きで表示されるのも見やすく、今後、自分が Makefile
を作成する際にも取り入れていきたいと思いました。
フロントエンドの学習
フロントエンドについては、今回のインターンで初めて本格的に学び始めたため、理解が追いつかない部分もありました。そのため、バックエンドの学習に集中しました(この記事もバックエンドの内容が中心です 🙇♀️)。チーム開発では、フロントエンドのチームメンバーとペアプログラミングをしながら、バックエンドとフロントエンドの繋ぎ込みを実装しました。
GoとGraphQLの実装の難しさ
バックエンドの開発では、GraphQLのディレクトリ構造が通常のWebアプリケーションとは異なるため、初めはどのファイルに何が書かれているのか把握することが難しかったです。また、GraphQLのスキーマから自動生成されるリゾルバ関数を実装する際には、GoのインターフェースやGraphQLリゾルバ層・サービス層・リポジトリ層とコードの分離を意識しながら書く必要がありました。慣れるまで苦戦しましたが、ディレクトリ構造を把握できるようになると、実装もしやすくなりました! 🕵️
以下は、バックエンドのコードの中でこれから積極的に使っていきたいと思ったコードとライブラリをピックアップしてお届けします。
スライスの効率的な初期化と make
関数の活用
実装時、スライスを宣言してから要素を append
していましたが、既存のコードでは make
関数を使用していました。スライスの初期化時に make
関数で容量(cap)を指定することで、スライスの容量をあらかじめ設定しておくことができ、メモリの再割り当てを防ぎ、パフォーマンスを向上させることができます。今後は、スライスを宣言する際には積極的に make
関数を使い、無駄なメモリ消費を抑えた効率的なコードを書いていきたいと思いました!
make([]T, len, cap)
スライス操作のユーティリティ関数 Map
ユーティリティ関数として、 github.com/akrennmair/slice パッケージに含まれているMap
関数のような実装がありました。ここでは上記パッケージの関数を紹介する形を取ります。以下は、Map
関数のシグネチャです:
func Map[T, U any](s []T, f func(int, T) (U, error)) ([]U, error)
Map
関数は、スライス s
の各要素に対して指定された関数 f
を適用し、その結果を新しいスライスとして返します。
-
s
: 操作対象のスライス。 -
f
: スライスの各要素に対して適用する関数。関数 f は、スライスのインデックスと要素を引数に取り、任意の型 U と error を返します。
この関数は、特にデータベースから取得したデータの変換などに用いられていて、汎用的に使える便利な関数だなと思ったので今後使っていきたいです。
バリデーション
ozzo-validation ライブラリを使うと、構造体の各フィールドに対してバリデーションルールを定義することができ、コードの簡潔性と一貫性を保てます。このライブラリは、フィールドのバリデーションが失敗しても他のフィールドのバリデーションを継続するため、複数のエラーを一度に把握することが可能です。バリデーションの実装には他に govalidatorが使用されていました!
後半:チーム開発
4日目からは、チーム開発に移行しました。24人の参加者が4人ずつ6チームに分かれ、LayerXのサービスであるバクラクビジネスカードを模して、インターン用に簡易的に実装されているアプリケーションに関連する機能開発を行いました。具体的には、全チームに同じお客様(複数の会社様)の要望が与えられ、それをもとに各チームが解決に向けた実装を行いました。
私のチームは、フロントエンドに強いメンバーが2人、フルスタックエンジニアが1人、そして私(バックエンド担当)の4人でした。各メンバーが得意分野を活かしつつ、LayerXの行動指針「Be Animal」を体現しようと、自分の専門外の領域にも積極的に取り組みました。フロントエンドとバックエンドの連携が重要だったため、毎朝バックログを確認し、タスクを細かく分割して効率的に進めました。
毎日17時にはチームごとにデモ会があり、チームメンバー全員がその日のタスクと成果をチームメンターに発表しました。私のチームはこのデモ会までにその日のタスクを終わらせることを目標に、デモ会駆動で開発を進めました。
チーム開発での学び
チーム開発で特に印象的だったのは、コードレビューを受けた経験です。これまでのハッカソンなどの開発では、フロントエンドとバックエンドで別々に作業を行ったり、チームの人数や開発期間の関係で、プルリクエストを出して自分でマージすることが多かったのですが、今回はすべてのプルリクエストに対してコードレビューを受け、レビュワーによってマージが行われました。(CIが導入されていたので、まずはCIを通すことからスタートしました!)
チーム開発では次のようなことを学びました!
-
Git操作:
慣れていると思っていたGit操作も、今回新しく覚える操作がいくつかありました。他のメンバーが作成したプルリクエストに対して、ローカルで内容を修正し、それをリモートにプッシュするため(具体的にはレビュワーにアサインされた時にその機能開発を引き継ぐ時に使用しました。)に、git fetch origin pull/<プルリクエスト番号>/head:<ローカルブランチ名>
のようにプルリクエストのブランチをローカルにチェックアウトする操作を学びました。また、今まであまり経験したことがなかったコンフリクトの解決方法も、チームメンバーに丁寧に教えていただきました。他に、今まで誤ってmainブランチで作業してしまった場合には
git stash
コマンドを使用して変更内容を退避させた後に、新しいブランチを切ってgit stash pop
を使用していましたが、チームメンバーがgit stash apply
を使用していたことをきっかけに、git stash pop
との使い分けを理解することができました。 -
データベースのバルクインサート:
コードレビューを通じて、データベースのバルクインサートについて学びました。バルクインサートとは、データベースに対して複数のレコードを一括して挿入する方法です。通常の挿入操作(INSERT
文)は1回のクエリで1レコードずつ追加しますが、バルクインサートでは複数のレコードをまとめて一度に挿入することで、パフォーマンスの向上やトランザクションのオーバーヘッド削減、一貫性の向上といった利点があります。SQLの高速化については、バッチ処理の活用などもチームメンバーに教えていただき、とても勉強になりました。最終日前のタスクでは、CSVファイルからの顧客一括登録のバックエンドコードをバルクインサートに変更しました。実行時間が大幅に短縮され、感動したのが良い思い出です。
-
タスク管理:
私のチームには、タスク管理が得意なメンバーがいて、率先してバックログをnotionにまとめてくれました。タスクはフロントエンドとバックエンドで分け、期間や担当者の設定などを行いました。また、レビューしてからマージすることがチームの開発ルールだったので、プルリクエストを出した際はURLをnotionとSlackで共有し、他のメンバーが積極的にレビュワーとして参加しました。レビューが終了し、マージされた場合にDoneとして扱っていました。このようなチーム開発のタスク管理の他、notionで他のテーブルと連携する方法などといった今まで知らなかったnotionの活用方法も知ることができました。
新しく学んだこと
アジャイル開発の実体験
今回のインターンでは、アジャイル開発を実際に体験することができました。これまで名前だけ知っていた「スプリント」や「バックログ」といった概念を、チームで実際に活用した経験はとても新鮮でした。
ワーキングアグリーメント
ワーキングアグリーメントとは、チーム内で協力し合いながら作業を進めるためのルールや合意事項を指します。私のチームでは、「早い時間においしい昼食をとる」といった軽いものから、開発上のルールまで幅広く決めました。これにより、心理的安全性が確保された環境で、安心して開発を進めることができました。
インセプションデッキ
インセプションデッキは、プロジェクトの初期段階でプロジェクトの目的や方向性をチーム全員で共有するためのツールです。これを作成することで、チーム全員がプロジェクトの目的や価値を明確に共有し、一貫性を持って開発を進められると感じました。また、プロジェクトの進行中に問題が発生した場合でも、インセプションデッキに立ち返ることで、プロジェクトの目的や方針を再確認し、軸を見失うことなく進行させることができます。
今後、チーム開発の際には、プロジェクト初期の段階でインセプションデッキを作成し、メンバー全員で目的と方向性を共有することで、より円滑な開発を進めていきたいと思います。
クリティカルパス
クリティカルパスは、プロジェクト全体の進行に最も影響を与えるタスクの連続した流れを指します。チーム開発の序盤で、私のチームはバックエンドとフロントエンドの依存関係を意識し、スムーズな連携を図ることを重視して作業を進めました。これにより、各タスクが無駄なく進行し、作業の遅延や停滞を防ぐことができました。
インターンを通して感じたこと
B2B向けのプロダクトだったため、最初はプロダクトの理解が浅く、実装もイメージがつきませんでしたが、メンターやドメインエキスパートへの質問を通じて徐々にプロダクトに対する理解を深めることができました。また、バックエンドの実装イメージが掴みづらいときは、フロントエンドのチームメンバーがホワイトボードを使って丁寧に説明してくれたため、実装迷子にならずに開発することができました。
そして、限られた期間内で顧客のニーズに応えるために必要な機能を特定し、それをどのように実装するかを考え抜き、実際に手を動かしていく過程はとても楽しかったです。技術力だけでなくビジネス的な視点も磨くことができました。
最終日にはプレゼンがあり、他のチームの発表を聞くことで、同じ課題でも解決にはさまざまなアプローチがあることを学びました。UIやUX、開発する機能の優先順位付け、その決定理由など、チームごとに異なる解決策が見られて面白かったです。また、どのチームも技術力が高く、素晴らしい発表で圧倒されました!
私のチームはフロントエンドのタスクとバックエンドのタスクを分けて機能開発していましたが、フルスタックエンジニアのチームは機能ごとにタスクを分けて開発していたと聞いて、改めて参加者の技術力の高さを感じました。
おわりに
今回のインターンを通して、お客様が抱える課題を考え、それを解決するためにどのような機能を開発すべきかを深く考える経験ができました。技術の追求に留まらず、エンジニアとして本当に求められている価値を提供する視点を学び、視野が大きく広がったと感じています。また、アジャイル開発を実践することで、エンジニアとしての実務理解が深まり、開発の流れやチームでの連携についての理解も進みました。何よりも、とても楽しく充実した時間を過ごすことができました。優秀なメンターやチームメンバーと一緒に開発を行い、実り多い2週間でした。
ブログ執筆の際にあたって講義資料を見直したところ、講義中には理解が難しかった内容も「なるほど」と腑に落ちることが多く、自分の成長を実感できてとても嬉しく感じました
インターン期間中に、懇親会を1週目と2週目に2回開催していただいたり、他にも、毎日15時ごろにはおやつタイムがありました。お菓子を楽しみながら、他のチームや社員の方々と交流できる貴重な時間でした。毎日違う方のお話をお伺いできて、とても刺激的でした。私が持っているGo言語の本の著者様もいらっしゃり、感動しました!
さらに、CEOの福島さんやCTOの松本さんのお話を伺ったり(こちらは講話といった形でお話を伺い、気になることは質問させていただきました!)、エンジニア共有会(レビュー会)に参加させていただいたりと、本当に楽しい時間を過ごすことができました。素晴らしいインターンに参加させていただきありがとうございます。感謝します⭐️
最後に、オフィス周辺の築地で美味しいご飯を堪能しましたので、ご紹介します(メンターの方に連れて行っていただいた場所については、参加時の楽しみとして、ここでは伏せさせていただきますね)。
魚好きは病みつきに!
-
Jige 築地店
所在地: 〒104-0045 東京都中央区築地2丁目14−3 NIT築地ビル
ランチタイムに、1400円で鮪かま焼き定食を食べました。定食にはお刺身もついてきて物凄いボリュームがありました。とても美味しかったです。ランチの数が決まっているようなので早めに行くことと、お腹を空かせていくことをお勧めします。 -
ステーキハウス チヤイム
所在地: 〒104-0045 東京都中央区築地1丁目12−22
私は実際には行っていないのですが、チームメンバーが1000円台で鉄板焼きのお肉を食べることができてすごく良かったとのことです👀。
最後になりますが、ここまで読んでくださり本当にありがとうございます!感謝します。
記事の内容に誤りがあった場合は、お手数ですがご指摘いただけると嬉しいです🙇♀️