TECH PLAY

スマートキャンプ株式会社

スマートキャンプ株式会社 の技術ブログ

226

こんにちは!スマートキャンプ社でWebエンジニアとして働いている中川( @let_mkt )です。 みなさんは普段ペアプロやっていますか? 今回の記事では弊社のエンジニア組織で定期的に開催している「ペアプロ会」について紹介します! 昨年の夏からこれまでに5回ほど開催してきたペアプロ会の開催までの経緯や概要、メリット、課題などについてが主な内容になります。 リモートワーク下でエンジニア間のコミュニケーションに課題感を感じている方には一例としてご参考になれば幸いです。 そもそもペアプロってなに?という方へ t_wadaさんによる素晴らしいスライドがあるので、ぜひそちらをご参照ください ペアプログラミングの5W1HとFAQ / 5W1H and FAQ of Pair Programming - Speaker Deck ペアプロ会とはなにか ペアプロ会とは、文字通り「ペアプロをする会」なのですが、ざっくり以下のような特徴があります。 担当プロダクトを超えて弊社に所属する全エンジニア(10名ほど)が参加する ペアを組む相手は当日ランダムに決まる 着手するタスク(プロダクト問わず)は事前に運営が用意したものをランダムに割り振る 集合から実装、解散するまでを1時間でやり切る ペアプロ会は定期的に(現在は月1)開催する なぜ始めたのか この取り組みは昨年の夏から始めていますが、当時エンジニア組織は以下のような状況にありました。 2つの開発・運用しているプロダクト間でメンバーが異動する動きを活性化しようとしていた エンジニアの入社ラッシュ こういった状況に対する課題感は大きく2つあり、1つは担当するプロダクト以外のプロダクトに対してもある程度勘所を掴んでもらいたいというもの、そしてもう1つはエンジニア間の相互理解を促進したいというものでした。 この課題感に対して、プロダクトを越えてエンジニアが集まり、ランダムな組み合わせでペアプロをすることで解決の糸口にならないかと思ったことで始めた取り組みでした。 どうやっているのか ここでは、実際にペアプロ会をどうやって開催しているのか、準備と本番に分けて説明します。 事前準備 アイテム探し ペアプロ会で取り組んでもらうアイテム(タスク)をかき集めます。 これは主に普段Asana(タスク管理SaaS)上で管理されているアイテム群から見繕ってくる形になります。 単純計算で 参加予定者/2 の数のアイテムが必要となるので、この数を目標とし、そのうえでプロダクト間でアイテム数の開きが出ないように調整します。 このとき集めるアイテムの選定基準として、実際に実装に取れる時間は4,50分程度なのでなるべく時間内に完結する見込みの高いものをピックするようにしています。 また、自分が担当していないプロダクトのアイテムは代わりに見繕ってもらったりしています。いつもありがとうございます! ドキュメント作成 ペアプロ会の開催ごとにドキュメントを作り、ルールや注意点、当日のタイムラインやペアとアイテムの組み合わせなどを記載します。 また、開催中は各ペアのメモや終了後の進捗共有などの欄として活用してもらっています。 ドキュメントの内容としてはペアとアイテムの組み合わせセクション以外はほぼ共通なので、テンプレート化しています。 以下のリポジトリに公開したのでご興味あればご覧ください! github.com 本番 チーム分け・役割分担(5分) 弊社プロダクト組織はGatherを利用しているのですが、ペアプロ会もGatherで行っています。 Gatherに集まったメンバーを自作のSlack botでペア分けし、上述のドキュメントに結果を記載します。 その結果を見たメンバーは各ペアに分かれ、各々の作業場所に解散していく流れです。 また、ドライバー・ナビゲーターといったペアプロ時の役割もここで調整します。 前述の通りメンバーによっては担当したことのないプロダクトのアイテムを着手する場合があるので、その場合はドライバーになってもらい、担当経験のあるメンバーがナビゲーターとしてサポートします。 アイテムを理解する(5分) それぞれのペアが、自分たちに割り振られたアイテムを確認し、理解するステップです。 各プロダクトのお作法や、必要なドメインの知識が存在するアイテムもあるため、そういった事情はこの段階で知っているメンバーが説明しておくようなイメージです。 このステップでペアのお互いがわからないアイテムだった場合や、アイテムの説明が十分でなく疑問点が出てきた場合は、運営(私です)にヘルプを出し説明を受けます。 作業(40分) ペアでタスクに取り掛かる時間です。 お片付け(10分) 作業内容のコミットとPR作成の時間です。(作業時間のバッファとしての位置づけとして長めにとっています). もしアイテムが作業時間中に終わらなかった場合はPRにその旨を記載してもらうようにしています。 以上が1回のペアプロ会の流れです。 ペアプロ会で得られた効果 ペアプロ会の終了後に受け取った感想などのフィードバックから抜粋して紹介します。 ペアプロ楽しい!スタイル修正だけでも、ペアが普段考えていることを知れたりTipsみたいなことを教えてもらえるので、かなりいい勉強機会になる。 初めて作業するメンバーとコード書いたけど楽しかった!スムーズに進んで良かった。 最近一緒に仕事をしていないメンバーと久々に作業できて楽しかった。 細かいコードのあれこれとか、意外と話す機会ないなと感じていい機会だった。(この人ここ知らなかったんだ!とか自分ここ曖昧だったなとか) 細かい機能単位だとしても、そのプロダクトを担当したことのないメンバーに知ってもらえる機会になった。 運営として各ペアを観察しているなかでも、普段担当していないプロダクトに戸惑うような場面もあれば、逆にサクッと実装できて本人の自信となっていそうなシーンを見ることができたり、アイテム外の雑談が盛り上がっている場面にも遭遇しました。 企画時の「担当するプロダクト以外のプロダクトに対してもある程度勘所を掴んでもらいたい」、「エンジニア間の相互理解を促進したい」という狙いはある程度満たせたのではないかと思っています。 今後やっていきたいポイント これまで数回開催してきたうえで、ペアプロ会で現状不足している部分や発展していきたい部分は以下です。 アイテムの準備に時間がかかる 普段からメンバーが思いついた段階で気軽にペアプロ用のアイテムとして蓄積できる仕組み作りが必要そう 長めに時間をとったバージョンでもやってみたい 現状は正味40分が作業時間なのでアイテムが簡単な修正タスクに寄りがち。もっと長い時間を確保して確保して設計やアーキテクチャを考えるなども取り組んでみたい 時間内に終わらなかったアイテムのPRをしっかり後始末する仕組みがほしい 現状はそのアイテムに取り組んだナビゲーターがオーナーとなって責任を持つルールにはしているが徹底されてない。たとえばSlackでメンション付きでリマインドするなど仕組みで解決したい ペアプロ会で発生した疑問を解消したい 主に複雑なドメインモデルやアーキテクチャに対して時間内で説明しきれない部分があるので、そういったものは蓄積してドキュメント化したいorまとまった説明時間を取りたい いずれも方法に検討の余地はあるものの解消可能なものばかりなので、今後少しずつ改善していきたいと思っています! まとめ 今回は弊社のエンジニア組織で開催しているペアプロ会について紹介しました。 そう大掛かりな準備も必要ないので、もしご興味あればライトに一度やってみてはいかがでしょうか。 最後に、このペアプロ会は昨年読んだ以下の記事に触発され企画するに至りました。貴重な知見をありがとうございました! www.yasuhisay.info イベント情報 2/28に弊社開発チームのリモートワークコミュニケーションについてお話しするイベントを開催します! ご興味ありましたら以下のリンクから内容や参加方法についての詳細をご覧ください! smartcamp.connpass.com
アバター
こんにちは!スマートキャンプでインサイドセールスに特化したSaaSであるBALES CLOUDを開発しているエンジニアの井上です。 皆さんは、開発・調査などでChrome DevToolsはよく使われているかと思います。 私達の開発するプロダクトでメモリリーク問題が起きたことがあり、 その際に調査方法で知っていれば助けになった内容をまとめていければと思っています。 JavaScriptのメモリ管理とは? GC(ガベージコレクション)とは? メモリリーク問題とGCで開放されないメモリ よく言われるメモリリークの種類 計測に使用したToolについて タスクマネージャー Chrome DevTools Memory Heap snapshot Allocation sampling Allocation instrumentation timeline Allocation instrumentation timelineで見れるもの memory view 実際にメモリの増加を計測してみる 実行前の状態をタスクマネージャーで確認する 計測準備 計測結果を確認する タスクマネージャー Statistics Allocation まとめ JavaScriptのメモリ管理とは? JavaScriptはメモリ管理にGC(ガベージコレクション)を採用しており、これにより使用されなくなったメモリを自動的に開放します。 GC(ガベージコレクション)とは? 使用されていないメモリであるかどうかを見つけ自動でメモリを開放する仕組みのことです。 これは定期的に行われるわけではなく、ガベージコレクションの処理を実行するのが効果的である、と判断された場合に実行されます。 メモリリーク問題とGCで開放されないメモリ GCの処理でメモリが開放されると言いましたが、上手いことGCがすべてのメモリを確認して開放してくれるわけではありません。 というのも、先述の通りメモリ開放の判断基準は「そのメモリが必要か不要か」なのですが、すべてのパターンにおいてGCが自動判定してくれるわけではなく、時にはその判定が開発者に委ねられることがあるからです。 普段意識しないですが、この委ねられた部分で開発者が問題のあるコードを書いてしまうことで GCで回収されないメモリが発生し内部処理を圧迫するほどメモリが肥大化した結果、メモリリークとなるパターンが多いかと思います。 よく言われるメモリリークの種類 開発者が意図的に破棄しない限り、GCにより開放されず残り続けるパターンとして代表的なものは下記です。 タイマー、コールバック Dom参照 クロージャ グローバル変数 こちらに関しては下記の記事が非常にわかりやすかったです。 GCのアルゴリズムやマークスイープアルゴリズムに関しても書かれておりとても参考になりました。 How JavaScript works: memory management + how to handle 4 common memory leaks 計測に使用したToolについて タスクマネージャー Shift + Escを押すか、Chromeのメインメニューに移動し、[その他のツール]>[タスクマネージャー]を選択して、 タスクマネージャー を開きます。 タスクマネージャーではページが現在使用しているメモリの量をリアルタイムでみることが可能です。 Chromeタスクマネージャーで開くと下記のような画面が出てきます。 そこでヘッダー部分から表示する項目でJavaScriptメモリを選択すると、JavaScriptのリアルタイムのメモリの変化を追うことが可能です。 自分はこの画面では主に、「どのような操作でメモリの増加があるのか」をログを元に確認していました。 当時、初期調査においてメモリ上昇の原因作を特定するには、この機能はとても重宝しました。 Chrome DevTools Memory Chrome DevToolsでは問題となっている箇所がどの部分かや、どのメモリが肥大化しているかを見ていました。 メモリを確認するMemoryパネルでは、メモリリークの追跡などのために以下のプロファイリングタイプを選択し確認できます。 Heap snapshot ページのJavaScriptオブジェクトおよび関連するDOMのスナップショットを記録し、グラフやDOMツリーなどで確認できる機能です。 Allocation sampling メモリ割当を記録します。パフォーマンス負荷がこれらのプロファイリングタイプの中でもっとも軽いです。 あまりこちらは使用したことがないのですが、大まかにメモリの分布を計測したいときに使うと良いようです。 Allocation instrumentation timeline Allocation instrumentation timelineは、Heap snapshot情報と、Chome DevToolsのタイムラインパネルの増分更新と追跡を組み合わせたような機能です。 記録を開始するとリアルタイムにメモリの増減を関ししながら記録できます。 ※これ使えばタスクマネージャーすらいらないのでは?とも思いました。 今回はこの3つの中でも一番使用していたmemoryのAllocation Instrumentation Timelineについてご紹介できればと思います。 Allocation instrumentation timelineで見れるもの memory view memoryパネルのなかでは4つの異なるViewでメモリの確認ができます。 パネルそれぞれで見れるものが異なり以下のような特徴があります。 Summary オブジェクトをクラス名別のグループにして表示し、クラス別にメモリの使用量を表示します。 Containment ヒープ領域のコンテンツを調査します。オブジェクトの構造に適したビューが提供されるため、グローバルの名前空間で参照されるオブジェクトを分析するのに役立ちます。 このビューでは、クロージャを分析して下位レベルのオブジェクトまで踏み込んで調査できます。 Statistics オブジェクトごとのメモリの統計情報を円グラフで提供します。 Allocation JavaScriptの機能別のメモリ割り当てを表示できます。 実際にメモリの増加を計測してみる 今回はPlayCodeを使用して下記のメモリー増加するようなコードとして クロージャを使用して長い文字列を返す関数を使用して計測していこうと思います。 ※BootstrapとjQueryはPlayCodeで初期で設定されていました(そして久々にjQuery書きました)。 https://playcode.io/new/ import 'bootstrap@4.6.0' import $ from 'jquery' $( 'button' ) .html( 'Click me' ) .on( 'click' , () => new TestMemoryLeaker()) class TestMemoryLeaker { constructor() { this .createLargeClosure() } createLargeClosure() { var largeStr = new Array (1000000).join( ',' ); var lC = function lC() { return largeStr; } ; return lC; } } これをPlaycodeで実行前のメモリと実行したあとでどう変わったか見ていければと思います。 実行前の状態をタスクマネージャーで確認する 実行前の状態だと182MBです。 ここからメモリー増加するようなコードを実行しChrome DevToolsで詳細を見ていきます。 計測準備 いよいよAllocation instrumentation timelineを使用して計測します。 Allocation instrumentation on timelineにチェックし、Startをクリックします。 StartするとTImelineが表示されメモリの増加をリアルタイムで確認できます。 この状態でPlayCodeでJavaScriptを実行します。 計測結果を確認する 計測を止めるとSnapshotが作成されます。 このSnapshotにメモリの増加の原因となるObjectなどが記録されているので、これを元に確認していきます。 タスクマネージャー まずは、タスクマネージャーを確認してみると182MBだったメモリが311MBまで上がっているのがわかります。 182MBだったのが311MBまで上がっているのがわかります。 Statistics StatisticsではどのObjectがメモリを使用している割合が大きいのかを確認していきます。 typed arraysの割合がとても大きくなっているのがわかります。 Allocation Allocationでは関数ごとにどのくらいメモリを使用しているか確認していきます。 今回はテスト用に作成したfunction createLargeClosureのsizeが大きくなっているのがわかります。 また、Allocationの良いところは、どのjsファイルの関数かもわかるようになっているので、特定のライブラリを使用していて問題となっている場合も特定がしやすいことです。 まとめ メモリの問題は対応も検知も難しいので頭を悩ませるところかと思います。 今後も、Chrome DevToolsはかなり高機能になってきているので、使いこなせるように更新を追っていこうと思います。
アバター
こんにちは!!!スマートキャンプでエンジニアをしている吉永です! 自己紹介記事はこちら 前回の記事はこちら 私は現在、スマートキャンプの主力サービスであるBOXIL SaaS(以下、BOXIL)の開発にフロントエンド、バックエンド問わず携わっています。 恐らく新年一発目になる弊社テックブログの記事は私の記事ということで、今年もよろしくお願いいたします。 はじめに なぜパフォーマンス改善を行ったのか Core Web Vitals(以下: CWV), Lighthouseとは? CWVとは Lighthouseとは CWV & Lighthouseの改善結果 サービスページ(改善前) サービスページ(改善後) レビューページ(改善前) レビューページ(改善後) 改善をするにあたってチームで行ったこと コミュニケーション的なお話 タスクの洗い出し、調査方法 優先順位付け DatadogやSearch Consoleでの定点観測 Sprint Reviewでの報告 技術的な改善内容 JavaScript、CSSの最適化 アクセシビリティに関する修正 カラーコード修正の手順 改善活動をするうえで大事なこと どうやればいいかが分からない?それはそう。 一人でやらない、組織でやる 改善結果は全員に共有 結果が出たら喜びを共有する 周知した結果 終わりに 参考資料 はじめに 弊社の主力サービスである BOXIL はリリースから時間が経っていることもあり、バックエンド・フロントエンドともにさまざまな技術的負債となる部分を抱えています。 BOXIL開発チームでは、そのような技術負債の返済を積極的に行っており、その一環としてReactの導入や、webpackのビルド時間の大幅な短縮などを行いました。 React、Webpack改善の記事はこちらから しかし、jQueryやCSSのサイズ問題など、パフォーマンスに大きく関わるが、普段の開発をしながらでは工数の問題や、バグを恐れて容易には手をつけられないものも存在します。 今回の記事では、そんな改善業務の中から去年の秋頃に行った、LighthouseやCore Web Vitals関連のパフォーマンス改善での成果や過程で意識していたことなどについてご紹介します。 なぜパフォーマンス改善を行ったのか 先ほど紹介したBOXILは、ユーザーとベンダーをマッチングさせるプラットフォームというサービスの特性上、SEOの評価が売り上げに大きく影響します。 これまでにも開発チームやメディアチームが主導してSEOの改善やコンテンツの質をあげ、順位を保ってきました。 しかし一時的ではなく継続して効果が出るような対応をしたいと思っても、サービスの規模的に大きな改善をしようとすると、開発メンバーの工数の問題やそれに付随する知識を持ったメンバーが少なくなかなか手をつけられずにいました。 そんな中、昨年8~9月頃、BOXIL開発メンバーに新たに3人のメンバーが加わり若干の余裕ができました。そのため私と新しく加わったエンジニアの永井さんの2人チームで約2ヶ月間BOXILのパフォーマンス改善活動を行なうこととなりました。 Core Web Vitals(以下: CWV), Lighthouseとは? ここで、先ほどから度々上がっているCWV,Lighthouseについて軽く解説します。 CWVとは CWVとは、以下の3要素で構成されるGoogleの定めたサイトのUX品質担保のための指標のことを指します。 Googleの検索エンジンでは、各項目の指標に沿ってそのページ内で改善が必要かスコアリングを行い、コンテンツ内容で競っている他のページがある場合、このCWVのスコアも順位の判断基準にします。 各指標の中身は、以下のような内容です。 Largest Contentful Paint(以下: LCP): ページの中で一番大きいコンテンツのロード時間を測定する。 First Input Delay(以下: FID): ページが読み込まれてからユーザーが最初にアクションを行ったときに、ブラウザが反応するまでの速度。 Cumulative Layout Shift(以下: CLS): 視覚的な安定性を測定し、読み込み時に起こるページのレイアウトのズレなどを計測する。 公式ページ Lighthouseとは Lighthouseとは、Google製のWebアプリの監査ツールの名称です。 パフォーマンス(CWV, Web Vitals項目)やアクセシビリティなど、Webアプリを作成するうえで重要な5つの項目をどれくらい満たしているのかをスコアリングしてくれます。 Lighthouseのパフォーマンスで計測している項目の説明は以下の通りです。 First Contentful Paint(以下: FCP): ユーザーがURLをクリックしてから最初の要素をレンダリングするまでの時間 Time To Interactive(以下: TTI): ページが操作可能になるまでの時間 Speed Index: ページの読み込み速度 Total Blocking Time(以下: TBT): FIDの類似指標、ページ表示からユーザーの操作に応答できるまでの読み込み待ち時間 LCP FID また、Web Vitalsの指標は今後も変更していくとGoogleから告知されています。 そのため、今回はより新しいLighthouseの項目で計測できる可能性が高い、Google Chrome Canaryを使用して各種スコアを計測し、パフォーマンス改善をしています。 CWV & Lighthouseの改善結果 まず、改善結果を発表します。 今回対応したページはBOXIL上で重要なコンテンツと認識しているページすべてにおいてこの対応をしました。 今回は効果がわかりやすく出た、SaaSの情報が記載されているサービスページと、そのレビューが記載されているレビューページを例にあげます。 計測は一貫して以下のページで行いました。 サービスページ レビューページ サービスページとレビューページの特徴として、弊社のメインコンテンツである関係上ページ内の要素数はかなり多く、無駄に読み込まれているJavaScriptやCSSによって、パフォーマンスの計測結果に毎度かなりばらつきがありました。 注: 画像でパフォーマンスの数値が違うのは、スコアを測定し直すなどした際にばらついてしまったためです。 サービスページ(改善前) TBTの計測結果が460msという結果からしても、リソースの読み込みに時間がかかっていることが伺えます。 また、画像にaltがついていなかったことや、widthとheightを明示的に示していないことでレイアウトシフトが起こり、警告され減点対象になっていました。 サービスページ(改善後) アクセシビリティ、SEO項目がほぼカンストする結果になりました。 また、大きな効果としてはTTIが1/6程に短縮されていることがわかります。 さらに、460msかかっていたTBTが0msになっており、絶大な改善効果を発揮していることがわかります。 これは、後ほど説明するJavaScript削減による効果が大きいです。 レビューページ(改善前) 先ほどのサービスページほど遅くはないですが、やはりTBTがかなり悪さをしていることがわかります。 また、画像が多いページのためaltがついていなかったり、aタグに識別できる属性がついてないことによるアクセシビリティ項目での減点が多々ありました。 レビューページ(改善後) Best Practice項目以外において、スコアが大幅に上昇しました。 SEO項目に関しては100点になり、さきほどのサービスページ同様JavaScriptの削減効果で410msかかっていたTBTが0msになりました。 パフォーマンス項目は、ページのパフォーマンス改善活動をすると決めてから一番重い課題の一つだと認識していたため、すべて緑色(Good)になったのは素晴らしい結果だと感じています。 以上のように、上記2つのページで大幅なスコアアップを達成し、他サービスページなどでも同様に改善の恩恵を得ることができました。 ここからは、劇的なパフォーマンス改善をするにあたってチームで行ったことや、技術的な改善点、力が及ばずできなかったことなどを発表します。 改善をするにあたってチームで行ったこと コミュニケーション的なお話 改善を一緒に行ったエンジニアの永井さんは8月に入社したということもあり、私と組んだ時点では、知り合ってまだ1ヶ月ほどでした。 そのため、お互いのスキル感や動きやすい環境などをあまり知らない状態でスタートしています。 まず最初に行ったことは、雑談形式でお互いの経験や、やりたい改善点を話しながら、自分の得意な領域でやれそうなことを開示していきました。 そして、今回の場合だと以下で決定しました。 私の場合。 外部向けの周知施策や依頼の取りまとめ 他社事例を参考にした、施策の実行 フロントエンドで完結するタスク全般の消化 永井さんの場合。 チーム向けの周知施策やドキュメントの取りまとめ 以前の経験や他社事例を参考にした施策の実行 インフラ・バックエンドに関連するタスク全般の消化 これは今後タスクを進めていくうえで、二人のうちのどちらが担当するかを決める重要な指標になりました。 私が外部向けの対応を担った理由としては、BOXILに関わるステークホルダーの幅広さから、入社間もない場合はどこに共有するべきかが迷いそうだからという考えがありました。 そのため、入社して時間が経っている私が担当するほうがロスタイムを少なくやり取りができると思い、引き受けました。 一方の永井さんには、私よりエンジニア歴が長く経験豊富なことから、深ぼって調査する必要がありそうなタスクをお任せしました。 このように、メンバーの性格やスキルを上手く話し合い、お互いの得意領域だけで戦える環境を作ることができたのは後々効果が大きかったです。 また、困ったときに「そういえばあんなこと言ってたな...」と聞きに行くことができたり結果的にお互い円滑にタスクを進めるうえで重要な対話でした。 タスクの洗い出し、調査方法 今回の改善活動は、1ページを早くすれば良かったわけではなく、BOXIL内でも特にSEOが重要な複数ページを対応する必要がありました。 そのため、対応ページの中でも優先度が高いもの、今回であればBOXILのメインの機能となるサービスページから着手することにしました。 また、Lighthouseで指摘される項目はページごとに共通して利用可能なものが多くなることが予想されたので、調査した項目ごとの対応方法をTipsとしてドキュメントにまとめながら作業しました。 また、開発者ツールのNetworkタブで読み込んでいるJavaScriptのサイズを調べた結果、ページの機能からしても明らかに過剰な量を読み込んでいることがわかりました。 パフォーマンスタブでは、ページ全体でどのタイミングでなんのイベントが起きているかを知ることができます。 LayoutShiftなどを対処したい場合、ここでページの画像付きでどう現象が起きているのかを知ることができるためとても重宝しました。 また、レンダリングされてから遅いのか(フロントエンド課題)、レンダリングされる前が遅いのか(インフラ・バックエンド課題)などの切り分けをするためにも使用していました。 そうして洗い出したものはすべてタスクとして積んでいきました。 優先順位付け 積まれたタスクは以下の順番で優先順位をつけることにしました。 比較的軽いタスク(直すだけの作業タスク) 重いが効果がありそうなタスク(不要なJS、CSSの削除) 重いうえに外部との連携が必要そうなタスク(カラーコードなどのデザイン修正) 重いうえにそもそも対処が難しいタスク(依存してる外部のScriptで指摘されている項目) 比率的には、軽いタスクが書き出したタスクのうちの6~7割ほどを占めていました。 また、予想通りLighthouseから指摘されている項目のほとんどは、いくつかのページで共通していました。 そのため共通した軽いタスクや、改修のコスパが良いものを最初の数週ですべて消化し、スコア上昇が見込めるかやその効果などを知ろうと考えました。 逆に少しでも重いと感じたタスクやコスパが悪そうだと感じたものからは、とりあえず逃げました。 逃げたタスクは後日調査が必要であれば調査タスクにし、時間をかけて対応していきました。 DatadogやSearch Consoleでの定点観測 Datadogでは、主にモバイルページのLighthouseスコアを計測し日々の朝会で確認をしていました。 計測するページは、パフォーマンス対応ページの中でも注力していて、なにか起きたときにクリティカルになりうるページに限定しました。 また、Search Consoleでサービス全体の不良URLの数なども確認し、Sprint Reviewで今週どれくらい減ったのかを全体に向けて発表する場がありました。 どちらも修正が入ったタイミングでスコアが上がっていたり、逆になぜか下がっていたりなどを日々確かめ、何か異常があったときに早期にキャッチすることが目的です。 Sprint Reviewでの報告 BOXILチームでは、毎週金曜日にSprint Reviewという今週の成果発表をする場を設けています。 そこで今週のLighthouseというコーナーを設け、Datadogで定点観測したスコアや上がったスコアを発表しました。初回は用語の説明やなぜやるのか、ご協力のお願いなどを混ぜながら、とにかくチームだけでなくBOXIL運営の組織全体を巻き込み共有をしました。 また、大きな工夫としては自分ではわからなかったことも積極的に報告をしました。 サードパーティ製のものを使用している場合や、管理権限を持つ部署が開発チーム以外だったりするケースも多くあったため、「これ、どうすればいいですかね...?」と言ったような問題も報告していました。 その結果、ある部署の方にその問題をキャッチしていただき、Sprint Review中に700以上の不良ページが改善された事例もありました。 技術的な改善内容 ここからは、上記のタスク内で行った改善内容をいくつか紹介します。 JavaScript、CSSの最適化 タスクを洗い出すタイミングで『開発者ツールのNetworkタブで読み込んでいるJavaScriptのサイズを調べた結果、ページの機能からしても明らかに過剰な量を読み込んでいることがわかりました。』と書きました。 これを詳しく調査したところ、元々はapplication.js or application.cssを読み込む設定になっていました。 これはLighthouseでも「Remove Unused JavaScript(またはCSS)」という項目で指摘されています。 そのため、サービスページ以外のファイルも読み込んでしまい、結果TBTが大きく膨れ上がるという現象が起きていました。 それを踏まえ、JavaScriptやCSSに以下の対応をしました。 def include_javascript_path path = "#{ controller.controller_name } / #{ controller.action_name } .js " asset_exists?(path) ? path : ' application ' end def include_css_path path = "#{ controller.controller_name } / #{ controller.action_name } .css " asset_exists?(path) ? path : ' application ' end private def asset_exists? (path) if Rails .configuration.assets.compile # 動的にコンパイルしているかどうか(dev環境のみtrue) Rails .application.precompiled_assets.include?(path) else Rails .application.assets_manifest.assets[path].present? end end これまでapplicationを読み込んでいたslimから、該当の記述を削除し、上記のinclude_javascript_pathなどに置き換えました。 BOXILではサービス用のservice_controllerというcontrollerがあり、サービスページを表示するためにはshowというメソッドが呼ばれています。 そのため以下のような名前のファイルを設置し、そのページ内だけで読み込まれているJavaScriptのみを読み込むことで脱却しました。 app/assets/javascripts/services/show.js ファイルがない場合はもともと読み込まれていたapplicationが読み込まれるので、動作は変わらずそのまま動きます。 修正前は217kbのファイルを読み込んでいましたが、結果として以下のようになりました。 72.0kbにまで減らすことができました。およそ1/3の削減です。 Chromeの開発者ツールからカバレッジなどを確認できるため、それらを参考に何がどれだけ使用されているかなどを確認しながら進めると良いです。 jQueryなどを読み込んでいる場合、かなり圧迫していると思います。 アクセシビリティに関する修正 Lighthouseでは、アクセシビリティに関する問題も報告してくれます。 一番多かったものは、imageタグにalt属性をつけていないことによるエラーでした。 # 悪い例 <img src="test.png"> # 良い例 <img src="test.png" alt="test画像"> alt属性には、画像が表示されなかった際の代替テキストの役割がありますが、スクリーンリーダーが画像情報を読み上げる際にalt属性を指定することによって綺麗に読み上げてくれるというメリットもあります。 ただし、適した画像に適したaltを付けないと、逆に読み上げの邪魔になるケースがあるため、むしろ読み上げてほしくない場合にはalt=""と空で指定できます。 テキストを入れるか入れないかの判断や、入れるテキストの内容は以下の記事を参考にさせていただきながら決定しました。 もうalt属性で迷いたくない 次に、サービス内で使用されているカラーコードの変更です。 BOXIL全体で使用している文字色や背景色の組み合わせによっては、「背景色と前景色には十分なコントラスト比がありません」と指摘されていました。 Lighthouseでのコントラスト比の判定は、 Web Content Accessibility Guidelines (WCAG) に則って行われており、少なくとも背景色と文字列には4.5:1以上のコントラスト比がないと指摘されます。 Lighthouseでは以下のように、問題のある点を指摘してくれます。 それを元にBOXILチームのデザイナーに置き換えるまでの手順や方向性を相談し、デザイナーから提案された色に置き換えていきます。 カラーコード修正の手順 置き換える際に、以下の手順で進めていきました。 置き換える対象の洗い出し 置き換えるカラーコードをデザイナーと相談 scssファイルに変数を設定 置き換え開始(今ここ) 古いカラーコードが定義されている変数を消す 本来であれば古いカラーコードの変数を置き換えるだけで良いと考えていたのですが、色を直接呼び出していたり、別で定義されている変数を呼び出していて、影響範囲が未知数だったりと置き換えの作業が難航しました。 そのため、一気に置き換えようという考えは捨て、いったん自分がすぐ発見した場所を直し、そのほかは見つけ次第どんどん修正していくという運用をすることにしました。 考えた運用方法はドキュメントにまとめながら開発チームに共有しました。 現在は新しい機能を開発する場合は、基本適切なコントラスト比が維持される色を使うことになり、古いカラーコードを呼び出しているところは、定期的な開発改善で置き換えていく予定です。 置き換えが終わったタイミングで古い変数を一掃し、できるだけクリティカルなバグが少なくなるように考えています。 このように、雑多なタスクに加えていくつか大きな変更を伴う開発を行なっていながらパフォーマンスを改善していきました。 その中で、CDNの最適化などにもチャレンジしており、永井さんがその記事を書いているのでぜひ読んでみてください。 Cloudflareの画像最適化料金をWorker KVで97%削減した話 改善活動をするうえで大事なこと ここからは、改善活動をするうえで特に大切だと感じたことなどを書いていきます。 どうやればいいかが分からない?それはそう。 まず、前提として改善活動をするにあたってどうやってやればいいかが分からないと感じても、それは悪いことではないです。 とにかく、最初は「ああできたらいいよね。こうできたらいいよね」という理想の完成形を挙げ、調査タスクから始めるのも良いです。 調査した結果はドキュメントに残し、改修の手間と効果、改善に使える期日などと相談しながらそのタスクの優先度を決めていきましょう。 改修内容や調査内容のドキュメントは、今後同じ問題に対処しなくてはいけなくなった場合のノウハウ消失が防げますし、自分達がこの一連のプロジェクトで何をしていたかを周りに教える重要な資料になります。 一人でやらない、組織でやる 改善活動をするにあたって、思うように改善結果が出なかったり、逆に大きなバグやこれまでの負債、自分の権限が及ばない部分に行き着いてしまうことがあります。 その時、自身の知識だけでなくサービスのドメイン知識を持ったステークホルダーの協力が必要になります。 BOXILのようにある程度成長したプロダクトでは、そのステークホルダは開発組織だけでなく、マーケ、企画、経営メンバーなど多岐に及ぶため、開発組織を超えて聞かないと分からないような問題も多々ありました。 以下の画像は、自分の権限がなく調査ができなかった際に弊社の社長を直接メンションして聞く私の姿です。 今回、開発組織を超えて改善の協力を要請するにあたって以下のことに気をつけながら進めていました。 改善結果は全員に共有 改善活動をするにあたって、一番懸念していたのは以下のようなことになってしまうことでした。 周囲に自分達が何をしているかをうまく伝え切らずに、「あの人たち、たまにメンションしてくるけど何やってる人たちなんだろう...」となってしまうのはとても勿体無いです。 それを回避するために以下の2点を開発チーム外に向けて積極的に展開し、周知しました。 Sprint Reviewで改善施策がどういうものなのかを報告(用語、やること、やった事) リリース時に改善結果をSlackで報告 その効果なども一緒に報告 また、助けていただいたメンバーへの積極的な感謝を混ぜることにより、次にまた依頼したときに協力を得やすくなると考えました。 また、Lighthouse計測結果などのBefore / Afterの画像があるとより、結果が目に見える形で共有されるため士気が上がりやすいです。 結果が出たら喜びを共有する 改善活動自体は、タスク単体が地味なものが多く、それを面白いと感じる人はかなり限られていると思います。 さらに、タスクの結果はスコアが上がったとか、CLSがどうたら...とか、なかなかエンジニア以外のメンバーは興味を持ちづらいような内容ではないかと考えています。 そのため、日頃から周囲に自分達のやっていることやその効果をアピールし、その結果が出たらそのサービスに関わる全員で喜べるような空気を作ることが大事です。 周知した結果 周知、結果を出す、喜ぶ、それを周知を繰り返した結果、先月行われた社内の表彰制度で賞をいただきました。 その後嬉しくなるコメントも多数いただき、技術との向き合い方に関しても自信が付く、良い数ヶ月間の取り組みになりました。 ただ、この結果は一人で行なってできたものではなくチーム内外での協力を得られたからこその成果だと認識しています。圧倒的感謝! 今回の記事で書いていた内容は、パフォーマンス改善以外でも自身がやったことを効果的に周囲に伝えるための術を多く書いています。 今後の開発も周りの協力をうまく得ながら進めていくことで、より良い開発体験が得られるのではないかと考えています。 終わりに パフォーマンス改善は、今日やったから終わりではなく今後も付き合い続けなければいけない問題です。 今後もWeb Vitalsの項目はアップデートしていくと発表があったとおり、SEOと付き合っていく以上長期的な改善施策を組んでいく必要があります。 その改善施策の中には、すぐに結果が出せなかったりそのために必要な工数を割かなければいけない場合があります。 パフォーマンス改善や技術負債の返済に、どのサービスでも共通で必ず改善できる銀の弾丸がない以上、『今できないからやらない』ではなく長期的に、長い目で見ながら、数年後を見据えて少しずつ向き合っていくことが重要です。 この記事が、サービスをよりよく改善するためにパフォーマンス改善を取り入れたいと思い始めた方の助けになれば嬉しいです。 参考資料 改善をすすめるうえで参考にした記事や、最近見つけた事例記事を紹介します。 https://hack.nikkei.com/jobs/Web/ hack.nikkei.com blog.recruit.co.jp techblog.yahoo.co.jp
アバター
BOXILでエンジニアをやっている永井です。前回は入社エントリを書きましたが今回は技術的な記事を書こうと思います。 今回はCloudflareにおける画像の最適化処理のコストカットをした話をします。ざっくりいうとCloudflare内のKVという機能を使い、最適化をした画像をキャッシュしました。似たような問題で悩んでいる方は参考にしてもらえると嬉しいです。 TL;DR Cloudflareで画像のリサイズ(形式変更)を行っていた リサイズ後の画像はデフォルトではキャッシュされず、都度リサイズの処理が実行されていた Cloudflare内のWorker KV機能を使いキャッシュの実装をしたところ、コストがおよそ97%削減できた TL;DR 前提 問題 対策 Workers KVとは 注意事項とか サンプル 事前準備 KVのnamespace作成 KVをworkerに登録 流れ Keyについて コード ポイント 結果 まとめ 前提 BOXILでは画像の配信にCloudflareを使っています。同時にCloudflareの機能で画像のリサイズや形式の変換(WebPなど)を行い、ブラウザ上で素早く表示されるための最適化を図っています。具体的な内容に関してはすでに記事を公開しておりますのでぜひご参考にしてみてください。 tech.smartcamp.co.jp 問題 この画像最適化はCWVのスコアやUXの向上に大きく貢献していましたが、 1~2ヶ月の間運用をしていくと「最適化済みの画像をキャッシュしていない」という事が発覚しました。 これは誰かがブラウザで画像を閲覧するたびに画像の最適化処理が実行されると言うことになります。10人が10個の画像入りページを開いた場合、100回最適化処理が走るため効率が良いとは言えません。 また金銭的コストにも大きな影響が出ていました。Cloudflareでは処理5万件につき$9の費用がかかります。BOXILでは平均して一日におよそ60万~70万回ほどの最適化処理が走っており、月におよそ40,000円のコストがかかっていました。t3.xlargeのインスタンスを2ヶ月借りられる位の額なので大きいですね。 対策 最初に書いたとおり、CloudflareのWorkers KVという機能を使って最適化後の画像をキャッシュしてコストを削減しました。 Workers KVとは Cloudflare Worker内で使用できるkey-valueストアで、キャッシュした静的ファイルと同じくらい迅速に応答するAPIやWebサイトの構築を可能にするらしいです(公式より)。 www.cloudflare.com valueには string , ReadableStream , ArrayBuffer の3つの型のいずれかを格納できるため、今回は最適化後の画像を ArrayBuffer に変換して格納しています。ざっくり言うと画像をバイナリに変換してkey-valueストアに突っ込むという少し強引な方法を取っています。 料金はストレージ1GBがプランに含まれており、BOXILの最適化後の画像は全体でも1GBに満たない位だったため、追加で料金が発生しませんでした(追加しても1GBで$2)。また読み取りも1M(100万)回につき$1なので、画像最適化の処理5万件につき$9の費用と比べると大きく節約が可能になります。(※料金は2021/12/20時点のものです) 注意事項とか KVに画像を格納することに関して良いのかという問題はコミュニティーで議論されていました。ブログのようなサービス用に画像、または音声データをKVに格納して使うことに関しては問題ないようです。(動画は駄目) https://community.cloudflare.com/t/is-it-ok-to-serve-images-from-workers-and-kv/179166 community.cloudflare.com サンプル では実際にKVを使って画像をキャッシュする方法をサンプルコードを載せつつ説明したいと思います。KVのnamespaceを作成後workerに登録して使用するといった流れです。順に説明していきます。 事前準備 KVのnamespace作成 Workers→KVに遷移し、"Create namespace"からKVのnamespaceを作成しておきます。名前は管理しやすいようにしておきましょう。 KVをworkerに登録 KVを使いたいworkerの設定→変数→KV名前空間のバインディングから、さきほど作成したKVのname spaceをworkerに変数として読み込ませます。変数名は任意なので、ここも管理しやすい名前にすると良いと思います。 流れ 準備ができたので、実際にコードを書いていきます。処理は以下のような流れで行います。 KV内に画像のバイナリがあるかどうか確認 もしあればクライアントに返却して終了 なければ元画像をS3から取得 元画像に最適化処理を実行する 最適化した画像をKVにバイナリ変換して格納する 最適化した画像をクライアントに返却して終了 Keyについて 今回はKeyに画像取得用のURLを直接指定することで、単一性を担保しています。もし同じURLで違う画像を取得するようなユースケースがある場合は何か別のKeyを考える必要があります。 コード addEventListener( "fetch" , event => { // WebPをサポートしていない場合はそのままプロキシする if (!isWebpSupport( event .request)) { return fetch( event .request) } // リクエストループ防止 if (/image-resizing/.test( event .request.headers.get( "via" ))) { return fetch( event .request) } if ( event .request.url.match(/ \ jpg$|jpeg$|png$/i) && ! event .request.url.match(/ \ +| \ %2B| \ %40| \ %5B| \ %5D| \ %21| \ %EF \ %BC \ %88| \ %EF \ %BC \ %89| \ %28| \ %29/)) { return event .respondWith(handleRequest( event .request)) } else { return fetch( event .request) } } ) async function handleRequest(request) { const cachedImageBuf = await RESIZED_IMAGE.get(request.url, { type: "arrayBuffer" } ) if (cachedImageBuf !== null ) { return createResponseBy(cachedImageBuf) } const originalImageRequest = new Request(request.url, { headers: request.headers } ) const optimizedImageResponse = await fetch(originalImageRequest, OPTIMIZE_IMAGE_OPTION) if (optimizedImageResponse.ok || optimizedImageResponse. status == 304) { const optimizedImageBuf = await optimizedImageResponse.arrayBuffer() await RESIZED_IMAGE.put(request.url, optimizedImageBuf, { expirationTtl: 1209600 } ) // keyはURL, 有効期限は2週間 return createResponseBy(optimizedImageBuf) } else { return optimizedImageResponse.redirect(originalImageRequest, 307) } } function createResponseBy(imageBuf) { return new Response( imageBuf, { status : 200, statusText: "OK" , headers: { "Content-Type" : "image/webp" , "Content-Length" : imageBuf.size, } } ) } function isWebpSupport(request) { return Object .fromEntries(request.headers).accept.match(/image \ /webp/) } const OPTIMIZE_IMAGE_OPTION = { cf: { image: { fit: "scale-down" , width: 1440, height: 900, quality: 90, format : "webp" } } } ポイント KVから値を取得するときは、バイナリを入れていてもデフォルトではStringで取得するためそのままでは意図した動作になりません。なので以下のように第2引数でtypeを指定してあげることでそのままバイナリとして使用できます。 const cachedImageBuf = await RESIZED_IMAGE.get(request.url, { type: "arrayBuffer" } ) 逆に値を入れるときは、ArrayBufferに変換して入れてあげましょう。 const optimizedImageBuf = await optimizedImageResponse.arrayBuffer() // レスポンス(画像)を変換 await RESIZED_IMAGE.put(request.url, optimizedImageBuf, { expirationTtl: 1209600 } ) 結果 実際にこの処理をデプロイして数日後の結果が以下の図の通りです。 どこで今回の施策を反映したか一発で分かる位、ガクッと画像のリサイズ処理の回数が減っています(すごく気持ちよかった)。料金も4万円/月→1,200円/月位に落ち着き、およそ97%のコストカットを行なうことができました。 まとめ 今回はCloudflareの画像最適化処理をWorkers KVでキャッシュすることで効率よく画像の配信ができるようになったのでその方法をご紹介しました。 CloudflareのImage Resizeはそのまま使用するとキャッシュをしなかったためこのような手段を取りました。効果がとても大きかったためしばらくはこれで運用を続けていきますが、Cloudflareの開発はとても活発なので、そのうちデフォルトでキャッシュするようになってくれると嬉しいなぁと思ったりしています。では良いお年を。
アバター
こんにちは!スマートキャンプエンジニアの関口です! 12/1~12/2にかけて弊社のプロダクトチームで合宿に行ってきました!今回の記事では、合宿レポートをお届けします! 今回の合宿は以下の感染症対策を充分におこなったうえで実施しました。 参加者全員のPCR検査、検温 一定時間ごとに換気 合宿を開催した目的 今回の合宿を開催した目的は以下の2点です。 リモートワークで稀薄になっていた密度の高いコミュニケーションを取ること 企画、開発メンバーがそれぞれの仕事内容の理解を深め、実務に活かせるようになること 業務がリモートワーク中心になったり、新しいメンバーが増えたことで部署間のコミュニケーションが稀薄になり、チームカルチャーが薄まっている懸念がありました。そのため合宿を通して普段よりも密度の高いコミュニケーションを取ることが目的の1つにありました。 またもう1つの目的は企画メンバーと開発メンバーの業務の相互理解を促進させるいうものです。 この目的を達成するために、企画サイドからは開発メンバー向けにプロダクトや機能のアイディアの煮詰め方についてや、計画や効果検証の方法など、企画サイドの業務について理解を深めることのできるコンテンツを用意しました。最終的には、開発メンバーから企画メンバーをサポートしたり自ら企画ができるようになることを理想としています。 反対に、開発サイドからは企画メンバー向けに、定義された要件に基づいてどのように開発が行われるかが分かるようなコンテンツを用意しました。企画メンバーが開発プロセスを理解することで、企画内容や計画の精度があがることを理想としています。 実施したコンテンツについての詳しい説明は後述します。 *弊社の企画職はデザイナー,プロダクトマネージャー,ディレクターの方々をさす。 宿舎紹介 今回開発合宿を行なわせていただいたのは、 マホロバ・マインズ三浦 というホテルです! 他社の開発合宿でも利用されており、合宿に必要な設備が充実していることが決め手となりました。Wi-Fiはもちろん、プロジェクターなどの備品の貸し出しも充実していました。 会議室の窓からはオーシャンビューの絶景が楽しめます! 昼休みにはメンバーみんなで海に行き写真を取りました。 コンテンツ紹介 今回の合宿の目的の1つが 企画、開発メンバーがそれぞれの業務内容についての理解を深め、実務に活かせるようになること だったため、コンテンツもその目的が達成されるように設計しました。 具体的には BOXILでユーザーがサービスを見つけやすくするためのアイデアソン をおこないました。企画プロセス、開発プロセスの要点を座学で学びながら、機能アイデアをグループで企画、設計するというものです。 それぞれの職種のメンバーが普段担当していないことを体験することで、相互理解を深めました。 企画パート まず機能を企画するうえでの考え方を学びました。企画プロセス全体の流れから、機能を作成するうえで役に立つフレームワークの紹介がありました。今回の合宿ではダブルダイヤモンドとValue Proposition Canvasを利用して、機能の企画をおこないました。 ダブルダイヤモンドの説明の中で課題と解決策の2つのそれぞれで仮説を立て、検証することが大切だと説明がありました。 具体的には企画パートとして以下のことを実施しました。 顧客セグメントの作成 ユーザーインタビュー バリューマップ、プロトタイプの作成 ユーザーテスト 顧客セグメント検討 VPCは顧客セグメント(Customer segment)と顧客への提供価値(Value Proposition)にわかれるため、最初に顧客セグメントの作成をおこないました。 グループごとに、ペルソナを考えたり、ユーザーのペインやゲインを考えたりしながら、課題の仮説を立てました。 ユーザーインタビューの実施 続いておこなったのは解くべき課題が正しいかを検証するためのユーザーインタビューです。 ユーザーインタビューの際の注意点や意識したほうが良いことの座学を事前におこない、トークスクリプトを作成し、実際に他グループの企画職の方にインタビューをしました。 ※ インタビューの良し悪し、スクリプトの例は 実践的ジョブ理論テンプレート から引用しています。 実際にインタビューをしてみると、良くないと分かっていながらも自分の欲しい回答を得ようと誘導尋問をしてしまいそうになったり、想定とは違った回答をもらい、スクリプト通りにインタビューができなかったりしました。しかし、自分たちでは想像もできなかったユーザーの課題を見つけることができました。 バリューマップ、プロトタイプを作成 ユーザーインタビューを元に解くべき課題を特定したら、続いて解決策の検証をおこないました。 課題を解決するための機能はどのようなものにしたら良いのかをバリューマップを作成しながら考えました。バリューマップを作成した後、figmaでプロトタイプを作成しました。このfigmaを使ったプロトタイプの作成に時間がかかるチームが多く、デザイナーが少し手伝うと一瞬でプロトが作れる場面が複数のチームでありました。デザイナーってすごいなぁとあらためて感じた瞬間でした。 あるチームのプロトタイプ ユーザーテスト プロトができたら、企画した機能をユーザーが想定通り使ってくれるかを確かめるためにユーザーテストを実施しました。機能を使ってユーザーにどんなアクションを起こしてほしいかをユーザー目線で設定し、よりリアルなテストにするため、サービス・機能を利用する理由や目的などのシナリオを作成しました。またユーザーテストの評価項目として以下の3つがあることを学びました。 効果 効率 満足度 実際にユーザーテストをやってみると、想像以上にユーザーに自分たちが想定したとおりに使ってもらえませんでした。自分たちの想定だけで機能企画を進めてはいけないということをユーザーテストを通して学びました。ユーザーテスト後はユーザーテストで得られた情報を元にプロトタイプをブラッシュアップしました。 開発パート 開発パートではシステムを開発の全体像と重要となる点の座学を受けたあとに、 企画パートで作成した機能の設計をおこないました。 座学では以下の内容を学びました。 開発プロセスの流れについて クラウドについて 技術選定について 非機能要件について 座学 V字モデルを利用しながら企画職の方向けに開発プロセスの説明をおこないました。基本設計、詳細設計の概要、それぞれのプロセスを踏む大切さを学びました。 クラウドについての座学ではオンプレミスとクラウドの違い、AWS, GCPなどのそれぞれのクラウドコンピューティングの違いを学びました。 技術選定についての座学では、弊社の利用技術を紹介したのち、技術選定の観点について学びました。さまざまな選択肢の中で、自社の状況にあった技術を使うことが大切であるという説明があり、弊社の利用技術の理解が深まりました。 非機能要件については非機能要件とは何か?なぜ非機能要件が必要なのかについて学んだ後、BOXILの1つの機能を例にしてどのようなことが非機能要件に当たるのかの説明がありました。 クラウドから非機能要件まで、エンジニアが企画職に知ってもらいたいことを座学中心で学び、エンジニアリングの世界に入門してもらいました。 実践 開発パートでは企画パートで作成したアイデアを機能に落とし込むためにUML図の作成と DB設計をおこないました。 UML図はユースケース図とシーケンス図を扱いました。 それぞれの図がシステム設計の際にどのように使われるのか、具体的な書き方などを説明したのち、グループごとに2つのUML図を作成しました。 UML作成後データベース設計をおこないました。データベースの設計の重要性を学んだのち、 企画職の方々と既存のBOXILのデータ構造を確認しながらデータベース設計、ER図の作成をしました。 発表 ER図の作成を終えたあとは2日間で企画、設計した機能をグループごとに発表しました。 BOXILのプロダクトマネージャーにフィードバックと、実際に機能として乗せられるかどうかの判断をしてもらいました。 評価が一番高かった機能は「自社と同じような規模の会社がどのようなSaaSを開発しているのか可視化させることができる機能」を企画したチームでした! 合宿を終えて 合宿でアイデアソンをおこなったことで、企画職やデザイナーの方々の仕事内容や機能を企画するうえで考えていることが理解するきっかけになりましたし、あらためて企画職やデザイナーの方々に対してリスペクトの気持ちが強くなりました。またリモートワーク化で交流が少なくなっていた他のチームの方々と久しぶりにコミュニケーションが取ることができ、大変充実した2日間になりました。今後もこのような企画を続けていきたいです!
アバター
スマートキャンプ エンジニアの瀧川です。 私は最近社内では開発をほぼせず、もっぱらエンジニア組織の課題に思いを馳せています。 そんな私ですがエンジニアとしての情熱がなくなったわけではありません。 個人でとても関心を持っているのが、みなさんもご存じであろう ISUCON です。 今回は ISUCON を題材として、Datadogの習熟・活用、パフォーマンス改善スキルの向上を狙った、 SMARTCAMP 社内ISUCON(以下S-ISUCON) をご紹介します。 ※「ISUCON」は、LINE株式会社の商標または登録商標です。 目的 弊社では9月にメインのアプリケーションの監視をMackerelからDatadogに移行しました。 移行理由としては、APM(Application Performance Management)やRUM(Real User Monitoring)、各種Log管理などを一元的に担えるところにDatadogの魅力を感じたことが挙げられます。 とても便利そうと感じる一方で、Mackerelと比較したときの長所の裏返しですが、Datadogの機能の多さ・複雑さが際立っており、移行に際して これらの機能をどのように開発者に浸透させていくか という課題がありました。 また別の軸で、Webアプリケーションのパフォーマンスに関するスキルについて課題感がありました。 N+1に代表されるようなパフォーマンスに関わる実装はどのような機能開発でも基本的に意識すべきものだと思います。 しかし現状社内ではあまりフォーカスされず、 属人化している のではという懸念がありました。 これは既存プロダクトではすでにキャッシュ戦略が整備されていたり、RailsだとN+1の対応が容易だったりと、なんとなくで改善できてしまうことが多いといった理由もあるかもしれません。 しかしこれでは、いざ新規プロダクトを開発したり、別の言語で開発する際に生産性が低下する要因になりうると感じていました。 そこで今回は ISUCON を題材として、 「Datadog(Infrastructure, APM)の習熟」と「パフォーマンス改善スキルの平準化」 に向けたイベントを社内で企画することにしました。 なぜ ISUCON なのかについては、 ISUCON11予選 に私が参加したことが関連しています。 過去問を解いたり実践に参加するなかで私が感じた重要なポイントは、 「ソースコードを見ただけで改善してもスコアが上がらないこと」「曖昧な知識で改善してもスコアは上がらないこと」 の2つでした。 しっかりとAPMやプロファイラを見てボトルネックを見つけないと、そもそもほぼリクエストがないエンドポイントかもしれません。 またなんとなくindexを貼っただけでは効かないことが多々あります。 そういった性質が今回の課題感とマッチしていると感じ、実施に踏み切りました。 事前準備 オリジナルで問題を作成したほうが公平に取り組んでもらえると思いましたが、今回はDatadogの習熟が急務だったこととパフォーマンス改善スキルの平準化が目的だったので、メンバー間での議論や教え合いが重要だと考えました。そこで今回は ISUCON10予選(過去問) を使わせていただくこととしました。 ※ 運営の皆さん、いつもおもしろい問題をありがとうございます。 加えて以下の条件で環境を準備しました。 各チーム1台のEC2インスタンス VPNからのアクセス設定(SSH: 22, HTTP: 80) ※ 本番は3台が多いが、アプリケーションの改善に集中してもらうため Datadog Agent(Infrastructure, APM)インストール Rubyの実装への切り替え ※ Rubyメインのプロダクトが多いため 各チームのサーバーのソースコードと対応したGitHubリポジトリ ※ Deploy Keyも登録 ISUCON の環境はAWSなどで簡単に作成できるように整備されているため、今回もスムーズに整えることができました。 https://github.com/matsuu/aws-isucon また参加者に向けて簡単な説明会も実施しました。 ほぼ ISUCON 未経験者だったので、 ISUCON の概要や修正の勘所、 ISUCON10予選(過去問) のレギュレーション・マニュアルの読み合わせ、修正・デプロイ・ベンチマークの流れなど少し厚めに説明をしました。 ISUCON10予選(過去問) について検索は一応控えてもらい、それ以外の勉強は許可としました。 当日の流れ 当日はリモートか出社かはチームで相談してもらうことにしました。 (出社していたチームはホワイトボードを駆使して仕様を紐解いていたようでした) 丸一日S-ISUCONに割かせてもらうよう調整して、競技は11:00~18:00の7時間とし、10:00~11:00の間でSSHやHTTPでの接続確認など準備、18:00~19:00を最終スコア発表と感想戦にしました。 競技中、ベンチマークは各チーム自由に自サーバー内で叩くこととして、ポータルサイトも用意してなかったので、ベストスコアを更新したらSlackでアピールするようにしてもらいました。 ※ 本番の ISUCON だと、各チームのスコアが随時反映されるポータルサイトがある。 当日の様子(Datadog APMを見ながら戦略を立てている) サポート ISUCON 経験者がほぼおらず、また練習期間も設けなかったので当日いくつかサポートが必要になる場面がありました。 ローカルでの環境構築方法は? 基本的には過去問リポジトリのREADMEを参照 構築に時間がかかりそうなチームは都度サーバーにデプロイして確認 本番環境でスキーマ変更を反映させるには? リポジトリ内の特定のファイルを編集してデプロイすればinitialize時に反映 MySQLやnginxの設定ファイルへの変更を反映させるには? 特に整備できてないので/etc/nginx以下のファイルを直接編集しsystemdで再起動 結果と事後FB ISUCON10予選(過去問) は実務で経験することが少ない座標計算のロジックがあったり、単純にDBのindexを貼るだけだと効かなかったりと難しかったと思いますが、各チーム試行錯誤しながらスコアを伸ばしておりホッとしました。 1位のチームは各種index、Redisによるキャッシュ、バルクインサートなどがしっかりと効果的に実装されていたため、スコアを伸ばすことができたようでした。 終了後に以下のアンケートを実施しました。 S-ISUCONの満足度 S-ISUCONでの活躍度 S-ISUCONで学べたこと Datadogで見れなかった情報があるか 感想や要望 アンケートの結果をいくつか抜粋して紹介します。 普段使うところのない脳の筋肉を使ってよいエクササイズになった みんなで速度を上げていくのが初めてだったのですごい楽しかった N+1の解消に少し時間かかりすぎた Ruby周りは貢献できたが、MySQL周りは微妙だったかも Datadog APMの見方がわかった indexが効かないパターンについて知った DatadogのSQL表示でパラメータが見れなかった Datadogがかなり見やすかったのでこれから活用していきたい みんな技術的に課題を感じながらも、楽しみながらパフォーマンス改善に取り組んでいたのが印象的でした。 ( ISUCON のスコアが上がるというモチベーションがやはり鍵だと感じています) 「Datadog(Infrastructure, APM)の習熟」と「パフォーマンス改善スキルの平準化」にしっかりと寄与するイベントになり、実施して非常によかったと感じています。 また感想戦でお互いの取り組みを話す中で、普段業務内で見えなかった各メンバーの強みに触れることができて、メンバー間の相互理解にもつながるよいイベントになりました。 満足度もとても高かったので、今後も継続して(次回はオリジナルの設問を...)開催していこうと思います。 来年の ISUCON に参加するメンバーがでるとなおよしですね! おわりに 今回「Datadog(Infrastructure, APM)の習熟」と「パフォーマンス改善スキルの平準化」を目的としたSMARTCAMP 社内ISUCON with Datadogを紹介しました。 こういったエンジニアのスキルアップにつながるイベントを時間をもらって実施できるのはよい環境ですし、必ずプロダクトの成長にも効いてくるので今後も継続して企画・運営していければと思います。 同じような課題感を持っている方や、弊社の雰囲気を知りたい方の参考になっていれば幸いです!
アバター
こんにちは!!!スマートキャンプでエンジニアをしている吉永です! 自己紹介記事はこちら 前回の記事はこちら 私は現在、スマートキャンプの主力サービスであるBOXILの開発にフロントエンド、バックエンド問わず携わっています。 初めに Supabaseとは PostgreSQLベースのデータベース 認証 ファイルストレージ APIの自動生成 料金プラン 他サービスとの料金比較 データベースに対する操作数 データベースの性能 認証 試してみる 環境構築 認証 データベースの作成、supabaseAPIの使い方 データベースの作成方法 データの取得方法 データの作成方法 リアルタイムデータベース TypeScriptの型サポート 良かった点・不満点 良かった点 不満点 まとめ 初めに 皆さんはFirebaseというBaaS(Backend as a Service)を利用したことがありますか? Googleが提供元のモバイルやWebアプリケーション向けのサービスで、RealtimeDatabase(NoSQLなリアルタイムDB)やFirebaseAuthentication(さまざまな認証機能)が搭載されている便利なサービスです。 私は現在SNS認証などを実装する場合、FirebaseやAuth0のようなBaaSを利用していますが、データベースに関してはRealtime Databaseなどは利用せず、PostgreSQLなどのRDBを構築して使っていました。 「嗚呼、RDBなFirebaseが欲しいなぁ」とたまに感じながら生きてたら、それをそのまま体現したかのようなSupabaseというサービスがあることを知り、興味を持った次第です。 今回の記事では、そんなFirebase Alternative(Firebaseの代替)と言われるSupabaseについてご紹介したいと思います。 Supabase公式サイト Supabaseとは Supabaseのドキュメントを読むと、「Supabase is an open source Firebase alternative.」と書かれています。 オープンソースになっているFirebaseの代替と謳っているこのBaaSは、現在以下の機能を提供しています。まずは簡単に各種機能の説明をしたいと思います。 PostgreSQLベースのデータベース Firebaseで実装されていたRealtimeDatabaseはその名の通り、データの更新に合わせてリアルタイムに変更を検知できることが最大の特徴となっています。 しかし、NoSQLではデータの整合性を保証しないため、削除や更新処理が頻繁に発生した際に整合性を保ちづらい点や、SQLを使用できないため複雑な検索をしづらい点などの課題感もあり、便利は承知の上で実用には足踏みしてしまう方も多いのではないでしょうか。 反対にSupabaseで導入されているのはPostgreSQLで、ここがFirebaseとの大きな違いになってくるかと思います。まさにFirebaseのRDB版と言った具合でしょうか。 テーブルやカラムの管理なども、Firebase同様管理ページから管理できます。 公式ドキュメント 認証 先ほども書いたとおり、私は現在SNS認証などを手軽に開発したい場合は、認証のみFirebaseかAuth0に頼る形で使っていました。 上がSupabaseでSNS認証対応しているサービスで、Firebaseと同じように多様なサービスがサポートされています。 公式ドキュメント ファイルストレージ お馴染みのファイルストレージ機能です。 ここは特に代わり映えがあまりないので特に説明はしません。 公式の説明を読んでいると動画やメディアのファイルプレビュー機能がしっかりしてそうで、今度色々なファイルを上げて試してみたいです。 公式ドキュメント APIの自動生成 管理画面からテーブルやカラムを追加した際に、自動的にAPIへのルーティングを生やしてくれる機能です。 公式ドキュメントにも例としてありますが、ToDoテーブルを作った際、ToDoに対するGET、POST、PATCH、DELETEのリクエストを走らせることができるようになります。 下にドキュメントに記載されている例文を載せておきます。 // Initialize the JS client import { createClient } from '@supabase/supabase-js' const supabase = createClient( [ SUPABASE_URL ] , [ SUPABASE_ANON_KEY ] ) // Make a request let { data: todos, error } = await supabase .from( 'todos' ) .select( '*' ) また、FirebaseのRealtimeDatabaseと同様、リアルタイムでのデータ変更の検知を可能としていると書かれているため、この後実際に作ったりしながら実験してみようと思います。 公式ドキュメント 料金プラン 現在、Supabaseの料金プランは以下のようになっています。 Supabaseの料金プラン テストや趣味用のフリープランは無料で使えますが、その他は25$からになっているようです。 リアルタイムデータベースのサポートや、APIリクエストなどはフリープランでも無制限になっています。 認証に関しても、無料プランでは10000ユーザーになっており、個人で使う分には十分です。 認証方法に関しても、無料プランと有料プランで違いはないです。 他サービスとの料金比較 ここでFirebaseやAuth0の無料プランとの比較をしてみたいと思います。 データベースに対する操作数 Supabase Firebase 書き込み 無制限に無料 1日2万回 読み込み 無制限に無料 1日5万回 削除 無制限に無料 1日2万回 データベースの性能 Supabase Firebase 容量 500MB 1GB このように、DBに対する操作に関してはSupabaseの方が優れていそうです。 しかし、無料で使えるDB容量に関してはFirebaseの方が大きいことがわかります。 認証 Supabase Firebase Auth0 アクティブユーザー 10000ユーザー 電話認証月1万 / その他無限 7000ユーザー 認証では、Firebaseが無料で使えるアクティブユーザーの登録数が多いことがわかりました。 試してみる 環境構築 早速試してみましょう。今回は公式ドキュメントにチュートリアルはありませんが、使い慣れているNuxt.jsでSupabaseを使用してみたいと思います。 まずはsupabaseのページで新しくプロジェクトを作ります。 regionは東京が用意されていたため、それを使っていきます。 次にNuxt.jsの雛形を作ります。 npx create-nuxt-app supabase-nuxt-example npm install nuxt-supabase Nuxt.js用にはSupabaseのコミュニティで開発されているnuxt-supabaseがあるため、それを使いながら試していきます。 nuxt-supabase リポジトリ 終わったら、nuxt.config.jsに以下を追記します。 modules: [ [ 'nuxt-supabase' , { supabaseUrl: 'YOUR_SUPABASE_URL' , supabaseKey: 'YOUR_SUPABASE_KEY' }] ] , YOUR_SUPABASE_URLとYOUR_SUPABASE_KEYは、Supabaseプロジェクトを作ったときに出てきたURLとApi Keyを入れてください。 また、プロジェクト作成時にTypeScriptを導入した方はtsconfig.jsonに以下の記述を追加してください。 { " compilerOptions ": { " types ": [ " @nuxt/types ", " nuxt-supabase " ] } } 認証 あらかた整ったところで、認証を試してみようと思います。 設定画面から認証のページに飛び、認証を使用したいサービスを選びます。 Create new credentialsをクリックすると、そのサービスの認証用トークンを発行してくれるページに飛べるので便利です。 今回はGitHubのアカウントを使って認証してみたいと思います。 フォームに沿って必要な項目を入力します。 そして、発行されたIDやSecretをSupabaseに入力して保存したら設定完了です。 そしたら軽く認証用のコードを書いてみたいと思います。 this.$supabase.auth.onAuthStateChangeで認証状態の監視をします。 変更が行われた際に自動でcheckUserが走る感じですね。 実際にページにアクセスして、ログインボタンを押すと以下のようになります。 このままGitHubアカウントで認証すると、ログイン済みのステータスになりログアウトができるようになったことが確認できると思います。 このように、Firebase同様SNSログインなどの自分で実装するとめんどくさいものが、簡単にできるようになるのは一つの強みではないでしょうか。 データベースの作成、supabaseAPIの使い方 データベースの作成方法 次にデータベースを作っていきます。 今回は、会社とユーザーを紐付ける1 : Nになるテーブルを作っていきます。 Enable Row Level Securityにチェックを入れると、データへの本人以外からの書き込みに制限を設けるなどのセキュリティ対策ができます。 また、Nullableやリレーションなどもこの画面から設定できます。 まずはダッシュボードから会社を追加してみます。 必要な項目を入力して保存すると、このように新規のデータが追加されていることがわかると思います。 データの取得方法 そして、コード上では以下を追記します。 const { data } = await this .$supabase.from( "companies" ).select( "*" ) if (data) { this .companies = data; } すると、このように先ほど追加したデータが取得できていることがわかります。 データの作成方法 次に、supabaseのAPIを使ってデータを追加してみます。以下をコードに追加してください。 await this .$supabase.from( "users" ).insert( { name: "test" , company_id: 1, } , { returning: 'minimal' } ); 実行すると、このように新しいユーザーが追加できていることがわかります。 リアルタイムデータベース データの変更をリアルタイムで監視するには、デフォルトでオフになっているオプションをオンにする必要があります。 サイドバーからデータベースを選択し、Replicationからリアルタイムの検知を有効にしたいテーブルを選択してください。 そして、以下のように追記すると、.subscribe()で監視ができるようになります。 await this .$supabase.from( "users" ).on( "*" , (payload) => { this .users = payload } ).subscribe(); また、on("INSERT")などとすると、INSERTやUPDATEのみでの発火設定も可能ですが、*を入れることによりすべての変更を監視できます。 が、現状セキュリティ的に不安な要因があるため、SupabaseはRealTime Databaseの使用をOFFにすることを推奨しています。 TypeScriptの型サポート TypeScriptで使える型をデータベースから自動生成したい場合、openapi-TypeScriptで自動生成できます。 本記事では時間の都合上触れられていませんが、以下のドキュメントにまとめられています。 OpenAPIを使った型の自動生成 良かった点・不満点 良かった点 個人的に良かったところとしては、やはりRDBが使えたことだと感じています。 これまでリアルタイムに更新されるデータを扱いたいと感じた場合、簡単に実装する一番の選択肢はFirebaseでしたが、そのためにはNoSQLを使わなくてはならず、なかなか踏み出せずにいました。 SupabaseではPostgreSQLに対応しており、SQLやSpreadSheetからテーブルの作成やデータの移行に対応しているため既存のDBからの乗り換えもスムーズにできそうでした。 また、Row Level Securityのような書き込み、読み込みの権限をいくつかのテンプレートから設定できたのも便利ポイントだったように感じます。 もともとFirebaseを触っていたからというのもありますが、思ったよりもスムーズにリアルタイムなデータ同期ができた点も、満足度としては高かったです。 不満点 Supabaseに限った話ではないですが、クエリビルダで複雑なクエリを書くのが難しいです。 NoSQLの話をしたときの「SQLを使用できないという点から複雑な検索をしにくい」という点とは違い、フロントエンドで使えるクエリビルダの現状の限界という話である気がします。 セキュリティの都合上いくつかの種類のクエリはライブラリから叩くことができないため、難しいクエリを書く場合も合わせてPostgreSQLでおなじみviewを書き、それをフロントエンドで呼び出すことにより対処することになります。 SupabaseでViewを使う方法 しかし、クエリビルダを好んで使う温室育ちの僕にとって、生のSQLを書く行為は中々レベルが高いのでちょっと辛そう...という感じです。けれど、クエリビルダが使えないというだけでSQL自体は書けますしNoSQLで感じていたようなそもそもの検索のし辛さのようなものは感じにくいと思います。 まとめ いかがでしたでしょうか。 本記事では、Firebase AlternativeことSupabaseについて紹介しました。 Firebaseでは毎度SNS認証だけ使用して、メインの機能であるRealtime Databaseを一切使用しないということをしていましたが、これからはその認識を改めざるを得ないかも知れないです。 最後までお読みいただきありがとうございました!
アバター
こんにちは。スマートキャンプでエンジニアをしている井上です。社内ではエースと呼ばれていますが、特にエースらしいことをしたわけではないです。 さて、突然ですが皆さんは資格取得に興味はおありでしょうか。周りに認められたい、資格取得奨励金が欲しい、会社での評価を上げたい、就職や転職に有利になるように、などの理由で資格取得に挑戦された方も多いと思います。しかし最初は資格勉強を始めたものの、なかなかうまくいかないというときもあるのではないでしょうか。 私の前職はSIerでしたが、そこで私はさまざまな資格を取得しました。難しいものから簡単なものまで色々あり、今は合計で9つほど保有しています。 基本情報技術者 応用情報技術者 AWS Certified Solutions Architect - Professional AWS Certified SysOps Administrator - Associate AWS Certified Developer - Associate AWS Certified Soluctions Architect - Associate AWS Certified Cloud Practitioner GCP Associate Cloud Engineer TOEIC 935 AWSの資格が半分以上を占めていますね。これらを大体2年半ほどで取得しています。TOEICについて断っておくと、いきなりこの点数を取得したのではなく、大学時代からコツコツ勉強してこの点数です。 何でこんなに取得したのかというと、資格取得奨励金と自己研鑽が理由です。前職だと資格取得に対して奨励金が出るのでそれが欲しかったのと、自己研鑽によって自分の仕事の範囲も広がったり、より良いクオリティで仕事ができると思ったからです。実際にどういうメリットがあったのかは後述させていただきますね。 今日はこれらの資格を取得した実績に基づいて、私の体験を交えつつ、 資格取得のメリットや取得のための勉強方法、おすすめの資格 などを紹介していきたいと思います。 エンジニアが資格取得するメリット 業務外の知識がつく チャンスが広がり、思わぬ仕事が舞い込んでくる 評価が上がる 資格取得で特に大切なこと わからないことをそのままにしないこと 反復すること モチベーションを維持すること モチベを維持するコツ スモールステップではじめよう できるだけ毎日やる 勉強内容を実際の業務にどう活かすかを想像する 資格取得戦略 合格基準との知識の差を知る スケジュールの立て方 具体的な勉強方法 AWS資格編 利用したサービスや教材 スケジュール IPA資格編 利用したサービスや教材 スケジュール おすすめの資格 AWS Certified Solutions Architect - Professional AWS Certified Developer - Associate 応用情報技術者 TOEIC まとめ エンジニアが資格取得するメリット まず資格取得するメリットとは何でしょうか。ここではエンジニアという視点から見た資格取得のメリットについて私が思うことを語りたいと思います。 業務外の知識がつく 普段業務で触れている知識は、あくまで業務で必要な範囲に限られてきます。取得する資格の内容にもよりますが、資格で勉強する内容は基本的に広範です。例えば「AWS Certified Solution Architect Associate」の資格一つをとっても下記のような範囲を学ぶことになります。 分野 出題の比率 第 1 分野: 弾力性に優れたアーキテクチャの設計 30% 第 2 分野: 高性能アーキテクチャの設計 28% 第 3 分野: セキュアなアプリケーションとアーキテクチャの設計 24% 第 4 分野: コストを最適化したアーキテクチャの設計 18% 合計 100% 試験ガイド から引用。 詳しくは試験ガイドを見てもらえれば良いのですが、各分野にまつわる色々なAWSのサービスを知ることになります。これらの範囲を勉強していくことで、 自分の知識の範囲を広げることができ 、また普段の業務に対してもその知識を活かして 多角的なものの見方 ができるようになります。もしかすると今向き合っているシステムは実はベストプラクティスから外れているような実装・運用かもしれません。そうした気づきを得ることができるのも資格取得するメリットと言うことができます。 また資格を取るという目標があることで、一貫した勉強の指針になります。例えば漠然と「AWSの勉強をする」と目標に定めても何から手を付けていいか分からないところがあると思いますが、資格取得という道筋があることで、 やるべき勉強や仕入れるべき知識が明確になり、勉強がしやすくなります。 チャンスが広がり、思わぬ仕事が舞い込んでくる 資格は一つの能力の指標です。それが誰にどのように評価されるかはその状況に依存するのですが、チャンスが広がることは確かです。 私自身の体験談として、SNS上で自分の資格情報を載せていたら、とある企業からAWS関係の仕事のお誘いをいただきました。そのオファーを受けて、今では副業としてその仕事をしています。「AWS Certified Solutions Architect - Professional(以下AWS SAP)」の資格やTOEICの点が高いところが評価されたようです。このように持っていると 自分の可能性を広げてくれる というメリットもあります。 評価が上がる 企業によっては資格取得を奨励しており、評価の対象としていることがあります。私の前職では自己学習の一環として評価の対象となっていたので、これによって社内での評価を上げることができました。 また資格の価値を知っている人も多かったので、周囲の人からの評価も上がりました。これは特に新卒入社したてのときは有効でした。入りたてのころは周りの人も私の実力を測りかねていたところもあったでしょうから、そういうタイミングで実力の証明となる資格を取ると、 私がどれくらいのことを知っているのか知ることができて 協業しやすかったのだと思います。 資格取得で特に大切なこと さて、ここからは実際に資格取得にあたって必要なことや大切なことをお話していきたいと思います。 わからないことをそのままにしないこと わからないことをそのままにしないこと は重要です。答えを聞いてなんとなく分かった気になっているのはほとんどただの暗記と変わらないです。そのままだと応用が利かないので、試験の問題に対応ができないです。わからないときはネットで検索してみましょう。 特にポピュラーな資格だとそれだけネットに出回っている情報も多く、調べると解説記事などがたくさんヒットします。こうした記事などを元に知識を自分の理解に落とし込みます。 こうして得た理解を元に資格勉強をしていくと、自分の理解と矛盾したことが載っている場合などもあったりします。そういうときもすかさず調べてみます。そういった既存の知識との矛盾を敏感に察知できれば凄く良いです。 それが今の自分が分かっていないこと だからです。 反復すること 一度学習したらそれで終わりでなく、 何回も繰り返すこと が大事です。特に今までの経験と全く関係のない分野の勉強となると一から知ることになるので特に忘れやすいです。 忘却曲線というもの を聞いたことがあるでしょうか。人は一時間もすれば半分以上のことを忘れているというやつです。これを信じるかどうかはさておいて、勉強した英単語を、翌日には完璧に忘れてしまっているというような体験は皆さんの実感としてあることだと思います。結局のところ人は忘れる生き物だということで、それを理解して勉強をしなくてはいけません。そのために何度も反復して学習します。 モチベーションを維持すること 資格取得はものによっては1週間程度で取得までできることもありますが、多くは数ヶ月単位での準備が必要になります。そこで難しくなってくるのがモチベーションの維持です。とりわけ難関資格であれば、その学習量の多さに辟易して、「また来年受験しよう」などと引き伸ばしてしまうことが多いです。そういうことを防ぐために、どうモチベーションを維持すれば良いのでしょうか。 モチベを維持するコツ スモールステップではじめよう まず スモールステップで勉強 をしましょう。すなわちいきなり一足飛びに難しい概念や問題に取り組むのではなく、簡単なところから始めます。そのためにまず基礎的な内容を勉強します。 いわゆる座学 です。 資格試験は問題集をひたすら解いていれば合格できると宣う人もいますが、それができるのはそもそも基礎的な知識がある人か一握りの天才だけです。いきなり本番で出てくるようなゲキムズの問題に直面して、そしてその答えの解説を見て、どれくらいの人が理解できるでしょうか。答えを聞いても全く理解できず、自分の実力の足りなさ加減にうんざりするのがオチだと思います。まずは基礎的な内容を勉強し、それから問題集に手をつけましょう。そうすることで、答えを聞いて理解できなかったとしても、 どこが理解できないかという取っ掛かりぐらいはできるはず です。この取っ掛かりがあることによって、気分が萎えることなく勉強をすすめることができると私は思っています。 できるだけ毎日やる できるだけ毎日勉強 をしましょう。というより、土日にまとめて勉強をする、みたいな勉強方法はやめたほうがいいです。まとめて勉強をするとその期間はかなり集中ができて勉強が進んだ感がありますが、一回にかかる労力が大きいため、次に同じことをやろうとすると腰が重くなりがちです。3時間拘束される勉強と、30分拘束される勉強とどちらが始めやすいか、ということです。ちょっとした空き時間にやりやすいのは後者ですよね。 勉強内容を実際の業務にどう活かすかを想像する 勉強しているときに、それが 自分の業務にどうやって活かせそうかを想像 しましょう。こうすることで自分の理解が深まり、また実際の現場で活きる知識となります。AWSなどベンダー系の資格は想像しやすいです。私の場合は当時の顧客がオンプレ環境で運用していたので、それをAWSに移行するという文脈で資格勉強中に出てくるAWSのサービスを使う場面を想像していました。身の回りの人物やアプリケーション、DBをそれぞれ当てはめて考えると、具体的なイメージができるので理解がしやすいです。 資格取得戦略 合格基準との知識の差を知る 自分があとどれくらい勉強すれば合格できるのかを知ることで、スケジュールを立てやすくなったり、「まあこれくらい勉強すれば合格できるだろう」という楽観論に逃げず、現実を直視できるようになります。そのためにも 試験問題に定期的に手をつける ようにしたほうがいいです。さらにそれを何回もループすることで、自分がどれくらい理解できるようになっているかの指標になりますし、また分からなかったことが理解できるようになっていることでモチベーションにもなります。 スケジュールの立て方 私のスケジュールの立て方を紹介します。 大まかに3つのフェーズで分けます。基礎編、中間編、試験準備編です。それぞれ詳しくは後述の具体的な勉強方法の項目で紹介します。ここでは大まかな概念だけ伝えます。 基礎編 特に問題集は特に使わず、基礎的な内容の理解に努める。 問題は解かずに基礎の理解に務める。先述の通り、いきなり問題集をやってしまうと全く理解できず萎えてしまうから。 中間編 基礎と問題集を解くのを同時並行で進める。例えば平日は基礎内容の理解に努め、休日で問題集を解くなどを行なう。 このフェーズは自分との闘い。なぜなら実際の試験問題を問題集などで目の当たりにして「全然分かってないじゃん・・・」と絶望しがちなのと、そこから分からないことを一つずつ理解していくプロセスが待っているから。 したがってここが一番長く、苦しい。 試験準備編 ひたすら問題集を解く。ここまで来ると基礎的な内容は出来上がっているので、あとは本番に向けて爪を研ぐだけ。わからないところはしっかりと潰していく。 具体的な勉強方法 ここからは私が実際に行った勉強方法を載せていきたいと思います。参考にしてもらえれば幸いです。 AWS資格編 AWSの資格はほとんど Udemy というサービスを使って勉強をしました。ここではSolution Architect Professional取得時の勉強方法を例に上げて説明します。 利用したサービスや教材 Ultimate AWS Certified Solutions Architect Professional 2021 基礎講座として Practice Exam AWS Certified Solutions Architect Professional 問題集として AWSのWebサイト上のドキュメント わからないときにサービスの詳細を調べる対象として 私はAWSの資格を英語で受験しています。そのため教材が英語で提供されているものになっています。日本語であれば基礎的な問題は こちらの書籍 が良さそうで、模試をする場合は こちらのUdemyコース が良さそうに思えました。 全くの余談になりますが、ある程度英語ができる方は、AWSの資格試験を英語で受験することをおすすめします。英語で受験することによって日本語訳に惑わされなくて済むからです。さすがに試験問題なのでありえないような翻訳はないものの、訳され方によっては分かりづらいときがあります。英語の勉強にもなるし一石二鳥と思える人は英語でチャレンジしてみてもいいのではないでしょうか。その場合は上記のUdemyコースがおすすめです。2つともボリュームがそれなりにありますが、それぞれ分かりやすく解説されています。 スケジュール およそ半年程度の時間をかけて勉強しました。これについてはもっと短くても良かったと思います。理由は後述します。 最初の1〜2ヶ月(基礎編) Udemyの基礎講座をひたすら閲覧して勉強 自分に足りなさそうな分野(オンプレからの移行など)を重点的に勉強 中間の2〜3ヶ月(中間編) Udemyの基礎講座をペースを落として閲覧 最初の1〜2ヶ月でカバーしていなかったところを勉強 Udemyの問題集を解いていく 毎日5〜6問程度。ただし分からないところはしっかり調べるのでそれなりに時間がかかる 最後の1〜2ヶ月(試験準備編) Udemyの問題集を解いていく ここまで来ると2,3周はできているので、自分が理解しているかどうかの確認がメイン かなり長いスパンで勉強していたので、毎日1時間も勉強はしていませんし、最初の頃は普通にサボっていたときもあります。期間がカツカツだったら毎日しっかりやっていたと思います。 このスケジュールで勉強したところ、合格点が750点のところ 942点 を取得して合格できました。正直なところちょっと勉強しすぎたところもあるかと思います。ネットを見ていると 2週間の勉強で受かっている人もいる ので、本当はもっと短い期間でも受かるように思います。 ちなみにAWSの試験は自宅受験ができます。控えめにいってこれはものすごく良いです。わざわざ遠いところまで出かけなくていいし、何より自宅という誰にも邪魔されない環境で受験ができます(このあたりは家庭の事情があるかもしれませんが)。 IPA資格編 IPAの資格としては基本情報技術者と応用情報技術者を私は持っていますが、こちらについての勉強方法も紹介します。2つとも似たような勉強をしましたが、今回は応用情報技術者の資格取得の勉強方法を紹介します。 利用したサービスや教材 (書籍) キタミ式イラストIT塾 応用情報技術者 基礎の勉強用 (Webサイト) 応用情報技術者試験ドットコムの過去問道場 午前問題の勉強用に (書籍) 応用情報技術者 午後問題の重点対策 (重点対策シリーズ) 午後問題の勉強用に 書籍の値段はそれなりにしますが、体系立てて分かりやすく説明してあるのでおすすめです。ドットコムさんはIPA試験を受ける人は誰でも知っているサイトですね。午前問題は各設問ごとにしっかりと解説がついているので大変勉強がしやすいです。また自分のアカウントを作れば自分がどれくらい勉強をしたか履歴として見ることができ、モチベーションに繋がります。 スケジュール こちらはAWSとそこまで変わりません。基本的には基本を勉強してから問題集を解いていくという感じです。 午後試験は選択問題があるので、そこをどうするかが戦略の鍵となります。試験に出た問題を見て、簡単そうなものを選ぶことができるように、勉強範囲を広くとって準備をするという方法もあります。しかし私はあらかじめどのような問題が出るかを見ておいて、自分が解けそうでかつ勉強したいと思う分野の問題を選び、そこに限定して学習をしました。すなわち他の分野は一切学習しないので本番で難しい問題が出ても立ち向かうしかありません。しかしその分迷いを断ち切ることができ、試験中どの問題を選択するか悩まなくて良かったり、勉強範囲も限定できるという利点もあります。この方法がフィットするかは人それぞれです。自分の場合はどんな方法が良いか慎重に検討しましょう。 最初の1〜2ヶ月(基礎編) 「キタミ式イラストIT塾 応用情報技術者」で基本的なところをざっと理解する 一通り読む ドットコムで午前問題を解く 一日20問程度のペース 中間の2〜3ヶ月(中間編) 「応用情報技術者 午後問題の重点対策」で過去問を解いていく 一日1〜2問で、休日は2〜3問程度 ドットコムで午前問題を解く 一日20問程度のペース 最後の1〜2ヶ月(試験準備編) 「応用情報技術者 午後問題の重点対策」で過去問を解いていく 一日2〜3問で、休日は3〜4問程度 ドットコムで午前問題を解く 一日20問程度のペース 試験日が近づいてきてもそこまで勉強量を増やしていないのがわかります。準備期間を長く取っていたので、無理な勉強をしていた記憶はないです。 結果としては、午前が90点ちょっと、午後が60点ちょっとといったところで、午後がかなり危なかったです。今思うと、勉強が午前問題に偏りすぎていたきらいがあります。ドットコムさんの過去問道場は使い勝手がよく、出勤中など 空いた時間にさくっと勉強できるので、暇があったら過去問道場を開いていた記憶があります。 おすすめの資格 最後におすすめの資格を紹介します。 AWS Certified Solutions Architect - Professional AWS SAPは取得が難しいだけあって得られる評価も大きいです。実際、前職でも評価されました。しかしSAPで学ぶ範囲はかなり広いのですが、その分実際の業務では活かしようのないものもあります。例えば移行の分野はそれを専門にしている事業を持つ企業なら有用ですが、そうでない場合は活用の余地があまりなかったりします。とはいえAWSのことを全般的に学ぶことができるので、AWSを用いている企業に所属しているなら学んでみて損はありません。 AWS Certified Developer - Associate Developerはアプリケーションエンジニアにおすすめです。LambdaやAPI Gateway、DynamoDBなどAWSを用いて開発するうえで知っておきたいサービスがメインです。さらにCodePipelineなどを用いたCI/CD環境の構築についても学ぶことができます。私が取った資格の中で一番実務に直結したのがこれでした。当時はLambdaやDynamoDBを使ってWebシステムを開発していたので、勉強すればするほど周辺技術の理解がクリアになりました。この資格の良いところは、上位資格であるDevOpsと違って開発者向きの学習分野で固められている点です。運用面の話があまり出てこないので、アプリケーションエンジニアにとって学びやすい資格になっています。 応用情報技術者 IPA系資格はSI系のエンジニアにおすすめです。SIerは私の観測する限り、基本的に資格に対しては何らかの支援をしていることが多いです。なかでもIPA資格は今も昔もIT系でポピュラーな資格なので、まず間違いなく資格取得支援の対象となっています。昇給、昇格の条件に据えている企業もあるようです。私の前職もSIerでしたが、昇給・昇格の条件とまではいかないまでも、資格取得奨励金の対象にはなっていました。こういう環境であれば、資格取得の価値(その資格がどれくらいの勉強をしないと取れないか)を認識している人が周りに多くいるため、それだけ評価されやすいということです。 TOEIC 最後にTOEICですが、これは英語そのものの勉強という話になります。やはりエンジニアは英語できると色々と重宝されます。詳しく語りだすとキリがないのですが、例えば海外の記事から情報を得ることができたり、転職についても可能性が広がったりします。私の場合は、今の副業先が海外の企業なので英語を活用しつつ働いています。スマートキャンプ内でも英語活用の機会はあり、最近では海外の教授と打ち合わせをしたりしました(が、大した話はしていないので次回に期待しています)。一つの勉強の指針という意味でTOEIC受験をしてみるのも良いと思います。 まとめ いかがだったでしょうか。人によって資格取得のモチベーションは違いますが、前述したように、資格取得をすることによって チャンスが広がったり、思わぬところから仕事の依頼が舞い込んできたり します。 自分の可能性を広げる という意味で資格取得にチャレンジしてみてもいいのではないでしょうか。
アバター
スマートキャンプ、エンジニアの入山です。 皆さんが運営されているWebサイトには、画像が何枚使われていますか? また、その画像たちは最適化(表示領域に対して画像のサイズや画質が適切に設定)されていますか? Webページの評価基準としてCore Web Vitals(CWV)が重要視されている昨今においては、表示速度(Performance)と向き合う機会が増え、配信する画像のサイズや画質にも気を配る必要が出てきました。 しかし、Webサイト上に数多く存在する画像を自前で最適な状態に管理して配信するのは簡単ではないため、対応できていないケースも多いのではないでしょうか。 弊社も画像の最適化には長らく対応できていない状況でしたが、CDNの機能を活用することで比較的簡単に改善できました。 本記事では、CDN(Cloudflare)での画像最適化(Image Resizing)について、紹介したいと思います! CDNでの画像最適化の概要 Cloudflareでの画像最適化設定 利用条件 Image Resizing機能の有効化 Workersの設定 画像最適化の効果 画像変換の例 Lighthouseの評価 まとめ CDNでの画像最適化の概要 CDNでの画像最適化は、Webサイトの画像をCDNのエッジ上で動的に加工・変換する機能です。 CDNで元画像を動的に変換して配信することにより、画像の管理コストはそのままにマルチデバイス対応などで必要な画像の加工・変換を簡単に実現できるメリットがあります。 画像最適化の主な機能としては、以下が挙げられます。 サイズ変更 画質調整 圧縮 WebPやAVIF形式への変換 機能名称や詳細な仕様は異なりますが、CloudflareやAkamaiなど、ほとんどのCDNで提供されています。 弊社ではCDNにCloudflareを採用しているため、本記事では Cloudflareでの画像最適化の設定方法 を紹介します。 Cloudflareでの画像最適化設定 Cloudflareで画像最適化(Image Resizing)を利用するには、以下2つの方法が存在します。 各画像URLに画像最適化のパラメタを付与する方法 Cloudflare Workersで画像へのリクエストをハンドリングして一括適用する方法 1の方法は、各画像URLにパラメタを含む形式で設定を行なうため、画像ごとに最適なパラメタを指定しやすいですが、各画像URLの修正が必要となります。詳細は 公式ドキュメント を参照してください。 // 形式 https://ZONE/cdn-cgi/image/OPTIONS/SOURCE-IMAGE // サンプル(後述のWorkersと同じオプションを指定) https://example.com/cdn-cgi/image/fit=scale-down,width=400,height=300,quality=90,format=auto/image.jpg 2の方法は、Cloudflare Workersを使って設定を行ない、画像リクエストをエッジ上で処理するため、既存コードの修正なしで画像最適化を広範囲に一括適応できます。 今回は、 2の方法 で設定していきます。 利用条件 前述2の方法には、以下が必要となります。 (※ 2021/10/28時点の情報です。最新の情報は、 公式サイト を参照してください。) Proプラン($20/月)以上の契約 利用料金 Image Resizing $9/5万リクエストごと(最初の5万リクエストまでは無料) Workers $5/月(1000万リクエストまで)+ $0.5/100万リクエスト毎 以前はBusinessプラン($200/月)以上でしか使えなかったのですが、現在はProプランから利用できるようになっており、導入しやすくなっています。 Image Resizing機能の有効化 Image Resizingを利用するには、Cloudflareの管理画面から機能の有効化が必要です。 Cloudflareダッシュボードの Speed を押す 最適化 タブを押す Image Resizing 項目を有効化 機能が無効化されていたり、前項のプラン条件を満たしていない場合は、次項でWorkersを実装しても画像が最適化されないため、注意が必要です。 Workersの設定 Cloudflare Workersは、CDNの各エッジでスクリプトを実行できるサーバーレスなサービスです。 今回は、画像へのリクエストをWorkersでハンドリングして、Image Resizingで最適化したものを返す仕組みとなります。 1. Workersの作成 以下の通り遷移し、Workerを作成します。 Cloudflareダッシュボードの Workers を押す Workers項目の Workersを管理する を押す Workersタブ内の Workerを作成 を押す スクリプトを記述するコンソールが表示されるので、Image Resizingを適用するためのスクリプトを記述します。 以下は、スクリプトのサンプルです。 addEventListener( "fetch" , event => { // リクエストループ防止 if (/image-resizing/.test( event .request.headers.get( "via" ))) { return fetch( event .request) } // 適応対象画像を拡張子で判定 if ( event .request.url.match(/ \ jpg$|jpeg$|png$/i)) { return event .respondWith(handleRequest( event .request)) } else { return fetch( event .request) } } ) async function handleRequest(request) { const url = new URL(request.url) const imageRequest = new Request(url, { headers: request.headers, } ) // リクエスト元ブラウザがWebPに対応していればwebpに変換 const acceptHeader = Object .fromEntries(request.headers).accept const format = acceptHeader.match(/image \ /webp/) ? 'webp' : '' // 画像最適化のオプション指定 const options = { cf: { image: { fit: "scale-down" , width: 400, height: 300, quality: 90, format : format } } } const response = await fetch(imageRequest, options) if (response.ok || response. status == 304) { return response } else { // 画像最適化がエラーになった場合は元画像にリダイレクト return response.redirect(imageRequest, 307) } } 上記スクリプトの画像最適化オプションは、以下の代表的な項目に絞っています。 fit 画像サイズ変更時のモード width / height リサイズ後のサイズ上限(fitの指定に従って、両方のサイズが設定値に収まるまでリサイズ) quality 画質設定 format 変換画像の出力形式 この他にも数多くの画像最適化オプションが存在し、細かく指定が可能となっています。詳細は、 公式ドキュメント を参照してください。 また、ブラウザやデバイスなどで設定値を動的に変更したい場合は、 format のように options の前段で変数化し、可変にすることで柔軟に対応できます。 コンソールでスクリプトを記述後、 保存してデプロイする を押すとWorkerが作成・デプロイされます。 なお、Workerは作成・デプロイしただけでは適用されない仕様となっており、この時点で既存のWebサイトには影響ありません。 次項で適用するURL(ルート)を指定することで、初めてリクエストがWorkerで処理されるようになります。 2. Workersの適用(ルーティング) 以下の通り遷移し、Workerを実行するルートを追加します。 Cloudflareダッシュボードの Workers を押す Workers項目の ルートを追加 を押す ルート 項目には、スクリプトを実行したいURLを指定します。 アスタリスク(*)で複数URLの一括指定も可能です。以下は、ルートの設定例です。 example.com/images/* Worker 項目には、前項で作成したWorkerを指定します。 最後に 保存 を押すとルートとWorkerが紐付けされ、CDNの各エッジで画像最適化が実行される様になります。 Workerとルートの紐付けは複数設定できるため、同様に適用したいURLを追加すれば設定完了です。 画像最適化の効果 画像変換の例 今回作成したWorkerによって画像が最適化された場合、画像がどのように変換されるのかを一例としてご紹介します。 以下は弊社BOXILで実際に配信されている画像で、Webページ上では 318×254 のサイズで表示されるようになっています。 before after 画像 形式 JPEG WebP サイズ 1024×576 400×225 容量 130kB 8.7kB 318×254 の表示領域に対して 1024×576 の画像が読み込まれていましたが、Image Resizingのリサイズオプションで指定したwidthの 400 を基準に、 400×225 まで画像サイズが縮小されています。 また、画像サイズの縮小と quality:90 の画質調整によって、容量も 130kB から 8.7kB まで小さくなっており、画像形式もWebPに変換されています。 私見ですが、 318×254 で両方の画像を表示した場合でも、目視で見分けはつきませんでした。 なお、quality値の適正値を検証する際は、SSIMという画質評価指標が存在するため、そちらを利用するのが良いと思います。 Lighthouseの評価 Lighthouseの評価指標の中に Properly size images という項目があり、配信されている画像のサイズが適切であるかが評価されています。 弊社BOXILでの例ですが、画像最適化の適用により Properly size images の評価は、以下のようになりました。 before after 警告(赤色)だった評価が正常(緑色)まで改善されており、画像最適化の効果が実感できます。 画像最適化を適用した他のページでも同様の改善効果が得られました。 まとめ 今回の記事では、CDNでの画像最適化について紹介しました! Cloudflareでの設定方法を取り上げましたが、他のCDNでも同様の機能が提供されているため、一度試してみてはいかがでしょうか? 見境なく適用すると利用料金が高くなってしまう可能性があるものの、手軽に画像最適化を行なう手段の一つとして有効だと思います。 また、Cloudflareでは直近で Cloudflare Images がリリースされていたり、 Polish や Mirage といった画像最適化も提供されています。 近年ではCDNの高機能化が進んでおり、画像だけでなくコンテンツ配信に関連する機能が多く提供されています。 あらためてCDNを調べてみると、有用な発見があるのではないでしょうか。参考になれば幸いです!
アバター
こんにちは。スマートキャンプ エンジニアの中田です。 皆さんはGoのORMには何を使われていますか? 有名どころだと機能の豊富な GORM や取得データのマッピング部分だけを担うシンプルな sqlx 、 最近だとテーブル定義からモデルコードの自動生成してくれる SQLBoiler など、Goには多くのORMがあります。 筆者のORM遍歴は以下のようになってます。 Active Record(Ruby on Rails): 2年ほど GORM(Go): 半年ほど 弊社のプロダクトのバックエンドは Ruby on Rails で作られているものがほとんどです。 Ruby on Rails を利用しての開発経験が私のキャリアの大半を占めていることもあり、個人的に ActiveRecord のような機能の網羅率の高いORMには安心感を覚えます。 半年前から新規で開発を始めたプロダクトにて、新たに Go を利用し始めました。 弊社のGo製プロダクトのORMには GORM を利用しています。 GORMはドキュメントも充実しており機能自体も豊富であるため、特に事なく利用できています。 しかし、validatorが組み込みでない点や、Auto migrationで up は可能ですが down はできない点など若干の物足りなさも感じています。 そこで、本記事では GORM に取って代わる新たなORMを探るべく、 Facebook Connectivity チームより開発された、 ent というORMを調査してみます。 「ent」とは 特徴 グラフ構造 触ってみた スキーマの定義 ent/schema/user.go ent/schema/company.go ent/schema/time_mixin.go Mixin Fields Edges ent/schema/user.go ent/schema/company.go コード生成 CRUD APIを作成してみる DB接続 READ UPDATE DELETE CREATE(Transaction) 良かった点・もうひとつだった点 良かった点 自動生成機能の強力さ ドキュメントの充実度 機能の豊富さ もうひとつだった点 Schemaの管理 まとめ 「ent」とは 前述したように、 ent は Facebook Connectivity チームにより開発されているGoのORMです。 GitHubリポジトリからRelease履歴を辿ってみると v0.1.0 が2020年1月に公開されており、GoのORMの中では比較的新しい方に分類されるのではないでしょうか。 特徴 ent の特徴を 公式 より引用すると以下です。 シンプルながらもパワフルなGoのエンティティフレームワークであり、大規模なデータモデルを持つアプリケーションを容易に構築・保守できるようにします。 ・Schema As Code(コードとしてのスキーマ) - あらゆるデータベーススキーマをGoオブジェクトとしてモデル化します。 ・任意のグラフを簡単にトラバースできます - クエリや集約の実行、任意のグラフ構造の走査を容易に実行できます。 ・100%静的に型付けされた明示的なAPI - コード生成により、100%静的に型付けされた曖昧さのないAPIを提供します。 ・マルチストレージドライバ - MySQL、PostgreSQL、SQLite、Gremlinをサポートしています。 ・拡張性 - Goテンプレートを使用して簡単に拡張、カスタマイズできます。 ・Schema As Code(コードとしてのスキーマ) - あらゆるデータベーススキーマをGoオブジェクトとしてモデル化します。 ・任意のグラフを簡単にトラバースできます - クエリや集約の実行、任意のグラフ構造の走査を容易に実行できます。 ent ではスキーマファイルの定義からGoのGeneratorを利用してモデル、DBスキーマを自動で生成してくれます。自動生成したモデルには定義内容を元にクエリビルド用の汎用関数も作成され、DBへの処理実行時にはそのクエリビルド用の関数をチェーンして実現したいクエリを組み立てていきます。 ・100%静的に型付けされた明示的なAPI - コード生成により、100%静的に型付けされた曖昧さのないAPIを提供します。 Goには v1.17.X 時点でジェネリクスが入っていないこともあり、GORMなど他のORMでは interface{} を利用した抽象化でオープンに引数を受け取り、内部で型を判別するような実装が多いと思います。 ent ではスキーマ定義からモデルやフィールドごとにコードを自動生成するため、それぞれの型に合った関数を利用でき100%の静的な型付けが実現されています。 また、 ent も GORM 同様にドキュメントが充実しています。 日本語翻訳もされており、本記事の執筆にあたり ent を実際に利用してみた際に生じた困りごとはほぼほぼ 公式ドキュメント を参照すれば解決できました。 グラフ構造 ent ではグラフ構造に基づいてスキーマを定義していきます。 グラフ構造とは以下の図のようにノード(節点・頂点、点)の集合とエッジ(枝・辺、線)の集合で構成される構造のことです。このように構造化することでさまざまなオブジェクトの関連を表すことができます。 この図では参照元、参照先を表現しない無向グラフが書かれていますが、 ent の場合は紐づきを矢印で表す有向グラフで構造化されます。 グラフ(WikiPediaより) WikiPedia - グラフ理論 ent におけるノードはモデル、エッジはモデルのリレーションを指します。 初期化時点でのスキーマには一つのノードに対して、 Fields 、 Edges メソッドが生えた状態でコードが生成されます。細かな定義方法は後述しますが、ここで Fields にはモデルのフィールドを、 Edges には、モデルのリレーションを定義します。 触ってみた それでは実際に ent を触ってみます。 実装環境は以下です。 OS: macOS BigSur Go: v1.17.1 MySQL: v5.7.35 -- サンプルアプリで使用しているライブラリ -- ORM: (ent)[https://entgo.io/ent] ルーター: (chi)[https://github.com/go-chi/chi] Null値: (null)[https://github.com/guregu/null] サンプルアプリのソースコードは(ココ) https://github.com/kiki-ki/lesson-ent から参照可能です。 初めに作業用にワークスペースを切り、 ent のCLIをインストールします。 go install entgo.io/ent/cmd/ent@latest スキーマの定義 今回は以下のデータ構造で作成していきます。 companies : users = 1 : N companies --- id: bigint auto increment pk name: varchar(255) not null created_at: timestamp updated_at: timestamp --- users --- id bigint auto increment pk company_id: bigint not null name: varchar(255) not null email: varchar(255) not null unique role: enum('admin', 'normal') comment: varchar(255) nullable created_at: timestamp updated_at: timestamp --- 以下のコマンドを実行して、 User 、 Company スキーマファイルを自動生成します。 ent init User Company ent/schema ディレクトリ配下に各モデルのスキーマファイルが生成されました。 このファイルを編集していきます。 上述のデータ構造を再現するために、以下のようにスキーマを定義しました。 ent/schema/user.go package schema ...snip // User holds the schema definition for the User entity. type User struct { ent.Schema } // Mixin of the User. func (User) Mixin() []ent.Mixin { return []ent.Mixin{ TimeMixin{}, } } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.Int( "company_id" ), field.String( "name" ), field.String( "email" ).Unique(), field.Enum( "role" ).Values( "admin" , "normal" ), field.Text( "comment" ). Optional(). Nillable(). GoType(null.String{}), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.From( "company" , Company.Type). Ref( "users" ). Unique(). Required(). Field( "company_id" ), } } ent/schema/company.go package schema ...snip // Company holds the schema definition for the Company entity. type Company struct { ent.Schema } // Mixin of the Company. func (Company) Mixin() []ent.Mixin { return []ent.Mixin{ TimeMixin{}, } } // Fields of the Company. func (Company) Fields() []ent.Field { return []ent.Field{ field.String( "name" ), } } // Edges of the Company. func (Company) Edges() []ent.Edge { return []ent.Edge{ edge.To( "users" , User.Type). Annotations(entsql.Annotation{ OnDelete: entsql.Cascade, }), } } ent/schema/time_mixin.go package schema ...snip type TimeMixin struct { mixin.Schema } func (TimeMixin) Fields() []ent.Field { return []ent.Field{ field.Time( "created_at" ).Immutable().Default(time.Now), field.Time( "updated_at" ).Default(time.Now).UpdateDefault(time.Now), } } コードの説明をしていきます。 Mixin 最初に Mixin メソッドに注目してみます。 ent では汎用性の高いフィールド群を Mixin として切り出して別スキーマに注入できます。サンプルコードでは time_mixin.go に created_at 、 updated_at の2フィールドをセットで切り出し、 company 、 user の両スキーマにMixinしています。同ペアのMixinはライブラリのデフォルトでも mixin.Time として組み込まれていますが、今回はカスタムMixinで新たに定義してみました。 Fields 次に Fields メソッドに注目してみます。ここにはメソッド名の通りにモデルのフィールドを定義します。 func (User) Fields() []ent.Field { return []ent.Field{ field.Int( "company_id" ), field.String( "name" ). Validate(validation.BlackListString([] string { "hoge" , "fuga" })),, field.String( "email" ).Unique(). Match(regexp.MustCompile(validation.EmailRegex)), field.Enum( "role" ).Values( "admin" , "normal" ), field.Text( "comment" ). Optional(). SchemaType( map [ string ] string { dialect.MySQL: "text" , }). GoType(null.String{}), } } 基本的にはフィールドごとに ent 組み込みの型から任意の型を指定しそのメソッドにテーブルのカラム名を渡せば、モデルのフィールドとテーブルのカラム定義は完了です。 id フィールドはデフォルトで作成されるため記載不要です(同名のフィールドを定義すれば設定の上書きも可能)。 あとは定義した各フィールドにメソッドチェーンする形で細かい定義をしていきます。 Unique: ユニーク制約をかける Values: Enum値を設定する Optional: モデルのCreate時などにこのフィールドを任意の項目にする(デフォルトは必須) SchemaType: データベースのカラム型を独自にマッピングする(Textメソッドのデフォルトは longtext ) GoType: モデルのフィールド型を独自にマッピングする(ここではnull値を許可できる型を指定) Validate: バリデーションを適用する Validate メソッドの引数にはフィールドの型を引数に error を返す関数をアサインします。以下に使用例を示します。 また、 ent 組み込みのバリデーションも多くあり、上記のコードで利用している Must もその内の一つです。定義されたバリデーションはモデルの Save メソッドをコールしたタイミングでフックされます。 package validation ...snip func BlackListString(blackList [] string ) func (s string ) error { return func (s string ) error { isBlackList := false for _, u := range blackList { if s == u { isBlackList = true break } } if isBlackList { return fmt.Errorf( "%sは許可されない文字列です" , s) } return nil } } ent ではサンプルアプリで利用しているものの他にも多くのフィールドのオプションメソッドが用意されています。 詳しくは 公式ドキュメント をご参照ください。 Edges 最後に Edges メソッドです。冒頭でも少し触れたように ent におけるエッジはモデル間のリレーションを指します。サンプルアプリではCompany-User間でOne to Manyなリレーションを定義します。 ent/schema/user.go ...snip // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.From( "company" , Company.Type). Ref( "users" ). Unique(). Required(). Field( "company_id" ), } } ent/schema/company.go ...snip // Edges of the Company. func (Company) Edges() []ent.Edge { return []ent.Edge{ edge.To( "users" , User.Type). Annotations(entsql.Annotation{ OnDelete: entsql.Cascade, }), } } 上記のようにリレーションを定義できました。この辺りはかなりライブラリ固有な書きっぷりになっている印象です。 Fields 同様に Edges にもオプションメソッドが用意されており、それらを使って細かな設定が可能です。One to Manyの他にもOne to OneやMany to Many、自己ループなどのリレーションにも対応しています。 詳細は 公式ドキュメント をご参照ください。 ※1つ疑問だったのが、Userに定義している外部キー company_id を Required メソッドで not null なフィールドとして定義したのですが上手くいきませんでした。公式ドキュメントと同様の記述をしたつもりだったのですが...。こちら有識者の方いらっしゃればSNS, はてブコメントなどでご教授いただけると助かります。 コード生成 前置きが長くなりましたが、定義したスキーマの情報を元に以下のコマンドでコードを生成します。 go generate ./ent 実行すると ent/ 以下に大量のコードが生成されます。 CRUD APIを作成してみる DB接続 まずはDBに接続します。 ent には組み込みでAuto migration機能があるのでそちらも利用してみます。 package main ...snip func main() { entClient := database.NewEntClient() defer entClient.Close() entClient.Migrate() ...snip } ...snip --- package database ...snip type EntClient struct { *ent.Client } func NewEntClient() *EntClient { dsn := fmt.Sprintf( "%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true" , os.Getenv( "DB_USER" ), os.Getenv( "DB_PASS" ), os.Getenv( "DB_HOST" ), os.Getenv( "DB_PORT" ), os.Getenv( "DB_NAME" ), ) client, err := ent.Open(dialect.MySQL, dsn) if err != nil { panic (fmt.Sprintf( "failed openning connection to mysql: %v" , err)) } env := os.Getenv( "ENV" ) // デバッグモードを利用 if env != "staging" && env != "production" { client = client.Debug() } return &EntClient{client} } func (c *EntClient) Migrate() { err := c.Schema.Create( context.Background(), migrate.WithDropIndex( true ), migrate.WithDropColumn( true ), ) if err != nil { log.Fatalf( "failed creating schema resources: %v" , err) } } go run ./main.go を実行するとMigrate処理が走ります。以下の内容でテーブルが作成されました。 mysql> desc users; +------------+------------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+------------------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | | name | varchar(255) | NO | | NULL | | | email | varchar(255) | NO | UNI | NULL | | | role | enum('admin','normal') | NO | | NULL | | | comment | text | YES | | NULL | | | company_id | bigint(20) | YES | MUL | NULL | | +------------+------------------------+------+-----+---------+----------------+ 8 rows in set (0.00 sec) mysql> desc companies; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | | name | varchar(255) | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+ 4 rows in set (0.00 sec) 続いてCRUD処理を作成します。controllerの定義は以下のようになってます。 package controller ...snip // *database.EntClientは*ent.Clientをラップした構造体 func NewCompanyController(dbc *database.EntClient) CompanyController { return &companyController{ dbc: dbc, ctx: context.Background(), } } type CompanyController interface { Show(http.ResponseWriter, *http.Request) Update(http.ResponseWriter, *http.Request) Delete(http.ResponseWriter, *http.Request) IndexUsers(http.ResponseWriter, *http.Request) CreateWithUser(http.ResponseWriter, *http.Request) } type companyController struct { dbc *database.EntClient ctx context.Context } READ func (c *companyController) Show(w http.ResponseWriter, r *http.Request) { cId, err := strconv.Atoi(chi.URLParam(r, "companyId" )) // error handling company, err := c.dbc.Company.Get(c.ctx, cId) // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, company) } 2021/10/20 09:11:15 driver.Query: query=SELECT DISTINCT `companies`.`id`, `companies`.`created_at`, `companies`.`updated_at`, `companies`.`name` FROM `companies` WHERE `companies`.`id` = ? LIMIT 2 args=[1] Showは受け取った id で companies テーブルに検索をかけ、マッチしたレコードを取得するメソッドです。 *ent.Client(dbc) から対象のテーブルを決め( Company )、主キーでの検索用の Get メソッドでレコードを取得します。 func (c *companyController) IndexUsers(w http.ResponseWriter, r *http.Request) { cId, err := strconv.Atoi(chi.URLParam(r, "companyId" )) // error handling company, err := c.dbc.Company.Get(c.ctx, cId) // error handling users, err := company.QueryUsers().All(c.ctx) // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, users) } 2021/10/20 09:18:40 driver.Query: query=SELECT DISTINCT `companies`.`id`, `companies`.`created_at`, `companies`.`updated_at`, `companies`.`name` FROM `companies` WHERE `companies`.`id` = ? LIMIT 2 args=[1] 2021/10/20 09:18:40 driver.Query: query=SELECT DISTINCT `users`.`id`, `users`.`created_at`, `users`.`updated_at`, `users`.`company_id`, `users`.`name`, `users`.`email`, `users`.`role`, `users`.`comment` FROM `users` WHERE `company_id` = ? args=[1] IndexUsersは、まずShow同様に受け取った id で companies テーブルに検索をかけ、取得した企業に属するユーザーの一覧を返すメソッドです。 まず、 *ent.Client(dbc) から対象のテーブルを決め( Company )、主キーでの検索用の Get メソッドでレコードを企業を取得します。 その後、取得した企業モデルから QueryUsers でスキーマで設定した Users エッジに向けてクエリを実行しています。 All は全件取得です。 UPDATE func (c *companyController) Update(w http.ResponseWriter, r *http.Request) { cId, err := strconv.Atoi(chi.URLParam(r, "companyId" )) // error handling company, err := c.dbc.Company.Get(c.ctx, cId) // error handling var req request.CompanyUpdateReq err := render.DecodeJSON(r.Body, &req) // error handling company, err = company.Update().SetName(req.Name).Save(c.ctx) // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, company) } // ---------- package request ...snip type CompanyUpdateReq struct { Name string `json:"name"` } 2021/10/20 09:27:14 driver.Query: query=SELECT DISTINCT `companies`.`id`, `companies`.`created_at`, `companies`.`updated_at`, `companies`.`name` FROM `companies` WHERE `companies`.`id` = ? LIMIT 2 args=[1] 2021/10/20 09:27:14 driver.Tx(5ca9d42e-1823-4956-8427-f9937f5fb5c6): started 2021/10/20 09:27:14 Tx(5ca9d42e-1823-4956-8427-f9937f5fb5c6).Exec: query=UPDATE `companies` SET `updated_at` = ?, `name` = ? WHERE `id` = ? args=[2021-10-20 09:27:14.615562 +0900 JST m=+1007.701267213 chan2 1] 2021/10/20 09:27:14 Tx(5ca9d42e-1823-4956-8427-f9937f5fb5c6).Query: query=SELECT `id`, `created_at`, `updated_at`, `name` FROM `companies` WHERE `id` = ? args=[1] 2021/10/20 09:27:14 Tx(5ca9d42e-1823-4956-8427-f9937f5fb5c6): committed Updateは受け取った id から企業を取得し、リクエストパラメーターを元に企業情報を更新するメソッドです。 まず、先ほどと同様に企業を取得します。 取得した企業モデルから Update を呼び出して更新用のクエリビルドを行います。後述の Set~ はセッターで最後の Save でクエリを実行しています。 DELETE func (c *companyController) Delete(w http.ResponseWriter, r *http.Request) { cId, err := strconv.Atoi(chi.URLParam(r, "companyId" )) // error handling company, err := c.dbc.Company.Get(c.ctx, cId) // error handling err = c.dbc.Company.DeleteOne(company).Exec(c.ctx) // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, fmt.Sprintf( "id=%d is deleted" , cId)) } 2021/10/20 09:31:41 driver.Query: query=SELECT DISTINCT `companies`.`id`, `companies`.`created_at`, `companies`.`updated_at`, `companies`.`name` FROM `companies` WHERE `companies`.`id` = ? LIMIT 2 args=[1] 2021/10/20 09:31:41 driver.Tx(55aded72-c284-490e-8096-8226edafc3f7): started 2021/10/20 09:31:41 Tx(55aded72-c284-490e-8096-8226edafc3f7).Exec: query=DELETE FROM `companies` WHERE `companies`.`id` = ? args=[1] 2021/10/20 09:31:41 Tx(55aded72-c284-490e-8096-8226edafc3f7): committed Delteは受け取った id から企業を取得し、該当企業を削除するメソッドです。 まず、先ほどと同様に企業を取得します。 *ent.Client(dbc) から DeleteOne で削除するレコードを指定し Exec で処理を実行しています。 CREATE(Transaction) func (c *companyController) CreateWithUser(w http.ResponseWriter, r *http.Request) { var req request.CompanyCreateWithUserReq err := render.DecodeJSON(r.Body, &req) // error handling tx, err := c.dbc.Tx(c.ctx) // error handling company, err := tx.Company. Create(). SetName(req.CompanyName). Save(c.ctx) if err != nil { err = util.Rollback(tx, err) // error handling } user, err := tx.User.Create(). SetCompany(company). SetName(req.UserName). SetEmail(req.UserEmail). SetRole(user.RoleAdmin). SetComment(req.UserComment). Save(c.ctx) if err != nil { err = util.Rollback(tx, err) // error handling } err = tx.Commit() // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, map [ string ] interface {}{ "company" : company, "user" : user, }) } // ---------- package request type CompanyCreateWithUserReq struct { CompanyName string `json:"companyName"` UserName string `json:"userName"` UserEmail string `json:"userEmail"` UserComment null.String `json:"userComment"` } // ---------- package util func Rollback(tx *ent.Tx, err error ) error { if rerr := tx.Rollback(); rerr != nil { err = fmt.Errorf( "%w: %v" , err, rerr) } return err } 2021/10/20 09:37:31 driver.Tx(66b33cc7-bf03-48ec-9ec1-029298c7e6c0): started 2021/10/20 09:37:31 Tx(66b33cc7-bf03-48ec-9ec1-029298c7e6c0).Exec: query=INSERT INTO `companies` (`created_at`, `updated_at`, `name`) VALUES (?, ?, ?) args=[2021-10-20 09:37:31.187128 +0900 JST m=+9.538263311 2021-10-20 09:37:31.187128 +0900 JST m=+9.538263623 nullcorp] 2021/10/20 09:37:31 Tx(66b33cc7-bf03-48ec-9ec1-029298c7e6c0).Exec: query=INSERT INTO `users` (`created_at`, `updated_at`, `name`, `email`, `role`, `comment`, `company_id`) VALUES (?, ?, ?, ?, ?, ?, ?) args=[2021-10-20 09:37:31.188519 +0900 JST m=+9.539654394 2021-10-20 09:37:31.188521 +0900 JST m=+9.539656522 a abcde@example.com admin {{ false}} 4] 2021/10/20 09:37:31 Tx(66b33cc7-bf03-48ec-9ec1-029298c7e6c0): committed CreateWithUserは受け取ったリクエストパラメーターから企業、ユーザーを作成するメソッドです。 まず、 *ent.Client(dbc) から Tx でトランザクションを作成し、トランザクション内で行なう処理を後述しています。 作成したトランザクションから *ent.Client(dbc) と同様に対象テーブルを指定し、 Create で作成用のクエリビルドを行い、更新処理と同様に Save で処理を実行しています。 error が返ってきた場合には util.Rollback でラップしてる tx.Rollback を実行しロールバックします。 最後に tx.Commit でトランザクションをコミットします。 CRUD通してどれも自動生成されたコードから簡単にクエリビルドができました。 今回利用した関数以外にも多くの関数が自動生成により用意されるため、少々複雑なクエリもそれらの組み合わせで構築できそうでした。 良かった点・もうひとつだった点 最後に ent を利用してみて感じた、良かった点・もうひとつだった点を挙げます。 良かった点 良かったと感じた点は以下になります。 自動生成機能の強力さ ドキュメントの充実度 機能の豊富さ 自動生成機能の強力さ やはりコレが一番のメリットに感じました。Repository層いらずと言いますか、初期段階でモデルに汎用関数が一通り揃っているため、新たに自前で拵えるコードの量は最小限で済みます。 スキーマの定義も比較的直感的にできそうでした。今回は初めて触ったということもあり実装に少々手こずる箇所もありましたが、慣れてしまえば効率良く実装できそうだと感じました。 また、カスタマイズ性が低い点が自動生成における懸念点かと思いますが、 ent にはスキーマの各メソッド定義に対して豊富にオプションが取り揃えられており、一般にORM利用時にネックになりやすいケースはどれもオプションでカバーされていそうでした。 スキーマからテーブル定義、モデルの定義の両方が行えるため、相互の間に定義のズレが生じることが無い点も管理の煩雑さが減って良いです。 ドキュメントの充実度 2020年v0.1.0発と比較的若いORMでありながら、公式ドキュメントが充実しており大抵の不明点はそこで解決できそうでした。日本語翻訳のドキュメントがあるのはとっても助かります。 機能の豊富さ 本記事で紹介しきれませんでしたが、一般的なORMで利用できる(トランザクション、Eagerローディング、フック、ページング)などの機能は ent でも一通りカバーされています。 また、GORMには組み込まれていないバリデーションなどの機能も ent には組み込まれています。 弊社のプロダクトでは、Gin + GORM構成だということもありGinにパックされている validator をモデルのバリデーションにも流用しています。 ent ではそこも1パックに利用できるため、ライブラリ間の橋渡し的な実装もする必要がなく便利でした。 他にも自動生成を独自テンプレートで行なうオプションなど拡張機能も用意されているようです。この辺りは今回調査できなかったので、またあらためて触ってみたいと思います。 もうひとつだった点 もうひとつに感じた点は以下になります。 Schemaの管理 Schemaの管理 これは ent 組み込みのAuto migration機能でプロダクションスキーマの管理が可能かという点での不満点です。 弊社のプロダクトではORMにはGORMを、マイグレーションツールには goose を利用しています。 ent ではGORMとは異なりAuto migrationによるリソースの削除ができます。しかし、バージョン管理なしに本番環境でAuto migrationを実行できるかというと少々心許ない気がします。 別途マイグレーションツールを導入して、 *migrate.Schema に用意されている WriteTo メソッドで一度DDLで導入したツールのマイグレーションファイルに定義を吐き出したうえで、本番ではそちらを実行する運用がありえそうでしょうか。 WriteTo のようなメソッドも用意されており便利ではありますが、ちょっと一手間では合ったためもうひとつの点として挙げました。 まとめ いかがでしたでしょうか。 本記事ではGoのORM ent についてご紹介いたしました。普段利用していた GORM とは勝手が違う部分が多く、新感覚で楽しめました。 まだまだ新しいライブラリなので今後の発展も楽しみです。 早くデファクトスタンダードが決まって、そちらへ倒れてしまいたい。という気持ちもありつつ、あれこれと色々なツールに触れてみての楽しさもあったりとどっちつかずな思いの秋の夜長です。 最後までお読みいただきありがとうございました!
アバター
こんにちは!スマートキャンプ21卒エンジニアの関口です。私は9月にBALES CLOUDというSaaSを開発するチームに移動しました。 突然ですが皆さんはテストを書いていますか? 私は今まであまり真摯にテストを書いてきませんでした。しかし直近で開発チームを移動した際にテストについて学ぶ機会があり、心機一転しテストと向き合うようになりました。 今回の記事では私がテストに向き合う中で学んだことをまとめていきます。 テストを書くことを意識した経緯 テストの必要性 自動テストについて 自動テストの種類 自動テストのメリット・デメリット メリット デメリット 実際にテスト書いてみる テーブル定義・プロダクトコード テスト まとめ テストを書くことを意識した経緯 前述したとおり、今まで私は真摯にテストと向き合ってきませんでした。なぜなら、テストを書くよりも開発に工数を充てる方が効率的なのではないかという疑念があったからです。また、個人的にテストには苦手意識がありました。それは、テストコードの作成に時間を割かれ、タスクの工数見積もりを超過してしまうのではないかという不安からです。しかし、移動先の開発チームは、新しい機能を追加する際には、基本的にテストを書く方針で動いていました。 私はこれは良い機会だと思い、「テストよりも機能開発を優先するべきではないか」「テストに苦手意識があり見積もりの工数をオーバーしてしまうかもしれない」という個人的な思いをチームの先輩に相談してみました。 私の相談に対して、先輩は以下の資料を共有しながらテストを書くことの重要性について教えてくれました。 質とスピード 共有してもらった資料に書かれていた事項のうち、特に印象深かった内容は以下です。 品質とスピードはトレードオフではなく、品質が高まることによってプロダクトの開発スピードが上がること スピードとトレードオフなものは、メンバーの学習時間や成長機会であること また、併せて先輩からは以下のことを教えていただきました。 テストにかかる時間も考慮してタスクの見積もりをおこなっていること 自分とプロダクトの成長のためにも最初に時間はかかってもテストは書いてほしいということ 和田さんのスライドを見ながらテストの目的、重要性について説明していただき、テストを書くことに時間がかかる不安をチーム内で解消できたため、テストを書くことに対して前向きになれました。 テストの必要性 まずテストの必要性について考えます。 テストの必要性について考える際に以下の記事が参考になりました。 testing-vs-checking この記事の中で以下の記述がありました。 確認とは、既存の信念を確認したいという動機で行なうものです。 テストというのは、新しい情報を見つけたいという動機で行なうものです。 テストとは、探求、発見、調査、そして学習のプロセスです。 テストは製品を評価する目的で、あるいは想定していなかった問題を認識する目的で、製品を構成し、操作し、観察することです。 これらの記述からテストは自分たちがすでに想定している問題を確認するためだけに行なう作業ではなく、自分たちが想定していなかった問題を発見するために必要な作業という認識に変わりました。 自動テストについて ソフトウェア開発におけるテストの実施手法には大きく手動テストと自動テストがあります。手動テストとは人間が自らの手でシステムを動作させながら、画面の出力やデータベースの値の変更などの確認を行なうテスト手法のことです。もう一方の自動テストとは手動テストで行なっていた作業をシステムで自動化したテスト手法のことを指します。本記事では特に「自動テスト」について考えていきます。 *今回の記事のこの章以降で表す「テスト」は「自動テスト」のことを意味します。 自動テストの種類 自動テストを書き始めるにあたってその種類について調べました。 自動テストには単体テスト、統合テスト、システムテストが存在します。 単体テスト 原子的な単位での機能の振る舞いを確認するテスト テスト対象の単位の範囲はチームによって異なるが、基本的にはクラス、メソッドを指すことが多い テスト対象の仕様を理解できるため、ドキュメントとしての働きももつ 統合テスト 複数のモジュールを組み合わせ、データの受け渡しなどのモジュール間の連携を確認するテスト システムテスト すべてのモジュールの動作が仕様どおり機能しているかを確認するためのテスト 開発者が行なうテストの最終段階に当たるため、本番と同などの環境を用いてテストを行なうことが望ましい 自動テストのメリット・デメリット メリット 続いて自動テストのメリットについて考えていきます。テストを自動化することによって受けられる恩恵はたくさんありますが、中でも自分がメリットに感じることは以下の2つです。 手動テストに比べて作業時間が掛からない リファクタリングがしやすくなる 手動テストに比べて作業時間が掛からない 2つのテストの作業時間を比較する場合、以下の計算式が考えられます。 自動テストにかかる時間 = 自動テストの実装時間 + 仕様変更などに伴いコード修正する作業時間(管理コスト) 手動テストにかかる時間 = 手動テストの1回あたりの作業時間 × 実行回数 実行回数が多くなればなるほど、手動テストにかかる時間が増えますので、 確認する頻度が高い機能ほどテストを自動化するメリットがあると言えます。 リファクタリングがしやすくなる 自動テストがあることによって機能のリファクタリングがしやすくなります。プロダクトコードを変更した際にテストがあることで、コードが正常に機能しているかどうかを即座に確かめることができます。 また手動テストでは確認が漏れてしまうような、リファクタリングによる他機能への影響を自動テストがあることで確認できます。さらにテストコードを通じてリファクタリング対象のメソッドやクラスを使用することで、その対象が利用しやすくなっているかを確認できます。 デメリット メリットがたくさんあるように見える自動テストですが、デメリットもあります。 私がデメリットに感じることは以下のことです。 プロダクトコードの仕様変更に伴い、テストの修正、追加など一定の管理コストがかかる プロダクトコードがずっと同じ仕様ということは稀です。 プロダクトコードが変更した際にはテストも修正する必要があり、その修正のための作業時間は生まれます。プロダクトコードの変更可能性が高い箇所のテストには相応の管理コストがかかります。 管理コストをかけてでも自動化するべき箇所を見極めてテストを自動化することが大切なのではないかと考えます。 実際にテスト書いてみる 理論の説明は以上にして、実際の開発タスクのテストを書く中で学んだことをサンプルコードを用いて紹介していきます。Railsのモデルのユニットテストを事例にします。お題の機能は、「リード」というオブジェクトに1人の担当者を追加or修正する機能です。リードとは、インサイドセールスにおいて、架電やメール送信などを行なう対象を表すオブジェクトです。 テーブル定義・プロダクトコード モデル間の関係性は以下の画像のようになります。 1つのリードには1人の担当者が紐づく仕様です。 リードに担当者を追加するメソッドを以下のようにモデルに定義します。 class LeadUser def self . assign_user! (user_id, lead_id) user = User .find(user_id) lead = Lead .find(lead_id) lead_user = LeadUser .find_by( lead_id : lead.id) lead_user&.destroy lead_user = lead.build_lead_user( user_id : user.id) lead_user.save! end end テスト テストケースを作成する際は実際の処理から、何を保証するべきなのかを考え、それをテストに落とし込むことが大切です。 メソッドの処理の流れは以下になります。 担当者を追加するリードと担当者のデータを検索 すでに登録されている担当者を削除 担当者が登録されていない可能性もあり 登録したい担当者をリードに紐づける この処理の結果から保証するべきものを列挙します。 このメソッドはリードに紐づく担当者の作成を担保します。1つのリードに紐づく担当者は必ず1人になります。 指定したリードに紐づく担当者が1人存在すること 指定した担当者が指定したリードに紐付いていること 上記の条件を保証するテストを作成します。 また今回はリードに担当者がすでに登録されている場合と登録されていない場合があるので、保証すべきものは同じですが、その条件を分岐させます。 テストは以下のようになります。 RSpec .describe LeadUser , type : :model do describe ' #assign_user ' do let( :user ) do create( :user ) end let( :lead ) do create( :lead ) end subject do LeadUser .assign_user!( user.id, lead.id ) end context ' 既に担当者が存在する場合 ' do let( :current_user_id ) do create( :user ).id end let( :lead_user ) do create( :lead_user , user_id : current_user_id, lead_id : lead.id) end it ' リードに紐づく担当者の数が1件 ' do subject expect( LeadUser .where( lead_id : lead.id, user_id : user.id).length).to eq( 1 ) end it ' リクエストで送られたユーザーが担当者として登録される ' do subject expect(lead.user).to eq(user) end end context ' 担当者が存在しない場合 ' do it ' リードに紐づく担当者の数が1件 ' do subject expect( LeadUser .where( lead_id : lead.id, user_id : user.id).length).to eq( 1 ) end it ' リクエストで送られたユーザーが担当者として登録される ' do subject expect(lead.user).to eq(user) end end end end テストコードの説明をします。 前述した”保証したいこと”はそれぞれ以下のようにテストコード中で保証しています。 指定したリードに紐づく担当者が1人存在すること 指定したリードと担当者のレコードがLeadUserテーブルに1つだけ存在することを確認することで保証されます。 指定した担当者が指定したリードに紐付いていること 指定したリードに紐付けられた担当者が作成された担当者と同じことを確認することで保証されます。 テストを書き始めるにあたって一番苦戦したことはメソッドの処理から保証するべきものを列挙する作業でした。 どんな条件を満たせばメソッドの処理が保証されるのか、漏れや被りが無い条件を探すことが難しいと感じましたが、パズルを解いているような感覚で難しさの中に楽しさもありました。 まとめ 今までテストは書いたほうが良いものと思いつつ書く習慣をつけることができませんでした。しかし、開発チームを移動したことがきっかけでテストを書くべき意味、目的、楽しさを学び、テストを書く習慣をつけることができました。 これからの開発では素早く、抜け漏れが無いテストをかけるように精進していきます。
アバター
こんにちは!スマートキャンプ ソフトウェアエンジニアの中川です。 リモートワーク全盛の昨今ですが、みなさんはチームのコミュニケーションをどうされていますか? 弊社のBOXIL開発チームはこのたびメインのコミュニケーションツールをDiscordからGatherに移しましたので、今回の記事ではそのなかで得られた知見やコツなどをご紹介できればと思います! 前提・リモートワークにおけるコミュニケーションの二大方針について Discordによる同期的なコミュニケーションで起きた課題 Gatherとは Gatherによって起きたポジティブな効果 カジュアルな雑談の創出 ほどよいプライベート空間の確保 オフィスの視覚的な再現 全体 執務室エリア キャンプスペース(広間的なエリアやなんとなく集まる場) 会議室エリア 1on1エリア Gatherに足りてないこと・期待したいこと 提供されていない機能は補完できない 音声に関する機能・品質があまり高くない まとめ 前提・リモートワークにおけるコミュニケーションの二大方針について いきなり細かい話で恐縮ですが、そもそもリモートワークにおけるコミュニケーションの方針は大きく2つに分かれると思っています。 1つは同期的なコミュニケーションをメインとするやり方で、これはZoomやDiscordなどのWeb通話ツールを(ほぼ)常時つなぎっぱなしにして、お互いの"そこにいる感"を重視する方法です。 もちろんSlackのような非同期的なコミュニケーションを一切取らないわけではないですが、要件が同じ部屋につないでいる人同士で完結する場合はその場で話してしまうので口頭でのやり取りがメインになります。 同期的なコミュニケーションの"最強"は当然レイテンシの低いオフラインの直接対話なので、ツール選定の焦点はオフィスにおけるコミュニケーションをどれだけ再現できるか、というところになります。 私の観測範囲ではスタートアップなどとにかくリーンに仕事を進めていく必要のあるチームであったり、メンバーのオンボーディングを手厚く行っている組織においてこの方針が採用される傾向があります。 弊開発チームでもこちらの方針で日々仕事をしています。 対してもう一方の方針は非同期的なコミュニケーションをメインとするやり方です。 これはMTGなどの同期的なコミュニケーションを極力廃して、Slackやその他メッセージツールを使用した"非"同期的なやり取りで置き換える方法です。メンバーが各自の作業に集中できる環境を作り上げることを目的とします。 また、フルフレックスや時差によって勤務時間を合わせることが難しいチームでは必然的にこちらの方針を取ることが多いかと思います。 弊社でもBALES CLOUD開発チームがこの方針で仕事をしていた時期があります。 tech.smartcamp.co.jp Discordによる同期的なコミュニケーションで起きた課題 さて、見出しの通りですが、弊社がリモートワークに全面移行してから開発チームは同期的なコミュニケーションをメインとしてDiscordをそのツールに据えていました。 これはDiscord上にボイスチャンネルを作り、そこを定常的なたまり場として定めて特に用事が無いときはこのチャンネルに入っておくといった運用です。 実際のDiscordサーバーの様子 以前は開発チームのメンバーが4,5人だったこともあり、この運用でも特に問題は起きていなかったのですが、直近で続々と新規メンバーが加入したことによって徐々に以下のような不満の声があがってくる ようになりました。 マイクをミュートにして作業していても少なくない人数が通話に入っているのでなんとなく緊張する 特定の人に話しかけたいときに全員の耳を専有するので気が引ける オフィス(≒オフライン)だと距離に応じて他人同士の会話の声量が逓減するので心地いい"そこにいる感"があるが、Discordで同じ通話部屋に入っていると全員の声がハッキリと聞こえてしまうので違う体験になっている 私個人の意見としても、10人規模の定常的な通話部屋というのはもはや会議的な属性を孕んできてしまう気がしていました。 具体的には、「問いかけに対して誰かが喋るかもしれないから一旦待つ」であったり、「会話が発生しても3人ぐらいが話していて他の人はミュート(内職)している」ようなことが発生していて、大きな問題は無いけど気持ちよいコミュニケーションができているかと言われると...のような状態でした。 そこで、いくつかの点を解決できそうな期待感から、以前からチーム内で試してみたいと話していたGatherを試験的に使ってみることに決めました。 Gatherとは GatherはGather Presence社が提供しているWebサービスで、ドット絵RPG風のマップと一通りのWeb通話システムを組み合わせたものです。 www.gather.town Gatherでの日常的な業務風景 それぞれのユーザーは登録時に作成したアバターで同一マップ上を歩き回ることができ、マップ自体もカスタマイズが可能です。 また、通話はマップ上の距離が近い者同士で自動的につながる仕組みになっていて、マップ上の距離に応じて相手側の声量が逓減されていく仕様になっています。 (Private Spaceという範囲を設定することで、その範囲内の声量は逓減されないようにする仕組みも存在するので、会議室などはそれを活用する形になります) 料金プランに関しては無料からはじめることができ、ユーザー一人あたりの利用する時間に対して一定の金額を支払うことで参加人数の上限解放などのオプションが利用できます。 Pricing 無料枠でも25人までは参加できるので、弊チームは現在無料プランで運用しています。 公式サイトが分かりやすいので説明はこのぐらいにして、ここからはGatherを導入したことによって起きた効果についてお話します。 Gatherによって起きたポジティブな効果 大まかに分けて3点のポジティブな効果を得られました。 カジュアルな雑談の創出 1点目は業務中にカジュアルな雑談が頻繁に生まれるようになったことです。 Discordでは一箇所の通話部屋にみんなが参加しておく方式でしたが、Gatherでは特にそういったルールは設けず、マップ上の好きな場所で過ごす運用にしています。 集中したい人はそういったエリア(後述します)に行き、逆に話しかけられてもOKなタイミングでは広場的なエリアに顔を出したりすることで、現状のお気持ちを居場所によってなんとなく表現できます。 話しかけたい側にとっても、Discordにおける部屋にいるorいないといった二値よりも詳細なステータスをマップ上の居場所から汲み取ることができ、それを参考に話しかけるか否かを考えることができます。 これは「呼び出すほどでもないけどちょっと話したいな...」というときにその人の元へ行き声をかけるというオフラインの体験をかなり再現しているなと感じました。 また、そうやって発生した雑談から、さらにその様子を見ていた他のメンバーが会話に加わってきて突如ワイワイが発生する、といったことも多く、こういった偶発的なコミュニケーションを発生させられるツールはなかなか貴重なのではないかという所感です。 突発的なワイワイが発生した様子 ほどよいプライベート空間の確保 距離に応じて声量が小さくなる仕組みは先にあげたとおりですが、Gatherには他にもプライベート空間を確保できる仕組みがいくつかあります。 まずはPrivate Area機能です。 GatherのマップはMapmakerという機能でユーザーが自由にカスタマイズできるようになっており、椅子や机などのオブジェクトを置いたり、壁やタイルなどを配置することなどができます。 また、マスそのものに対して効果を付与できるTile Effectsという機能も備えており、Private Areaはそのひとつとして範囲を設定することで通話圏をその範囲に制限できます。 Tile Effectsは他にも便利な効果を備えたものが数種類用意されている このPrivate Area機能を使って、ある程度広い範囲は会議室、デスクを模した2x3マスほどの空間は集中スペース、1x1の空間はフォンブースといった区分けを実現でき、これが各人のプライベート空間の棲み分けに大きく寄与しました。 また、Gatherはシチュエーションによってマイクやカメラが自動的にミュートになる仕組みが実装されており、こういった常時接続系ツールならではの問題に関しても一歩先ん出ている印象です。 たとえば、誰とも通話していない状態でウィンドウのフォーカスがGatherから外れると、その瞬間マイクがミュートされカメラがオフにされます。 勝手にオフされるのかと当初は困惑しましたが、よくよく考えると誰とも繋がっていないのであればマイクやカメラがオンになっている必要はなく、むしろこういったツールはオンにしていることを忘れて事故ってしまいがちなので今では理に適っていると思うようになりました。もちろんこの状態でGatherを再度アクティブにすれば自動的にマイクやカメラはオンに戻ります。 また、同じようにウィンドウのフォーカスをGatherから外して作業していると、他ユーザーはそのユーザーに対して呼び出しベルが利用できるようになります。 呼び出された側にメロディが鳴る単純な仕組みですが、呼び出す側は「あの、いますか・・・。いないか」のような少し恥ずかしい体験をしなくて済みますし、呼び出される側にしても作業に集中していたら突然人間に声をかけられておののく体験が少なくなるので意外と効能が大きかったです。 本来はWebカメラが投影されるスペースが呼び出しベルに変化する オフィスの視覚的な再現 これは完全に個人の功績なのですが、あるメンバーが前述のMapmaker機能を使って弊社のオフィスを再現したマップを作成してくれました。 再現度が高いのはそうなのですが、それでいてPrivate AreaなどGatherの機能もふんだんに使われており、非常に完成度の高いマップになっています。 リモートワーク移行前からいるメンバーは「そういえばオフィスってこんな感じだったなー」と懐かしんだり、逆に移行後に入社したメンバーのなかにはむしろこれでオフィスのレイアウトを覚えたメンバーもいて、思い思いに楽しむことができています。 全体 執務室エリア キャンプスペース(広間的なエリアやなんとなく集まる場) 会議室エリア 1on1エリア ※これはGatherに用意されているデフォルトマップです Gatherに足りてないこと・期待したいこと 次にいくつか2021/10時点のGatherでは難しいこと・できないことを挙げます。 これらのデメリットを差し置いてもは弊チームとしては継続利用する判断をしていますが、もしかするとマストでできて欲しいよねという判断になるチームもあるかもしれません。 提供されていない機能は補完できない 発端としてはタイマー機能がない、ランダマイザー(メンバーをいくつかのチームにランダムで分ける機能)がないといった不満からでした。 Discordではサードパーティのbotが利用できるので、こういったなにかが足りない場合は都度botを導入することで解決してきました。 翻ってGatherにおいてはそういったサードパーティ的なアドオンを載せることはできず、公開されているWeb APIもオブジェクトの配置系に終始しており、ラインナップに乏しいです。 Gather HTTP API それぞれの不満は他のツールで代替できる小さいものですが、これらをGather上で解決したいとなったときにその手段がないことはエンジニアにとってフラストレーションが溜まる一因になるかもしれません。 音声に関する機能・品質があまり高くない これはDiscordやZoomと比べて、といった話なのでNice to have的なニュアンスなのですが、どうしても一線級のWeb通話ツールと比べてしまうと見劣りする部分がありました。 具体的には以下のようなことです。 受け手側の音量調整手段がない Discordでは他の参加者の音量を自由に調整出来る機能が実装されており、たとえばAさんの音量が小さいときはもっと大きく設定したり、突然の来客対応でミュートを忘れたBさんをミュートしたりすることができました 発話側のノイズキャンセリングがないので環境音が乗ってしまう DiscordやZoomでは備え付けで出来ていた部分だったので これに関してはたとえば Krisp のアカウントを全員に付与することで解決出来る部分だとは思っています まとめ 今回の記事ではなぜGatherに移行したか、また弊チームがGatherをどういう使い方をしているか紹介しました。 私見ですが、リモートワークにおける同期的なコミュニケーションでは今後鉄板になる可能性のあるツールだと感じたので、興味のある方はぜひ一度使ってみてください。 それではまた!
アバター
ご挨拶 2021年9月にスマートキャンプ株式会社に入社しました林です! そろそろ入社して1ヶ月が経つので、入社した経緯や入社してみて感じたことを、熱のこもった自己紹介と共に振り返っていこうと思います。 ご挨拶 これまでの経歴 学生時代から就職まで 就職からスマートキャンプへの転職まで なぜスマートキャンプを選んだか コミュニケーション能力を鍛えられそう チーム開発を通して切磋琢磨できる環境がありそう リファラルについて 入社してみて コミュニケーションはやっぱり多い 前職との違い 技術 仕事の進め方 入社後にしてたこと 抱負 まとめ これまでの経歴 初カキコ…ども… って感じで半生を振り返りながら、自由に経歴を記してみようと思います。 要約としては以下のような経歴でこれまで歩んできております。 大学を卒業 → 中小インフラSIer(2年) → 受託開発会社(10ヶ月) → 建設系スタートアップ(1年) → スマートキャンプ株式会社(現在) これだけだと、ただのジョブホッパーに見えてしまう可能性もあるのでもう少し深ぼって自己紹介をさせていただきます。 学生時代から就職まで 自分がエンジニアを始めた経緯が、後にスマートキャンプに転職する経緯にも繋がるため初めに少しお話しします。 自分がITと接点を持ったきっかけは 無料オンラインFPSゲーム でした。 中学時代に友人に誘われて始めたのですが、友人は早々にゲームに飽きたようで、結局自分だけがどっぷりとネットの世界にハマっていきました。 その熱が高じて部活も辞めてしまい、授業中もほぼ爆睡。夏休みは二週間以上家を出ないでゲームをしていたこともありました。 そうして成績も悲惨に落ちこんでしまい、 ただPCのタイピングだけ無駄に速いマン が爆誕していました。 この時点では将来への希望も乏しく、起伏もなく味気ない毎日を自堕落に過ごしていました。 その後も大きな確変を迎えられないまま、なんとか大学に入ります。 コミュニケーションに苦手意識があり、充実とは程遠い大学生活を送る間に卒業を迎えることになるのですが、 最後には就活という日本社会の現実を突きつけられ、半ば強制的に人生の岐路に立たされることになります。 仕方なく人生を振り返る中で、「ネットに人生を振り回されたんだがらITしかない」と短絡的な答えを出します。 当時の自分はプログラミングに対して「理系のエリートがやる高尚なもの」だと考えており、意識の外にありました。 しかし、当時の無知なワタシは何を思ったか「インフラなら簡単そうだしできるんじゃね?」という結論に着地します。 そうして、自分の人生を大きく揺るがせたITの世界をエンジニアとして生きていくことに決めました。 就職からスマートキャンプへの転職まで 新卒入社後はネットワークを構築・運用する業務に携わることになりました。 大学のネットワークを運用する業務に配属され、その中で直接顧客と話して技術を説明する機会がありました。 それまでの人生であまり人と関わってこなかったので、それが苦痛で仕方なく、かつ業務も全くと言っていいほどできませんでした。 最終的には顧客から「林さんNG宣言」を出されてしまい、失意の中退場することとなります。 上司からもポンコツ扱いされ、完全に自信が底を尽き、トイレで泣いてました。 その経験から、インフラエンジニアは自分が想像していた以上にコミュニケーションが必要で緻密な職種だということを思い知らされ、 この職種では生きていけないんじゃないかと思い始めました。 こうして、またも外的な要因で人生を見つめ直すこととなったのですが、全く行動に移せず、毎日YouTubeをボケ〜〜〜と見ては、仕事の愚痴を垂れ流す人生を変えられませんでした。 そんなこんなで、毎日つまらなく仕事をしていましたが、人生最大の転機が訪れました。 あるネットワーク構築案件を任され、100台程のネットワーク機器の設定を、 Tera Term というソフトを使用しマクロをプログラミングして設定を機器に流し込む作業を行っていました。 そこで初めてプログラム的なものと出会うことになります。 マクロの内容自体は簡単なものでしたが、「ロジックを考え、実装し、動してみる」事が楽しくて楽しくて、家に帰ってもその熱は収まらず、時間も忘れて夢中になっていました。 その時、「作るのがメインのプログラマーなら俺向いてるんじゃ?」と、またも思い込みを発揮し、勉強をスタートさせました。 業務中に空き時間を見つけて勉強、終業後や休日もひたすら勉強する日々でしたが、一度も辛いと思ったことはなく、「天職に出会った」と思いました。 その後、とある受託開発会社の採用担当者に「目が綺麗」という謎の理由で採用され、Webエンジニアとして働き始めます。 その会社で今の自分の支えとなっている多くの人と出会いました。その内の一人である吉永くんは将来リファラルでスマートキャンプを紹介してくれることになる人物でもありました。今までとは一変、楽しい毎日を過ごせるようになりました。 それからは、インフラからバックエンド、フロントエンドと多岐に渡り、さまざまな案件を任せていただきました。 別け隔てなく技術を習得していく中でも、特にフロントエンドの技術に惹かれ、コンポーネント設計やNode.jsを使用したバックエンドサーバーの開発など、ひたすらに技術を磨きました。新しい技術も積極的にキャップアップし、かなりスキルアップできたと思います。吉永くんにも「お前は最強や」と言ってもらえるレベルまで成長できました。 結果、フロントエンド分野において、自信を持って開発ができるようになりました。 こうして過去の自分が抱えていた「人生つまらない問題」を克服し、技術においては自信を持って開発できるようになりました。 しかし、相変わらず人と話す自信はないまま、その後も転職を重ねることになります。 長くなったのでこの辺で鳴りを潜めようと思いますが、この 「人と話す自信がない」 というコンプレックスが後にスマートキャンプに転職する理由にも繋がっていきます。 なぜスマートキャンプを選んだか かなり息切れしてしまいましたが、気を取り直して自分がスマートキャンプを選んだ理由について述べていきたいと思います。 コミュニケーション能力を鍛えられそう これが一番大きな理由だったと思います。 先述の通り「技術力はある程度ついたけど、人前で全然しゃべれないし、コミュニケーションが苦手」という意識が自分にはありました。 そんな中、吉永くんに誘われてスマートキャンプのSprintReviewの様子を見に行った際に、ここで働きたいという気持ちが強くなっていきました。 SprintReviewは毎週金曜日に実施されており、開発サイド、ビジネスサイドどちらのメンバーも参加します。 開発サイドがその週リリースした機能についての詳細を発表し、それに対してビジネスサイドの意見も交えたディスカッションが行われます。 会を通して和気あいあいとした雰囲気で進行されていき、チャットでは開発サイドの発表に対して「すごい!」なんて言葉も飛び交ったりしており、部署を跨いだグルーヴ感に感動を覚え、 この会社なら自分を変えることができそう だと思いました。 前職では新しいサービスを作っても褒められた経験はあまりなく、開発サイドとビジネスサイドは啀み合うのが世の常だと考えていた自分にとってはかなり衝撃でした。 他にもスマートキャンプには「最近のワイ」というメンバーの最近の出来事や考えていることを報告し合う会や、「SMARTCAMP Tech Talk」と呼ばれる技術的な興味関心を共有し合う会など、コミュニケーションを求められる場が多くあります。こうした機会が、自分の「できない」を「できる」に転換するための挑戦の場にできそうだと思いました。 チーム開発を通して切磋琢磨できる環境がありそう 前職は建設系スタートアップで、自分は新規サービス開発業務に携わっていました。 ほぼほぼ一人でフロントエンドを開発していたため、コードレビューを受けずにmasterブランチに直プッシュなんてのも当たり前にしていました。 しかし、将来を考えたとき、新しいモノを作ることは得意だけど、良い実装を知らない。このままでは独りよがりの開発しかできないエンジニアになってしまうと思い、 チーム開発を通して切磋琢磨できる環境に行きたいと考えるようになったのも理由の一つです。 これらの理由からスマートキャンプで働きたい!という気持ちが強くなり、選考を進める決意をしました。 リファラルについて 自分はリファラル採用でスマートキャンプに入ったので決まるまでの時間が非常に短かったのを覚えています。 全体としては3ヶ月かからなかったと思います。紹介者の信頼があることでスピード感を出せるリファラル採用は採用者・求職者の双方にメリットがある、非常に素晴らしい制度だと思いました。 全体スケジュールはこんな感じで、めちゃくちゃ早く決まりました。 - 2021年4月頃カジュアル面談 - 2021年7月上旬会社見学 - 2021年7月下旬最終面接 & 合格 - 2021年9月入社 入社してみて コミュニケーションはやっぱり多い スマートキャンプでは毎週全社で報告会があったり、週の終わりにはSprintReviewがあり、会社全体で情報を共有する文化が根付いているので、毎日多くのMTGがあります。 また、入社直後はオンボーディング施策の一環として会社の色んな人と1on2という形で雑談をする機会が多くあり、コミュニケーションを大事にするスマートキャンプの洗礼を良い意味で受けました。 前職との違い 技術 スマートキャンプのコードは綺麗で、負債と断言できる場所が個人的には少ないです。 単純な技術の流行り廃りに対しての課題はあるかと思いますが、全体的に綺麗なコードが多く、開発するうえで辛い。みたいな感情になることは今のところありません。 前職ではさまざまな人がコードを比較的自由に書いていたため、すでに辞めてしまった人が残していった負債が各所にあり苦しみましたが、そういったこともなく安心して開発できています。 仕事の進め方 前職は、1人に1案件ずつがアサインされ担当範囲は各人が責任を持って進める文化でした。 そのため、他のメンバーへの質問もしづらく担当範囲に不備があれば担当者へ責任が課されました。 一方でスマートキャンプでは、大きめのタスクはチームにアサインされ、それをメンバーで分担して進めます。 不明点があればチームメンバーへすぐに質問できるため、非常に開発がしやすいです。 実際、チームメンバーにわからないことを聞いた際には、ペアプロをして色々教えていただきました。 自分が時間をとってしまったことを謝ったところ、「こういうのもチームの成果になるから全然大丈夫」と言ってくれ、チームとして働くという事が根付いている会社だと思いました。 入社後にしてたこと 入社してからはスマートキャンプの主力サービスであるBOXILの環境構築から始まり、簡単なタスクから取り掛かりました。 今は BOXIL SaaS AWARD 2021 Autumn のLP作成を任され、徐々に本格的に開発業務をスタートさせています。 これからもっとシステム理解を深めていき、オーナーシップを持ってタスクをたくさんこなしていきたいなあと思っています。 抱負 スマートキャンプでは自分自身の成長はもちろんのこと、会社が実現したいことを深く理解し、使う人が幸せになれるサービスを作っていきたいと思います! まとめ 非常に長くなってしまいましたが、ここまで読んでいただいた方、本当にありがとうございました。 スマートキャンプは非常にコミュニケーションが活発な会社なので、中で働いている人は最初からそういう性格の人が多いんじゃないの?って印象を持つ方もおられるかと思いますが、必ずしもそうではなく、自分のようにコミュニケーションに自信のないメンバーもいます。 それでも、熱い想いを持ってスマートキャンプで頑張っている(いこう)としていることが、少しでも伝われば幸いです。
アバター
ご挨拶 はじめまして、8月に入社した永井です。リファラルによる入社以降BOXILの開発に従事しています。 一部のメンバーからは「ながい」→「長居陸上競技場」→「ヤンマースタジアム長居」→「ヤン坊マー坊天気予報」の流れで「ヤン坊」と呼ばれています。名前が原型をとどめてないですね。ちなみに雨男です。 前職は約千人規模の会社に新卒入社し、7年間社内ツールを担当する部署にて開発・運用をしていました。中でも同じ社内システムにはおよそ6年間携わっていました。その事を面接中に話すと面接官に驚かれたこともあるので、Web業界では割と珍しい部類かと思います。 今回はそこからどういった考えで転職したのかも含めてひたすら赤裸々に自分語りをするので、一つのエンジニアの体験談として転職やキャリアの参考になればいいな、と思っています。 ご挨拶 これまでの経歴 同じプロダクトチームに居続ける不安 転職活動始めてみる なぜスマートキャンプを選んだか カルチャーマッチしていると思えた 最終面接 入社してみて 自分の強みを知れた テレワークにも負けないコミュニケーション量 前職との違い 情報源が多い ツールの変化 入社して感じたギャップ この一ヶ月してたこと まとめ これまでの経歴 2014年に大阪の情報系の大学を卒業後上京してインターネット広告の企業に新卒入社をしました。1年目は3ヶ月の研修の後、配属された先で半年間単発の社内ツールの開発をしました。 そこからの2~7年目は前述の通りほぼ6年間同じシステムにて、初期の開発からローンチ、保守運用はもちろんのこと追加開発やリニューアル、体制変更などさまざまな経験をしました。もはやこのシステム≒自分の経験といっても過言ではない状態だと思います。 最初の先輩に頼っていた状態から月日が経つに連れ徐々にできることも増え、後輩もでき仕様にも詳しくなり先輩の仕事を巻き取り成長を実感しつつ日々を過ごしていました。 同じプロダクトチームに居続ける不安 移り変わりの激しい業界なので、メンバーの移り変わりも多くありました。個人のスキルアップのためや組織の変化など理由はさまざまですが、異動や退職は珍しいことではありませんでした。その中でも私は何の縁か同じチームに居続けていました。 メンバーの入れ替わりにより初期メンバーも皆いなくなり、自分しか知らない箇所が増え「長老」や「生き字引」などと呼ばれるようになった頃からとある不安を抱え始めました。それは 「Webエンジニアとして成長しているのか、このシステムに最適化しているだけか良く分からない」 というものです。 何しろローンチから在籍しているので、仕様に関した大抵の質問には答えられます。「この機能はどこで管理されてます?」「あーそこを弄ればOKっす」「ここが上手く動かなくて」「あーその負債はこのドキュメント読んでみてください」「この化石みたいなタスク何すか」「あー昔こういうことがあって……」といったやりとりを多くしました。 仕事は回ったり「やっている雰囲気」を感じることはあるのですが、月日が経つに連れ、それが 技術力によるものなのか、ただシステムについて知っているだけ なのかが自分でも良く分からなくなったのです。 そこそこ新技術は導入していましたし、勉強も業務に必要な知識ベースでしていましたが、それもエンジニアとしての力量に繋がっていたのか確信が持てませんでした。とある案件が上手くいくと「システムについて知っていたからだし?」と思い、上手く行かなかったときは「力不足かな?」と思っていました。そんな日々を続けているうちに自己効力感が底辺のエンジニアが生まれつつありました。 転職活動始めてみる 何となく先行きが不安 転職すれば変わる? でも怖いし、何だかんだ今働きやすい状態だしな 長老だし、やめてチーム大丈夫かな そのうち困る気はするんだよな でも転職こえーなーうーん みたいな堂々巡りをしていたある日、相談をした人から「永井くんってめっちゃ重たい鎧を着てるよね」と言われ、ハッとしました。長い年月同じ場所にいる間に保守的になっていき、自分で自分の足を引っ張り視野も狭めていました。自分がいなくても案外チームは回るものだし、知らないものを知らないまま悩むより、さっさと動いて体感した方が早いし後悔しないなと思い始めました。 幸いチームメンバーは優秀で、自分しか知らないようなことも無くなってきた頃でした。これは動くなら早い方がいいなと思っていた矢先に「このシステムの開発を止めるかもしれない」とのお達し。「もうこれで動かなかったら一生動かねえな」と思い、転職活動を始めました。 結果としては転職活動自体はとても体力を使うものでしたが、とても視野が広がっていくように感じたので良かったと思っています。同じような環境の人がもしいれば「転職」自体は活動の結果によるので何とも言えませんが、「転職活動」をしてみるのはおすすめです。 なぜスマートキャンプを選んだか そうして転職活動を始め、スマートキャンプに縁があって転職をすることを決めました。 理由としては技術的なチャレンジもありますし、特に会社に入るうえでの安心感が大きい割合を占めていました。 カルチャーマッチしていると思えた 最初にもお伝えしたとおり私はリファラルで、つまり既存社員の紹介がきっかけで入社をしました。 リファラルなのでカルチャーの部分は合うかも知れないという雰囲気はあったのですが、さらに選考の中でもそこをお互いに見極められるように尽力していると感じました。 選考では技術力などの能力を見るのはもちろんですが、お互いに情報をできる限り出し合って相性が良いか見極める時間でもあったと思います。なので内定をもらえたときは合っているんだなと信用できたし、仮にお祈りされたとしても納得できたと思います。そんな選考だったのが入社を決めた大きい理由の一つです。 最終面接 後は最終面接で会長とお話したときが印象的でした。話の中で何か挑戦したいことを聞かれた際に私は馬鹿正直に「いやー経歴が経歴なんで、転職先で上手いことやっていくことがもう挑戦なんですよねー」と答えました。今考えても本音なので後悔は無いにせよ なんて面接ウケしない回答なのだろう と思います。 それに対して笑いながら 「なるほど!そりゃーそうだ!」 と納得したうえでこれから会社としてやっていきたいことやそれに対してエンジニアの力がいることを話してくれました。 その面接を通じて内定をいただき、このような経歴でも必要とされることがあることを知り、同時に本音で話すことへの安心感を覚えました。自己効力感も底辺から1~2段くらいは上がったと思います。 入社してみて 自分の強みを知れた 「自分の強みとか転職活動時に考えたでしょ?」という意見ももっともなんですが、自分で考える強みとは別に 強みなんてものは時と場所で変わること に気づきました。前職ではあまり長所だと意識せずにやっていた事が強みとして活かせたのです。 例としては、前職で割とテストコード(RSpec)が好きでつらつらと書いていたのですが、たまたまBOXILチームでは自動テストに課題感を覚えていたらしく「ヤン坊さんRSpecいい感じに書けるんですね!」と手放しで褒めてもらいました。 そこからちょこちょこ相談に乗ったりレビューをしたりといった事が増え始めました。自分の「できること」と環境の「課題」が上手く噛み合い自分の「強み」に昇華した瞬間でした。前職の1つのシステムに依存してしまった自分でも、その業務を通じて得たものが他の環境でも活かせられる事が分かり、とても嬉しく思いました。「周りの足りない部分を自分の出来る事で補える」と一言で言うと単純ですが、ここに強みの正体を見たような気がしています。 逆もしかりで前職で問題無かったものが自分の弱みとして浮き彫りになることもあります。例をあげるとスマートキャンプでの業務を通じて私は自分の CSS力のポンコツぶり を知りました。今更ながら前職でCSSを書いてくれていた方々に感謝の念を伝えたいと思います。 テレワークにも負けないコミュニケーション量 初転職+テレワークという圧倒的孤独を感じそうな組み合わせでしたが、スマートキャンプではコミュニケーションの機会が多く助かりました。初対面の緊張を感じることには変わりないのですが、その機会が無いことより万倍良いのでありがたく感じました。 最近ではGatherというバーチャルオフィスを再現できるツールを導入して、気軽にコミュニケーションを取ることができるかを施策しています。 既存メンバーがフラっときて「ヤン坊さん何か困ってないですか」と疑問を解消して去っていく姿はリアルオフィスさながらです。 (※実際のGatherの画面、今自分の横で リリース作業 が行われています) コミュニケーションに関しては私の1ヶ月間に入社された井上さんの入社エントリにも詳しく書かれているのでぜひ読んでみてください。 tech.smartcamp.co.jp 前職との違い 情報源が多い エンジニアが作った機能がどのように数字に結びついていくかを身近に感じられるのことが大きな違いでした。 前職ではエンジニアの開発した機能がどう影響するか、各部署がどのような狙いで何をしているのかといった情報の量が、個人の情報収集力によって大きく異なるような印象でした。特にコロナ渦でリモートワークが始まるとさらにその印象が強まりました。 スマートキャンプでは毎週全社員に対する報告会があったり、毎月各事業部の方針を共有される機会があったりと情報を得る機会がとても多いなと感じました。 逆に現在Zoomで行われているSprint Reviewではステークホルダー以外でも参加できるし、質問もできるため開発された機能をキャッチアップしやすいのかなと思います。お披露目した機能にチャットで「嬉しいです!」なんてリアクションが来たときは開発陣もニッコリです。 ツールの変化 前職ではGitlabを使っていたのですが、スマートキャンプではGitHubを使っています。それに伴い自分も初めての業務でのGitHubを経験することになり、「草を生やすとはこういうことか……」と体感できました。 (※急に草が生え始めたエンジニアの図) 後はドキュメントにkibelaを使ったり、タスク管理にはasanaを使ったりと前職では使わなかったツールばかりなので割と新鮮な気持ちでお仕事できているなーと感じています。 入社して感じたギャップ リファラルなので、色々お話も聞いており、かつ面接後もコミュニケーションを取る場面は多くあったので、それほどギャップを感じることはありませんでした。強いて言うならば緊急事態宣言の延長によって思いの外テレワークをしていることと、思いの外自分より若い人たちが多かったことです、私は転職で長老からベテラン層になりました。 ただ私も含め最近BOXIL開発メンバーがグッと増えているため、ちょっとした過渡期を迎えているなーと感じています。入社して感じたギャップというより、自分が入ったことにより生まれたギャップです。まあよくあることだと思うので、どう動くのが良いかなーと考えつつ日々過ごしています。 この一ヶ月してたこと サービス理解などの研修を受けた後はBOXILチームでデザイン修正や一部ページの改修などの案件をこなしつつ、システムの理解を深めてできる範囲を広げていった一ヶ月でした。 ようやく慣れてきたかなという感じではあるので、次はエンジニアブログっぽい記事を書けると良いなと思っています。 まとめ ここまで読んでいただいた方、本当にありがとうございます。あらためて見ると「めっちゃ空回りしてんなコイツ」という印象を持ちましたが、「ふーん」って感じで読んでいただき、何か一つでも残るものがあれば幸いに思います。自分は空回りした経験も糧にして自分の強みを見出しつつ頑張っていく所存です。
アバター
スマートキャンプ、エンジニア井上です。 突然ですがみなさん、負荷試験はどのように実施していますか? 私はJmeterなどで負荷試験をすることがあるのですが、テスト作成からテスト実施までがとても時間がかかり継続的にやるのはかなり大変だなと感じてます。 そんなときに見つけた、負荷テストツールArtilleryについて簡単にご紹介できればと思います。 Artilleryとは Artilleryで負荷試験を実装してみる 事前時準備 まずはクイック実行 yamlからテストを実行する テストを実行する さらに高負荷にしたい Serverless-artilleryとは Serverless-artilleryで負荷試験をする利点 Serverless-artilleryを準備する AWSにデプロイする Lambdaでのテスト実行 Github Actionsで実行する まとめ Artilleryとは Artillery は yamlファイルで宣言的にシナリオを作成し、負荷をかけることができる Nodejs 製の負荷テストツールです。 Artilleryのドキュメント にも開発者の生産性が何よりも優先され、そのために簡単に記述して実行できる構造になっていると記載がある通り 複雑になりがちなテストをyamlで書けるので、簡単にかけて理解がしやすい構造が魅力の1つと感じています。 また、CircleCIやGithubActionにも簡単に組み込むことができるのも良いなと思う点です。 Artilleryで負荷試験を実装してみる では、実際にArtilleryでのテスト実行を試してみます。 事前時準備 npm i -g artillery まずはクイック実行 負荷試験でシンプルに下記のコマンドで負荷をかけることができます ※ quickはお試し用のコマンドなのでhttpのみテスト可能で、WebSocketsなどはテストできません 下記は例として20人の仮想ユーザーが5回ずつ、計100リクエストを http://example.com に投げます artillery quick --count 5 --num 20 http://example.com コマンドを実行すると下記のような実行結果が表示されRPSやLatencyが計測可能です。 All virtual users finished Summary report @ 08:15:22(+0900) 2021-09-15 Scenarios launched: 5 Scenarios completed: 5 Requests completed: 100 Mean response/sec: 10.71 Response time (msec): min: 110 max: 445 median: 230 p95: 373 p99: 417.5 Scenario counts: 0: 5 (100%) Codes: 200: 100 yamlからテストを実行する yamlで定義すると、より詳細に負荷テストのシナリオを構築可能です。 今回は例として下記のような負荷試験を行いたいと思います。 例) http://example.com に対して、10秒間に1人のユーザーで下記のようなシナリオを実行する example.comにログインする indexにGet Requestする createにPost Requestでnameを送る config: target: "http://example.com" phases: - duration: 10 arrivalRate: 1 scenarios: - flow: - log: "get index" - get: url: "/index" - log: "post create" - post: url: "/create" json: name: "test" テストを実行する $ artillery run script.yml All virtual users finished Summary report @ 09:45:20(+0000) 2021-09-13 Scenarios launched: 10 Scenarios completed: 10 Requests completed: 20 Mean response/sec: 2.11 Response time (msec): min: 0 max: 1 median: 0.5 p95: 1 p99: 1 Scenario counts: 0: 10 (100%) Codes: 200: 20 さらに高負荷にしたい さらに負荷を上げたい場合には Serverless-artillery というツールを使います。 Serverless-artilleryとは serverless-artillery はArtilleryをサーバーレス環境で実行できるツールです。 Serverless-artilleryで負荷試験をする利点 通常の負荷試験をする際には、負荷をかける側のサーバースペックが懸念点になります。 高い負荷をかけるにはそれに見合うサーバースペックが必要になります。 サーバースペックの不足により想定していただけの負荷がかからず試験失敗となるケースがあるため、負荷試験計画時にはそれを考慮する必要があります。 Serverless-artilleryを利用すると、Serverless Frameworkと組み合わせてArtilleryをAWS Lambdaで実行できるためサーバースペックを意識せずにテストが可能になり、前述の懸念が解消できます。 Serverless-artilleryを準備する Serverless-artilleryのinstall $ npm i -g serverless@1.83.3 $ npm i -g serverless-artillery ※2021/09時点では2系のserverlessではエラーになるため1系をinstallしています。 AWSにデプロイする $ slsart configure $ slsart deploy Deploying function... Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service serverless-artillery-test.zip file to S3 (16.85 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... Lambdaでのテスト実行 AWSへのdeployが完了したら先程つくったテストファイルでテストを実行してみます。 $ slsart invoke Invoking test Lambda { "timestamp": "2021-09-13T09:27:44.639Z", "scenariosCreated": 10, "scenariosCompleted": 0, "requestsCompleted": 0, "latency": { "min": null, "max": null, "median": null, "p95": null, "p99": null }, "rps": { "count": 10, "mean": 1.06 }, "scenarioDuration": { "min": null, "max": null, "median": null, "p95": null, "p99": null }, "scenarioCounts": { "0": 10 }, "errors": { "ENOTFOUND": 10 }, "codes": {}, "matches": 0, "customStats": {}, "phases": [ { "duration": 10, "arrivalRate": 1 } ] } Your function invocation has completed. { "timestamp": "2021-09-13T09:27:44.639Z", "scenariosCreated": 10, "scenariosCompleted": 0, "requestsCompleted": 0, "latency": { "min": null, "max": null, "median": null, "p95": null, "p99": null }, "rps": { "count": 10, "mean": 1.06 }, "scenarioDuration": { "min": null, "max": null, "median": null, "p95": null, "p99": null }, "scenarioCounts": { "0": 10 }, "errors": { "ENOTFOUND": 10 }, "codes": {}, "matches": 0, "customStats": {}, "phases": [ { "duration": 10, "arrivalRate": 1 } ] } Github Actionsで実行する GithubActionsで Serverless-artilleryを実行してみます。 やることはシンプルで事前にslsart deployを実行し環境を構築していれば テストを実行するだけになるので下記の設定ファイルで実行できます。 name: serverless-rtillery sample on: push: branches: - main tags: - "!*" jobs: serverless-artillery-sample: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v1 - name: setup Node uses: actions/setup-node@v1 with: node-version: 12.x - name: build run: | npm i -g artillery npm i -g serverless@1.83.3 npm i -g serverless-artillery - name: run test env: AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} run: | slsart invoke 実行すると下記のようになります。 まとめ いかがでしたでしょうか 負荷試験はテスト自体の設計だけでなく、準備のコストも高いため継続的に実施が難しいですが、Serverless-artilleryのようにサーバレスで構築されたものであればリソースが足りず負荷が想定よりかけれてないなどの問題もなくなるのでテストの設計だけに集中できそうで良いなと思いました。
アバター
みなさん、 WebAssembly 聞いたことありますよね? スマートキャンプでエンジニアをしている瀧川です。 私が初めてWebAssemblyを目にしたのは確か2018年、VimをWebAssemblyに移植してブラウザで動くようにしたという記事だったかなと思います。 https://github.com/rhysd/vim.wasm 当時は「はー、なんだか未来を感じるけど、どう使われてくんだろう」くらいな認識で、最近までほとんど注目していませんでした。 しかし、少し前に ffmpeg.wasm についての記事がバズっているのを見かけたときビビっときましたね。 ブラウザ上でffmpegが動かせる のはWebアプリケーションを作る上で可能性が広がりますし、何よりWebAssemblyのポテンシャルが活かされていると感じました。 そこで今回、WebAssemblyの世界観を味わうために、 代表的なWebAssemblyで使われている言語をピックアップ して試してみようと思います。 その中でWebAssembly自体のメリット、各言語でWebAssemblyをやることの是非を書いていければと思います! (今後プロダクトでも利用していく可能性は大いにあるので、連載で継続して調べていきたいですね) WebAssemblyとは 実装してみる 参考実装(JavaScript) AssemblyScript Rust C++ Go TinyGo 各言語ファイルサイズ比較 まとめ WebAssemblyとは 公式サイト より WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications. Wasmとも略されますが、簡単に説明すると、WebAssemblyとはプログラミング言語をコンパイルすることで生成する ブラウザ上で実行可能なバイナリフォーマット になります。 2015年に公表され、2017年に主要なモダンブラウザで対応されることとなりました。 主なメリットは以下が挙げられるかと思います。 JavaScriptの実行に比べ、構文などの解析を伴わないため高速 コンパイル後のコードはネイティブに近い性能で高速 JavaScriptに比べ、WebAssemblyのフォーマットはコンパクトなので読み込みが高速 ※ 各言語の実装でも触れますが、JavaScriptとWebAssemblyのグルーコードなども存在するため一概には軽量とは言えないかもしれません JavaScript以外の多くの言語で実装可能 JavaScript以外の言語で実装された既存ライブラリなどを移植可能 etc... 対応言語はかなり幅広く、なんとRubyでも実現はできるようです。 https://github.com/appcypher/awesome-wasm-langs ただWebAssemblyの強みを活かす観点ではこちらにまとめられている通り、 Rust、AssemblyScript(TypeScript)、C++、Go あたりの採用が多くなっているようです。 https://blog.scottlogic.com/2021/06/21/state-of-wasm.html 国内での採用事例はそこまで多くみかけないですが、グローバルに目を向けると Google Meetの背景ぼかし なんかは身近に感じられるかと思います。 https://zenn.dev/kounoike/articles/google-meet-bg-features 実装してみる さて今回は先程の利用事例数を参考に、上位の以下の言語でWebAssemblyを利用してみようと思います。 AssemblyScript Rust C++ Go まずはJavaScriptでの参考実装を示し、それを各言語で実装&WebAssembly化して所感などまとめていきます! それぞれの言語について、ほぼ触ったことがなかったので、細かい実装についてはご容赦いただき、よりよいやり方等あればご教示お願いします 🙏 参考実装(JavaScript) 実装するお題として以下のような エラトステネスの篩 のナイーブな実装を使っていこうと思います。 sieve.js function sieve(maxCount) { const max = maxCount + 1; const primes = new Array (max); const result = new Array (); for ( let i = 1; i < max; ++i) { primes [ i ] = true ; } for ( let i = 2; i < max; ++i) { if (primes [ i ] ) { result.push(i); for ( let j = i; j * i <= max; ++j) { primes [ j * i ] = false ; } } } return result; } index.html(抜粋) < body > < script src = "sieve.js" ></ script > < script > const n = 1_000; const result = sieve ( n ) ; console.log ( result ) ; </ script > </ body > AssemblyScript まずは一番導入ハードルが低そうなイメージがあったAssemblyScriptから試していきましょう。 AssemblyScript は、あまり聞き馴染みないかもしれませんが、WebAssemblyのために作られた TypeScriptのシンタックスを持つ言語 になります。 なにはともあれ、AssemblyScriptをWebAssemblyにコンパイルする環境を整備しましょう。 $ cd your_assemblyscript_wasm_dir $ volta install assemblyscript # npm install -g assemblyscript $ asinit . # 雛形の生成 $ npm install $ ls asconfig.json assembly/ build/ index.js package.json tests/ $ npm run asbuild # wasmファイルの生成(Optimize版含め) $ ls ./build # .wat はWebAssemblyTextでデバッグ時に人が見る用途 optimized.wasm optimized.wat untouched.wasm.map optimized.wasm.map untouched.wasm untouched.wat これでAssemblyScriptを開発、コンパイルする準備は完了です。 続いてAssemblyScriptでお題のエラトステネスの篩を書くとこのようになります。 assembly/index.ts export function sieve ( maxCount: i32 ) : Array < i32 > { const max = maxCount + 1 const primes = new Array < bool >( max ) const result = new Array < i32 >() for ( let i = 1 ; i < max ; ++ i ) { primes [ i ] = true } for ( let i = 2 ; i < max ; ++ i ) { if ( primes [ i ] ) { result.push ( i ) for ( let j = i ; j * i <= max ; ++ j ) { primes [ j*i ] = false } } } return result } シンタックスについては完全にTypeScriptですね。 i32 については、 こちらのドキュメント にあるように、より低レベルの制御をするためにJavaScriptとは違った型を使うこととなっています。 次にコンパイルされたWasmファイルを呼び出すコードを見てみましょう。 index.html(抜粋) < body > < script src = "https://cdn.jsdelivr.net/npm/@assemblyscript/loader/umd/index.js" ></ script > < script > ( async () => { const imports = { env: { abort () { throw new Error ( "Abort called from wasm file" ) ; } } } let instance; if ( !loader.instantiateStreaming ) { const response = await fetch ( 'build/optimized.wasm' ) ; const bytes = await response?.arrayBuffer () ; instance = await loader.instantiate ( bytes, imports ) ; } else { instance = await loader.instantiateStreaming ( fetch ( 'build/optimized.wasm' ) , imports ) ; } const { sieve } = instance.exports; const { __getArray } = instance.exports; const n = 1_000; const arrPtr = sieve ( n ) ; const values = __getArray ( arrPtr ) ; console.log ( values ) ; } )() ; </ script > </ body > 少し複雑ですね。 ここでWebAssemblyで重要な問題の一つを説明します。 それは、 通常WebAssemblyとJavaScriptとのやり取りでは数値型しか使えない ということです。 仮に標準で実装されている WebAssemblyオブジェクト をそのまま使って今回の sieve 関数を読んだとしても、 配列のサイズのみしか取得できない といったことが起きてしまいます。 これはどの言語でコンパイルしても起こりうる問題で、それぞれ解決する手段があると覚えておいたほうがよさそうです。 AssemblyScriptだと公式で用意している loader を使います。 使い方としては loader を使い loader.instantiate または loader.instantiateStreaming を使いWasmファイルを読み込みます。 読み込みと instance.exports に自身が定義した関数がはえてきます。 それと同時に __getArray や __newArray といったヘルパー関数も取得できるようになっているので、それらを使うことでWebAssemblyからArrayを返すことを実現しています。 loader のドキュメントを読んでいただければ分かる通り、stringなども同様のアプローチで解消することとなります。 少し癖はありますが、そこそこ簡単に実装することができました! Rust 次に一番WebAssemblyで使われているというRustを試してみます。 まずは ドキュメント を読みつつ環境を整えましょう。 # Rustをインストール(rustup, rustc, cargo) $ cd your_rust_wasm_dir $ cargo install wasm-pack # Wasmへのコンパイルをはじめ、グルーコードの生成なども担う $ cargo init . Cargo.toml [package] name = "sieve" version = "0.1.0" [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" コードはこのようになります。 src/lib.rs extern crate wasm_bindgen ; use wasm_bindgen :: prelude :: * ; #[wasm_bindgen] pub fn sieve (max_count: usize ) -> Vec < usize > { let max = max_count + 1 ; let mut primes: Vec < bool > = Vec :: with_capacity (max); let mut result: Vec < usize > = vec! {}; for _ in 1 .. = max { primes. push ( true ); } for i in 2 ..max { if primes[i] { result. push (i); let mut j = i; while j * i < max { primes[j * i] = false ; j += 1 ; } } } return result; } 注目すべきは wasm_bindgen を読み込んでいるのと、エクスポートしたい関数の前行に #[wasm_bindgen] と記述していることくらいかなと思います。 次にコンパイルしてみましょう。 $ wasm-pack build --target web $ ls ./pkg package.json sieve.d.ts sieve.js sieve_bg.wasm sieve_bg.wasm.d.ts 型定義ファイルなんかも生成されていますね。 呼び出すコードは以下になります。 index.html(抜粋) < body > < script type = "module" > import init, { sieve } from '/pkg/sieve.js' ; ( async () => { await init () ; const n = 1_000; const result = sieve ( n ) ; console.log ( result ) ; } )() ; </ script > </ body > とても簡潔に書けました。 wasm-pack と wasm_bindgen がよしなにグルーコードを生成してくれているおかげだと思いますが、エコシステムの成熟度からしてもコミュニティの熱量を感じます。 C++ 次はC++です。 個人的にOpenCV×WebAssemblyでなにか作りたいなと考えていたので、C++は注目していました。 C/C++のWebAssemblyへのコンパイルでは、 Emscripten を用いることが多いようです。 Emscriptenは、C/C++を始めとするLLVMを使用する言語をWebAssemblyにコンパイルするツールとなっています。 さてコンパイル環境の整備ですが、便利なDocker Imageが公式で用意されていたのでそちらを使っていこうと思います。 https://hub.docker.com/r/emscripten/emsdk それではコードです。 sieve.cpp #include <emscripten/emscripten.h> #include <emscripten/bind.h> #include <cstdlib> #include <vector> EMSCRIPTEN_KEEPALIVE std :: vector < int > sieve ( int maxCount) { auto max = maxCount + 1 ; auto primes = std :: vector < bool >(max); auto result = std :: vector < int >{}; for ( auto i = 1 ; i < max; i++) { primes[i] = true ; } for ( auto i = 2 ; i < max; i++) { if (primes[i]) { result. push_back (i); for ( auto j = i; j*i <= max; j++) { primes[j*i] = false ; } } } return result; } EMSCRIPTEN_BINDINGS (module) { emscripten:: function ( "sieve" , &sieve); emscripten::register_vector< int >( "vector<int>" ); } いくつかポイントがあります。 1つ目は EMSCRIPTEN_KEEPALIVE についてです。 Emscriptenでコンパイルをする際にデフォルトの挙動だと、 main関数以外の呼び出されていない関数は消されてしまう ようです(dead code elimination)。 それを回避する方法の一つがこれになります。 その他の方法もあり、以下の記事がとても参考になりました。 https://qiita.com/chikoski/items/462b34db61daf13a7897 2つ目は EMSCRIPTEN_BINDINGS についてです。 AssemblyScriptの項目でも説明しましたが、通常WebAssemblyとJavaScriptとのやり取りは数値型しかできません。 それを解消するための仕組みが Embind になります。 この機能を使い、C++のvectorをEmscriptenが用意しているJavaScript互換のregister_vectorにBindする設定をしています。 それではコンパイルし、呼び出してみましょう。 $ docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) emscripten/emsdk emcc sieve.cpp --bind -o sieve.js # Embindを使う場合、--bindが必要 index.html(抜粋) < body > < script src = "sieve.js" ></ script > < script > Module.onRuntimeInitialized = () => { const n = 1_000; const result = Module.sieve ( n ) ; // JavascriptのArrayではなくC++のvector互換 for ( let i = 0 ; i < result.size () ; i++ ) { console.log ( result.get ( i )) } } </ script > </ body > 呼び出すコードはRust同様簡潔で良いですね。 一つ特徴的なのはEmbindしたregister_vectorで、JavaScriptのArrayになるわけではなく、C++のvectorと同じAPIになっています。 完成形としては簡潔ですが、C++でのWebAssembly実現は結構苦戦しました。 その要因は、dead code eliminationの回避方法がいくつかあるという話とも繋がりますが、様々なやり方が散見していてどの組み合わせが正しく動作するのかわかりにくいことです。 例えばコンパイル時のコマンドのオプションについて以下のように呼び出す方法が紹介されているものも多かったですが、うまく動かすことができませんでした。 https://developer.mozilla.org/ja/docs/WebAssembly/C_to_wasm $ emcc sieve.cpp --bind -o sieve.js -s WASM=1 -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" Go 最後にGo言語を試していきます。 Go1.11から正式にWebAssemblyサポートが導入されているようです。 そのためWebAssemblyへのコンパイルは非常に簡単で、以下のようなコマンドによりコンパイルすることができます。 $ GOOS=js GOARCH=wasm go build -o main.wasm しかし、GoはRuntimeが大きくRustやC++と比べると、WebAssemblyに向いていないという話をよく目にします。 そこでよく使われるのがTinyGoです。 TinyGo https://tinygo.org/ あまり詳しく把握できてはいませんが、GoのRuntimeからGCやgoroutineなど不要なものを除き、LLVMベースで新しく作られたコンパイラのようです。 Goの言語としての差はほぼありませんが、Wasmのグルーコードの生成などはこちらのほうが進んでいるみたいです(Export周り)。 こちらもEmscripten同様、公式でDocker Imageを用意してくれているので、そちらを使いコンパイルしていきます。 まずはコードです。 sieve.go package main //export sieve func sieve(maxCount int ) [] int { max := maxCount + 1 primes := make ([] bool , max) result := [] int {} for i := 1 ; i < max; i++ { primes[i] = true } for i := 2 ; i < max; i++ { if primes[i] { result = append (result, i) for j := i; j*i <= max; j++ { primes[j*i] = false } } } return result } func main() { fmt.Println( "loaded!" ) } 大切なことは2つです。 1つ目は //export 関数名 です。 これはTinyGoの機能で、このコメントが書いてある関数がJavaScriptで呼び出せるようになります( // export 関数名 にしていてしばらくハマりました...) 2つ目はmain関数についてで、main関数はコンパイル時に必須となっているため、特にWasmロード時に必要なくとも書いておくようにしましょう。 それではコンパイルと呼び出しです。 docker run --rm -v $(pwd):/src tinygo/tinygo:0.19.0 tinygo build -o /src/sieve.wasm -target=wasm ./src/sieve.go < body > < script src = "wasm_exec.js" ></ script > < script > ( async () => { const go = new Go () ; const result = await WebAssembly.instantiateStreaming ( fetch ( "./sieve.wasm" ) , go.importObject ) ; const instance = result.instance await go.run ( instance ) ; const n = 1_000; const result = instance.exports.sieve (100) ; console.log ( result ) ; // undefinedになってしまう... } )() ; </ script > </ body > 呼び出し時に使うグルーコードとして、 wasm_exec.js を読み込んでいます。 これはGoやTinyGoのインストールされたディレクトリから取得、またはリポジトリから取得する方法があります(少し不安になりますね)。 コンパイルをしたバージョンと同一の wasm_exec.js を取得するほうがいいので、リポジトリから取るよりインストールディレクトリから取得するほうがいいような気はします。 # リポジトリから取得する場合 $ wget https://raw.githubusercontent.com/tinygo-org/tinygo/master/targets/wasm_exec.js # Dockerでコンパイルした場合 $ docker run -v $(pwd):/src tinygo/tinygo:0.19.0 /bin/bash -c "cp /usr/local/tinygo/targets/wasm_exec.js /src" これでTinyGoでの実装も完了...と言いたいところですが、まだこの実装ではArrayの取得がundefinedになってしまいます。 実はこの問題、今回調べた限り回避することはできませんでした... 追々分かれば追記していこうと思います。 (情報をお持ちの方はご一報ください) 各言語ファイルサイズ比較 月並みではありますが、それぞれの実装言語で読み込まれるファイルサイズを比較してみます。 今回Optimizedなバージョンも取り上げていますが、OptimizeLevelなどはデフォルト値を使っているのと、コンパイラによってOptimizeの内容は変わると思うので、この比較だけでは判断できないことはご認識ください。 またOptimizeについては、以下のリンクにあるように、ファイルサイズと実行スピードはトレードオフの場合もあるようなので、気をつけて設定する必要がありそうです。 https://rustwasm.github.io/book/reference/code-size.html#tell-llvm-to-optimize-for-size-instead-of-speed JavaScript AssemblyScript AssemblyScript(Optimized) Rust C++ C++(Optimized) TinyGo TinyGo(Optimized) Wasm File 0 12.3kB 7.3kB 14.8kB 46.5kB 20.1kB 2.0MB 223kB Other Files(Glue,etc) 559B 5.5kB(loader) 5.5kB(loader) 3.0kB(Glue) 206kB(Glue) 57.6kB 15.9kB(wasm_exec.js) 15.9kB(wasm_exec.js) Goはランタイムが大きく、WebAssemblyには向いていないとはよく聞きますが、TinyGoでもまだ比較するとサイズは大きいみたいですね。 まとめ 以上、各言語での実装はいかがでしたでしょうか? AssemblyScriptは今回調べるまで知らなかったのですが、やはりTypeScriptで書けるというのはフロントエンドエンジニアとしてハードルが低いので導入は進めやすいと感じました。 また、流石よく使われているだけあって、Rustがエコシステムの成熟度が高く、一つ頭抜けている印象を受けました。 個人的にはGoが好きなので、期待はしているのですが...。 全体通しての感想ですが、やはりまだまだWebAssemblyは枯れていないため、正しい情報を得て実装していく難易度の高さを感じました。 ただ、このペースでアップデートが進んでいくと、早いタイミングでWebの必須技術になる可能性は大いにあるなとも感じましたね。 今回は初めて触ることが多く、かなり軽いプログラムで試してみましたが、次回は例えば外部のライブラリを含めたビルドや、JSとのやり取りなどにフォーカスして調べ、よりメリットを感じられるようなことをしていきたいですね!
アバター
こんにちは!!!スマートキャンプでエンジニアをしている吉永です! 自己紹介記事はこちら 前回の記事はこちら 私は現在、スマートキャンプの主力サービスであるBOXILの開発にフロントエンド、バックエンド問わず携わっています。 私が入社した去年の8月からしばらくは週一で出社していましたが、今年にかけてはコロナの状況が悪化していたためほぼフルリモートの環境でやり取りをすることが増えました。 BOXIL開発部の特徴として、社内のプロダクトの中でもステークホルダーとなる人物・部署が多く、開発をするにあたってエンジニア以外の社内メンバーとのやり取りが多く発生することがあります。 私は以前から部署間のコミュニケーション改善について興味を持っており、入社当初に弊社で取っているコミュニケーション施策の数々に感銘を受け、一年前のテックブログでもそのような内容の記事を書いています。 tech.smartcamp.co.jp はじめに 完成形が返されることによるデメリット 勿体無いコミュニケーションの弊害 弊社で行っている施策 タスクの依頼フローの整備 タスク開始後の相談・報告チャンネル 着手中のタスクのデザイン・仕様に関する質問・報告チャンネル リリース後の報告チャンネル まとめ はじめに 先日、いつものようにTwitterを眺めていると、このツイートがバズっているのを目にしました プログラマにすごく気を遣うのが「◯◯ってできますかね?」みたいに質問した際に、しばらくして「実装しました」と実装して返事が返ってくる事が経験上多くて。 — ぽこぺん (@pokopen_cg) 2021年8月12日 このツイートには「できるかどうかの調査を進めた結果、完成形に近くなってしまっていることがよくある」といった依頼された側の目線に立ったリプライや、「目的や意思がうまく伝わっていないのではないか」というようなコミュニケーションのあり方について言及するようなリプライがついており、エンジニア、非エンジニア問わず多くの方から意見が寄せられていました。 コロナ禍でリモートワークを取り入れた会社などでも、上記のような事例にかかわらず他の部署が関わるやり取りでのコミュニケーションがより複雑になってしまっているのではないかと思います。 今回の記事では一作目の執筆から一年が経ち、まだオフラインのコミュニケーションも視野に入っていた頃からフルリモートに変わった時に、弊社で取り入れた「なるべく気を使わない」ためのコミュニケーション施策を紹介したいと思います。 完成形が返されることによるデメリット まず、上記ツイートのようなコミュニケーションにおける問題点について考えてみました。 上記のツイートにある「できますか?」→「実装しました」のフローにおいて、エンジニアの立場からすると先述のような「調査がてら実装していたらそれが完成形に近いものだった」というのはよくある話かと思います。 エンジニアからすれば「作ってみないとわからない」といった状況だったり、「仮で実装した結果を踏まえて、再度プランニングをする」と言った考えもあるのではないでしょうか。 しかし、ツイートにもある通り本来依頼側がやって欲しかったのは実現可能かの調査であり、結果的に実装されてしまった物に対して負い目を感じてしまったり、気を遣ってしまうのは双方にとって勿体無いコミュニケーションのありかただと感じました。 なので、上記ツイートに対して私が感じた問題はエンジニアが調査依頼に対して結果的に実装を終えてしまった部分ではなく、 実装をしてしまったことに周りが驚いてしまっている、更にそれに対して依頼者が気を遣ってしまう結果になっている ことだと感じました。 勿体無いコミュニケーションの弊害 お互いの意図が伝わらないまま進んでしまい、勿体無いコミュニケーションが続いてしまうと 本来はカジュアルに進められるやり取りに気を使ってしまう 不要な手戻りが発生してしまう恐れがある 依頼者と実装者がお互いに何を考えているかを把握できない など本来懸念しなくていいリスクを負う可能性があると考えました。 また、私がこの件に関してブログを書きたいと弊社Slackに投稿したところ、実際にデザイナー・企画をやっている方から以下のような意見をいただきました。 実装できる場合に考慮したいことや、追加でやりたいことなど細かいコミュニケーションが疎かになってしまうことにより、後出しで出てくる要望に対するエンジニア側の混乱などもありえない話ではないのかなと感じました。 弊社で行っている施策 ここからは、上記の考えを踏まえた上で弊社で実際に取られている依頼から実装までの流れや、お互いに気を遣うことを減らすために最近始めた新しいコミュニケーション施策を紹介します。 タスクの依頼フローの整備 まず紹介するのは弊社メンバーが開発チームに依頼する時に制定しているフローについてです。 図解すると基本は以下のようになっています。 他部署からの依頼は、基本的にProduct Requestという名前のSlackチャンネルに投稿され、規模が大きいものは翌日に各チーム(エンジニア、デザイナー、企画)のリーダー陣が揃った会議で揉まれます。 そして、Asanaというタスク管理ツールのTodoに積まれ、スクラム開発の手法に則った方法で、メンバー全員の稼働時間に合わせたポイント分の課題を次のスプリントのタスクとして積んでいくことになります。 この時、リーダーが実装者にタスクの説明や、実装方針の設計をするプランニングという作業が行われますが、弊社ではこのプランニング時に 出来るだけ細かい単位でのサブタスクの切り出し 参照する可能性が高いファイルや関数の洗い出し 説明された上で、煮詰まり切っていないと感じた考慮事項などの洗い出し などを行います。こうすることにより、実装時の関連ファイルの調査時間の短縮や、考慮漏れのカバー、実装時に着手しているタスクを他メンバーに引き継ぐ際に、ある程度の粒度で引き継げるなどの恩恵があります。 タスク開始後の相談・報告チャンネル タスク開始後、エンジニアが外部とのやり取りに使うチャンネルは主に3つあります。 1つ目は、先ほども紹介した Product Request という部屋です。 この部屋の役割は、多部署メンバーからの質問や、気がついたバグの共有、追加機能の依頼などを集約する目的の部屋になっており、タスクがはじまった後、そこにリクエストを書き込んだ対応者と直接やりとりができる部屋になっています。 上記の例ではバグ修正になっていますが、依頼に対しての実装可否や、機能追加などの際には「この動作をしたときはこうなって良いですか?」といった質問のやりとりをスレッド上ですることになります。 対応時期の相談などもここで行われるため、「あの時依頼したもの、いつ対応してくれるんだろう...」と言ったような不安も解消することができます。 逆に、急ぎではないけどいつか対応してほしいといったような温度感が低い依頼なども来るため、納期や工数と相談しながらスケジュールを組むことができます。 また、全ての投稿に必ずBOXILチームに所属するエンジニアや企画メンバーが答えてくれるため、チャンネルの乱立や「この会話ってどこでしてたっけ...」といったリスクも防ぎつつ、カジュアルな相談をしたり、お互いの部署の考えや施策を共有する場としても一役買っています。 着手中のタスクのデザイン・仕様に関する質問・報告チャンネル 上記のProduct Request部屋とは別に、着手中のタスクのデザインや仕様に関する質問をする boxil_dev_困 という名前の部屋があります。 この部屋では、主にタスクを進める上で疑問に感じたことや、調査タスクの結果の報告、追加の要望などをリーダー陣や企画(デザイン)メンバーにヒアリングできる部屋になっています。 上の画像のタスクは、とある外部サービスをBOXILに埋め込むことができるのかという文脈から発生した物でした。 埋め込めるかどうかを調査した上で、可能だった場合は既に上がっているデザインのもので実装してほしいといった内容のものでしたが、実際に埋め込んでみると以下のような問題がありました。 外部サービスの縦幅が想定より長い 長かった結果下に大幅なスペースができてしまった デザインに使い道のわからないボタンが載っていた その為、調査の結果埋め込めることを伝えた上で、上記の問題を細かく報告し、工数に見合った追加対応ができるかどうかを話し合いました。 こうした細かいコミュニケーションを繰り返すことにより、出来上がった物に対する認識の齟齬や、遠慮せずにまずは提案してみる空気感を作ることができています。 リリース後の報告チャンネル BOXILチームではリリースした内容を関係する各部署のメンバーに共有する部屋があります。 リリースしたコンテンツは、毎週金曜日に行われているSprintReviewという場所でも発表されることになりますが、このチャンネルではその速報を流しているといった運用になっています。 速報で流す理由としては、 新機能などの認知のため スタイル修正などで意図して変更したものがバグだと誤認されないため 逆にバグっていた際に、非エンジニアでもおかしいかもと気がつけるようにするため など、些細なことでも正確な情報を共有することにより、誤認により生じる不要なコミュニケーションを防いだり外部部署との連携を取りやすくしています。 また、新メンバーが初リリースをした際にはそれを報告したり、待望の機能がリリースされた際には様々なリアクションがついたりと、開発モチベーションの維持にもつながっています。 まとめ 上記の施策を通してわかるように、基本非同期的になってしまうオンラインでのコミュニケーションをよりオープンにしつつ、やりとりを密にすることで認識の齟齬を回避するようにしています。 この施策の成果として、外部からはわかりにくいエンジニアリソースや進捗がわかりやすくなり、依頼をする際にどこまでをやって欲しいのか、どこまでは考慮できてないから相談したいのかなど、依頼者の目的や意図、温度感などに合わせた柔軟でカジュアルな対応を実現することができています。 エンジニアからしても外部からの依頼に対する温度感がわかることは、今後の開発スケジュールの作成や毎週のスプリントの構築に不可欠であり、認識齟齬が減ることによる手戻りリスクなども減らすことができるため、安心した開発ができているかと思います。 また、このようなフランクなやりとりをするためには、普段からのメンバーに対する理解があってこそだと感じています。 最後に面白い取り組みを紹介すると、 某若手エンジニアが自画自賛をしていたことから始まったこの「オレがエラい」という企画は、毎週金曜日になるとデザイナーの方が毎週違う画像を貼ったスレッドを作ってくれて、そこに今週やった自分が誇れる内容を開発チームメンバーが書けるようになっており、エンジニア・デザイナー内での相互理解も積極的に進めています。 オンラインでの働き方が認められつつある今、この記事がチーム内 or 会社全体でのコミュニケーション不足に悩んでいるという方々や、タスクを進めるにあたって認識の齟齬が目立ってきたと感じる方々の助けになれば幸いです。
アバター
はじめまして!2021年7月にスマートキャンプに中途社員として入社した井上です!  入社時から現在まで、 BOXIL の開発業務に携わっています。前職はSIerで顧客のシステム開発や新規プロダクトの開発などをしていました。スマートキャンプ、前職ともにフロント・バックエンド問わず開発をしていますが、スマートキャンプでは使用技術や開発するサービスの形態が前職と大きく異なり、新鮮な気持ちで業務に取り組めています。 今日は入社エントリーとして、私の経歴を含めた自己紹介と、なぜスマートキャンプに入社したか、そして入社後のギャップをメインに書いていこうと思います。 これまでの経歴 スピード感のある開発がしたい 技術的なチャレンジ なぜスマキャンを選んだか プロジェクト運営 技術的なチャレンジ 面接 入社してみて 前職との違い コミュニケーションが「密接」 開発スタイル 入社して感じたギャップ コミュニケーションが密接な文化に戸惑う 異なる文化に馴染めた経緯 知識がついたこと メンバーからの支え この一ヶ月してたこと まとめ これまでの経歴 前述のように、私は新卒でSIerの企業に入社して、3年と少しの間、在籍していました。 1, 2年目は塾経営会社のWebサイトのシステム開発に携わっていました。同システムの保守業務をしながら新規機能の追加・改修プロジェクトに参加させていただきました。業務を経験していく中で開発リーダーのポジションになり、要件定義・設計・開発・テストなどウォーターフォール開発の工程を一通りこなしました。 その後、同じ会社の新規事業開発チームに異動しました。SIerの中で新規プロダクトを作るという特殊な組織でした。このチームではエンジニアの裁量が大きく、自分たちで要件を調整することができました。基本的にはサーバレスのようなモダンな構成を採用しており、プロジェクトの進め方もウォーターフォールよりはアジャイルに近かったです。周りのエンジニアの方々のレベルも高く良い刺激になりました。関わったプロダクトは3つほどあり、うち2つは立ち上げ時から関わることができ良い経験になりました。 2つの組織を通して基本的には開発業務に従事していましたが、領域はあまり限定せず、幅広く活動しておりました。おかげで視野が広がり、エンジニアとして成長できたと思います。 また会社としても良い会社だったと思っています。評価や待遇の面では、自分が出した成果や努力をしっかり評価してもらえて満足していました。一年目のときは、業務で成果を上げ、プライベートでも資格取得などを頑張っていたら、新人賞をいただけたこともあったりしました。実はチームを異動したのも私が要望を出したからなのですが、これも却下されることなく承認され、望んだ働き方をすることができました。また人間関係においても特に問題はなく、周囲の人は優しくて、特に新卒として入ったときには、周りとの人間関係が円滑になるように定期的に上長に状況確認会を開いてもらったりして面倒を見てもらいました。 このように全体としてみれば良い環境で働くことができていたのですが、そのうちポツポツと気になることが現れ始めました。それは段々と私のエンジニアとしての違和感や危機感を生むものになり、それが後々転職する理由になっていくことになりました。それは主に下記です。 スピード感のある開発がしたい 私が所属していたのはかなりの大企業で、グループ全体では数万人規模の会社でした。各部署がどういう仕事をしているのか把握し辛かったし、関係者とのMTGでは知らない人も多く、ITリテラシーが高い人もいればそうでない人もいます。 そのような組織であることもあり、全社的なルールが厳しく設けられていました。お客様のシステムを預かるのが主業務なので当然なのですが、例えば開発効率化のために新しいサービスを使いたくても利用申請を通す必要があり、プロセス上で承認を得る必要のある関係者も多かったりして、最終承認まで数ヶ月かかったりしていました。これでは便利そうに見えるサービスを導入したいとなってもその気が起きません。 このあたりに開発しにくさを感じていました。例えばCI/CD環境を作りたいとしても、CircleCIなどのサービスは社内承認されてないし、申請するのもすごい体力を使うということで、ローカルにJenkinsを立てるという苦肉の策を実施していましたが、結局ローカルで立てるのが面倒で使わなくなって手動でデプロイしていました。 このように使えるサービスの選択肢が狭いせいで「〇〇のサービス使えたらもっと開発スピード上がるのにな」と感じるようになりました。こうした観点から、使うツールに縛られることなくスピード感のある開発ができるところで仕事をしたいと思いました。 技術的なチャレンジ 前職ではJavaScriptによる開発がメインでした。新規のプロダクト開発であったとしても言語の選択肢は基本的にJavaScriptしかなく、他の言語に触れる機会があまりありませんでした。これは前職では開発において特に速度を優先する文化だったことに関係していると思います。最速で開発できる=使い慣れた言語という図式になりがちで、エンジニアの技術的成長が考慮されているような感覚はあまりありませんでした。 もちろん会社としてはエンジニアの技術的成長より、目の前のプロダクトを早く作ることを優先してほしいという考えもあるでしょうし、それも正しいと思います。ただ、見えている(知っている)言語が少ないのはエンジニアとしての視野の狭さに繋がると思っています。すべての言語を知る必要はないですが、他の言語も知ることで見える世界も変わってくるのではないかというのが個人的な意見です。 なぜスマキャンを選んだか 上記のような事情で転職活動をしていた私ですが、なかなか「これ」という会社が見つかりませんでした。そんな中でスマキャンに決めたのは、下記のような理由からです。 プロジェクト運営 スマキャンではタスク管理はAsana、CI/CDではCircleCIとAWS CodeDeploy、デザインツールはFigma、ドキュメント共有でKibelaなど、様々なツールを駆使して効率化を図っています。また使うツールは特定することなく状況に応じて変えていたりもします。また定期的なレトロスペクティブで改善サイクルを回す動きをしていて、健全なプロジェクト運営をしようと努めています。この組織であれば、核心的な業務への集中やスピード感のある開発ができると思いました。 技術的なチャレンジ スマキャンでは主にフロントエンドでVue、バックエンドでRuby on RailsやGolangを用いています。インフラはECSを用いてコンテナで運用されています。Macで開発をしますし、GitHubでリポジトリは管理しています。アプリケーション・インフラともに仕事で利用したことのないもので、非常に魅力的に思えました。 面接 カジュアル面談や面接、技術試験を通して見えてきたスマキャンの採用哲学のようなものに共感しました。詳しくは割愛しますが、実際に面接官の人と話していてエンジニアとして非常に話しやすかったですし、自分のことを知ろうとしてくれているというのが伝わってきました。そのため入社時も不安を抱えることなく安心して入社できました。スマキャンに決めたのはここが最も大きいです。 入社してみて 以上のような期待を持って入社した私ですが、期待をしていた部分は概ね想像どおりで、今はホッとしています。しかし想像をしていなかった違いもありました。そこで感じた前職との違いを下に記します。 前職との違い コミュニケーションが「密接」 スマキャンはコミュニケーションがかなり「密接」であるように感じました。例えば、下記のような施策がスマキャンでは行われています。 毎週火曜にはシャッフル朝会があって別の部署の人とランダムにマッチングして話す機会があります。 ランダムで来るSlackのBotの質問に答えるとその答えが全体に共有されます。 日報はSlackの全体チャンネルに投稿します。他の人の日報も見れます。 雑談をするという目的のためだけの時間が定期的にあります。 本部MTGで最近の仕事内容(+身の回りのこと)を全体に共有する時間があります。 上記のように、組織やチーム内で交流の機会がかなり多く設けられています。入社してから現在に至るまでにも、そうした交流の機会を通して自分のパーソナリティについて話すことがよくありました。 前職は良くも悪くもビジネスライクな関係性でした。大企業だったゆえ、チームによるところも大きいと思いますが、私のいた部署はそのような関係性でした。このような点が前職と大きく異なる部分だと思います。 開発スタイル 組織が変われば開発スタイルも大きく変わります。前職では一人で開発する時間が多く、相談したいときもコミュニケーション手段がSlackしかなかったのでなかなか相手の様子を伺うことができず、少し相談しづらかった記憶があります。スマートキャンプでは開発メンバーが常にDiscordにいるので「ちょっといいですか?」という感じで相談しやすく、特に入社したての現在は助かっています。やっぱりSlackメンションを飛ばすより声をかけるほうが心理的抵抗が低いです。 また週に1度、その週に取り組んだ開発の内容を、エンジニア以外のステークホルダーの方々へ共有する「スプリントレビュー」というイベントがあります。これまでの経験では、開発した成果を共有する機会があまりなく、するとしてもPMへの共有のみでした。スプリントレビューでたくさんの人に見てもらい「ありがとう」と言われるのは、実装した側からすると嬉しいです。またプレゼンのような形式で共有するため、自分の考えをまとめて話す力もつきます。大学のときはこういうことに慣れていたのですが、エンジニアとして働くうちに感覚を忘れてしまっていました。 入社して感じたギャップ さて、上記のようにスマキャンと前職の違いをつらつらと述べてきました。主に良い点が多かったと思いますが、もちろん気になった部分や、ギャップもありました。ここではそのようなことを書いていきます。 コミュニケーションが密接な文化に戸惑う コミュニケーションが密接と書きましたが、この文化は良くも悪くも私にとってチャレンジとなりました。 入社したての時には、周囲の方々が私に快く話しかけてくれました。全体MTG時など事あるごとに私が話題に挙がりましたし、入社初回の雑談の時間では私がフィーチャーされて話題の中心になりました。また、業務で接するメンバーとは一通り1on1(30分)の機会をセッティングしてもらいました。 しかし私はそもそも人見知りで、そこまでコミュニケーションが得意ではありません。初対面の人と自信を持って喋ることはできないですし、気を利かせたような言い回しもできません。人間性がペラペラだという自覚がありました。 とはいえ、中途エンジニアの採用は一年ぶりと聞いており、採用に際しては「カルチャーフィット」にも重点を置く文化とのことで、技術面のみでなく人間性の部分にも期待されているだろうと考えていました。 そのような意識からか、最初のうちは人と話していると人間性を試されているような感じがしていました。「こいつはスマキャンにふさわしい人物なのか?」というような感じで。これに対して応えようとするのですが、それだけのコミュ力はないので、1on1のときはなんだかぎこちない事が多かった記憶があります。MTGのときには発言することをためらったりすることも多くありました。自分の人間性を見抜かれるのが怖かったのです。 正直なところ、この時期は「この会社合わないんじゃないか」と考えていました。前職に出戻りしようかとすら思ったこともあります。 前職のビジネスライクな関係は、仕事をしていればそれで何も言われませんし、人間性を試されるようなことはありませんでした。そのような文化に染まりきっていた私からすると、スマキャンは正反対の文化を持つ組織で、それだけにその文化に飛び込むのは、適温の風呂から煮えたぎる熱湯に浸かるような感覚がありました。 ただ、入社してから一月半経った今となっては、ある程度楽になってきています。チームメンバーに冗談を言うことも多くなりましたし、自然体で会話できている自覚があります。 異なる文化に馴染めた経緯 上述のように、最初は前職と異なるチームの文化に戸惑っていた私ですが、入社して一月半が経過した今では大分打ち解けることができています。 こうして早々に新たな文化に馴染めた理由としては大きく以下の2つが挙げられそうです。 知識がついたこと 単純に業務知識と技術知識がついて、普通にタスクがこなせるようになってきました。そうなると一つの戦力になった自覚が出てきて、チームの一員になったという自信が自然とついてきました。そのおかげで最近では雑談時も物怖じせずに喋れるようになりました。この「チームの一員になった」という感覚は重要だと思います。どこかお荷物になっている自覚があると、何事においても自信が持てません。結果として萎縮してしまって自分を出せないという状態に陥ります。 そのため、私と同じようなパーソナリティを持っている人であれば、とにかく知識(業務知識・技術知識)を優先的に身に付けることをおすすめします。これはエンジニアが他の職に比べてパーソナリティの差が仕事に影響する範囲が狭く(決してゼロではないです)、知識を身に付けることによってカバーできる範囲が広いという性質を持っているからこそ有効な手段だと思います。こういった面は、個人的にエンジニアという職種の良い所だと思います。 メンバーからの支え また、「メンバーからの支え」があったことも大きい要因でした。 入社して間もない頃、仕事をする中で出てきた疑問点について、チームメンバーのいるDiscordにてポツポツと質問をするようになりました。最初のうちは、この質問をするのにも緊張していました。とんちんかんな質問をして白けさせたらどうしようと考えるのです。その覚悟をしてから質問するのですが、みなさん私の質問に対して馬鹿にしたような態度を全くすることなく、快く応えてくれました。みなさんBOXILについて詳しいので、1を聞いたら10を返すように、非常に丁寧に回答をしていただきました。 質問に対して誠実に回答をしてもらうと、1メンバーとして対等に向き合ってもらえた感覚になり、次第に「質問してもいいんだ」と思えるようになりました。 毎週行っているスプリントの振り返り会でも「質問をしてくれて嬉しい」とフィードバックをいただくことが何回かありました。はじめは「質問をしてくれて嬉しい」と思う感覚がよく分からず誇張かと思っていたのですが、やがて嘘ではなく、おそらく本当のことなんだろうと思うようになりました。そこには、入って間もないために「正体の分からない」私に対して、質問を通して 分かっている/分かっていない ことなどの区分が明確になり徐々に「解像度が上がっていく感覚」がメンバーにはあったのかなと思いました。 そうすると、周りの人は私を「試している」のではなく、私の「正体を知りたい」だけなのだと思うようになりました。みんな単に私に対して興味があるのであって、それで値踏みしようなどと考えているわけではなかったのだと気づきました。自分の中で創り上げていた「スマキャンに相応しい人物像」と「私」とのズレに自信が持てず、自分に対して意識が過剰に向き、結果として周囲からの言葉に過敏になってしまっていました。 「ただ正体を知りたいだけなんだ」と考えると色々と楽になりました。別に背伸びして自分を大きく見せる必要はないし、自分という人間を隠して逃げ回る必要もなかったのです。 そう考えて以降は自分を出すことが苦ではなくなりました。このように持ち直せたのは、周りのエンジニアが私に真摯に対応してくれたことや、振り返り会でのフィードバックによるところが大きいです。この人達とこの制度があるからこそ、早く持ち直せたと思っています。 この一ヶ月してたこと 最後に、私が入社してから今まで業務としてやっていたことをまとめます。 BOXIL開発 デザインの修正 簡単な機能改修 クエリ作成 別部署からの要望に合わせたデータの抽出 まだ何も成し遂げていない、という感じです。このあたりはタスクの都合というのもあります。今後大きな開発をできるように今はBOXILシステム理解と業務理解に努めているというような段階です。次のブログ記事を書くときには、語り甲斐のあるトピックになるような仕事ができていたらなと思います。 まとめ 長々と自分語りをさせていただきました。ここまで読んでくれた方にお礼を申し上げます。今後、スマキャンやスマキャンに似たような組織に入る、私のような経歴や考え方を持つ人の参考になれば幸いです。 今後もスマートキャンプのエンジニアとして活躍していこうと思っています。
アバター
スマートキャンプ、エンジニアの入山です。 前回のブログで、弊社プロダクトのインフラをEC2基盤からECS/Fargate基盤へ移行した話を紹介しました。 tech.smartcamp.co.jp 上記プロジェクトは大規模なインフラの刷新だったこともあり、CI/CDについても従来の仕組みからECS/Fargateの構成に合わせて変更しています。 CI/CDは、安定したプロダクト開発には必須且つ長期に渡って継続的に利用するものなので、いかにストレス少なく効率的に出来るかが重要だと考えています。 また、CI/CDは一度構築してしまうと放置されがちですが、日々の開発チーム全体の生産性にも大きな影響を与えるため、こういった数少ない再構築のタイミングではコストを掛ける価値があるのではないでしょうか。 今回は、弊社のインフラ移行時に実施したCI/CDの改善について紹介したいと思います。 従来のCI/CD構成と課題 新しいCI/CD構成 改善点・工夫点 まとめ 従来のCI/CD構成と課題 インフラ移行前(EC2運用時)のCI/CDは、CircleCI + Jenkinsの構成で実施していました。 CIはCircleCIでの自動テスト、CDはJenkinsでのデプロイと明確に分かれている構成です。 CircleCI バックエンドテスト RSpec RuboCop フロントエンドテスト Jest ESLint Storybook Buildテスト Jenkins デプロイ(EC2) Railsのassetsプリコンパイル webpackによるビルド EC2へのコード反映 マイグレーション実行 CIについては、GitHubへのPushをトリガーに自動で実行されます。 基本的にコードをPushする度に必ず実行されますが、フロントエンドとバックエンドでテストフローを分割しており、各コードに差分がない場合はテストをスキップして時間短縮&コスト削減するように少し工夫しています。 ちなみにこのテストスキップ処理は、数ヶ月毎に取り組んでいる開発改善の中で実施したものです。 CDについては、Jenkinsのワークフローでassetsプリコンパイルやwebpackによるビルド、各EC2へのコード反映を実施する仕組みとなっていました。 各EC2へのコード反映は、ALBを活用してEC2を2グループに分け、1グループずつ最新のコードを反映して切り替えることで実施していました。 Jenkinsによるデプロイは、全てワークフローやスクリプトで作り込んでいたため、拡張性や柔軟性の面を中心に、以下のような課題がありました。 デプロイ時間が長い 約45分 / Staging, Pre, Productionまでの1リリース 自動ロールバック不可 ロールバックは可能だが、手動でのイレギュラー対応が必要 スケーリングし辛い 予め用意しておいた予備のEC2しか自動デプロイ出来ない Jenkinsサーバーがデプロイ中に落ちる(たまに) デプロイ環境依存のエラー(たまに) 新しいCI/CD構成 インフラ移行後のCI/CDは、GitHub Actions + CircleCI + AWS CodeDeployの構成に変更しました。 CircleCIが従来から実施していた自動テストに加えてデプロイに関する作業の一部分を担うようになり、AWS CodeDeployが最終的なECS/Fargateへのデプロイを管理する構成です。 CircleCIの自動テストの部分については、今回特に変更してないため割愛し、デプロイ作業に関する部分のみを説明します。 GitHub Actions デプロイ開始トリガー(Gitタグ発行) Production : masterブランチ固定 Staging : 任意のブランチをデプロイ可能 CircleCI デプロイ(ECS/Fargate) Dockerイメージビルド DockerイメージのECRプッシュ Railsのassetsプリコンパイル webpackによるビルド AWS CodeDeployのデプロイ開始 AWS CodeDeploy デプロイ(ECS/Fargate) Blue/Greenデプロイ マイグレーション実行 従来のJenkins単一でのデプロイ構成と比べて、デプロイに必要な作業がCircleCIによって並列処理可能となったことや、デプロイ部分をCodeDeploy管理としたことで柔軟なデプロイ方式を簡単に選択できるようになりました。 今回のデプロイ構成では、CircleCIのデプロイトリガーをGitタグにすることで、デプロイのタイミングやStagingに反映するブランチを柔軟に選択出来るようにしています。(Productionはmasterブランチ固定) また、トリガーのGitタグのPushやGitHub上でリリースタグを発行する作業も手動だと手間なため、GitHub Actionsのworkflow_dispatchを使ってリリースタグを自動生成できるようにしています。 つまり、GitHub Actionsでデプロイのワークフローを実行すれば、(Stagingであれば指定したブランチで)リリース作業が開始出来るような仕組みになっています。 改善点・工夫点 新しいCI/CDの構築により改善された点や構築にあたって工夫した点を簡単に紹介します。 今回のCI/CDの再構築によって、従来の構成で主な課題となっていた部分は全て解消することができました。 CI/CD全体 デプロイ時間短縮(約15分短縮) マネージドサービスの利用による環境管理コストやエラー率低減 GitHub Actions 定常的に行う手動作業(Gitタグ発行)を自動化 同様のトリガーを一箇所に集約することで管理コスト削減 CircleCI Gitタグをトリガーにした柔軟なデプロイ運用 デプロイに必要な作業を並列にすることで効率化 キャッシュ機能(ファイルキャッシュ、Dockerレイヤーキャッシュ)による時間短縮 CodeDeploy Blue/Greenデプロイによる自動での即時ロールバックに対応 カナリアやリニア方式などの柔軟なリリース方式が選択可能 スケールを含むデプロイも簡単に対応可能 検証したコンテナがそのまま本番へ昇格するため切り替えが速い CodeDeployの承認フローをChatOpsにして利便性向上( 以前のブログ で紹介) まとめ 今回は、弊社のインフラ移行時に実施したCI/CDの改善について紹介しました! 色々細かい改善も実施しましたが、個人的にはBlue/Greenで即時ロールバックが可能になった事がリリース作業時の心の余裕に繋がっており、何よりも導入してよかったと思っているポイントです。 CI/CDは開発チームが常に付き合っていくものであり、上手く構築することでチーム全体における日々の開発効率を向上させることが出来るものです。弊社では今後も少しずつ改善を実施し、更に快適なCI/CDにしていこうと考えております! 今回ご紹介した構成は数多あるパターンのうちの一つですが、何かの参考になれば嬉しいです。 また、読んでいただいた方々が構築しているCI/CDで「これは最高だ!」というものがあれば、ぜひ教えていただきたいです!
アバター
こんにちは!スマートキャンプ、エンジニアの中田です。 以前書いた記事の内容に引き続き今回も、現在業務で利用している Go のお話しです! 以前の記事 tech.smartcamp.co.jp 突然ですが、みなさんはテストを書かれてますか? 僕も「書いてます!」と声を張りたいところですが、4 月に新卒入社をしてから開発を始めた Go 製の API には何を隠そうテストがございません...。 開発初期は API へリクエストを手動で送りテストするような運用で特に事なかったのですが、開発が進むにつれコード差分による影響範囲が網羅できなくなったり、またそれにより大きな変更がしづらくなったり、とテストがないことによる悪影響が徐々に出現してきました。 そこで、テストを書こう。と思い立ってはみたものの、Go で API のテストってどう書くんだろう?と困ったのでその辺りを調査しながらサンプルアプリを実装してみました。 本記事ではそのサンプルアプリのお話、Go の API テストの中でも特に Unit テストを実装する方法についてご紹介いたします! 本サンプルアプリについて 使用ライブラリ Unit テスト Model プロダクトコード テストコード Repository プロダクトコード テストコード Handler プロダクトコード テストコード まとめ 本サンプルアプリについて 今回テスト用に作成したサンプルアプリのコードは以下から参照できます。 ( https://github.com/kiki-ki/go-test-example ) 使用ライブラリ 使用しているライブラリは以下になります。 HTTP ルータ: chi ORM: gorp SQL ドライバの mock: go-sqlmock 各ライブラリの用途を説明します。 chi と gorp は、業務で製作中の API の構成に似せるために導入しました。実際、制作中の API にはそれぞれ gin 、 gorm と違うライブラリを使用していますが、筆者の興味で今回は別ライブラリを用いています。 go-sqlmock はテスト用に mock DB を作成する意図で導入しています。詳しくは後述の Unit テスト項にて説明いたします。 Unit テスト それでは早速、テストを書いていきます。 今回は、 model 、 repository 、 handler の 3 パッケージに対してそれぞれに Unit テストを作成していきます。 Model まずは model のテストです。 プロダクトコード type User struct { Id int `db:"id" json:"id"` Name string `db:"name" json:"name"` Email string `db:"email" json:"email"` Age int `db:"age" json:"age"` } func (u *User) IsOverTwentyYearsOld() bool { return u.Age >= 20 } テストコード model パッケージに依存先がある場合は少ないと思うので、通常の関数などのテストと同様、特に考えることなく以下のように書けました。 func TestUser_IsOverTwentyYearsOld(t *testing.T) { cases := map [ string ] struct { in model.User want bool }{ "eqaul" : {in: model.User{Age: 20 }, want: true }, "above" : {in: model.User{Age: 21 }, want: true }, "below" : {in: model.User{Age: 19 }, want: false }, } for k, tt := range cases { t.Run(k, func (t *testing.T) { got := tt.in.IsOverTwentyYearsOld() if tt.want != got { t.Errorf( "want: age(%d) = %v, got: %v" , tt.in.Age, tt.want, got) } }) } } Repository 次に repository のテストです。 プロダクトコード type executer = gorp.SqlExecutor type UserRepository interface { Find(uId int , e executer) (model.User, error ) ...(snip) } type userRepository struct {} func (r *userRepository) Find(uId int , e executer) (model.User, error ) { var u model.User err := e.SelectOne(&u, "SELECT * FROM users WHERE id = ?" , uId) if err != nil { return model.User{}, err } return u, nil } ...(snip) テストコード 上記の *userRepository.Find(-) メソッドの動作が確認できるようなテストを書いていきます。この際に困るのが、テスト用の DB をどうするのかという話です。この解として、大きく以下の 2 パターンがあるかと思います。 実際にテスト用の DB を立てる mock DB を使用する 今回は導入の容易な「mock DB を使用する」パターンでテストを書いてみます。 まず、mock のライブラリとして go-sqlmock を導入します。 今回は ORM を利用しているので、DB のコンストラクタに sqlmock で作成した *sql.DB を渡してテスト用の DB のインスタンスを生成するようにしました。 この生成処理は DB の絡むテストでは頻用されるので、 testutil パッケージを切り関数を作成しておきます。 db type DB interface { Conn() *gorp.DbMap Close() error } func NewDB(sqlDB *sql.DB) DB { dbmap := &gorp.DbMap{Db: sqlDB, Dialect: gorp.MySQLDialect{}} dbmap.TraceOn( "[gorp]" , &logger{}) addTableSettings(dbmap) return &db{ connection: dbmap, } } type db struct { connection *gorp.DbMap } func (db *db) Conn() *gorp.DbMap { return db.connection } func (db *db) Close() error { err := db.connection.Db.Close() if err != nil { return err } return nil } ...(snip) testutil func NewMockDB(t *testing.T) (database.DB, sqlmock.Sqlmock) { t.Helper() sqlDB, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } db := database.NewDB(sqlDB) return db, mock } 上記より、mock として使用できる db が生成できるようになったので、テストを作成します。 Test func TestUserRepository_Find(t *testing.T) { db, mock := testutil.NewMockDB(t) defer db.Close() want := model.User{ Id: 1 , Name: "taro" , Email: "taro@chan.com" , Age: 5 , } rows := sqlmock.NewRows([] string { "id" , "name" , "email" , "age" }). AddRow(want.Id, want.Name, want.Email, want.Age) mock.ExpectQuery(regexp.QuoteMeta( `SELECT * FROM users WHERE id = ?` )). WithArgs(want.Id). WillReturnRows(rows) got, err := repository.NewUserRepository().Find(want.Id, db.Conn()) if err != nil { t.Error(err) } if err := mock.ExpectationsWereMet(); err != nil { t.Error(err) } if want != got { t.Errorf( "want: %v, got: %v" , want, got) } } このように書けました。 go-sqlmock 周りのコードの解説をすると、 ExpectQuery(-) メソッドで期待するクエリを指定し、 WillReturnRows(-) メソッドで取得結果を指定しています。最後に ExpectationsWereMet() メソッドで mock に設定された期待値通りのクエリが実行されたかの検証をしています。 その他の流れは通常と変わりなく、関数の戻り値が期待値とイコールかの検証を行ってテストを終えています。 Handler 最後に handler のテストです。 プロダクトコード type UserHandler interface { Show(http.ResponseWriter, *http.Request) ...(snip) } type userHandler struct { db database.DB userRepo repository.UserRepository } func (h *userHandler) Show(w http.ResponseWriter, r *http.Request) { uId, err := strconv.Atoi(chi.URLParam(r, "userId" )) if err != nil { w.WriteHeader(http.StatusBadRequest) render.JSON(w, r, err.Error()) return } u, err := h.userRepo.Find(uId, h.db.Conn()) if err != nil { w.WriteHeader(http.StatusInternalServerError) render.JSON(w, r, err.Error()) return } w.WriteHeader(http.StatusOK) render.JSON(w, r, u) } ...(snip) テストコード func TestUserHandler_Show(t *testing.T) { w := httptest.NewRecorder() r := testutil.NewRequestWithURLParams( t, "GET" , "/dummy" , nil , testutil.URLParam{Key: "userId" , Val: "1" }, ) want := testutil.AssertResponseWant{ StatusCode: 200 , Body: "a" , } db, mock := testutil.NewMockDB(t) defer db.Close() u := model.User{ Id: 1 , Name: "taro" , Email: "taro@chan.com" , Age: 5 , } rows := sqlmock.NewRows([] string { "id" , "name" , "email" , "age" }). AddRow(u.Id, u.Name, u.Email, u.Age) mock.ExpectQuery(regexp.QuoteMeta( `SELECT * FROM users WHERE id = ?` )). WithArgs(u.Id). WillReturnRows(rows) h := handler.NewUserHandler(db) h.Show(w, r) res := w.Result() defer res.Body.Close() testutil.AssertResponse(t, res, want, "./testdata/user/show_res.golden" ) } net/http/httptest パッケージの NewRecorder() 、 NewRequest(-) でリクエストとレスポンスライターを作成し、ハンドラーを通った後にレスポンスを検証することでテストしています。また、前項同様にここでも repository を利用するので mock DB を呼んでいます。 testutil でラップしている関数の詳細は以下になります。 NewRequestWithURLParams(-) type URLParam struct { Key, Val string } func NewRequestWithURLParams(t *testing.T, method string , target string , body io.Reader , params ...URLParam) *http.Request { t.Helper() r := httptest.NewRequest(method, target, body) return addURLParams(t, r, params...) } func addURLParams(t *testing.T, r *http.Request, params ...URLParam) *http.Request { t.Helper() ctx := chi.NewRouteContext() for _, p := range params { ctx.URLParams.Add(p.Key, p.Val) } newR := r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx)) return newR } 今回は HTTP ルータに chi を使用しており、URLParameter の読み込みを Handler 内で行っているため、この関数で URLParameter を埋め込んだ *http.Request を生成できるようにしています。 AssertResponse(-) func AssertResponse(t *testing.T, got *http.Response, want AssertResponseWant, path string ) { t.Helper() if want.StatusCode != got.StatusCode { t.Errorf( "statusCode: want=%d, got=%d" , want.StatusCode, got.StatusCode) } assertResponseBodyWithFile(t, got, path) } この関数では、HTTP ステータスコードとボディの検証を行っています。 この辺りのラップの仕方は BASE さんの以下の記事に詳しく、参考にさせていただきました。 devblog.thebase.in 上記のテストコードでは、Unit テストと言いつつも mock DB の生成/設定処理を行っており統合テストのように見えます。より Handler のみにフォーカスしたテストを書く方法があるのか、次いで調査していければと思います。 まとめ いかがでしたでしょうか? 本記事では Go の API の Unit テストの書き方についてご紹介いたしました。 今回は mock DB を使用しましたが、このパターンではあくまでクエリが想定の通りかのみのテストとなるため、そのクエリで想定した動作が行われるかの保証はできないというのが難点だなと感じました。 テスト用に実 DB を作成するパターンであればこの問題も解決できるので、またそちらも試してみようと思います。 また、本記事で扱っているテストコードは筆者がテストの書き方を学びながら実装したものなので、ご指摘/ご意見は大歓迎です! 最後までお読みくださりありがとうございました!
アバター