CyberAgentさんの開発インターン「ACE」に参加してきました

こんにちは.夏休み中は専ら実家に入り浸り,親のすねをかじりまくっているいとゆうです.

基本的に実家周辺の気候は,自然豊かなこともあって夏でも比較的快適です.虫が多いのと,大雨のたびに土砂災害に警戒しなければならないことがたまにキズですが…

さて,タイトルの通り,先日サイバーエージェントさんの方で開発インターンに参加させていただきました.今回参加させていただいた「ACE」というインターンはCAさんにとって初の試みだったようで,前例がないため手探りな部分や不安要素もあったようです.ですが,僕個人としては非常に刺激的で学びの多い機会でした.今回は,CAさんのインターンで学んだことについて,「高品質なコードの書き方」という部分を中心に記事にしようと思います.

インターンの内容

今回のインターンは,現場のエンジニア社員から実務視点でのフィードバッグを受けながら,実務レベルの開発に挑戦するという点が特徴でした.3~4人でチームが組まれ,全チームが共通の要件に沿って開発を行いました.

本番の開発期間は3日間でしたが,その2週間前くらいには既にオリエンテーションが終了しており,そこから当日まではチームで自主的に開発を進めてください,という感じでした.本番の3日間で開発をする,というよりは,本番までにある程度のものを作っておいて,本番期間はレビュー→改善のサイクルをできるだけ回す期間,というスタンスでした.

ハッカソンなどとは異なり,作るものやその仕様については最初から明確に決まっており,今回は「ABEMAの番組表機能」を予め与えられた仕様に則って作成しました.

評価される項目としては「コードの可読性/保守性」や「耐障害性」「パフォーマンス」など実務で重視される部分が中心で,「オリジナルの機能の追加」や「UIの変更」などは評価しないことが明言されていました.本当に「実務的な開発」にフォーカスした内容でした.

使用した技術

私はバックエンドエンジニアとしての参加だったので,バックエンドとインフラに絞って書かせてもらいます.

バックエンド(Rest API

本家のABEMAではバックエンドにgoが使われており,実際他のチームにはgoで開発をしているチームも存在しましたが,私たちのチームはnodejsで開発を行いました(ESとか面接でnodejsの経験をアピールしていたため).

nodejsで開発をするとなるとjavascriptかtypescriptか,という2択が生じますが,現状自分のtsの知識としては「あーあれね,型が決まるやつでしょ?」ぐらいのものだったので,まずはjsで書くことにしました.後々tsに書き換える?という話も一瞬浮上しましたが,学習コストと開発コストを割いてまで書き換えるほどのメリットがない,という結論になりそのままjsを採用しました.

使用したパッケージのうち代表的なものを下に書きます.

  • express ... サーバ構築やルーティングに使用
  • jest ... テストに使用
  • swagger-jsdoc, swagger-ui-express ... APIドキュメントの自動生成に使用
  • sequelize ... ORMとして使用
  • eslint ... linterとして使用(後述)

ローカルでの開発/動作検証は基本的にDockerコンテナで行いました.

インフラ

インフラ環境はAWSで構築しました.「耐障害性」や「管理のしやすさ」というところが評価項目としてあったため,障害に強く管理しやすいインフラ構築が求められました.

僕自身のAWSの経験はというと,せいぜいdockerコンテナをEC2で立ち上げ,RDSに接続してアプリをデプロイしたくらいで,評価項目を満たす設計を考えろといわれてもピンときませんでした.

ありがたいことにインフラについてはCAの方が予め初期設計と構築をしたものを与えてくださり,構築されたインフラを眺めながらECSやALBといった初めての概念を吸収していくことができました.

インフラに強いメンターさんのアドバイスももらいつつ,既存のインフラに以下のような変更を施していきました.

  • APIサーバをECSのFargate起動モードで起動
    • 初期のインフラではAPIサーバをECSのEC2起動モードで起動していました.これをFargateに変更することで,低レイヤを意識する必要がなくなり管理がしやすくなるというメリットがあります.
  • AuroraレプリカをAuto scalingに対応
    • 初期のインフラではマスターとレプリカがそれぞれ1台という構成でした.CPUの使用率に応じてレプリカをAuto scalingさせることで,負荷分散を図りました.
  • s3に番組表データの基になるjsonファイルを配置し,更新されるたびにラムダでDBに反映
    • 番組表データはAuroraに保存されていますが,そのデータはjsonファイルから取得してInsertする仕様になっています.jsonファイルをs3に保存し,内容が更新されるたびにラムダを実行してAuroraの番組表データが更新されるようにすれば運用がしやすいのではないかと考え,この方針をとりました.
  • 本番用とテスト用のデプロイ環境を用意
    • 初期のインフラでは本番用のデプロイ環境のみ用意されていましたが,実務の開発に即してテスト環境も用意することにしました.本番環境と同様にECRでサーバを起動させ,ALBのリスナールールでサブドメインに応じて接続先を変更するようにしました.(api.xxxx.netなら本番環境につなぎ,test.xxxx.netならテスト環境に繋ぐ)

他にもやりたいことは色々ありましたが,時間が限られていたこともあって断念…

学び

実務開発にフォーカスしたインターンということもあり,実務経験の乏しい私にとっては短期間で頭がパンクしそうなほど多くの情報が得られました.というか,終盤はほぼパンクしてました笑

今回は冒頭で宣言したとおり,「高品質なコードの書き方」に絞ってまとめていきます.

ただし,ここでいう高品質なコードとは,「可読性や保守/拡張性,あるいは開発効率の高いコード」を意味しています.これを前提として読んでいただければと思います.

アーキテクチャ設計

私はこれまでMVCモデルに基づいたバックエンド開発しか経験がなかったのですが,実務で開発するような大規模で長期的に運用していくことを前提としたサービスでは,事前にアーキテクチャ設計をよく練っておくことが大切であると分かりました.

私たちのチームは当初,serverファイルにルーティングもロジックもDB接続も全部書き込んでいました.実装すべきAPIの機能がそこまで多くなかったため,1ファイルにまとめてもそこまで煩雑にはならないだろう,という考えのもとでしたが,あくまで「実務を想定した」開発なので,今後機能の追加や仕様の修正があることを前提において設計を考えた方がいい,というアドバイスをメンターさんからいただきました.

他のチームはオニオンアーキテクチャドメイン駆動設計など本格的なものを取り入れていましたが,1~2日でこれらの概念を理解し,実装まですることはさすがに厳しいと判断し,使い慣れたMVCをベースにビジネスロジックだけServiceに分離するMVC+Sを採用することにしました.

Serviceは初めて扱う概念で,最初はControllerとの役割の違いがいまひとつ分かりませんでした.いろいろと調べていく中で,以下のような理解に落ち着きました.

Controller
  • リクエスト内容のバリデーション(パラメータが足りているか・適切なフォーマットか)を行う
  • リクエストで受け取ったデータをServiceに渡せる形に加工して渡す
  • Serviceが返したデータをViewに渡せる形に加工して渡す

Controllerはリクエストのバリデーションやデータのフォーマット変換など,サービスの実現したいコアな機能からは外れた部分の処理を書くイメージです.ViewとServiceを直接連携してしまうと,パラメータが不正だったりデータのフォーマットが扱いづらかったりして面倒なので,そこの橋渡しをControllerでうまくやってあげる感じです.

例えば今回,「指定した日付の番組データ一覧を取得する」という機能を実装したのですが,リクエストでUNIXタイムを受け取ってそれを日付に変換する仕様にしていました(フロントで日付に変換して渡す方がよかったかも).このUNIXタイム→日付の変換などは,機能とは直接関係のない部分なのでControllerに書くべき処理なのかなと思います.

Service
  • そのサービス(今回の場合はAPIの各機能)が実現したい一連の処理を書く
  • データの取得や更新など,データベースへのアクセスを行う(Repositoryとして分離するケースもある)

Serviceは,「番組表一覧の取得」や「番組の予約」など,サービスとして実現したい一連の処理を書くイメージです.ドメイン駆動設計では,アプリケーションサービスやドメインサービス,ドメインオブジェクトなどの更に細かい分類がありますが,ここではあくまで「機能を実現するためのビジネスロジックを全て書く場所」という位置づけにしています.

また,ドメイン駆動設計ではデータベースにアクセスする処理ををさらにRepositoryという形で分離していますが,ここではそれもServiceに含めています.ただし,Modelは別の階層で定義しており,Serviceではこれを呼び出してアクセスする形にしました.

ざっくり言うと「Controllerに書くべき処理以外は全てServiceに書く」という感じでしょうか.

アーキテクチャについてはまだまだ理解が浅いので,これから学んでいく必要がありそうです.ちょうど現在,他社のインターンでクリーンアーキテクチャについて学んでいるので,時間ができたらまた記事にしようと思います.

凝集度

オブジェクト指向開発において,コードの品質を評価するのによく用いられる指標のようです.これについては,CA社員の方がnoteに分かりやすくまとめてくださっているので,詳しい説明はそちらを参照していただければと思います.

note.com

クラスやメソッドといったまとまりにおいて,1つのまとまりにはできる限り1つの目的に沿った処理を書くことが,クラスやメソッドの再利用性やコードの可読性を高めることに繋がるのだと思います.記事にも書かれている通り,これは「クラスやメソッドをできる限り小分けする」ということではなく,あくまで「そのクラス(メソッド)がもつ意味を明確にする」ということです.

個人的に,「意味」は「価値」に置き換えられると思います.例えば,日付を指定して番組一覧を取得する機能を実装するうえで,日付を受け取ってDBから番組表一覧を取得するメソッドはたしかに価値があるといえます.このメソッドは内部的に日付をその日の0時を表すUNIXタイム(ミリ秒)に変換し,さらにそれを秒に変換し,Modelに渡すという処理を行っています.この1つ1つの処理をメソッドしておいたとき「日付をミリ秒に変換するメソッド」や「ミリ秒を秒に変換するメソッド」に価値を見出せるでしょうか.価値が見いだせないメソッドは,用途が曖昧なので再利用性が低いですし,無駄なメソッドばかり増えてもかえって可読性が損なわれると思います.

記事を読んでいて気になったのは,「論理的凝集」として定義されているIfによる分岐処理が極力避けるべきものであるという考えです.

記事の中ではIfとelseの中身をそれぞれ関数化しています.このようにして関数に分けたとしても,結局その関数の呼び出し元で条件分岐することになるのでは?と思います.論理的凝集を避ける理由として,機能追加があった場合に条件が増えたり複雑になることが挙げられているので,条件分岐が必要ないように(バックエンドであれば)エンドポイントを分ける,それでも条件分岐が生じてしまう場合は,Ifの内部で行っている処理は関数化してシンプルに記述できるようにする,といった対処になるのかな,と解釈しました.

凝集度の理解もなかなか苦労しそうです.併せて用いられる「結合度」という概念についても,コードを書きながら少しづつ理解していこうと思います.

変数への再代入を避ける

個人でコードを書くとき,私はこれを結構頻繁にやっていました.しかし,変数への再代入は,現在その変数に入っているものが何であるかを曖昧にし,コードの可読性を損なったりバグの原因になったりします.なので,できるだけ変数への再代入は避け,再代入したい場合は新しい変数(定数の方が望ましい)を定義するべきであると学びました.nodejsであれば,letやvarよりconstを使え,ということですね.

非常に分かりやすいですし,今後の開発で意識していこうと思います.

linterを使う

実務における開発では,1つのサービスを複数人で開発する場合がほとんどだと思います.その際,コードを書く人によって書き方の方式(例えば,インデントの大きさ,数値と演算子の間にスペースを入れるか,等)が違うと,サービス全体として統一感のないコードになってしまいます.

linterは,エディタに導入することで書き方の方式についての共通ルールを設定してくれます.linterに定められたルールにそぐわない書き方をしていると,エディタ上でそれを指摘してくれます.フォーマッタと組み合わせて用いることで,指摘だけでなく修正まで行ってくれます.開発メンバーで共通のlinterを用いることで自然と書き方が統一され,サービス全体として可読性の高いコードにすることができます.

今回はVSCodeで開発をしていたので,VSCode拡張機能として,javascriptのlinterであるESLintを導入しました.

使用する言語に合わせて,今後もlinterを導入していきたいところです.

おわりに

今回はコード品質に絞って書きましたが,他にも負荷分散やインフラ構築など学ぶことだらけのインターンでした.

実務の開発でどんなことが重視されているのかが分かったので,今後身に付けるべきものが明確になったと思います.

Web開発はまだまだ歴が浅くひよっ子ですが,就職する頃にはトサカのあるひよこぐらいにはなっていられるよう頑張ります笑

文字ばかりの記事でしたが最後までお付き合いいただき,ありがとうございました!