TECH PLAY

dely株式会社

dely株式会社 の技術ブログ

236

はじめに こんにちは。クラシルのAndroidアプリチームのテックリードのうめもりです。 android-developers.googleblog.com 12/14に新しいアプリアーキテクチャガイドがAndroid公式からアナウンスされました。読まれた方もいらっしゃると思いますが、非常によくまとまったアーキテクチャガイドであり、新しくアプリを作る際も、既存のアプリのアーキテクチャを整理する際にも役に立ちそうな文章です。 クラシルのAndroidチームは去年の2月にAndroidアプリをリアーキテクチャしたのですが、そのアーキテクチャがアプリアーキテクチャガイドと似通った個所が多く、クラシルのアプリアーキテクチャを説明するのにもちょうど良さそうな文章だと思いました。 ですので、今回は新しいアプリアーキテクチャガイドとほとんど同じ構成で、クラシルのアプリアーキテクチャについて解説してみようと思います。なお、元の文章は一般的なアーキテクチャについてのTipsや考え方について説明している個所もあり、そういった個所については記述を割愛しますので、是非元の文章を読んでみてください。 この記事のタイトルは、アプリアーキテクチャガイドの Guide to app architecture から拝借して Guide to "kurashiru android" app architecture とさせていただいてます。 なお、分量が多いため、今回は概要の部分を公開します。vol.2, vol.3ではより詳しいレイヤーの話をしていく予定です。(この構成もGuide to app architectureに倣ってます) はじめに 余談 概要 クラシルのアプリアーキテクチャ UI layer Data layer Domain layer コンポーネント間の依存関係を管理する クラシルの設計プラクティス UI Componentのライフサイクルよりも短命な領域にデータソースを置かない Android framework SDK APIへの依存を局所的にする 各モジュール間のアクセスは抽象度の高いインターフェースを通して行うようにする クラシルが提供すべき価値に焦点を当てるために、ボイラープレートコードを削減する おわりに 余談 どうせ日本語版はすぐ用意されないだろう…と思って英語版を頑張って翻訳しながらブログを書いていたら、年が明けたら日本語版が公開されていました。悔しかったので自家翻訳版のまま公開しており、日本語版とはニュアンスが違う部分が多々あるかもしれません。 概要 クラシルのアプリアーキテクチャ クラシルは、Android公式の推奨されたベストプラクティス https://developer.android.com/jetpack/guide#common-principles に従ってアプリを構成しています。 そして、クラシルは3つのレイヤーでアプリケーションを構成しています。 アプリケーションデータを画面に表示するためのUI layer ビジネスロジックを内包し、アプリケーションデータを他の層に公開するためのData layer Data layerをUI layerから隠蔽し、複雑なビジネスロジックを内包するためのDomain layer そして、UI layerとDomain layer+Data layerは明確に別のモジュールに分離されています。Domain layerとData layerを別のモジュールにするかは、将来的なアプリケーションの拡張次第ですが、今のところ分離する予定はありません。 UI layer UI layerの役割はアプリケーションのデータを画面に表示することです。ユーザーによる操作(例えばボタンのタップ)や外部的な入力(例えばネットワークレスポンス)などでデータが変わるたびに、UIはデータの変化を反映します。 UI layerは二つの要素で成り立っています。 データを画面にレンダリングするためのUI要素。クラシルではまだJetpack Composeは導入されていないので、Android Viewを使っています。 データを保持するためのState holder。クラシルではMVIアーキテクチャを採用し、 Android ViewからUIイベントを発行するためのIntent 、 UI StateをAndroid Viewに反映するためのView 、 UIイベントや外部的な入力をハンドリングし、Stateに反映するためのModel の三つの部分でState holderが出来ています。 去年のAdvent Calendarでクラシル版MVIアーキテクチャに触れているので、ご興味ある方はぜひこちらをご覧ください。 tech.dely.jp Data layer Data layerはビジネスロジックを内包します。ビジネスロジックは クラシルのアプリに価値(value)をもたらすものであり、アプリがどのようにデータを作成し、保存し、変更するかを決定するルールで構成されています 。 Data layerはRepositoryで構成されており、それぞれが0から複数のDataSourceを含むことが出来ます。クラシルで扱うデータの種類ごとにRepository classを作成する必要があります。例えば、Recipeに関連するデータには RecipesRepository classを、Userに関連するデータには UsersRepository classを作成するとよいでしょう。 Repository classは以下のような役割を担っています。 データをリポジトリーの外に公開する。 データの変更を一か所に集約する。 複数のデータソース間の競合を解決する。 リポジトリーの外からデータのソースを隠蔽し、抽象化する。 ビジネスロジックを内包する。 各DataSource classにはファイル、ネットワーク、ローカルデータベースなど、1つのデータソースのみを扱う責務を持たせるべきです。DataSource classは、アプリケーションとシステム間のデータ操作の橋渡しをします。 Domain layer Domain layerはUI layerとData layerの中間のレイヤーです。 Domain layerは複雑なビジネスロジックや、複数のUI Modelで再利用されるシンプルなビジネスロジックをカプセル化する役割を果たします。クラシルではさらにData layerをUI layerから隠蔽する役割を担っており。オプションではなくUI layerは必ずDomain layerを通してData layerへアクセスします。これはデータへのアクセスのやり方を統一し、アプリケーション全体の依存関係をシンプルに保つための意思決定としてそれを行っています。 クラシルでは、モジュールごとに単一、または複数のファサードインターフェースとしてFeature接尾辞を持つインターフェースをDomain layerからUI layerに公開し、それを通してDomain layerの機能にアクセスします。 モジュールごとのファサードの実装が大きく、複雑になりそうな場合は、Interactorクラスに処理を分割することを検討します。(その場合、単一のInteractorクラスは単一の責務を持つことが好ましいです。) コンポーネント間の依存関係を管理する クラシルではDependency injection (DI) パターンを使ってクラス間の依存関係を整理しています。クラスのオブジェクトインスタンスの生成やライフサイクルの管理はDIコンテナに依存出来るため、実装者は依存関係のオーケストレーションの手間のことを考えずに適切なインターフェース、クラス設計に専念すべきです。また、テスト用の実装の差し替えなどはDIコンテナの機能を活用して行うことを推奨します。 クラシルでは、 https://github.com/stephanenicolas/toothpick を使っています。ToothpickはJSR-330( https://jcp.org/en/jsr/detail?id=330 )の仕様を満たしており、Dependency Injectionの機能的には必要十分ですし、今後これ以上複雑な機能をもったDIライブラリーに移行する予定はありませんが、KSP等のビルド時間が高速になることが期待できる仕組みを利用したスタンダードなDependency Injectionのライブラリーがリリースされた場合は、そちらに移行していく可能性があります。 クラシルの設計プラクティス クラシルはコードベースの堅牢性、保守性、テスタビリティを高めるため、なるべく以下のようなプラクティスに従っています。 UI Componentのライフサイクルよりも短命な領域にデータソースを置かない UI ComponentのModelオブジェクトや、その他インスタンスフィールドをデータソースとして指定するのは避けてください。Domain layer及び、各Componentのシリアライズ可能なStateをデータソースとすべきです。 Android framework SDK APIへの依存を局所的にする クラシルのアプリコンポーネントの中で、Android framework SDK APIに依存する個所は局所的にし、それらのコンポーネントへのアクセスは抽象インターフェースや、抽象メソッドを通して行うこととします。それによってテストのしやすさを高め、Android SDKへの結合度を下げることが出来ます。 各モジュール間のアクセスは抽象度の高いインターフェースを通して行うようにする モジュールの具体的なクラスに対して直接アクセスするショートカットを作りたくなるかもしれませんが、モジュール同士が密結合することは、コードベースの進化に伴った技術的負債の発生につながりやすくなります。 クラシルが提供すべき価値に焦点を当てるために、ボイラープレートコードを削減する 可能であればJetpackなどのスタンダードなライブラリーや、その他ボイラープレートコードを削減できるライブラリーの活用を検討し、ビジネスロジックやUIロジックの実装にエンジニアが集中できるようにします。 おわりに 実は、今回この記事を書くにあたって、既存のクラシルのアーキテクチャをAndroid公式のものに沿っていくつか再解釈したところがあります。今まで作ってきたアプリケーションの構造としては変わらないものの、今まで明確にそういった役割を持たせていなかった部分に対して名前を付け、整理するのに、アプリアーキテクチャガイドはとても役に立ちました。皆さんも是非、Guide to app architectureを使って自分たちのアーキテクチャを見直してみてはいかがでしょうか。 クラシルのAndroidチームでは、生産性の高いアーキテクチャを維持・アップデートしつつも楽しくプロダクト開発できる人、したい人を求めています。 今回の記事を読んで興味が出てきた方、是非お話ししましょう! 下記の採用リンクから応募いただいてもOKですし、Twitterから気軽にDM頂けてもOKです! dely.jp twitter.com
アバター
こんにちは。TRILL開発部でバックエンドエンジニアをしている安尾です。 この記事はdely Advent Calendar 2021 22日目の記事です。 昨日はクラシル開発部SREチームの松嶋さんの「 クラシルのSREをチーム化するときに意識した3つのことと半年間の実績 」という記事でした。SREに興味がある人、開発チームの立ち上げに興味がある人、是非参考にしてみてください! 今日は僕が所属するTRILL Backendチームにおける アウトソーシングを有効活用した適材適所爆速開発術 について紹介したいと思います!今回の記事ではTRILL開発チーム全体のことではなく、僕が所属するBackendチームにフォーカスした事例の紹介になりますが、他の職種でも活用いただけるのではないかと思っています。 昨今どこの企業でもエンジニア不足という課題がある中で思うような開発速度が出なかったり、機能開発でいっぱいいっぱいになってしまい中長期を意識した技術的負債の解消や新しい技術の検証などといった 重要度は高いけれど緊急度は低いタスク が後回しになってしまっている企業やチームは多いのはないでしょうか? 今回の記事はそういった方々に少しでも参考にしていただると嬉しいです! TRILL Backendチームの責務 まず前提を揃えるためにまずTRILL Backendチームについて少し紹介させてください。 僕たちのチームの役割についてですが、TRILLにはSREやインフラチームが現状存在しないため、 サーバーサイドの開発に加えてインフラ(AWS)の構築や運用保守も含めて責任を持っている チームになります。 最近はアプリにフォーカスしているため機会は少ないですが、Webのフロントエンドの開発も必要に応じて行います。 TRILL Backendチームの体制 そんな割と責任範囲が広めなTRILL Backendチームですが、現状僕を含め 社員は2名 しかいません。(僕も今年10月にクラシルから移動してきたばかりなのでそれまではたった1名でした!) 月間利用者数が2500万を突破するTRILLのサービス規模からすると、社員2名というのはかなり少ない方ではないでしょうか。 移動後直近3ヶ月での成果 社員2名のBackendチームですが、9〜12月の直近 3ヶ月間で約15個の新機能リリース に加え、Rubyのバージョンアップ、自動テストの充実、CI・CDの改善など 優先度が高いいくつかの技術的負債の解消 も行うことができ、プロダクトチームとしての目標KPIも無事達成することができました 🙌 どうやっているのか? では、いよいよ本題の どうやって少人数で爆速開発を行っているのか についてお話ししていきたいと思いますが、タイトルにもある通り最も効果があったことが アウトソーシングの有効活用 になります。 現在社員以外でBackendの開発に関わっているのは、インターン1名、業務委託1名、オフショア開発会社所属の3名の合計5名です。社員2名をあわせると合計7名のエンジニアで構成されていることになります。 先程ご紹介した成果も2名では到底不可能ですが、エンジニアが7名いると考えるとかなり現実的に思えてきます。 とは言え、アウトソーシングを活用したことがある方ならイメージがわくかもしれませんが、人数が増えたからと言ってそれに比例した成果が簡単に出せるかというとそうでもないのでないでしょうか。 多かれ少なかれ下記のようなことに頭を抱えた経験がある人は多いと思います。 社外のエンジニアに依頼したタスクがスケジュール通り出来上がってこない 依頼したタスクの進捗が分からない 出来上がったものが想定と全然違う コードのクオリティが低く、レビューに想定以上の時間が取られる 常に社外エンジニアの稼働を埋めるほどのタスクを社員が作ることができない レビューが間に合わずリリースされない機能が大量に溜まってしまう 僕自身もちろん最初からすべてうまく行ったわけではなく前職も含めて過去これらの課題を経験してきました。 その都度改善を繰り返してきたのですが、その中でも有効だと考えていることをいくつかご紹介できればと思います。 カンバンで全体像の可視化と疑似スクラムの導入 改善と言えば、最初にやるのはやっぱり可視化です。 ステークホルダーが増え、さらには直接顔を合わせることがない社外メンバーが過半数を占めている状態なので、意識して可視化を行っていないとすぐに誰が何をしているのかが分からなくなってしまいます。 僕たちのチームも可視化を行う前は、機能ベースで担当がアサインされていたため、 お互いが何のタスクに取り組んでいて、どんな進捗になっているのかは、直接担当者に聞かない限り分からない状況 になっており、進捗の把握にコストがかかったり、 それぞれの機能がいつリリースされるのか把握するのが困難 な状況でした。 カンバンで各タスクの担当者、進捗、StoryPointを可視化 することで 各エンジニアが何のタスクに取り組んでいて、それがいつ終わる予定なのかを誰でも把握できる状態 を作ることができました。 次に擬似スクラム(以下、シンプルにスクラムと呼ぶ)を導入しました。疑似とつけているのは、Backendチームだけで行っており、プロダクトオーナーやアプリエンジニアが含まれていない状態で行っているため正確にはスクラムとは呼べないと思ったためです。ではスクラムの何を取り入れたのかというと スプリントという概念とスクラムイベント を取り入れました。 スクラム導入前はざっくり下記のようなフローで開発を行っていました。 社内エンジニアがPMと相談しながら、全員分のタスクを作成し、開発依頼 社外エンジニアはタスクに取り掛かり、終わったら社内エンジニアにレビューを依頼する 社内エンジニアはレビューをしつつ、手が空いた社外エンジニア用のタスクをPMと相談して決めアサインする 2と3の繰り返し このフローには以下のような課題がありました。 社外エンジニアの人数分、2と3が五月雨に発生するため、社内エンジニアは自分のタスクを計画的に進めづらい 社内エンジニアのレビューや新しいタスクの作成が遅れると社外エンジニアに待ちの時間が発生してしまう 社外エンジニアの着手中のタスクに問題が発生した際に気づくタイミングが遅れる 開発フローに関する振り返りが行われないため、フロー自体の改善がされづらい そこで、スクラムイベントを導入し、 スプリントプランニングでタスク作成 デイリースクラムで各自のタスクにおける課題の確認 スプリントレトロスペクティブで1スプリントを振り返りKPTの実施 というような 決まったサイクルを作る ことで、Backendチームで感じていた課題の多くを解消することができました。 設計レビュー会導入で属人性の排除と手戻り防止 次に出てきた課題は 属人性と開発における手戻り でした。 これはもともと社内エンジニアが1人のときには存在しない課題でした。なぜから実装をアウトソーシング場合でも設計とレビュー、リリースの工程は必ず1人の社内エンジニアが行っていたためです。 ところが、僕がジョインしたことで社内エンジニアが2名になりました。各機能の設計はどちらかの社内エンジニアが行い、実装はそのまま自分でやるかアウトソーシングされることになります。 設計者自身が実装した場合、もう1人の社内エンジニアがコードレビューを行うことになるため、属人化は防げますが、実装をアウトソーシングした場合はコードレビューは設計者が行い、そのままリリースされていました。 前者の場合は、コードレビュー時に 設計変更を伴う指摘が行われると大きな手戻りが発生 してしまいます。 後者の場合は、1人のエンジニアはまったく関与しない状態でリリースが行われてしまうため、 属人化が起こり その機能でなにか問題が発生したり、機能の変更を行う場合に対応できるエンジニアが限られる(または機能に関与していなかったエンジニアがキャッチアップから入る。しかもドキュメントがないためコードを1から読んだり口頭での引き継ぎが必要)状況が発生します。 そこで、これらの課題を解決するために設計レビュー会というフローを新たに追加し、 実装前に設計の合意を社内エンジニア間で取ることを必須 としました。設計レビュー会は指定フォーマットに従った設計ドキュメントをもとに行うため、新たな機能にはかならずセットで設計ドキュメントが存在する状態になります。そのため、 現在のチーム内での属人性が排除されることはもちろん、今後新たに入ってくるエンジニアも過去の機能に関するキャッチアップが容易 になります。 適材適所のタスク割り振り 最後に紹介するのが適材適所のタスク割り振りです。つまり各タスクを最も適した人にアサインするということです。 一例ですが、僕たちの場合は以下のような基準で判断をしていたりします。 不確実性が高いタスクは社内向き 例: 使ったことがないAWSの機能やSaaS、ライブラリを用いた開発 例: UXに関わる作りながら細かい調整が必要な機能開発 不確実性が低いタスクは社外向き 例: UXに関わらない基礎機能の開発 実装が重いものは社外向き 例: ログイン機能の実装 実装が軽いものは社内向き 例: 既存機能の微調整 重要度が高いが緊急度が低いものは社外 例: Rails version up等の技術的負債の解消 なぜ上記のような基準になるかと言うと、 働き方の違いにおける特性とタスクの相性が良い と考えているためです。 例えば、社内エンジニアの特性で言うと 基本的にプロダクトが存在しつづける限り、関わり続けるため、中長期を意識した思考になりやすい PMや他職種のエンジニアと物理的に近い距離で働いているため、細かいコミュニケーションや調整がしやすい 利害や価値観が揃っており、共通認識が取れていることが多いため、上流工程などの抽象度の高い議論がしやすい ミーティングや面接、レビューなど開発以外にも必要な業務があるため、長時間まとまった時間を取るのが難しい などがあり、反対に社外エンジニアの特性で言うと 1つのプロダクトに関わる期間が比較的短いので、短期的な思考になりやすい 必ずしも利害や価値観が揃っているわけではなく、共通認識が取れていることも少ないため、上流工程などの抽象度の高い議論がしづらい 他のメンバーと物理的な距離があるため細かな調整はしづらい 長時間まとまった時間開発に集中できる などがあると思います。 これを理解せずにタスクを振ってしまうと、宝の持ち腐れになったり、タスクの質の低下、開発スピードの低下につながってしまいます。 相性を意識した適材適所なタスク割り振りを行うことで、プロダクト開発のスピード、クオリティは大きく変わってくるので、是非意識してみてください。 まとめ この記事では、僕が所属するTRILL Backendチームにおける アウトソーシングを有効活用した適材適所爆速開発術 について紹介させていただきました! アウトソーシングをうまく活用してエンジニア不足を解消していきましょう!! 最後に、さらなる爆速開発を目指してdelyではエンジニア、デザイナー、PdMを積極採用しています。 少しでも興味がありましたら、お気軽に話を聞きにきていただけると嬉しいです! dely.jp
アバター
こちらは、「 dely Advent Calendar 2021 」21日目の記事です。 昨日は、PdM櫻本さんの「とりあえずやってみる。精神について」という記事でした。 何か新しいことにチャレンジしてみたいと思っている方は、ぜひ読んでみてください! はじめに こんにちは、クラシル開発部SREチームの松嶋です。 今年の10月に「SREがプロダクトの価値を最大化するためにチームとして取り組んできたこと」と題して、私たちが足元課題解決型の体制から脱却し、チームとして効果的に機能するために取り組んできたことについて5つ紹介しました。 https://tech.dely.jp/entry/2021/10/01/173000 tech.dely.jp こちらの記事で紹介している取り組みは、クラシルというプロダクトの成長を加速させていくために私たちは何をすべきなのか議論し、必要なことを地道に取り組んできただけなので、「これをやれば上手くいく!」というような銀の弾丸になるアクションは特にありませんし、SREがチームとして価値を発揮できるようになるまでにも 約1年 と長い時間がかかっています。 「SREの追求」の3.1章 *1 でも述べられている通り、「適切に機能するSREチームには時間をかけて熟成される文化があり、組織であらたににチームを始めるのは、長期に及び多大な取り組みを要するプロジェクトとなる」とありますし、SREを自分たちの所属する組織に適用させていくことの難しさ、大変さをこの期間に身を持って感じました。 しかし、日々チームメンバーと議論を重ね、トライアンドエラーを繰り返してきたので、Googleが提唱しているSREの信条に基づいた上で現状のクラシルの規模やフェーズにあったSRE文化を確立するための基盤ができつつあると思います。 その中でも私たちが特にこだわってきた部分は、データ駆動型のアプローチを取れるチームにすることでした。 なぜなら、拡張可能なチーム体制を構築していくには、メンバー間の共通認識を増やし、できる限り客観的な判断を下すために数値に落とし込む必要があったためです。 実際に私たちがこのアプローチを取れる状態になってからは、以前より意思決定がしやすくなり、運用改善スピードが格段に向上したと感じているので、本記事では、前回触れなかったSREチーム体制を構築する上で意識したことやそれによって得られた成果について書きたいと思います。 徹底して言語化する データ駆動で動こうとする前に、まずは自分たちの目指していくゴールや役割を徹底的に言語化し足元を固めることが重要だと思います。 なぜなら、ゴールが定まっていない状態で何らかの数値を取得、計測し始めても、その値は自分たちが目指すべきゴールに向かうために必要な指標になっているとは限らず、本質的なアプローチができないと考えているからです。 また、言語化するということはメンバー間の共通認識を増やし、認識齟齬を最小限にすることにも役に立つので、自分たちの核になる部分は早いうちに言葉に落とし込んだ方が、普段のコミュニケーションや業務を円滑に進められるのではないかと思います。 例えば、現在の私たちのミッションは「delyが持つ熱量と幸せをできるだけ多くの人に届けられるように、プロダクトの価値最大化をシステム運用において実現する。 その過程において最善の選択が何であるかを考え、システムの設計と運用を改善し続ける。」となっていますが、このミッションに辿り着くまでにも何度も議論と推敲を重ねてきました。 下記のミッション決定までの変遷を見ると、delyのバリュー *2 がSREのミッションと紐付けられ、「事業継続性」の言葉が持つ意味が噛み砕かれて「プロダクトの価値最大化」に置き換えられていることが分かります。 「事業継続性」という言葉を置き換えたのは、SRE以外の開発部メンバーにもヒアリングを行ったところ、この言葉の受け取り方が人によって異なっていたためです。ミッションはチームの顔でもあり、SRE以外のメンバーにも私たちの考えが正確に伝わる必要があると考えているため、delyのSREらしさを表現できる「プロダクトの価値最大化」という言葉を採択しました。 # 再考前 クラシルの信頼性や事業継続性を担保しつつ、ユーザーに価値を素早く提供できるように設計と運用を改善し続ける。 # 仮決定 delyが持つ熱量と幸せをできるだけ多くの人に届けられるように、事業継続性の維持・向上をシステム運用において担保する。 その過程において最善の選択が何であるかを考え、システムの設計と運用を改善し続ける。 # 本決定 delyが持つ熱量と幸せをできるだけ多くの人に届けられるように、プロダクトの価値最大化をシステム運用において実現する。 その過程において最善の選択が何であるかを考え、システムの設計と運用を改善し続ける。 このように、SREチームの目指すべきところを明確に言語化することで、「私たちはプロダクトの価値最大化を実現するために、システムの設計と運用において最善なアプローチは何か」という思考を持って業務に取り組めるようになったと思います。 また、SREが解決していくべき課題のアプローチ方法として、「信頼性」、「開発速度」、「運用効率化」の指標に重みを付けているという話は前回の記事で書きましたが、こちらも同様で、メンバーによって重みの定義の解釈が異ならないように具体性を持たせ、重みの判断に迷わないようにしました。 # 信頼性 4pt: サービスダウンやパフォーマンス悪化等、ユーザー体験が著しく損なわれる可能性が高い or 既にユーザーへ悪影響がでている 3pt: 信頼性を下げている原因となっているが、現段階ではユーザー体験が著しく損なわれる可能性が低い 2pt: 現状は問題として顕在化していないが、解消するとより信頼性の向上につながる 1pt: 信頼性の向上には影響がない # 開発速度 4pt: 全体、または各Squadの機能開発速度を既に大きく妨げている原因となっている or 開発速度を著しく下げている可能性が高い 3pt: 開発速度を下げる要因の1つではあるが、現段階では致命的な問題になっている可能性が低い 2pt: 現状は問題として顕在化していないが、解消するとより開発速度の向上につながる 1pt: 開発速度の向上には影響がない # 運用効率化 3pt: SREの運用業務の負荷が既に高く、自動化することで運用コストが大幅に削減できる 2pt: SREの運用業務の負荷がそこまで高くないが、自動化することで運用コストが削減できる 1pt: 運用作業の自動化によって運用コストの削減につながらない 細かい部分ではありますが、自分たちのコアとなる部分の共通認識を揃えることで、課題レビュー時の観点としても流用できるので、レビュイー、レビュアー双方の議論も円滑に進みやすくなったと感じています。 効果が伝わりやすい指標を持つ SREのようなシステム運用に責務を持っているチームは、ビジネスや機能開発を担うチームと異なりKPIとなり得る指標を定めることが難しい側面を持ち合わせていますが、SREが行った改善業務がどの程度システムにインパクトを与えるものなのかを定量的に示す指標がある方が良いと考えています。 なぜなら、チーム内の意思決定を円滑にするだけではなく、チーム外のメンバーにも私たちSREの成果を伝えやすくするためです。 SREと言えば、SLO/SLIで使われている信頼性や開発速度を表すメトリクスに対して目標値を設定し、チームのKPIとして定めるのが分かりやすいですが、これらの指標計測だけだと私たちの改善業務がどれくらいのインパクトを与えるものなのかはチームや部署が異なると伝わりづらいのではないかと思います。 *3 その理由について、クラシルを支えるシステムを例に説明します。 クラシルは、デプロイ起因やAWSの障害によって一時的に信頼性の指標が悪化することはありますが、SLO違反することはほとんどなく安定した状態を保っています。 これは、想定外の複雑さによって信頼性が低下することを防ぐために、SREがサーバーサイドの設計レビューに参加していたり、サーバーサイドのメンバー自身も意識的にアプリケーションエラーの改善に取り組む文化が既に形成されているからです。 このような安定したシステムにおいて、例えば、信頼性が下がるリスクを未然に防ぐような改善を実施したとしても、SLOに定めている指標の変化は微々たるもので、他のチームから見るとSREの改善業務による影響度が小さく感じてしまうかもしれません。 このような問題の有効な解決案の1つとして考えられるのは、私たちのチームが課題の優先度を決定するときにも使っている信頼性、開発速度、運用効率化の指標でSREの改善業務の影響度を示すことです。 これらの指標は、私たちが解決すべき課題の優先度順を定量的に表している値ですが、これはシステムに対してどの程度インパクトを与える改善なのか示す指標でもあるので、業務内容を詳しく知らないチームから見てもシステムの改善状況や影響度を理解することが容易になり、自分たちが上げた成果がより伝わりやすくなります。 SLOで定めている指標の他にもSREの改善業務のインパクトを示す指標を持っておくことで、複数の側面からSREが成し遂げた成果について話すことができますし、数値データがあることによって客観的な判断を下すことが容易になるので感情論を挟まなくても良くなるでしょう。 単純さを保つ 「SRE サイトリライアビリティエンジニアリング」の9章「単純さ」 *4 にも、システムの想定外の複雑さを減らし単純さを追求することが信頼性と開発速度の向上につながると書かれている通り、運用負荷の計測など直接システムに関わらない業務においても、できる限り単純さを保つことはチームの負担とストレスを増大させないためにも重要だと思います。 例えば、私たちのチームの運用負荷計測におけるルールは、基本的に以下の3つです。 プロジェクト業務(SRE課題の取り組み)とオーバーヘッド(MTG、1on1、評価等)以外のものは全て計測対象すること 計測対象の運用業務に費やした時間と対応した回数を記録すること 1週間の運用負荷が50%を超過する場合、対応フローに沿ってネクストアクションを決めること ルール自体はシンプルさを保ち、あらかじめ振り返り観点・高負荷時の対応フローを明確に定義しておけば、運用コストを最小限に抑えられます。 高負荷時における対応フロー 何かしら新しい仕組みを導入するときは、人間の判断疲れを防ぐためにもどの程度の負荷であれば許容できるのかメンバーと話し合って設計してください。 必要かもしれないという理由だけで最初から多数の計測を始めてしまうと、自分たちの目的からずれて意味のない計測になってしまったり、管理コストだけでなくチームのストレスが蓄積してしまったりするので気をつけましょう。 また、単純さを保つためには、定期的な振り返りや見直しも必要だと思います。 delyはクォーター制を導入しており、3ヶ月に1回目標設定するタイミングがあるので、その機会に合わせて運用負荷計測やSRE課題の運用方法について振り返り会を実施しています。 振り返り会以外にも「これやりづらいのでは?」と感じたらSlack上でヒアリングしたり、サクッと口頭で話し合うようにしているので、微調整は随時行うことで運用が形骸化しないように工夫しています。 SREチームの半年間の成果 私たちのSREチームが本格的に始動してから約半年が経過したので、実際にどれくらいの改善が進んでいるか報告したいと思います。 SRE課題 私たちのプロジェクト業務であるSRE課題の取り組みの進捗は、全51個の課題中8個の課題が対応完了し、優先度の消費ポイントに換算すると全体の29.4%の改善が進みました。 実際にどんな課題が解消されたかというと、アプリケーション動作に対する権限付与の最小化や動画変換で使われている自社開発基盤のブラックボックス化の解消、その他運用効率化のための改善業務が実施されました。 SRE課題表 また、対応完了した課題はSRE月報にも記載するようにしており、Slackの開発部チャンネル上でも共有を兼ねて報告しています。どんな取り組みが行われたかについては、より多くのメンバーに興味を持ってもらえるようにポップ感と分かりやすさを重視しています。 SRE月報を発行したときのお知らせ 運用負荷の変動 運用負荷に関しては、8月に計測を始めてから次第に減少傾向を示し、現在は20%付近で推移しています。運用負荷が減少傾向であるのは、この半年間で取り組んだ課題が運用負荷に影響を与えるものが多く含まれていたことや変動が可視化されることで自分たちの意識が運用負荷やその偏りを減らす方向に向くようになってきたことも要因の1つかと思います。 運用負荷の計測 最後に 今回は、前回の記事では触れなかったSREチーム体制を整備する上で意識したことやそれによって得られた成果について書きました。 書かれていることは当たり前のことではあるのですが、当たり前だと思っていても意識していなければおざなりになりやすい部分でもあると思います。そういった部分を改めてチームで議論し、クラシルのSREはプロダクトに対してどのようにアプローチするのがベストなのかを考えられたので、時間はかかってしまいましたが良い機会だったと感じました。 実際に取り組みを進めていくと、チームの暗黙知になっていた部分が可視化されたことで隠れたストレスが減少したり、数値データに基づいて意思決定できるのでチームが自律的に動けるようになったなど、メリットが大きいと感じています。これからも、よりSREの価値を最大化しプロダクトの成長を加速させていくために、システム設計、運用を改善していきたいです。 最後になりますが、クラシル開発チームでは、現在以下の職種を 絶賛大大大募集中 です! iOSエンジニア Androidエンジニア サーバーサイドエンジニア SRE データエンジニア 本記事を読んで少しでもdelyに興味を持っていただいた方はこちらをご覧ください。 dely.jp SREチームリーダーの高山がMeetyでもカジュアル面談を実施しているので、お気軽にお声がけください。 meety.net *1 : SREの探求――様々な企業におけるサイトリライアビリティエンジニアリングの導入と実践 *2 : delyのバリューの詳細はこちら: https://speakerdeck.com/delyinc/delyculture *3 : もちろん、私たちも信頼性の指標としてシステムの稼働率、レイテンシ、開発速度の指標としてテスト実行時間、リリース数等を追跡していますし、SREの役割を果たすためにも必要な指標です。 *4 : SRE サイトリライアビリティエンジニアリング――Googleの信頼性を支えるエンジニアリングチーム
アバター
こんにちは。クラシル開発部でバックエンドエンジニアをしているyamanoiです この記事は dely Advent Calendar 2021 19日目の記事です。 昨日はparayaさんの「delyにAndroidエンジニアとして入社して5ヶ月が経ちました」という記事でした 今回は先日発表させていただいた新サービス「クラシルショート」の動画変換にAWS MediaConvertを利用してみたので Rubyから利用する方法や、実際に使ってみた感想を書きたいと思います! prtimes.jp aws.amazon.com なぜAWS MediaConvertを選択したのか 最初になぜ動画変換にAWS MediaConvertを利用したのかを軽く説明すると クラシルには動画変換を行う自社開発された基盤が存在しているのですが、 自由度が高い一方で開発されてから時間が経過しているため、当初作ったエンジニアが存在しなかったり内部の処理がブラックボックスになっていたりと、メンテナンスが困難な状態にありました。 先日発表したクラシルショートでも動画変換処理を行う必要が出てきたのですが、AWS MediaConvertでやりたい事が実現できそう & 今後はできるだけマネージドサービスで完結できるようにしたいという想いから利用する選択をしました。 AWSにはElasticTranscoderという似たようなサービスも存在しますが、あまり開発が活発でない & 公式もMediaConvertの利用を推奨しているように感じました。 クラシルでの使い方 クラシルではどんな感じに利用しているかを書きたいと思います Rubyからジョブを作成する 弊社はバックエンドはRubyを利用しており、 RubyにもAWS MediaConvertのSDKが存在するため、こちらを使ってジョブを作成しています。 require ' bundler/inline ' gemfile do source " https://rubygems.org " gem ' aws-sdk-mediaconvert ' end endpoint = ' endpoint name ' queue_name = ' queue name ' role_name = ' role name ' settings = {} client = Aws :: MediaConvert :: Client .new( endpoint : endpoint) client.create_job({ queue : queue_name, role : role_name, settings : settings }) ジョブを作成する際に、ジョブの定義を渡す必要があります。 ジョブの定義はAWSのマネジメントコンソール上でジョブの設定を行い、「ジョブのJSONを表示」を押すことで定義をJSONとして吐き出すことができます。 ただこれをそのまま利用することができず、RubyのネイティブHashに置き換える必要があります。 ドキュメントにオプションが記載されているので、これと睨めっこしながら置き換えていく必要があります { " Settings ": { " Inputs ": [ { " TimecodeSource ": " ZEROBASED ", " VideoSelector ": {} , " AudioSelectors ": { " Audio Selector 1 ": { " DefaultSelection ": " DEFAULT " } } , " FileInput ": " s3://some-s3-bucket/input.mp4 " } ] } } ↓ settings = { inputs : [ { " timecode_source " : " ZEROBASED " , " video_selector " : {}, " audio_selectors " : { " Audio elector 1 " : { " default_selection " : " DEFAULT " } }, " file_input " : " s3://some-s3-bucket/input.mp4 " } ] } https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/MediaConvert/Client.html#create_job-instance_method JSONをそのまま突っ込めるように今後のアップデートに期待します ジョブテンプレートの利用について MediaConvertにはジョブテンプレートという概念があります ジョブの定義をテンプレートとして保存し、ジョブ作成時にテンプレート名を指定することで、ジョブ定義の記述を省略することが可能です。 クラシルでもジョブテンプレートの利用を検討したのですが、terraformがリソース対応しておらず、CloudFormation等の別の手段を検討する必要がありました。 そのため一旦ジョブテンプレートの利用はせず、ジョブを作成する際にジョブ定義を書く方針にしました。 ちなみにCloudFormation(CDK)で管理する場合はこんな感じでジョブテンプレートを作成できます import cdk = require ( '@aws-cdk/core' ); import { Construct } from 'constructs' ; import * as mediaconvert from '@aws-cdk/aws-mediaconvert' ; const json = ` { "Inputs": [ { "TimecodeSource": "ZEROBASED", "VideoSelector": {}, "AudioSelectors": { "Audio Selector 1": { "DefaultSelection": "DEFAULT" } } } ], "OutputGroups": [ { "Name": "File Group", "OutputGroupSettings": { "Type": "FILE_GROUP_SETTINGS", "FileGroupSettings": {} }, "Outputs": [ { "VideoDescription": { "CodecSettings": { "Codec": "H_264", "H264Settings": { "RateControlMode": "VBR", "MaxBitrate": 10000, "Bitrate": 10000 } } }, "AudioDescriptions": [ { "CodecSettings": { "Codec": "AAC", "AacSettings": { "Bitrate": 96000, "CodingMode": "CODING_MODE_2_0", "SampleRate": 48000 } } } ], "ContainerSettings": { "Container": "MP4", "Mp4Settings": {} } } ] } ], "TimecodeConfig": { "Source": "ZEROBASED" } } ` export class CdkSampleStack extends cdk.Stack { constructor( scope: cdk.Construct , id: string , props?: cdk.StackProps ) { super( scope , id , props ); new mediaconvert.CfnJobTemplate ( this , 'sample' , { settingsJson: JSON .parse ( json ), description: "sample description" , name: "sample" } ) } } 作成したジョブテンプレートは以下のように利用できます require ' aws-sdk-mediaconvert ' client = Aws :: MediaConvert :: Client .new( endpoint : endpoint) client.create_job({ queue : queue_name, role : role_name, job_template : ' sample ' }) 変換結果を受け取る 動画の変換が終わったかどうかをアプリケーションが知る必要があるため、Cloudwatchイベントを利用して ステータスの変更を受け取っています。 変更通知はlambdaを経由してアプリケーションに通知されるようになっています。 EventBridgeを使うことでLambdaを咬ますこと無くエンドポイント通知できるようになったので、今後はこちらを使おうと思います aws.amazon.com 最後に 今回はMediaConvertについて紹介しました! MediaConvertには動画変換に必要な基本的な機能に合わせて、独自の拡張機能も多く大変便利なサービスです 少しでも気になった方は是非試してみてください delyではエンジニア、デザイナー、PdMを積極採用しています。ご応募お待ちしております! dely.jp
アバター
この記事はdely Advent Calendar 2021の18日目の記事です。 昨日はumemoriさんの Jetpack Composeにおける画面遷移とは? でした。 Jetpack Composeの内部処理にも言及していますので興味のある方は良ければ覗いてみてくださいね🤖 挨拶 軽い経歴 入社を決めたポイント 入社時のオンボーディング 入社して良かった点 メンター制度 情報の透明度が高い 受け身のエンジニアが少ない 5万円の機材購入サポート More まだまだ改善の余地のあるプロダクト 最後に 挨拶 初めまして。delyでクラシルAndroid開発を担当しているparayaです。 7月にdelyに入社してからもう半年近く経とうとしています。月日が経つのは早いですね。 昨年に引き続きdelyでは年末のアドベントカレンダーをやろう!という流れで、私も入社エントリを書いてみました。 軽い経歴を紹介しつつ、入社を決めたポイント、入社してよかった点を紹介します。 軽い経歴 殆どスマホ業界でエンジニアとして仕事をしてきました🧑‍💻 iOS/Androidで小さいアプリを4本ほどと、大きめのアプリでは某婚活マッチングアプリのAndroid版を作ったり、 直近ではVR業界にいてDayDream向けVRや、スマホVRをUnityで作っていました。 いわゆるVTuberの裏の人の仕事ですね。 入社を決めたポイント 料理が好きだったから。というのが一番の理由です🍳 最近ではリモートワークも多くなったこともあってほぼ毎日自炊していますし、トレーニングを少ししていることもあって栄養学を調べるのが好きです。 好きなことと事業がマッチしていたからというのが大きいですね。 前職までは職場環境だったりで決めていたのですが、興味のある事業で仕事をするというのはやっぱり純粋に仕事が楽しいです。 面接してみてエンジニアのスキルが高そうというのも魅力でした。実際に入社してみてスキルレベルの高い人が多く、刺激を受けることも多いです。 この辺りの話はPodCastでインタビュー受けましたのでそちらもぜひ😄 open.spotify.com Androidチームの社内風景 入社時のオンボーディング 大体入社後のコミュニケーションは割と飲んだりランチ行ったり〜が多いイメージですが、クラシルAndroidチームでは weboxを使ったコミュニケーションをしたり、ドラッカー風ワークショップ、who am iを行ったりしています。 こちらは別途記事があるので良ければどうぞ!(私が入社した時の記事です☺️) tech.dely.jp 入社して良かった点 メンター制度 1on1のメンターとは別に、新卒&中途入社者向けの業務のサポートを行ってくれるメンター制度があります。 中途なのであまりサポートを考えていなかったのですが、メンターとして何でも聞いていいよ。悩んでるならとりあえず聞いていいよ。 というのは直近Androidから離れていた身としてはメンタル的にとても助かりました。ConstraintLayout久しぶりで結構忘れていたりとかね😇 実務では、クラシルAndroidではMVIアーキテクチャで独自にView/Component階層を構築しているので、そういった実装のポイントを教えていただきました。 私のメンターはnozakingさんなのですが実装から会社のルールまで丁寧に教えていただけて感謝しています。 情報の透明度が高い delyは情報の透明度がとても高い会社です。業務上のやり取りがSlackやドキュメントでほぼ全て見れます。 例えば役員同士のアイデアのぶつけ合いなんかもSlackで見れたりします。 前職でも情報の透明度は高いと思っていましたが、役員だったり上層部のドラフトな情報は見れなかったりして唐突に大きい話が出たりしていたので、この点はとても良い文化だと思います。 その分情報量が多いので必要/不必要な情報のフィルタリングは必要ですが、知りたい情報を知れるというほうがメリット大きいと感じています。 受け身のエンジニアが少ない エンジニア全体的に誰かがやってくれるのを待つ、タスクを待つ。というよりは自分から仕事を拾っていく、回していく人がとても多い印象です。 実際にPdMをしたり、エンジニアリングマネージャーをしたり、プロジェクトマネジメントをしたり、採用に関わったり…実装以外のことも積極的にやっていく人が多いし、会社の文化でもあります。 割と実装以外の事は全くやらないというエンジニアは結構多いですが、同僚のエンジニアはそういった事がないので良い刺激になっています。 5万円の機材購入サポート 通常のPCとディスプレイ供給以外にも、5万円まで業務で必要な物を購入可能です 💰 ディスプレイを増やしたり、スタンディングデスクにしたり、専用iPhone購入したり…とても便利な制度です。 私はフットレストを購入して快適に業務できるようにしています😋 More まだまだ改善の余地のあるプロダクト クラシルは割と知名度が高く、サービスとしての完成度も高いと思っていましたが、入社してみるとサービスとしてもAndroidのプロジェクトとしてもまだまだ改善すべきポイントは多いです。 最近クラシルショートというユーザ投稿型機能もリリースされましたが、他にも新機能開発まだまだ盛りだくさんです。 Github上のissue数もまだまだ多いですし、パフォーマンス改善、ビルド速度改善、CIなど私が見えてる範囲でもまだまだ改善の余地がある状態です。 最後に そんなまだまだ改善できるクラシルでは、Androidエンジニアを絶賛大募集しています🙌 テックリード的なシニアなエンジニアはもちろん、料理好きなスキルアップしたい若手エンジニアも大歓迎です🤝 カジュアル面談も行っていますし、どういった仕事をしているのかエンジニアと話してみたいという方でもOKです。 下記の採用リンクから応募いただいてもOKですし、Twitterから気軽にDM頂けてもOKです! お待ちしております😄 dely.jp Tweets by dely_developers twitter.com
アバター
はじめに こんにちは。クラシルのAndroidアプリチームのテックリードのうめもりです。 この記事はdelyアドベントカレンダー17日目の記事となっています。 16日目である昨日はshita-shunさんの swift-algorithmsを初めて触ってみた。 という記事でした。ご興味ある方は是非読んでみてください。 Jetpack Composeでは画面という概念が大きく変わる Jetpack Composeの正式版がリリースされて半年ほど経過し、すでに本番プロダクトに投入している方や、現行のプロダクトへの導入を検討しようとしている方などいらっしゃると思います。弊社でも向こう1年以内くらいの導入を検討しており、現在は実際に導入するとしてどのような設計に移行するかを検討している段階です。 実際にJetpack Composeを採用するにあたり、Jetpack Composeのような宣言的UIツールキットを使ったUIプログラミングに移行するためには、既存のUIプログラミングの考え方はある程度捨て去る必要があります。そして、それは画面や、画面遷移という概念においても同じで、今までのAndroidのUIプログラミングの考え方とは違う考え方を採用する必要があります。 Androidにおける画面遷移とは何か? さて、Jetpack Composeにおける画面遷移とは何なのかという話をする前に、そもそもAndroidにおける画面遷移とは何なのかを振り返っておきましょう。 Androidアプリケーションにおいて、画面を表現する手段は様々なものが用意されています。 Activity Androidアプリケーションにおける最も基本的な画面を表現する手段はActivityでしょう。アプリケーションを立ち上げる時の初期画面としてAndroidManifestに指定することで、特定のActivityを起動して画面表示を行ったり、 this @Activity.startActivity(intent) といったコードで画面遷移を行うことが出来ます。 そして、AndroidOSがタスクという単位でアクティビティのバックスタックを管理し、バックボタンなどを使ったナビゲーションを行うことが出来るようになっています。 Fragment Android 3.0から導入されたのがFragmentです。Fragmentは画面の一部を表現するコンポーネントや、画面全体を表現するコンポーネントとして使うことができます。 Activityの中で、 val tx = this @Activity.getFragmentManager().beginTransaction() tx.replace(R.id.fragment_container, SampleFragment()) tx.commit() といったコードでFragmentを表示したり画面遷移することが出来、FragmentManagerがバックスタックを管理し、バックボタンなどを使ったナビゲーションを行うことが出来ます。 今ではJetpack Navigationを使うことで、Activity内でのFragmentを使った画面遷移がやりやすくなりましたね。 今表示されている画面が何かというViewの状態をコントロールするのが画面遷移だった ここまでActivityやFragmentといったAndroidにおける画面の管理を行うための仕組みを振り返ってきましたが、ActivityにしろFragmentにしろ、Activityであれば 今表示されているActivityが何か、 Fragmentであれば 今表示されているFragmentが何か 、というViewの状態があり、それを変化させることを画面遷移と呼んできました。 では、Jetpack Composeにおける画面遷移とはなんでしょうか? Jetpack Composeにおける画面遷移とは何か? Composable functionは (State) -> Composition という変換を行う関数 Jetpack Composeが今までのUIプログラミングと大きく違うのは、Composable functionの内部では副作用を発生させずに、引数とrememberされたStateのみでCompositionを構築することを要求する点にあります。この仕組みによって常に(State) -> Compositionという参照透過な関数を適用することで、宣言的にUIを構築することが出来るようになっています。 重要なのは、 必ずStateを表現するオブジェクトが先にあり、それを使ってViewが構築されている 、という点です。そして、それは画面遷移においても同様なのです。 Navigation ComposeのNavHostの実装をのぞき見してみる Jetpack Composeでは画面遷移にNavigation Composeを使うことが推奨されています。Navigation Composeは、Navigation componentをJetpack Compose上で使うことが出来るようにするライブラリーですが、実装自体は非常に薄いものになっています。 Navigation ComposeではNavHostというfunctionを使うことでナビゲーショングラフを定義することが出来ます。Android公式のNavigation Composeのコードを引用すると、このようになっています。 NavHost(navController = navController, startDestination = "profile" ) { composable( "profile" ) { Profile( /*...*/ ) } composable( "friendslist" ) { FriendsList( /*...*/ ) } /*...*/ } https://developer.android.com/jetpack/compose/navigation NavHostの実装をのぞき見してみましょう。 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt;l=139;drc=4043c16795c46b8a3bc915e1994bf9dccd2eeb15 細かい実装の話は割愛しますが、重要な部分はこの val backStack by composeNavigator.backStack.collectAsState() 部分で、Navigatorから更新されたバックスタックが通知されるたびに、NavHostがrecomposeされるようになっています。 (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry) そして、NavHostがrecomposeされる度にこの処理が実行されます。ComposeNavigator.Destinationのコードは以下のようになっており、 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt;l=78;drc=4043c16795c46b8a3bc915e1994bf9dccd2eeb15 public class Destination( navigator: ComposeNavigator, internal val content: @Composable (NavBackStackEntry) -> Unit ) : NavDestination(navigator) 該当の個所では末尾にあるBackStackEntryを引数にComposable functionを呼び出しています。つまり、NavHostはrecomposeされる度に (NavBackStackEntry) -> Composition となるComposable functionを呼び出すことで、画面を更新しているのです。 画面の実態はBackStack Navigation Composeでは、NavigatorからBackStackを取り出し、最終的にはリストの末尾にあるBackStackEntryを使って画面を構築しています。 つまり、画面遷移を行う側として実際に行っていることは、 バックスタックのリストを操作することだけ なのです。 必ずバックスタックを表現するオブジェクトが先にあり、それを使って画面が構築されている(結果的に画面遷移が行われる) 、というのがJetpack Composeにおける画面遷移、といえます。 画面遷移の構成要素 NavHostのコードはわずか100行弱のコードですが、 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt;l=92;drc=4043c16795c46b8a3bc915e1994bf9dccd2eeb15 この中に画面遷移に必要な全ての要素が入っています。 バックスタックを表現するオブジェクトのState バックボタンのコントロール トランジション もちろんJetpack Navigation並みの機能(ディープリンク対応など)を実現するにはさらなる実装が必要になりますが、基本的にはこの3つがそろっていれば画面遷移を行うことが出来るようになるでしょう。もし興味がある方は、冬休みに自作の画面遷移ロジックを書いてみてはいかがでしょうか。 おわりに クラシルのAndroidチームでは、生産性の高いアーキテクチャを維持・アップデートしつつも楽しくプロダクト開発できる人、したい人を求めています。 今回の記事を読んで興味が出てきた方、是非お話ししましょう! 下記の採用リンクから応募いただいてもOKですし、Twitterから気軽にDM頂けてもOKです! dely.jp Tweets by dely_developers twitter.com
アバター
どうも、クラシルAndroidエンジニアの @MeilCli です。記事タイトルの通りdelyではドキュメント管理ツールをNotionに移行中で、その作業の一部に関わったのでいろいろと共有しようと思います また、Notion運用を始めたばかりでベストプラクティスにたどり着けていないところがあると思うので、よりよい運用方法の知見があればコメントで共有していただけると助かります はじめに delyではクラシルやTRILLなど様々なプロダクトを開発しています。今までの組織構造は歴史的経緯もあり、クラシルが複数の部門にわかれている一方、TRILLは1つの部門となっていました。そのため先日組織改編があり、カンパニー制が導入されることになりました。平たく言うとプロダクトごとに部門を再編したという感じですね 一方で各カンパニーでは様々なツールが使われています。delyは情報の透明性に力を入れており、いろんなツールに情報が分散したらアクセス性悪くなるよね、カンパニーに所属している人にしか閲覧できない情報があったらまずいよねなどといった課題感が出てきたため、このたびNotionを導入することになりました どういう経緯で関わったか Notionを導入すると言っても、それまで使用してきたドキュメント管理ツールから既存ドキュメントを移行しなければなりません。クラシルAndroidチームでは自分がそのあたりの作業の担当となりました 一方で全社的にドキュメントは各カンパニーごとに用意したDocumentDBで一括管理しようという話になっていました。DBの設計にはエンジニアが担当したほうがいいだろうという話になり、たまたま目に入った自分にそのDBの叩きを作ってもらおうという流れになったため、自分がクラシルカンパニーのNotion運用設計に大きく関わることになりました そもそもNotionとはなんぞや Notionをあまりご存知でない方向けに簡単に説明をすると、WikiとDBが融合したようなツールです www.notion.so 詳しくは公式ページをご覧ください 階層を作ってページを作っていける他、DBに情報を入れていくことによってタスク管理やドキュメント一覧を作ることができます。DB機能はデータとなるDB本体とそれをどうページに表示するかで別れていると言え、同じDBをソースとするけどあるページではタスクボードのように表示し、あるページではロードマップのように表示することができます。このDBをどう設計し、どう運用するかは利用者次第という感じになっています まずは運用設計をしてみる 自分が運用を考え始めた時点ではある程度の階層構想ができていました。部門・プロジェクトによる第一階層とチームや機能などによる第二階層です。全社的にあまり階層を増やさずにDocumentDBにドキュメントを集中させることで情報へのアクセス性を担保しようという考えがありました 現在のクラシルカンパニーの領域の抜粋ですが、こういう感じの構造になっています。また、画像には写せていないですがクラシルカンパニー直下にDocumentDBが配置されています DocumentDBに格納したドキュメントをこの各階層のところで表示する必要があるので、それを表すカラムを用意しました また、以前までのドキュメント管理で感じていた問題として、その場その時限りのドキュメントと今後もメンテし更新していくドキュメントが混ざり、情報へのアクセス性が減っていたということもありましたので、構造としてその場限りのものと更新していくドキュメント(クラシルではWikiという表現をしました)を表すことにしました まとめると以下のカラム構造になります 名前 First layer : Linked DB Viewerのフィルタリング条件で自動設定 Second layer : Linked DB Viewerのフィルタリング条件で自動設定 Wiki : Linked DB Viewerのフィルタリング条件で自動設定 Tag Participants 更新日時 : DBによって自動収集 作成日時 : DBによって自動収集 作成者 : DBによって自動収集 このDBを各階層に配置したLinked DBのViewerでフィルタリング表示しています。また、新しくドキュメントを作成するときは配置したい階層のViewerから新規ドキュメント作成してもらうことで必要なDBプロパティが自動で入力される状態になります 利用ハードルを下げる さて、DocumentDBの設計が概ね完了した頃にある疑念が湧いてきました。 果たしてNotionをみんな使ってくれるのだろうか という疑念です 今までのドキュメント管理ツールはUIの通りにドキュメントを作成すれば良いというシンプルな使い方でした。しかし、Notionは自由度の高いツールです。自由度が高い反面、覚えることが多かったり、本来やってほしくないような使われ方も当然できたりします。特に今回はDocumentDBにドキュメントを集約させていこうという運用ですので、ある程度はクラシルカンパニーの運用に沿った使い方をしてほしいのです また、一部マークダウン記法に互換性があるものの、完全にマークダウン記法で書けるわけでもない感じになっています。今までのドキュメント管理ツールから記法が変わったり、エンジニアの方にはマークダウンで書けるのか書けないのかよくわからないという状況に陥りそうでした そこでまずNotionの使い方ページを作ることにしました 使い方ページの目次の抜粋 実際の説明の抜粋 内容は画像のような感じです そしてこのページや運用に関するページを含む 最初に読む ページをクラシルカンパニー直下の目立つところに配置し、クラシルカンパニーのNotionを初めて使う人に読んでもらい理解してもらえやすくする構成にしました ハードルはもっと下げれる 最初に読む ページにはNotion自体の使い方やクラシルカンパニーにおける運用などのページがあります。それを読んで、覚えて、新しくドキュメントを書いていける自信を持ってもらうにはまだまだハードルが高いなと感じていました 幸い、NotionはGUIだけでも装飾付けしたドキュメントを書いていけるツールです。ほとんどのメンバーは新しくドキュメントを書けるようになれればそれでいいということに気づきました *1 そこで これさえ覚えておけばいいリスト を作成することにしました これさえ覚えておけばいいリストの抜粋 Notionは動画を埋め込むことができるので最も一般的であろう記事を書く動作は動画でわかりやすく説明を加えました *2 また、自分自身を含めて、Notionでどんなことができるかを試してみたい場面があったりするので、何でもしていい空間として Playground を用意しました。Playgroundで名前と説明のとおりに、個人領域を作成してくれたらその中なら何をやってもいいよという形にしています みんなの使い方としてはドキュメントの下書きをしたり、DBの機能を確かめたり、移行してきた既存ドキュメントの仮置き場にしたりと、様々な使われ方をされているようです 結果 結果としては利用ハードルを下げることを頑張ったのは成功だったかなと感じています。正式なアナウンスをして使い方を案内する前から、 最初に読む ページを閲覧してドキュメントを書いていってくれるメンバーがいたり、正式アナウンスをした際に取ったアンケートでも今のところはNotionで新しくドキュメントは書いていってもらえそうという結果になっています 課題点 カンパニー内すべてのドキュメントを1つのDBで管理するのは現実的には厳しい部分もあります。一部ドキュメントはどうしてもDBに入れれなかったり、一部部署の運用に合わせてDBのカラムを用意したりと、例外的な運用になっています この点は今後も柔軟に対応したいと考えていますし、そもそも1つのDBで管理するのは正しかったのかは継続的に振り返っていきたいと考えています Notionに要望を出せるとしたら分散されたDBを見た目上は1つのDBとして扱うUnion DB的な機能が欲しいところです。各階層単位などでDBを用意し、Root DBはそれらをUnionで繋げ、各階層のDB ViewerはRoot DBを表示したり、個別最適化が必要な場面では階層特有のDBを表示するということができるようになるとNotionの強みであるDBをさらに機能的に使っていけそうな感じがします 今後について まずは新規ドキュメントをNotionで書いていってもらえるように仕組みを整備したり、各担当者と調整をしたりしていきました。今後は既存ドキュメントを移行していく必要があるので、情報資産を失うことなくドキュメント移行をスムーズに進めていきたいと考えています また、タスク管理なども合わせてNotionで行うようにし始めているのですが、ドキュメント管理とはまた違った属性でもあるので、そのあたりもしっかり運用できていけるようにしていきたいですね Notionに詳しい人がいたらお力添えお願いします *1 : 運用していく人はごく一部ですしね *2 : 添付画像で説明していること以外にも覚えてもらうことはあります
アバター
こんにちは!2022年卒予定でクラシル開発部にて内定者インターンを行っている @takeshi_o4 です。 この記事はdelyアドベントカレンダー13日目の記事となっています。 (1人だけプロフィールがデフォルト文字になっていて恥ずかしい) 12日目である昨日はサーバーサイドエンジニア高松さんの SnowflakeのSQL APIをRubyで試してみた という記事でした。普段から開発をバンバン引っ張って頂いている高松さんの記事ですので、興味ある方はぜひ見てみてください!! はじめに 今回僕が伝えていきたいことは以下の3点です。 インターンとしてこれまで行ってきたこと インターンで感じたdelyの印象 千里の道も一歩から 現在delyに少しでも興味を持っている大学生の方には絶対参考になる記事にしますので、ぜひ最後まで見ていってください! インターンとしてこれまで行ってきたこと 少し紹介が遅れたのですが、僕はクラシルのサーバーサイドエンジニアとしてdelyで内定者インターンを行っています。 そんな僕が2021年5月にdelyにジョインして、これまでやってきた主な実装はこんな感じです。 クラシルショートBGM機能 著作権付きBGMデータのデリバリー機能 CGM関連の新機能(現在) 現在クラシルはクラシルショートという新規機能の開発を進めていますが、その機能に関する実装がほとんどです。 prtimes.jp クラシルショートBGM機能 クラシルショートBGM機能は、ショート動画作成に使用するBGMを扱えるようにする機能です。delyにジョインして初のタスクがプロダクトにとってクリティカルだったので、リリース時に緊張しまくっていたことを鮮明に覚えています。 著作権付きBGMデータのデリバリー機能 こちらのデリバリー機能は、著作権楽曲データを正しく受け取れるようにする機能です。著作権楽曲のデリバリーには 世界的に統一した仕様 が定められており、そこからキャッチアップする必要がありました。 社内に経験者も居らず、右も左もわからない状態からリリースまで持っていけたことはとても自信になりました。 CGM関連の新機能(現在) こちらは今も取り組んでいる内容ですが、CGM(クラシルショートなど)関連の新機能です。新機能ということでどうなるか分からない点が多いのですが、最大限スケール可能でかつ技術的負債を残さないような設計になるように進めています。 インターンで感じたdelyの印象 業務内容の公平性 今まで約7ヶ月程delyでインターンをしてきましたが、事実としてかなり主要な機能に関わることが出来ています。インターンだからといって、仕事上のタスクに配慮はあっても差別はないことは断言できます。 エンジニアが足りていない説も唱えられますが、僕にとっては最高のチャンスです。 どんどん主体的に開発をしていきたいという方にとっては最高の環境だと自信を持って言えます!! 会話の多さ 個人的に一番良いなと思っていることは、会話量の多さです。 分からないことがあったら推測をせず本人に聞くというカルチャーですが、それが形骸化しておらずしっかり社員レベルで実行されていると感じています。 そんな環境下なので聞くのが怖いとかは全くなく、誰でも席まで行って話すなんていうことは日常茶飯事です。部署を跨いだとしてもその人の席まで猪突猛進していく人が開発部には多い印象です。 最近だとリモートワークもありますが、普段から話しまくっているおかげでメンションするのが申し訳ないとかは微塵もないです。素直に仕事に向き合える文化です!!(本当です) マネジメント 次は少し目線をマネジメント側に移して、マイクロマネジメントをしていない点ということを話していきます。 マイクロマネジメントという言葉をなんとなく使いましたが、ガチガチに細かく言動をマネジメントをするという意味で使っています。 以前リモートワーク導入の話があり、坪田さん( @tsubotax )に出社のルールを質問した際に、 「そのへんのマイクロマネジメントをガチガチにするというよりも、この環境下で成果を出している人が評価される構造が適切だと思う。」ということをおっしゃっていて、とても腑に落ちたことを覚えています。 delyは所謂成果主義の会社です。どう頑張ったとかではなくて、何をしたかを見られます。(勿論その人のカルチャー的ふるまいなども評価に入りますがメインは成果) 良くも悪くも自己責任な環境下で過ごす時間は、1社会人として成長出来る可能性がとても秘められていると思います。 グローバル化 少しマネジメントの話と被りますが、delyではグローバル化の動きが進んでいます。 open.spotify.com 既に外国籍の方も在籍しており、中にいるとグローバル化の進行をひしひしと感じる毎日です。僕は英語レベルがテストによってB1とB2のちょうど間くらいですが、全社的に英語力向上に力を入れているのでB2,C1レベルと英語力あげていこうと燃えています。具体的に言えば、TOEFL iBT100点以上、IELTS8.0以上になりたいです。 英語B1 CEFR -定義とテスト | EF SET 英語が出来るようになることが目的ではなく、英語でコミュニケーションを取ることが目的になると一気に目的の具体性があがります。 明確な目的・目標がある中で過ごす日々はとても刺激的です。 千里の道も一歩から インターンを始める前はクラシルレベルのサービスなら派手なことをやっているんだろうなと思ったりもしていたのですが、実際そんなことはなく泥臭いことの積み重ねです。大きな目標は立てているものの、目の前の一歩がないことには始まらないということを日々痛感しています。 これはキャリアにおいても同じだと思います。 自分がどんなキャリアを考えていても、それは目の前の仕事の積み重ねです。逆に言えば目の前の仕事に向き合えていなかったらキャリアもそれ相応のものになると思います。 キャリアを考えるとそれで頭がいっぱいになることもあると思いますが、少しだけ脳筋になって「目の前の仕事の積み重ね」くらいシンプルが僕は好きです。 最後に どうでしょうか。皆さんが思っていたdelyと少し印象は変わりましたでしょうか? 本記事を読んで少しでもdelyに興味を持っていただいた方はこちらをご覧ください。 dely.jp 最後まで読んで頂きありがとうございました!
アバター
こんにちは。クラシル開発部でバックエンドエンジニアの高松です( @takarotoooooo ) この記事は dely Advent Calendar 2021 12日目の記事です。 昨日はknchstさんの「 クラシルiOSのパッケージマネジメントについて 」というお話でしたiOSの開発にも興味がある方はぜひ見てみてください! 今回は先日データエンジニアのharryさんが「 クラシルでのSnowflakeデータパイプラインのお話&活用Tips 」で紹介していたSnowflakeをアプリケーション側から利用する方法を試してみたので紹介します はじめに アプリケーションからログデータを利用できると何がうれしいのか? クラシルでは現在Snowflakeを用いてログデータの可視化・分析を行っています 分析結果を元に意思決定を行い、改修することで収集したデータを開発に還元しています しかし可視化・分析だけでなく、アプリケーションからログデータを利用できるようにすることで、コンテンツ配信やレコメンドの最適化など実現可能な施作を増やすことができるため、結果としてサービス、プロダクトで解決できる課題の幅を広げることができます 注意点 今回はキーペア認証方式で利用する方法をご紹介しています また、SQL APIはプレビュー機能なので正式リリースされる際には仕様が少し変わっている可能性があるのでご注意ください docs.snowflake.com 準備 公開鍵と秘密鍵のペアを生成 公開鍵をSnowflakeユーザーに割り当てる 公開鍵と秘密鍵のペアを生成 秘密鍵を生成します openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 上で生成した秘密鍵を元に公開鍵を生成します openssl rsa -in rsa_key.p8 -pubout -out rsa_key.pub 公開鍵をSnowflakeユーザーに割り当てる 上で発行した公開鍵の内容を接続に利用するユーザーに設定します。 (ユーザーの変更はSECURITYADMINロールのユーザー以上である必要があるので、権限が足りない場合は然るべき人にお願いしてください) alter user [USER_NAME] set rsa_public_key= ' [PUBLIC_KEY_VALUE] ' ; 接続してみる 必要なもの 秘密鍵 Snowflakeのアカウント識別子 Snowflakeのアカウント Snowflakeのユーザー名 公開鍵のフィンガープリントを作る private_key = OpenSSL :: PKey :: RSA .new(pemlines) public_key = private_key.public_key public_key_fp = %( SHA256: #{ Base64 .encode64( Digest :: SHA256 .digest(public_key.to_der)).strip }) JWTの作成 ペイロードに以下のフィールドを持つJWTを作成します なお、ここではGem jwt を利用して生成を行なっています フィールド 値 iss ` `.` `.` ` sub ` `.` ` iat JWT が発行された時間(UTC のエポック開始からの秒数) exp JWT の有効期限が切れる時間(UTC のエポック開始からの秒数) jwt_created_at = Time .now.getutc lifetime = 60 * 60 qualified_username = %(#{ account.upcase } . #{ user_name.upcase }) payload = { ' iss ' : %(#{ qualified_username } . #{ public_key_fp }) , ' sub ' : qualified_username, ' iat ' : jwt_created_at.to_i, ' exp ' : (jwt_created_at + lifetime).to_i } algorithm = ' RS256 ' token = JWT .encode(payload, private_key, algorithm) APIリクエスト 生成したJWTを利用して、下記のURLにPOSTリクエストを送信します https://<アカウント識別子>.snowflakecomputing.com/api/statements リクエストヘッダ 項目名 値 Authorization Bearer ` ` Accept application/json Content-Type application/json X-Snowflake-Authorization-Token-Type KEYPAIR_JWT リクエストボディ 項目名 値 statement 実行するSQL timeout タイムアウト秒数 database データベース名 schema スキーマ名 warehouse ウェアハウス名 role ロール リクエストパラメータ 項目名 値 requestId リクエスト毎のuuid async falseで同期的にデータを取得 pageSize 取得するデータサイズ http = Net :: HTTP .new( %(#{ <アカウント識別子> } .snowflakecomputing.com ) , 443 ) http.use_ssl = true query_string = { requestId : SecureRandom .uuid, async : false , pageSize : 15 , nullable : true }.map { |k, v| %(#{ k } = #{ v }) }.join( ' & ' ) path = %(#{ uri.path } ? #{ query_string }) header = { ' Authorization ' : %( Bearer #{ token }) , ' Accept ' : ' application/json ' , ' Content-Type ' : ' application/json ' , ' User-Agent ' : ' applicationName/applicationVersion ' , ' X-Snowflake-Authorization-Token-Type ' : ' KEYPAIR_JWT ' } params = { statement :'SELECT * FROM CUSTOMER;' , timeout : 60 , database : ' SNOWFLAKE_SAMPLE_DATA ' , schema : ' TPCH_SF1 ' , warehouse : <ウェアハウス名>, role : <ロール>, } response = http.post(path, params.to_json, header) pp JSON .parse(response) こんな感じで結果が返ってきます { " resultSetMetaData " => { " page " => 0 , " pageSize " => 15 , " numPages " => 10000 , " numRows " => 150000 , " format " => " json " , " rowType " => [{ " name " => " C_CUSTKEY " , " database " => " SNOWFLAKE_SAMPLE_DATA " , " schema " => " TPCH_SF1 " , " table " => " CUSTOMER " , " scale " => 0 , " precision " => 38 , ~~~~~ " data " => [[ " 0 " , " 30001 " , " Customer#000030001 " , " Ui1b,3Q71CiLTJn4MbVp,,YCZARIaNTelfst " , " 4 " , " 14-526-204-4500 " , " 8848.47 " , " MACHINERY " , " frays wake blithely enticingly ironic asymptote " ], ~~~~~ " code " => " 090001 " , " statementStatusUrl " => " /api/statements/01a0cf10-0000-30b4-0000-40d5007afd72?requestId=30139506-efcb-40c0-af02-1cfb97c64b30&pageSize=15 " , " requestId " => " 30139506-efcb-40c0-af02-1cfb97c64b30 " , " sqlState " => " 00000 " , " statementHandle " => " 01a0d3bf-0000-30ee-0000-40d5007cf11e " , " message " => " Statement executed successfully. " , " createdOn " => 1639031029314 } おわりに 今回はSnowflakeのデータをアプリケーションで取得する方法を紹介しました 実用に向けていろいろと試していきたいと思います SQL APIはまだプレビュー機能なので、公式にリリースされて実用できることを楽しみに待っています より詳しい情報は下記に掲載されていますので、試してみてもらえると良いと思います docs.snowflake.com delyではエンジニア、デザイナー、PdMを積極採用しています。ご応募お待ちしております! ぜひ一緒にクラシルをより良いサービスにしていきませんか? dely.jp
アバター
SwiftUIをクラシルに導入した話 こんにちは。これはdely アドベントカレンダー10日目の記事となります。 今年も残りあと少しとなりました。クラシル開発部でiOSエンジニアをしている @yochidros です。 前回はharry( @gappy50 )さんの クラシルでのSnowflakeデータパイプラインのお話&活用Tips でした。 日頃redashを利用して分析をしている中でより便利になっていっているなと感じました!tipsも今後使っていこうと思います! 今日はSwiftUIをクラシルに導入した話を書いていきたいと思います。 背景 2019年に発表された SwiftUI ですが、iOSのバージョンが13.0以降でないと利用できない等制約があったためなかなかクラシルも導入できずにいました。 しかし、今年のクラシル自体のiOSのサポートバージョンを13.2に引き上げたことによってSwiftUIやCombineなどが特に制限を何もしなくても利用できるようになりました。 クラシル自体はUIKit x RxSwiftベースで構成されていてまだSwiftUIでの実装がありませんでした。 自分がちょうど新しく機能を開発するタイミングとサポートバージョン13.2にあげるタイミングが重なったのでチャレンジ的な意味で1機能を全てSwiftUIを使って開発しました。 自分も含め、iOSチームメンバーはSwiftUIについてまだそこまで経験・知見がないため、これらを全てSwiftUI x Combineに書き換えるよりかは新機能開発においてSwiftUI x Combineを導入していくという方針で進めました。 やったこと 今回新機能を開発したのはログイン・新規登録機能です。 ログイン画面 新規登録画面 すでにクラシルではログイン・新規登録画面は存在していましたが、webview経由でアカウントのログイン・新規登録をしていました。今回、SNSログインを導入するに当たってWebviewだった画面を全てNativeに置き換えるようにしました。 ...( 画面自体はシンプルな実装なのでSwiftUIでもできると思い、チャレンジした次第です。) View 機能自体の画面数は10画面ほどあります。基本的に1画面につき、1 view という構成です。 UI自体似ているところが多く一つのviewで内部のviewを切り替えたりすることもできましたが、コードの肥大化等後々のメンテナンスコストが高くなるため今回は1画面ごとに実装するようにしました。 ログイン画面ですと大まかに以下のようなコードになっています。 struct LoginView : View { var body : some View { VStack { Image( "logo" ) Text( "ログインする" ) ForEach(ProviderType.allCases, id : \. self ) { providerType in Button(action : { // tap provider button },label : { ProviderButtonLabel(type : providerType ) }) } HStack(spacing : 0 ) { Text( "クラシルは初めてですか?" ) Button(action : { // tap create account button }, label : { Text( "アカウント作成" ) }) } } } } UIKitだとオートレイアウトや UIStackView を駆使して記述しなければいけないところがSwiftUIだとこんなにシンプルにかけます。先ほど1画面につき1viewと言いましたが、中のボタンなどで他の画面にも利用するUIなどは一つのUIコンポーネントとして定義しています。(ex. ProviderButtonLabel) 本来であれば、bodyの中に全て記述するのではなく細かくviewを切り出してやる方が複雑なレイアウトなどになった時に見通しがよくなると思います。 var body : some View { VStack { headerView providerButtonListView footerView } } private var headerView : some View { Image() Text() } private var providerButtonListView : some View { ForEach(ProviderType.allCases, id : \. self ) { providerType in Button(action : { // tap provider button },label : { ProviderButtonLabel(type : providerType ) }) } } private var footerView : some View { HStack(spacing : 0 ) { Text( "クラシルは初めてですか?" ) Button(action : { // tap create account button }, label : { Text( "アカウント作成" ) }) } } Viewを作るときに苦労したこと 最新のAPIは気軽に利用できない LazyVStack, AsyncImage など iOS 14 , 15 でしか利用できないものは今回入れていません。iOS 13では利用できないので何かしら独自の機構を用意して #if availeble(iOS 14.0, *) などをしなければならないのでコストがかかるためです。 SwiftUIのコンポーネントだと足りないところが出てきた 上記のAPIにも関連しますが、iOS13を視野に入れて実装しようとするといくつか機能が不十分なところがありました。 例えば メールアドレスやパスワードの入力にTextFieldを利用して画面遷移時に TextField にフォーカスを合わせようとしましたがiOS15以上であれば focused(_): を利用すればできそうですが、iOS13以上となるとそれができないです。TextField自体、UITextFieldをラップしているのでやろうとすれば内部のsubviewの中のUITextFieldを見つけてゴニョゴニョするなどできるのですが、あまり良い方法だと思いません。 ViewInspecter を使えばより効率的にできると思いますが、今回は導入せず、 UITextField をラップしたViewを自前で用意しました。 ViewModel クラシルのアーキテクチャーはMVVMを採用しているのでSwiftUIでも同じように実装しました。UIKitでは書き方を揃えるために自前のframeworkを作ってそれを利用してMVVMを構築していました。自前のframeworkはSwiftUIには対応していないので新規にprotocolだけ作ってそれに沿って開発できるようにしました。 public protocol SwiftUIViewModelProtocol { associatedtype State associatedtype Action associatedtype Dependency init (state : State , dependency : Dependency ) func send (_ action : Action ) } 基本的には ViewModelはStateとActionを持ち、 initとfunc send(_ action: Action) 以外関数を持たないようにしています。 もっとシンプルにしようとすれば public protocol ViewModelProtocol { associatedtype State associatedtype Action func send (_ action : Action ) } これだけ定義すればUIKitとSwiftUI両方で使えるインターフェースになると思います。 実際上記のSwiftUIViewModelProtocolインターフェースを用いて定義したクラスをみてみます。 import ViewModelInterfaces import Combine protocol LoginRepositoryProtocol { func login (providerType : ProviderType ) -> AnyPublisher < AuthResponse , Error > } final class LoginViewModel : ObservableObject , SwiftUIViewModelProtocol { @Published private ( set ) var state : State private let actionSubject = PassthroughSubject < Action, Never > () var cancellables = Set < AnyCancellable > () init (state : State , dependency : Dependency ) { self .state = state var tmpProviderType : ProviderType? actionSubject  .sink(receiveValue : { [ weak self ] actionType in switch actionType { case let .tapProviderButton(providerType) : // do login case let .tapCancelButton : // do dismiss view case .onAppear : self? .state.providerTypes = [ .email, .apple, ... ] } }) .store( in : & cancellables) } } extension LoginViewModel { struct State { fileprivate ( set ) var providerTypes : [ ProviderType ] } struct Dependency { let loginRepository : LoginRepositoryProtocol } } // MARK : Action extension LoginViewModel { enum Action { case tapProviderButton(ProviderType) case tapCreateAccountButton case onAppear } func send (_ action : Action ) { actionSubject.send(action) } } ViewModel自体はStateとcancellablesと内部でアクションをハンドリングするSubjectだけを保持してその他は持っていません。Protocolに準拠するとこのようになり、誰もが似たよう実装になるので書き方がバラバラにならず統一すると思います。 ただactionSubjectをsinkしたときのクロージャー内で処理を書くとその内部でさらにsinkしたりしてネストが深くなるので見通しが悪くなると思います。なので自分はそれを回避するためにViewModel内でのみ使うActionを別途定義してそれを経由して状態の更新やAPIの通信を走らせたりしてます。 import Combine import ViewModelInterfaces protocol LoginRepositoryProtocol { func login (providerType : ProviderType ) -> AnyPublisher < AuthResponse , Error > } final class LoginViewModel : ObservableObject , SwiftUIViewModelProtocol { @Published private ( set ) var state : State private let actionSubject = PassthroughSubject < Action, Never > () var cancellables = Set < AnyCancellable > () private struct InnerAction { let loginAction = PassthroughSubject < ProviderType, Never > () } init (state : State , dependency : Dependency ) { self .state = state let innerAction = InnerAction() innerAction.loginAction .flatMap { dependency.login(providerType : $0 ) .handleEvents(receiveCompletion : { completion in switch completion { case let .failure(error) : // TODO : show error alert break case .finished : break } }) .map(Optional. init ) .replaceError(with : nil ) } .sink(receiveValue : { responseOptional in guard let response = responseOptional else { return } // Success }) .store( in : & cancellables) actionSubject .sink(receiveValue : { [ weak self ] actionType in switch actionType { case let .tapProviderButton(providerType) : innerAction.loginAction.send (providerType) case let .tapCancelButton : // do dismiss view } }).store( in : & cancellables) } } 以下省略 ... ここで注意なのがRxSwiftでも同じですが、 flatMap をするときにErrorを失敗時に返すようにするとそのaction自体の川はerrorを受け取って川を閉じてしまい、それ以降値は流れてこないです。なのでerrorを流さないようにerrorを置き換えて Never 型にしてます。 .map(Optional. init ) .replaceError(with : nil ) 何かしらerrorが起きてアラート等を出したい時は handleEvents を介して表示するようにしてます。 sink 時に受け取る値は必ずoptionalになってしまいますが、unwrapできるということは成功したと受け取れるのでわかりやすいかもしれません。 別の方法として AnyPublisher<Result<T, Error>, Never> を返すようにすれば Optional 型に変換しなくても川を閉ざすことなく処理を続行できます。 protocol LoginRepository { func login () -> AnyPublisher < AuthResponse , Error > , Never > } final class LoginViewModel : ObservableObject , SwiftUIViewModelProtocol { ... init ( ... ) { innerAction.loginAction .flatMap { dependency.login(providerType : $0 ) } .sink(receiveValue : { result in switch result { case let .success(response) : // success case let .error(error) : // error } }) .store( in : & cancellables) ... } } View側でViewModelを使う時はこうなります。 struct LoginView : View { @ObservedObject private var viewModel : LoginViewModel init (viewModel : ViewModel ) { self .viewModel = viewModel } var body : some View { VStack { Image( "logo" ) Text( "ログインする" ) ForEach(viewModel.state.providerTypes, id : \. self ) { providerType in Button(action : { viewModel.send(.tapProviderButton(providerType)) }, label : { ProviderButtonLabel(type : providerType ) }) } HStack(spacing : 0 ) { Text( "クラシルは初めてですか?" ) Button(action : { viewModel.send(.tapCreateAccountButton) }, label : { Text( "アカウント作成" ) }) } } } } ※ @StateObject はiOS14からなので今回は @ObservedObject を使用しています。 Viewは stateとsend しか使っていないことがみてわかると思います。 View <--- ViewModel.state View ---> ViewModel.send と単一方向での処理が簡潔に書けると思います。 View側ではLoginViewModelを直接持っていますが、今後protocolで持つようにはすればより疎結合になったテストがしやすくなると思います。 苦労したこと(ViewModel編) RxSwift --> Combineの変換 既存の実装だとRxSwiftで書いているのでCombineではそのまま利用できないです。なので何かしら変換する処理を書かないといけないのが大変でした。 OSSに RxCombine というRxSwiftとCombine間を双方向変換できるライブラリがあったのですが、当時RxSwiftをCarthageからCocoapodsに移行している段階で導入を断念しました。Carthage経由だとxcode13でビルドするのに一工夫必要だったりして大変だった記憶があります。。。🙏 今回は既存のAPI通信をする時に返り値が RxSwift.Single だったのでそこだけCombineに変換するようにしました。 import RxSwift enum APISession { static func send < Response : Decodable > (request _ : URLRequest ) -> Single < Response > { .create(subscribe : { _ in ... Disposables.create() }) } } import Combine import RxSwift struct Repository { func login () -> AnyPublisher < Response , Error > { var disposable : Disposable? return Deferred { Future < Response, Error > . init { promise in disposable = APISession .send(request : . init (url : URL ())) .subscribe( onSuccess : { response in promise(.success(response)) }, onFailure : { error in promise(.failure(error)) } ) } .handleEvents(receiveCancel : { disposable?.dispose() }) }.eraseToAnyPublisher() } } 現在はRxCombineを導入してもっと簡潔に書けるようになりました。 Routing 画面遷移などのrouting処理はSwiftUIだと Navigation, NavigationLink や .sheet .alert Modifierをview内で利用すれば遷移できるのですが、UIKitベースのViewと組み合わせると相性がよくありません。 さらに NavigationBar を画面ごとにカスタマイズするときにiOS13以上となると UINavigationBar.appearence() でしかできないのでこの画面だけ背景を変えたい時でもしっかりとライフサイクルを考えながら設定しないと全体の NavigationBar に影響が出るので注意が必要です。 なので今回はUIKitベースで遷移できるように UIHostingController を利用してます。 enum LoginBuidler { static func build () -> UIViewController { let vm = LoginViewModel() let vc = UIHostingController(rootView : LoginView (viewModel : vm )) vm.routePublisher .receive(on : DispatchQueue.main ) .sink(receiveValue : { [ weak vc] routeType in switch routeType { case let .presentAlert(message) let alert = UIAlertController(title : nil , message : message , preferredStyle : .alert) alert.addAction(UIAlertAction(title : "cancel" , style : .cancel, handler : nil )) vc?.present(alert, animated : true , completion : nil ) case .dismiss : vc? .dismiss(animated : true , completion : nil ) } }) .store( in : & vm.cancellables) return vc } } enum LoginRouteType { case dismiss case presentAlert(String) } final class LoginViewModel { let routePublisher : AnyPublisher < LoginRouteType , Never > var cancellables = Set < AnyCancellable > () init ( ... ) { ... let routeSubject = PassthroughSubject < LoginRouteType, Never > () routePublisher = routeSubject.eraseAnyPublisher() ... routeSubject.send(.dismiss) ... } } まとめ いかがでしたでしょうか? SwiftUIでのプロダクション開発について全体的に説明をしたので少しボリュームが大きくなってしまいました。 現在のクラシルだとログイン・新規登録画面や設定画面は全てSwiftUIで実装されています! 今回、初めてプロダクションでSwiftUIを使いましたが、まだまだ複雑なUIの実装をするとなるとUIKitで実装した方が良いところもあると感じました。 最後まで読んでくださった皆様に少しでも役に立てたら幸いです! 機能開発以外にもクラシルのiOSではリアーキプロジェクトも並行して動いています。 詳しくはdely Tech Talkで @inamiy さんと @RyogaBarbie さんが話しているので興味ある方は聴いてみてください! open.spotify.com 最後に delyではエンジニア、デザイナー、PdMを積極採用しています。新しい技術とかも積極的に導入しているので 少しでも興味がありましたら、お話だけでもできればと思います。 dely.jp
アバター
はじめに はじめまして。 クラシル開発部でデータエンジニアをしておりますharry( @gappy50 )です。 この記事は dely Advent Calendar 2021 および Snowflake Advent Calendar 2021 の9日目の記事です。 昨日はうっくんさんからのNotionでJiraを作ろう!というとても興味津々話でした!! やっぱりNotionは色々できるのでいいですね◎ それと私のお話で恐縮ですが、昨日はSnowflakeのイベント Snowday にてクラシルでのSnowflakeを活用したニアリアルタイム分析の事例についてお話をさせていただきました。 www.snowflake.com 今回はSnowdayでお話した内容のデータエンジニア寄りな詳細と、どのようにSnowflake *1 を活用しているかを紹介させていただきたいと思います! 最近のクラシルデータ基盤のお話 これまでのクラシルでのデータ基盤については以下の記事でも昨年ご紹介させていただきました! tech.dely.jp これらのデータ基盤は今のクラシルにとっては最適化されたデータ基盤として成熟しており、最小限のリソースでも日々のサイクルが回せるようになっています。 ただし、最近のクラシルでは以下のニーズを叶えられるデータ基盤が必要になってきています。 それらを実現すべく2021年の8月からSnowflakeを導入し、リアルタイム分析やアプリケーション利用が可能なデータ基盤を構築すべくデータパイプラインを整備しています! クラシルでのSnowflakeデータ基盤構成 ニアリアルタイムでのデータ分析を実現するために、まずは以下の構成でログデータを取り込むためのデータパイプラインを構築しました。 一連の流れは以下のようになってます。 s3に吐かれるログデータをSnowpipeにて取り込み Data Lakeに対してStreamを設定 マイクロバッチ的にTaskにてDWHへ加工処理を実施 Snowpipeはs3にログが配置されたのを検知して自動的にロード処理を実行してくれます。 ロード処理をしたデータはStreamで追加・削除・更新などの変更を追跡しており、新規で発生したデータのみを対象にして後続のテーブルへのデータ加工・格納をマイクロバッチで実行しています。 このプロセスはいわゆる変更データキャプチャ(CDC)といわれるプロセスで、Snowpipe+Stream+TaskでCDCを実現したデータパイプラインを構築しています。 こちらの詳細はSnowflakeのドキュメントにも記載されていますので、そちらをご覧ください! docs.snowflake.com これによりログが吐かれてから最短数分でログデータの分析が可能な状態になっています。 また、これまでの分析よりも高速なレスポンスを日々のチューニングなしで実現できました! (ここに至るまでの紆余曲折はSnowdayでもお話しているので是非ご視聴ください) 今やっていること クラシルでは様々な方が分析目的でSQLを記述する文化や、Slackで分析結果を共有しあいながら意思決定をする文化が醸成されています! (入社時に一番驚いた点です!) ただし、都度DWHから分析を行うためにクエリを記述していくと毎回同じようなクエリを書かなきゃいけなくなったり、同じような分析だけど指標の算出ロジックが異なったりと結構カオスになりがちです。 これらの現状から日々のモニタリングや分析の運用負担を削減しつつ、あわせてアプリケーションで利用ができるだけのデータ品質を保証できないかと考えました。 そのため、現在は中間ビューやデータマート相当のビューを作成し分析工数の削減と品質担保をするべくdbtを導入しはじめています。 現時点では分析目的の利用にとどまっているので以下の方針で少しずつ活用をしていこうと考えています。 Snowflakeのコンピューティングパワーを信頼した論理データウェアハウスアーキテクチャ 実テーブルを作らずにまずはビューを作ろう 論理的なモデリングであればリアルタイム性も引き続き担保できる 要件を明確にした上で実テーブル化しよう 学習途中、徐々に利用もスケールしていくのでデータマート(DM)などが乱立したときにも戻しが可能な構成(仮想DM) 実テーブル化するときのコンピューティングリソースのコストが必要なインサイトを得るのに見合うものか データ基盤のエンジニアリングリソースが少ない状態で活用を目指しているフェーズではこの考え方が理想ではないかと思っています。 将来的には、アプリケーションで利用するデータのモデリングなどを司る基盤としてSnowflake+dbtを活用していきたいと考えています! 活用Tips RedashのアノテーションをQueryTagに設定 概論ばかりだとせっかく読んでいただいたのに残念な感じになってしまいそうなので、ちょっとしたTipsも落としておきます! 先程の仮想DMや中間ビューの利用が進むのはいいことですが、それらが乱立しても誰が何を利用しているかはわかりにくくなりますし、破壊的な変更が必要なときにどこに影響があるのかもわからなくなります。 それでは、本来目指していたカオスの解消にはなりません。 そこで、我々が利用しているRedashのアノテーション(Redashのクエリ情報やユーザー情報)をSnowflakeのquery_tagに仕込むことにしました。 query_tagにアノテーションがJSONの文字列で格納されているので、Snowflakeの ACCESS_HISTORY と QUERY_HISTORY を結合することでどのテーブル/カラムが、どのRedashユーザー/ダッシュボードのクエリで使われている実績があるかまで特定可能になりました! SELECT DISTINCT try_parse_json(Q.QUERY_TAG):Username::string as redash_user ,try_parse_json(Q.QUERY_TAG):query_id::string as redash_query_id ,D.value: " objectName " ::STRING AS DIRECT_OBJECT_NAME ,ColD.value: " columnName " ::STRING AS DIRECT_COLUMN_READ_FROM FROM ACCOUNT_USAGE.ACCESS_HISTORY H JOIN SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY Q USING (QUERY_ID) ,LATERAL FLATTEN(H.DIRECT_OBJECTS_ACCESSED) D ,LATERAL FLATTEN(D.value, RECURSIVE=> TRUE ) ColD WHERE H.QUERY_START_TIME >= dateadd(week, -1 , current_timestamp ) AND NOT (D.value:objectName::string LIKE ANY ( ' SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY ' , ' %TABLE_ACCESS_LOGS% ' )) -- 確認したいテーブル名 AND DIRECT_OBJECT_NAME IN (<利用実態を知りたいテーブル名>) -- 該当テーブルののカラムをクエリで参照しているかどうか AND Direct_Column_Read_From in (<利用実態を知りたいカラム名>) AND len(Q.QUERY_TAG) > 0 ; ↓参考にした記事↓ galavan.com 例えばテーブルAのカラムAとカラムBに数値的な問題があった場合、どれだけ影響あるか知りたくなりますよね。 その場合は上記のようなクエリを実行するだけで、誰がどのRedashを使っているかを確認することができます。 この場合は主にAさんとコミュニケーション取らないとダメですね。 その他でもSnowflakeのquery_tagはいろいろな用途にも使えると思いますので、他にもTipsあるようでしたら是非教えて下さい! 最後に いかがでしたでしょうか? Snowflakeを導入してからこれまでとTipsをお話させていただきました。 改めてですがクラシルのログデータは1日で3億レコードほどあるため、かなりのデータ量になります。 それらのデータ量にも関わらずニアリアルタイムでのELTや分析性能の向上などの、思っている以上のパワーを発揮してくれているかなと思っています。 ほぼほぼエラーのリカバリー等もなく、日々運用周りのメトリクスを確認する程度の運用のみで済んでいるのは本当にすごいなと改めて思っていますし、パイプラインや分析ツールの開発や活用への活動等、本当に必要なことに集中できるのはいいものですね。 少ないリソースでも最大限のパフォーマンスと信頼性を発揮しながら大規模ログデータのデータパイプラインを構築できるのは、Snowflakeの恩恵を受けられているおかげかなと思っています。 クラシルのデータパイプライン構築は徐々に形になり始めてきており、今後はデータ基盤をより活用するフェーズになってくるかなと思っています。 Snowflakeやdbtを使い倒してクラシルのデータからユーザーへの価値提供に貢献してみたい!と思うデータエンジニアの方々に関わらず、delyではエンジニア、デザイナー、PdMを積極採用しています。ご応募お待ちしております! dely.jp *1 : Snowflakeは2016年に提供開始、2020年にAWS東京リージョンでも提供開始されたクラウドサービス。BigQueryやRedshiftなどのようなDWHの機能を持ち合わせたCloud NativeなDWHといった感じです。
アバター
挨拶 こんにちは。kurashiruでチラシ機能のサーバーサイドを担当している遠藤です。みんなには訳あって「おぺん」と呼ばれています。 今日はdelyに入って半年経った感想的なのをつらつらと書いていこうと思います。 今年もdelyではアドベントカレンダーを行っており、本記事はその7日目の記事となっています。 昨日は坪田さんの「 進撃のプロダクトマネジメント 」でした。PdMの心得や考え方、おすすめの本などがわかりやすくまとまっています。 blog.tsubotax.com はじめに まず簡単に僕の経歴について 2019/04~ 不動産テックベンチャー 新卒エンジニアとして入社 SQLずっと書いてた Railsもちょっと書いてた 広告運用的なやつちょっとかじった(全部忘れた) シャレオツbuildingの40階(標高200mくらい) 2020/01~ 長野に移住してリーガルテック領域で起業 3つくらいサービス作ったけど、リリース前にピボットしたり誰からも使ってもらえなかったり営業電話かけたら怒られたり、、 週100時間やってた 築2年の5LDK一軒家を職場兼自宅として利用 kurashiru使って週替わりでメンバーの食事を作ってた 長野市寒い(標高600mくらい) 2021/01~ 実家に戻りプー太郎生活 自分でサービス作ってどこにも属さず生きると決意 孤独に耐えられず3ヵ月で精神を病み働くことを決意 フリーで仕事したり転職活動したり 海辺の街だったので風が冷たい(標高10mくらい) 2021/04~ dely(入社は6月) シャレオツbuildingの23階(標高100mくらい) 夕方のオフィスから見える景色です 採用までの経緯 上に書いてある通り、1人で生きていくと決意して3ヶ月で心折れた遠藤は転職活動を始めます。wantedly経由でクラシルに応募して、速攻で弊社エンジニアの高松さんとカジュアル面談が決まりました。カジュアル面談では開始3分前に突然の腹痛に見舞われ死を覚悟しましたが、高松さんの優しそうな人柄のおかげもあってか、なんとか1時間耐え切ることができました。自分が普段から使っていたサービスということもあってそのまま選考に進み、2~3週間ほどで内定が出たような気がします。 入社してから 入社してから感じたことを書いていきます。 人が良い delyに入社して良かったと思う理由の第1位です。仕事をする上で一緒に働くメンバーはとても重要です。心理学者のアドラーは「すべての悩みは対人関係である」とまで言い切っています。 ja.wikipedia.org チームごとのミーティングでも「これを言ったら否定されるかも」「初歩的な質問すぎて呆れられるかも」などのネガティブなことを考えずに自由にHeart to Heartで発言できます。どのメンバーと話していても心理的安全性が担保されているのでストレスなく仕事を進めることができます。 僕はストレス耐性が皆無なので嫌なことがあるとすぐにお腹を壊してしまうのですが、delyに入社してからストレス起因の腹痛になることがなくなりました。※本当です この辺りは弊社カルチャーデックを見ていただけるとわかりやすいかと思います! https://speakerdeck.com/delyinc/delyculture?slide=24 speakerdeck.com 手を挙げればなんでもできる環境 ベンチャーの最大の利点でもありますが、一人一人の裁量が大きい&常にリソース不足なので「やりたい」と言えばなんでもやらせてもらえます。 この前も採用に首を突っ込みたいと言ったら良いですよと即答いただきました。 コミュニケーションの大切さ 12月からチラシチームが事業部として独立したことに伴い、事業部内のサーバーサイドエンジニアが僕1人になりました。必然的に他部署のエンジニアとのコミュニケーション機会が減ってしまったので、週一のサーバーサイドmtgで、今どんな課題を抱えていてどんな施策を考えているのかを積極的に発信していくように心がけています。こうすることでレビューの際の認識の齟齬が少なくなったり、困った時に助けてもらえたりと、かなりの恩恵を受けることができます。 面倒くさがらずににコミュニケーションすることが大事です。 エンジニアとして 新卒の時はごく稀にRails製社内アプリをいじる程度でしたし、起業した時もエンジニアは僕だけだったのでセルフレビュー&セルフマージで開発をしていました。なので、チーム開発というのをそもそも経験したことがなく、複数のエンジニアとレビューを回しながら大規模toCサービスを開発するというのはdelyが初めてです。 仕様書の書き方、大規模サービスのサーバー負荷を考慮したコーディング、使ったことのない技術など全てが初めてだったのでこの半年は勉強の毎日でした。最近は少しずつではありますが、レビューする側にもなってきたので今後はアウトプットの方にも力を入れていきたいなという所存です。 最後に 「小さいチームで裁量大きくやりたい!」とか「大規模サービスぶん回したい!」とか「人間関係で悩みたくない!」とか考えている人には最適な職場です。少しでもご興味がありましたら、ぜひぜひご応募ください。 dely.jp
アバター
自己紹介 こんにちは、松岡です。 私はコマース事業部でインフラ兼バックエンドエンジニアをやっています。 器用貧乏に幅広くいろいろなことができることを売りにしてきましたが、本格派の方々が続々と加入しているため居場所がなくなりつつあります笑。 この記事はdelyアドベントカレンダー4日目の投稿です。 昨日は偉大なEMのtakaoさんの「delyで働くパパエンジニアの日常を紹介」でした。偉大なEMは偉大な父親でもありました。ぜひご覧ください。 tech.dely.jp 事業の紹介 最初に私が所属するコマース事業部を紹介させてください。 コマース事業部はクラシルデリバリーというサービスを運営しています。 「あらゆるスーパーマーケットで最短30分配送を実現するグロサリーデリバリーサービス」です。 詳細は次のプレスリリースをご覧ください。 prtimes.jp クラシルデリバリーは大きく分類すると生鮮食品EC市場のサービスになります。 この生鮮食品EC市場は近年で最も熱い市場の1つではないでしょうか。 私たちもこの市場で刺激のある毎日を送っています。 こんな毎日にご興味がある方はぜひ次の採用情報をご覧ください。 https://delyjp.notion.site/dely-kurashiru-delivery-2c5199d383cd4ce8ac50942e65da2fb6 delyjp.notion.site CIを改善した話 それでは本題のCIについてお話しします。 *この投稿内でのCIの定義はコードをプッシュするとデプロイしてくれるやつをあらわします。 改善前の様子 コマース事業部はCIをAWSのCodePipeline、CodeBuild、CodeDeployで行っていました。以後、この3サービスをCode3兄弟と呼びます。 GitHubとAWS CodePipelineを連携してプッシュするブランチとデプロイする環境を1対1に関連づけます。具体的な例は次のとおりです。 main ブランチにプッシュするとプロダクション環境にコードをデプロイする。 test1 ブランチにプッシュするとテスト環境その1にコードをデプロイする。 test2 ブランチにプッシュするとテスト環境その2にコードをデプロイする。 開発の流れは次のとおりです。 main ブランチからトピックブランチを作り、トピックブランチで開発する。 開発が終わったら test1 ~ test2 のいずれかの1ブランチにトピックブランチをマージしてテスト環境にデプロイし、テスト環境でテストする。 テストが終わったら main ブランチにトピックブランチをマージし、プロダクション環境にリリースする。 Code3兄弟は基本的には最高ですが、1つ不満がありました。 それは学習コストが高いことです(私は何度か心が折れました)。そしてそれによりCIを運用・改善するエンジニアが特定の人に限られていました。 CIには開発で行う様々なことを自動化できる力があるため、その開発を行っているアプリケーションエンジニア自身も運用・改善できたほうがよいと私は思っています。 そのほうがきっとよい改善が行われるでしょう。 ですが、、、学習コストの高さが壁になり、それが行いづらい状況でした。 GitHub Actionsは上記の不満を解決してくれるよい選択肢でしたが、私たちの事業部ではできる限りAWSのアクセスキーを発行しないという方針を持っているため、AWSと連携させていませんでした。 GitHub Actionsの大きなニュース こんな状況でしたが今年の9月にうれしいニュースを知ります。AWSのアクセスキーなしでGitHub ActionsとAWSが連携できるようになりました。 何がどのくらい最高かと言いますと! GitHub Actions に AWS クレデンシャルを直接渡さずに IAM ロールが使えるようになることがまず最高で! クレデンシャル直渡しを回避するためだけの Self-hosted runner が必要なくなるところも最高です!!✨✨✨ https://t.co/IUQmfzkIB0 — Tori Hara (@toricls) 2021年9月15日 GitHub ActionsとCode3兄弟の連携 さっそく私たちはこの方法でGitHub ActionsとCode3兄弟を連携します。 具体的なコードは次のとおりです。ECSにデプロイする例です。 GitHubとAWSの連携をいままではWebhookで行っていましたが、この機会にS3のバケット経由に変更しました。 こうするとWebhookに必要だったGitHubのアクセストークン等の管理も不要になります(ここも意外と大きな改善)。 GitHub Actions on : push : branches : - 'main' jobs : push-to-bucket : runs-on : ubuntu-latest permissions : id-token : write contents : read steps : - name : Configure AWS uses : aws-actions/configure-aws-credentials@master with : aws-region : ap-northeast-1 role-to-assume : arn:aws:iam::123456789012:role/github-actions role-session-name : <session-name> - run : aws sts get-caller-identity - uses : actions/checkout@v2 - run : echo $GITHUB_SHA > github_sha env : GITHUB_SHA : ${{ github.sha }} - run : zip -r source.zip . -x *.git/* - run : aws s3api put-object --bucket bucket-to-deploy --key backend.zip --body source.zip aws-actions/configure-aws-credentials のバージョンは強気の master です。安定版をお勧めします。 echo $GITHUB_SHA > github_sha はDockerイメージのタグをコミットIDにしたいための手段です。 SourceをS3バケットにするとCodeBuildのCODEBUILD_RESOLVED_SOURCE_VERSIONがコミットIDではなく、S3オブジェクトのバージョンIDになります。 そこで上記のとおりgithub.shaをファイルに書き出してZIPファイルに格納しています。書き出したファイルは後続のCodeBuild内で行うDockerイメージのビルドに使います。 IAM ロールその他 すみません、Terraformな表現です。 resource "aws_iam_openid_connect_provider" "github_actions" { url = "https://token.actions.githubusercontent.com" client_id_list = [ "sts.amazonaws.com" ] thumbprint_list = [ "a031c46782e6e6c662c2c87c76da9aa62ccabd8e" ] } resource "aws_iam_role" "github_actions" { name = "github-actions" assume_role_policy = jsonencode ( { Version = "2012-10-17" , Statement = [ { "Sid" : "" , Action = "sts:AssumeRoleWithWebIdentity" , Principal = { Federated = aws_iam_openid_connect_provider.github_actions.arn } Condition = { StringLike = { "token.actions.githubusercontent.com:sub" = [ "repo:dely-no/repository-dayo:*" ] } } , Effect = "Allow" } ] } ) } thumbplint_list の a031c46782e6e6c662c2c87c76da9aa62ccabd8e は公式サンプルの引用です。 公式のサンプルもご覧ください。 github.com Code3兄弟 CodePipeline SourceはGitHub Actionsのワークフローで指定したS3バケットのオブジェクトです。 - run: aws s3api put-object --bucket bucket-to-deploy --key backend.zip --body source.zip CodeBuild version : 0.2 phases : pre_build : commands : - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com - echo $DOCKER_HUB_PASSWORD | docker login -u $DOCKER_HUB_USERNAME --password-stdin - COMMIT_ID=$(cat github_sha) - TAG=$ECR_REPOSITORY_URL:$COMMIT_ID build : commands : - docker build -f Dockerfile -t $TAG . - docker push $TAG *出力アーティファクトに関する部分は省略しています。 連携方法は以上です。 結果発表〜〜〜 GitHub Actionsと連携した結果、CIの学習コストが下がったためか自分を含めアプリケーションエンジニアの方々がCIを運用・改善するようになりました(あっさり)。 アプリケーションエンジニアが作ったワークフローが爆誕し、運用されています。 いかがでしたでしょうか! この投稿を最後まで読んでくださった皆様方に少しでも役に立てたらうれしいです!!!
アバター
こんにちは。クラシル開発部でエンジニアリングマネージャをしている @takao です。 この記事はdelyアドベントカレンダー3日目の投稿となっています。 昨日の2日目の記事はiOSエンジニア石田さんの「 機械学習を使ってUIを補完するAppleの研究の紹介 」という記事でした。 先日、社内のiOS勉強会でこの記事の内容をシェアしていただいたのですが、UI検出の手法がおもしろくて図なども分かりやすいのでアプリでの機械学習の活用に興味がある方はぜひご覧ください。 今回は1歳半の子育てをしながら働いている僕が、普段どのように仕事と家庭の両立をしているのかをご紹介したいと思います。 delyは比較的若い会社ですが、社員の平均年齢がちょうど30手前ということもあり、ここ1,2年でパパママ社員が非常に増えてきています。 それに伴って社内の環境などもどんどん整備されていっているので、社内制度なども合わせてご紹介したいと思います。 2021年5月時点での平均年齢は29歳です。 dely株式会社 会社紹介資料/dely - Speaker Deck delyやベンチャー企業に興味があるが、子どもがいて仕事との両立が不安という方や社内で子育ての不安を抱えている方などの参考になれば幸いです。 ※本記事に記載されている情報は2021年12月3日時点のものです。 delyの働き方 フレックスタイム制(コアタイム 10時~16時) 週2日リモート、週3日出社のハイブリッド (執筆時点のルールなので、新型コロナウイルスの感染拡大状況などにより変更することがあります) 家族構成 パパ(私) iOSエンジニア、EM(主に採用とマネジメント) 29歳 出社とリモートのハイブリッド フルタイム勤務 ママ ベンチャー企業でQAエンジニア フルリモート 時短勤務(10:00~15:30) 子ども 1歳半の男児 最近「ぶーぶー」や「わんわん」など発するようになってきた 平日は9時~16時まで保育園 育休 日常の話ではありませんが、育休を取得→復帰して今働いているのでそのあたりも軽くご紹介します。 期間 期間は約2ヶ月半取得しました。 当時のdelyでは男性の育休取得の実績はなかったのですが、我が家の育児環境的に育休は必須だと考えていたので、取得できるか分からない状態でしたが当時の上長にまずは相談をしました。 最初は仕事も忙しいし、1ヶ月取れれば良いかな程度で考えていて当時の上長に相談したのですが、仕事のことを考えずに本当に取得したい期間をもう一度夫婦で相談して出直してこいと言われたので、妻と話した結果、2ヶ月取得したいということになり復帰のタイミングをキリよくするために2ヶ月半にしました。 期間中の過ごし方 育休中は基本的には仕事から完全に離れ、Slackなどのやり取りもほぼ行っていませんでした。 (育休中に2,3回程度しか投稿してないと思います) そのため気負いなく100%の時間を家庭に注ぐことができ、近くで子供の成長を見ることができて本当に良かったです。 当時業務の引き継ぎを行ってくれたチームメンバーにはとても感謝しています。 普段の働き方 では、実際に普段どんなタイムスケジュールで過ごしているかをご紹介します。 一日の流れ 出社日 06:30 起床 07:00 身支度、子供のご飯 07:30 家を出る 08:00 会社で業務開始 17:00 退社 18:00 子どもとお風呂、保育園の準備 20:00 料理、寝かしつけ 20:30 パパママご飯 21:00 自由時間 23:00 就寝 リモート日 06:30 起床 07:00 身支度、子供のご飯 07:30 洗濯、子どもと過ごす 08:00 業務開始 16:00 仕事一時退勤 16:15 保育園お迎え 16:30 買い物 17:30 子どものご飯 18:00 子供とお風呂、保育園の準備 18:30 仕事 20:00 料理、寝かしつけ 20:30 パパママご飯 21:00 自由時間 23:00 就寝 それぞれに使っている時間はこのような感じになっています。 育児の時間:3時間 家事や食事などの時間:1時間 仕事の時間:8時間 睡眠:7時間30分 自由時間:2時間 出社日とリモート日でそんなに変化はありませんが、だいたいこのような毎日を過ごしています。 朝は家族3人で一緒に起きて一日が始まっています。 リモートの日は保育園のお迎えに行くようにしているので少し早めに仕事を切り上げて後述のファミリーリモート制度を活用しています。 また、子どもができてから大きく変わったことですが、自分の自由時間がかなり少なくなったので子どもをいかに早く寝かしつけるかを毎日試行錯誤しながら過ごしています。笑 育児に費やす時間 クラシルの開発チームはみな効率良く働いており、開発部の直近2ヶ月間の残業時間の平均は10時間を下回っています👏 また、僕自身も子どもが産まれてからはほとんど残業をしなくなり(というか物理的にできなくなり)、仕事以外の時間を家族のためにしっかりと確保できている方だと思っています。 困りごと 子どもがまだ小さいのでとにかく風邪をよくひきます。保育園で少しでも誰かが鼻水を垂らし始めるとすぐさま風邪が拡散され我が家にやってきてしまいます。 さらに困ったことに子供の風邪が治るのに普通に1週間くらいかかります。その間は半休や後述のファミリーリモートなどを駆使し、夫婦で仕事の調整をしながら頑張っています。 会社で利用している制度 ファミリーリモート こちらは家族の看病が必要なときや何らかの理由で子どもが保育園に登園できない際に出社日でもリモート勤務ができる制度となっています。 子どもが体調を崩して近くにいないといけないときなどは出社日でもリモート勤務を行うことができます。 うちの子どもは現在1歳半で保育園に通っているのですが、ほぼ毎月風邪をひいたりして保育園を休まないといけない日が発生するので、そういったタイミングでリモートにできるのは助かっています。 リモートワーク自体は社内でも浸透しているので、急にリモートになったからといって特に業務効率が著しく低下するなどの影響はありません。 会議や打ち合わせを行うときはGoogleMeetやSlackのハドルなどを使って柔軟に対応することができています。 とはいえ、リモートの日に誰かが子どもを見ていないとすぐにこういう状況になってしまい仕事になりません。 なので我が家では午前と午後で夫婦で分担しながら育児と仕事を両立するようにしています。 ファミリーフレックス 保育園のお迎えや夕方の忙しい時間帯は一度退勤して夜落ち着いてから業務を再開することができます。 僕の場合は夜に採用系の面談や面接がある日や妻の仕事が忙しいとき、保育園のお迎えに行くときなどのタイミングで、子供の世話が一通り終わってから業務を再開するような形で利用しています。 途中退勤する場合、Slackではこのような感じで共有をして退勤しています。 (リアクションをつけてもらえたりするので、気兼ねなくこういった働き方ができています) さいごに いかがでしょうか。 子育てをしながら働くエンジニアチームのイメージを少しでも持っていただけていれば幸いです。 最後になりますが、クラシル開発チームでは、現在以下の職種を絶賛募集中です! iOSエンジニア Androidエンジニア サーバーサイドエンジニア SRE データエンジニア 本記事を読んで少しでもdelyに興味を持っていただいた方はこちらをご覧ください。 https://dely.jp/recruit/category/engineer また、個人的にパパ・ママエンジニアの人と雑談したいと思っているのでMeetyでぜひお話しましょう。 meety.net
アバター
TRILL開発部のiOSエンジニアの石田です。 今年もdelyではアドベントカレンダーを行っており、本記事はその2日目の記事となっています。 昨日の1日目の記事は奥原さん ( @okutaku0507 ) の「 プロダクトマネージャー3年目の教科書 」という記事でした。delyのエースPdMである奥原さんによる大作となっていますので是非ご覧ください。 本記事では、機械学習を使ってUIを補完するAppleの研究について紹介します。 Appleは Machine Learning Research で機械学習に関する様々な研究を発表しています。 その多くはコンピュータビジョンや音声・テキスト認識のような研究なのですが、機械学習xUIという研究も行っております。 本記事ではその中でも、アプリのスクリーンショット(画像)から機械学習を使ってUIコンポーネントを認識し、アクセシビリティ機能を補完する Making Mobile Applications Accessible with Machine Learning という研究を紹介したいと思います。 ちなみにこちらはCHI 2021という国際会議で発表された Screen Recognition: Creating Accessibility Metadata for Mobile Applications from Pixels という論文を元にしており、CHI 2021ではBestPaperにも選ばれています。 論文自体は こちら からアクセスできます。また、サマリーが動画としても 公開されている ので、ざっくり研究の概要を知りたい方はご参照ください。 背景 iOSには様々なアクセシビリティ機能があります。 その中でも、画面を音声で読み上げる視覚サポート機能であるVoiceOverは初期のiOSから備わっています。 以前の職場で、目の不自由な先輩が音だけ (VoiceOver機能) でiPhoneを使いこなしているのが記憶に残っています。 しかし、そのVoiceOverにも限界があります。 開発者が isAccessibilityElement などの設定を適切に行わなければ十分な形でVoiceOverを利用できません。 また、iOSアプリをネイティブで開発している場合は良いのですが、Unityのようなクロスプラットフォームフレームワークでアプリを開発すると、VoiceOverはほぼ使えない状態になります。 しかし当たり前なのですが、アプリを使っているユーザは視覚的にボタンやテキストを認識し、適切にUIを操作しています。 人間と同様に、機械学習を使って視覚的に (アプリのスクリーンショットのような画像から) UIコンポーネントを認識しVoiceOverとしてアウトプットすれば、アクセシビリティのメタデータが存在しなくても視覚に障害のある方がUIを適切に操作できるようになるのでは、というのがこの研究のモチベーションとなっています。 手法の概要 この研究は、入力がアプリのスクリーンショットのような画像であり、出力はVoiceOverのような画面の読み上げとなります。全体の流れは下図のようになります。 ( 論文 より画像引用) アプリのスクリーンショットやUI Treeのデータを集める スクリーンにメタデータを付与する (40人の作業者が実施) 機械学習を使って、画像からUIの検出をデバイス上で行う 画面読み上げの精度向上のために、UIの順序の認識やグルーピングを行う 生成されたアクセシビリティのメタデータから画面読み上げを行う 本研究のキモは以下の2点です。 iPhoneのVoiceOverで使えるほどUIの検出スピードが早く、処理が軽量であること UIの状態やUIグループの認識などによってVoiceOverのUXをさらに向上させていること これらについて詳しく解説していきます。 UIの検出と評価 スクリーンショットからUIを検出する問題設定はコンピュータビジョンの物体検出と同じです。そこで本研究ではまずFaster R-CNNのような物体検出で評価の高いモデルを採用しました。 しかし検証したところ、Faster R-CNNでは検出に1秒以上かかり、また120MBのメモリを使用するため、iPhone上で動作させるには適していませんでした。 そこで SSD (Single Shot MultiBox Detector) を使うことで、高速化と使用メモリの削減を行いました。 また、UIは他の物体検出のターゲットに比べ小さいため、FPN (Feature Pyramid Network) を用いることで、その問題を解決しました。 最終的に、計算速度は10ms、メモリ使用量は20MBを実現しています。 ここで簡単にR-CNNとSSDのアルゴリズムについて紹介します。 物体検出は、画像から対象のオブジェクトの位置・大きさ (矩形) とそれが何か (犬か車かなど) を認識する問題です。 そのため、画像認識とは違い、対象オブジェクトの領域も推定する必要があります。 R-CNNは2つのステージに分かれていて、まず Selective Search などを使って領域提案を行い (オブジェクトと思われる領域を最大で2000検出) 、次にその抽出された領域画像に対してCNN (Convolutional Neural Networks) を使ってそれが何かを推定します (下図参照) 。このように領域の検出と物体の検出の2つの問題を問いていることが処理の遅さの原因でした。 ( 論文 より画像引用) それに対してSSDでは、Single Shotという名前の通り、ステージを2つに分けることなく1ステージで領域検出から物体認識まで行います。 まず様々なスケールの特徴マップ (4x4や8x8) を用いて画像を表現し、それぞれの各要素に対して異なるアスペクト比のデフォルトボックスという矩形を用意します。 そして各デフォルトボックスについてクラス分類を行い、スコアを出します。 スコアの高いデフォルトボックスの重なりを使って、領域を検出します。 ( 論文 より画像引用) このアルゴリズムを用いて実験を行いました。実験では、色んなカテゴリーのアプリから得た5,002のスクリーンショットを用いています。 各UIエレメントの検出結果は以下のようになります。APはAverage Precisionの略で、物体検出では適合率と再現率がトレードオフなのですが、それをよしなに評価する指標です。 適合率と再現率は、例えばCheckboxと検出したものが実際にCheckboxだった割合が適合率で、存在するCheckboxのうちCheckboxと検出できた割合が再現率となります。 また、物体検出では物体が矩形で検出されるのですが、その矩形が正解とどれくらいオーバーラップしているかがIOUとして表現されます。 ( 論文 より画像引用) IOUが0.5より大きい場合の平均APは71.3%となります。 これに、UIタイプの出現頻度を重み付けすると82.7%となりました。 アプリのUIは同じ出現頻度ではなく、テキストやアイコンは頻度が高く、逆にチェックボックスやスライダー、トグルは出現頻度が低いのですが、こういった出現頻度を重み付けしています。 結果を見ると、チェックボックスは精度が低く、これは出現頻度の低さに起因するものと考えられます。 しかし、同様に出現頻度が低いトグルは高い精度で検出できています。 深ぼってみると、チェックボックスはアイコンと誤認識される場合が多く、このような低い精度となっているようです。 UI検出の問題点とその改善 前項で、UIを検出する手法と評価を紹介しました。 しかしUIには、選択状態か否か、ボタンなのかタブなのか、タップ可能なアイコンか否か、などのメタデータが含まれます。 理想のアクセシビリティ機能を提供するためには、これらのメタデータをユーザに提供する必要があります。 このような問題を、本研究ではヒューリスティックな方法で解決しています。 ( 論文 より画像引用) 例えば上図のようなタブやSegmented Controlsでは、それぞれの色の出現頻度などからtint colorを類推できます。 そのようにして、タブでは90.5%の精度で、Segmented Controlsでは73.6%の精度で選択状態を認識することができました。 また、タップ可能か分からないアイコンなどもありますが、それに対してはGBDT (Gradient Boosting Decision Tree) を使って判別し、90.0%の適合率を維持したまま73.6%の再現率を達成しました。 さらに、VoiceOverのためにはUIのエレメントを認識するだけではなく、UIをグルーピングしたり順序付けする必要があります。 グルーピングがないとUIエレメントを読み上げるだけなので (例えばタブはアイコンとテキストに分解される) 、ユーザはUIを適切に認識するのに多くの時間がかかってしまいます。 下図はUIをエレメントで検出したものと、グルーピング・順序付けをした比較になります。 タブやUITableViewのCellが適切にグルーピングされているのが分かります。 ( 論文 より画像引用) これらは、UIのタイプや距離、サイズを用いて検出しています。 例えばタブであれば画面の下部にあることや、テキストのグループであれば位置関係 (xが同じとかcenterが同じとか) を使っており、機械学習に任せるというよりはヒューリスティックな方法で検出しています。 このようにして単純なUIの検出を超えて、アクセシビリティがより良いUXとなるよう改善を行いました。 実際のユーザによる評価 前項の検出の改善を含めて、VoiceOverを実際に数年以上使っているユーザを対象に、本研究の評価を行いました。 評価に用いたアプリはApp Storeで入手可能なもので、VoiceOverが適切に機能しないものを選びました。 使いやすさを5段階で評価してもらった結果は下図のようになりました。 ( 論文 より画像引用) 青は既存のVoiceOverで提案手法はオレンジとなります。 提案手法のほうが明らかに評価が高いことが分かります。 また、ゲームのようなVoiceOverがほとんど使えないアプリも遊ぶことができているようです。 まだ完全な精度で検出ができているとは言えませんが、VoiceOverがほとんど動作しないアプリに対しては、非常に大きい効果が出ていると言えます。 まとめ 本記事では、機械学習を使ってアクセシビリティ機能を補完する研究を紹介しました。 実験では、実際にVoiceOverを必要とするユーザに使ってもらい、適切に補完ができていることが分かりました。 いわゆるディープラーニングを用いて華麗にUIを検出しているのですが、それだけでは達成できない精度の部分はヒューリスティックな方法 (tintColorやUIの位置情報を使ったり) で改善しているのが印象的でした。 この研究はアクセシビリティ機能にフォーカスしていますが、スクリーンショットからUIを認識するという技術はアクセシビリティ機能以外にも応用が可能と考えられます。 例えば、開発者がアプリを開発している中で機械がUIについての提案を行ったり、UIの自動操作にも使えるかもしれません。 また、スクリーンショットからUIを検出するという手法はiOSアプリに依存していないので、AndroidアプリやWebなどの別のプラットフォームにも応用可能と論文では述べています。 UIデザインというと、機械学習からは遠い人間のフィールドというイメージがありますが、将来的には人と機械が連携しながらより良いアプリを開発していく、という未来が来るかもしれません。 さて、本記事はアドベントカレンダー2日目でしたが、明日はtakaoさんの記事「delyで働くパパエンジニアの日常を紹介」です。お楽しみに。 delyではエンジニア、デザイナー、PdMを積極採用しています。 少しでも興味がありましたら、お話だけでもできればと思います。 dely.jp
アバター
こんにちは、えんじにゃーの @MeilCli です。猫よりペンギンのほうが好きです 今回はタイトルの通り名前の衝突を回避するテクニックを紹介したいと思います @JvmName Test.kt: fun method(value: List < String >) {} fun method(value: List < Int >) {} ファイルのトップレベルにこういう関数を定義したくなったとします *1 これはJVMにおいてはシグネチャー(名前と引数の形)の衝突によってコンパイルエラーとなってしまいます。どういうことかというとListのジェネリクス要素のStringとIntはランタイム時にはKotlinで言うところのAnyとして解釈されるため、コンパイル時にエラーとして処理されているのです *2 @JvmName ( "methodB" ) fun method(value: List < String >) {} @JvmName ( "methodA" ) fun method(value: List < Int >) {} こういう場合は @JvmName アノテーションを利用することで回避することができます。こうすることでJVM的にはこういう名前のメソッドとして扱ってくれよと指定できるのです このKotlinコードのJavaから見えるシグネチャーは以下の感じになります public final class TestKt { public static final void methodB(List<String> value) {} public static final void methodA(List<Integer> value) {} } JVM的に名前が違うメソッドになっているので前述のKotlin的には同名関数でもシグネチャーの衝突でエラーにはならないということです interfaceでは@JvmNameを使えない Test.kt: interface Test { @JvmName ( "methodB" ) fun method(value: List < String >) { } @JvmName ( "methodA" ) fun method(value: List < Int >) { } } こんどはこのようなコードはどうでしょうか? このコードをIntelliJ IDEAやAndroid Studioにかけると @JvmName annotation is not applicable to this declaration のようなエラーがでてしまいます。これはどうやらinterfaceでは多重継承・実装の際などに名前解決で問題になることがあるからこうなってるようです *3 実は使える このエラーを回避するいいテクニックないかな〜と調べたところ、Suppressすることができるとのことでした interface Test { @Suppress ( "INAPPLICABLE_JVM_NAME" ) @JvmName ( "methodB" ) fun method(value: List < String >) { } @Suppress ( "INAPPLICABLE_JVM_NAME" ) @JvmName ( "methodA" ) fun method(value: List < Int >) { } } このようにすることでコンパイルをすることができます。ただし、JavaなどのKotlin以外のJVM言語では前述の問題を引き起こす場合が出てくると思われるので使い所には注意が必要です(自分が確認した限りではKotlinで使う分には問題のない書き方になってるようにIDEによるチェックが効いてそうでした) *4 おわりに さて、Javaとの互換性のために用意されてるようなアノテーションでJavaとの互換性を諦めるような書き方ができるのはそれはそれでいいのか🤔という感じがしてますが、困った際の小技としては有用なんじゃないかなと思います *5 *6 それでも、使う場面としては限られているのと関数名を変える対処もできることから、同じ関数名を使いたくなるケース(たとえばKotlin専用のライブラリー開発)ぐらいでしか使うことはないんじゃないかなとも思うところです まとめ: シグネチャーが衝突したら @JvmName を使うといいよ @JvmName が使えない場面でもKotlinのみで利用するなら @Suppress("INAPPLICABLE_JVM_NAME") が使えるかもよ *1 : そんなことないよというのはなしでお願いします *2 : C#ならそんなことないのでジェネリクスでお困りの諸氏にはC#をおすすめします *3 : https://youtrack.jetbrains.com/issue/KT-31420#focus=Comments-27-4211763.0-0 *4 : 使う際には自己責任でお願いします *5 : 使う際にはほんとに自己責任でお願いします *6 : ちなみにこの技を発見したMeilCliはその場では関数名を変えることによって対処しました
アバター
はじめに こんにちは。クラシルのAndroidアプリチームのテックリードのうめもりです。 皆さん、Gradleのモジュール機能は活用していますか?ソースコードの依存の方向をモジュール単位で強制出来ることでアーキテクチャーの制約を強制しやすかったり、並列ビルド・差分ビルドの局所化によるビルド高速化を期待できたり、大規模なAndroidアプリを作るにはとても役に立つ機能ですよね。 そんな役に立つ機能ですが、実際どうやって活用していけばいいか分からなくて導入に踏み切れない方や、導入してみたがいまいち恩恵が感じられない、そんな方もいらっしゃるのではないでしょうか。 ところで、Androidアプリを開発してきた皆さんなら一度は聞いたことがある言葉にClean Architectureというものがあると思います。 引用: クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.net そして多くの人はこの図と一緒にClean Architectureというものがどういうものか説明するという記事を一度は読んだことがあると思いますが、実はあの図が出てくる「Clean Architecture 達人に学ぶソフトウェアの構造と設計」であの話を取り扱っている部分はごくごく一部で、本全体を通してはどのようにプログラムを設計すべきか、どのようにプログラムのアーキテクチャを考えるとよいのか、といったことが網羅的に説明されています。素敵な本なのでもし読んだことない方がいたら是非一読をおすすめします。 www.amazon.co.jp 今回は、みんな大好きClean Architecture本の第IV部、「コンポーネントの原則」に書かれている内容がGradleモジュールの設計を考える際に非常に役に立つので、その内容をもとに、Gradleモジュールを設計する際にどう適用していったらいいのか、という話をします。 その内容はAndroidアプリにどういったアーキテクチャパターン(MVP、MVVM、VIPER...)を適用するかどうかに関わらず、共通して適用できる話です。なのでGradleのモジュール設計に困っている方には役に立つと思います。 はじめに モジュールの粒度をどうするか 再利用・リリース等価の原則(REP) 閉鎖性共通の原則(CCP) 全再利用の原則(CRP) 3つの原則のバランスを取ることが大事 モジュールの依存関係をどうするか Gradleモジュールの特性を生かす 非循環依存関係の原則(ADP) 安定依存の原則(SDP) 安定度の指標 安定度・抽象度透過の原則(SAP) 抽象度の計測 安定度と抽象度の関係 苦痛ゾーン 無駄ゾーン 主系列 主系列からの距離 クラシルのモジュール構成はどうか? app ui:recipe ui:base data:recipe data:base まとめ おわりに モジュールの粒度をどうするか Clean Architecture本の第12章、「コンポーネント」ではコンポーネントというものは次のようなものであると説明されています。 コンポーネントとは、デプロイの単位のことである。システムの一部としてデプロイできる、最小限のまとまりを指す。Javaならjarファイル、Rubyならgemファイル、.NETならDLLなどがそれにあたる。 (第12章 コンポーネントより) Gradleでは、JARファイルをモジュール単位で生成することもできます。AndroidアプリならAARファイルですね。 複数のコンポーネントをリンクして単体の実行可能ファイルにすることもできる。あるいは、複数のコンポーネントを.warファイルのようなアーカイブにまとめることもできる。 (第12章 コンポーネントより) そして、複数に分かれたモジュールを1つのアプリケーションとしてビルドすることも出来ます。 Androidアプリ開発においては、デプロイの最小単位はGradleモジュールと考えるのがよさそうです。 そして、続く第13章ではどのクラスをどのコンポーネントに含めればいいのか、つまりどのくらいの粒度でモジュールを設計すればいいのか、という原則が3つ紹介されています。 再利用・リリース等価の原則(REP) 再利用の単位とリリースの単位は等価になる。 (第13章 コンポーネントの凝集性より) この原則については、単一のチームでマルチモジュールのアプリを運用している場合はあまり重要ではないかもしれません。Androidアプリのリリースは原則としてモジュールを結合した状態で行われるので、お互いのモジュールの「リリース」を意識する必要はないためです。 ですが、複数チームで同じアプリケーションを開発している場合は、それぞれのチームが意味のある単位でモジュールを「リリース(アップデートを共有)」することで、お互いにお互いのチームのモジュールをうまく使うことが出来るようになりそうです。逆に、お互いのチームの修正内容が固有のモジュールに閉じておらずお互いのモジュールにまたがっていたりすると、協調したアプリ開発は困難になりそうですね。 閉鎖性共通の原則(CCP) 同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめること。変更の理由やタイミングが異なるクラスは、別のコンポーネントに分けること。 (第13章 コンポーネントの凝集性より) SOLID原則を知っている方なら、単一責任の原則(SRP)のコンポーネント版という説明が一番わかりやすいでしょう。これを実際にAndroidアプリに適用することを考えると、どうやってモジュール分けするかの大きなヒントになりそうです。アプリの差分ビルドの高速化という観点からも、 同じ理由、同じタイミングで修正されるコードが同じモジュールに集まっていた 方が、影響がないモジュールの再ビルドの時間が抑えられることになるので、とても大事なポイントです。 例えばよくある分け方として + UI + Repository + APIアクセス + DBアクセス といったプログラムのレイヤー単位でモジュールを分ける、といったやり方を紹介されていたりします。 しかし、モバイルアプリ開発においてはUIだけ、データだけ変更されるということはそう多くなく、UIを変更する場合はそれに合わせてデータも変更される、データが変更される場合はUIも変更される、といったことも多いのではないでしょうか。クラシルの場合レシピ、チラシ、ネットスーパー、クラシルショートなどの機能がありますが、 + レシピ + チラシ + ネットスーパー + クラシルショート のように機能単位のモジュールに分けた方が、一度の変更が単一のモジュールに閉じるようになるかもしれません。実際のクラシル開発では、機能単位とUI/データという2つの軸でモジュールを分割しています。 もちろん、実際にどういったプロダクトを開発しているかによって、どういった単位で同じタイミング、理由で変更されうるかについては大きく変わってくるはずです。どういった形でモジュールの凝集性を管理するかについては、もっとも開発チームの実情に沿った形にするのが好ましいでしょう。 全再利用の原則(CRP) コンポーネントのユーザーに対して、実際には使わないものへの依存を強要してはいけない。 (第13章 コンポーネントの凝集性より) この原則は、端的に言えば 参照先のモジュールに、参照元のモジュールが利用しないクラスやインターフェースが含まれていてはいけない という原則です。 クラシルであればレシピ機能しか使わない機能が含まれたモジュールにチラシ機能がアクセスすることになると、レシピ機能を修正する際にチラシ機能の再ビルドが必要になってしまうということになります。全再利用原則を極力守るようにモジュール設計を行うことで、ビルド時間の短縮や、機能間の依存関係の整理が行いやすくなるでしょう。 3つの原則のバランスを取ることが大事 もうお気づきかもしれないが、凝集性に関するこれらの原則には相反するところがある。再利用・リリース等価の原則(REP)と閉鎖性共通の原則(CCP)は、包含関係にある。どちらも、ひとつのコンポーネントを大きくする方向に働くものだ。一方の全再利用の原則(CRP)は、これらとは相反する原則で、ひとつのコンポーネントを小さくする方向に働くものだ。これら3つの原則のバランスをうまくとるのが、アーキテクトの腕の見せどころだ。 (第13章 コンポーネントの凝集性より) Clean Architecture本にもこう書かれているように、再利用・リリース等価の原則(REP)と閉鎖性共通の原則(CCP)を考慮すればモジュールは大きく、全再利用の原則(CRP)を考慮すればモジュールは小さくなるほうが好ましいということになります。 並列ビルドや差分ビルドの効率を考えるのであれば全再利用の原則(CRP)を考慮してモジュールを小さくした方が良いですが、再利用・リリース等価の原則(REP)を考慮して、1つの変更が影響するモジュール数を減らして開発チームの連携のしやすくするためにモジュールを大きくした方が良いタイミングもあるでしょう。 あるいは閉鎖性共通の原則(CCP)を最大限考慮すべきタイミング(例えばアプリのリリース直後など)では、そもそもマルチモジュール構造を選ばない、というのも選択肢の1つに入ってくるはずです。 開発チームの規模や開発フェーズ、プロダクトの特性によって3つの原則のバランスを考えていくことが大事 です。 モジュールの依存関係をどうするか Gradleモジュールの特性を生かす 第14章 コンポーネントの結合ではコンポーネント間の依存関係を設計する上での指針が、3つの原則を用いて説明されています。その中の1つの原則に、Gradleモジュールを使う限り強制的に満たすことになる原則がありますので、そちらを先にご紹介します。(幸い記述的にも先の方にあります。) 非循環依存関係の原則(ADP) コンポーネントの依存グラフに循環依存があってはいけない。 (第14章 コンポーネントの結合より) Gradleのモジュールはモジュールの循環依存が存在するとエラーが発生するようになっています。 Gradleのシステム上どこかのモジュールを先にビルドする必要があるので、循環依存が存在するとどのモジュールを先にビルドしていいか分からなくなるため、エラーになるようになっていると思うのですが、循環依存が禁止されていることは設計上のメリットもあります。 例えば A -> B -> C -> A とモジュールが循環依存していた場合、Aを修正したければB, C両方のモジュールへの影響を意識する必要があり、かつモジュールAをユニットテストしたい場合にモジュール同士を切り離すことが困難になります。そして、モジュールA, B, Cは実質密結合した状態になってしまい、モジュールを分割した意味がなくなってしまうこともあり得ます。 循環依存が禁止されていることによってこういった問題は発生しないことが保証されていますが、循環依存を解消するための方法はGradleのモジュールシステム上でどうやってクラスを設計するか考えることにも役に立つので、書籍で紹介されている2つの方法を簡単にご紹介します。 依存関係逆転の原則(DIP)を適用し、例えばC->Aで依存しているメソッドを持つインターフェースを別に用意し、Cモジュールの中ではそれを利用する。C->Aへの依存関係が逆転する(AがCのインターフェースを実装したクラスを作る形でCに依存する)ことで、循環依存が解消される。 CとAが依存するDモジュールを作り、DモジュールにC->Aで依存しているクラスを移動する。 書籍ではより実例に沿った形で説明されているので、より深く理解したければ書籍を参照してください。 安定依存の原則(SDP) 安定度の高い方向に依存すること。 (第14章 コンポーネントの結合より) モジュールからモジュールへの依存、ということを考える上で、そのモジュールがどの程度の頻度でどのくらい変更されうるか、ということは一つの重要な観点になります。そして、 よく変更されることを想定したモジュールは、変更しづらいモジュールから依存されるべきではありません。 何故ならば、変更しづらいモジュールから依存されてしまうと、本来よく変更されることを想定したモジュールが変更しづらい状態になってしまうからです。 例えば何らかの共通の処理を行うために切り出したモジュールが、特定の機能の処理を行うためのモジュールに依存してしまっている、といった状況がこれにあたるでしょうか。こうなってしまうと、特定の機能の処理に対して変更を加えると共通の処理に波及してしまうようになるので、その特定の機能の処理を行うためのモジュールを変更することが行いにくくなるでしょう。 安定依存の原則(SDP)の原則は、よく変更されることを想定したモジュールが、変更しづらいモジュールから依存されていないことを保証されるために、満たすべき原則と定義されています。 安定度の指標 書籍では、 モジュールの変更しにくさを安定度と定義しています。そして、安定度を計測するための指標として、 そのモジュールがどのくらいの数のモジュールから依存されているか ということと、 どのくらいの数のモジュールに依存しているか という二つの指標に着目しています。 そして安定度(不安定度)を次のように定義しています。 + ファン・イン: そのモジュールに依存している別のモジュールの数 + ファン・アウト: そのモジュールから依存している別のモジュールの数 + 安定度(不安定度): I(Instability) = ファン・アウト / (ファン・イン + ファン・アウト) Gradleでは、明示的にモジュール間の依存を記述しなければ別モジュールへのコードへアクセスすることはできないので、各モジュールのbuild.gradleファイルを見ることで各モジュール間の安定度を簡単に算出することが出来ます。(使わないモジュールにアクセスしていない限りですが) 例えば、 + app + ui:recipe + ui:netsuper + data:recipe + data:netsuper + api + db + infra というモジュールがあり、 のような形でモジュールが依存しているとします。 infraモジュールを例えば見てみると、 ファン・イン = 5 ファン・アウト = 0 I = 0 / (5 + 0) = 0 となり、不安定度が0なので非常に安定した(変更しにくい)モジュールになっていることが分かります。 一方appモジュールは ファン・イン = 0 ファン・アウト = 8 I = 8 / (0 + 8) = 1 となり、不安定度が1なので極めて不安定な(変更しやすい)モジュールといえます。 そして、実際に矢印の数を数えてみると分かるのですが、必ずIが高い=不安定なモジュールから、安定したモジュールに依存するような形になっています。 実際にアプリを設計する際の感覚としても、アプリケーション全体が依存するinfraは安定していて変更しづらく、uiは不安定で変更しやすいモジュールというのは実感にあっているのではないでしょうか。同じuiの中でも、ui:recipeの方が複数のモジュールに依存していてよく変更がありそうですね。 安定度・抽象度透過の原則(SAP) コンポーネントの抽象度は、その安定度と同程度でなければいけない。 (第14章 コンポーネントの結合より) 安定依存の原則(SDP)に従うと、基本的にモジュールの依存関係は安定度が低い(変更しやすい)モジュールから安定度が高い(変更しにくい)モジュールに依存することになります。そうなると、必然的に 安定度の高いモジュールはあまり変更の必要がない設計にする必要があります。 プログラムコードには実装とインターフェースという二つの側面がありますが、基本的にインターフェースは変更されにくく、実装は変更されやすい傾向にあります。そのことを考えると、安定度の高いモジュールはあまり実装を含まず、インターフェースを多く含む(Kotlinであればinterfaceやabstract class)モジュールである必要があると考えられます。 そのことを書籍では 安定度・抽象度透過の原則(SAP) と表現しています。 安定度・抽象度等価の原則(SAP)は、安定度と抽象度の関係についての原則だ。安定度の高いコンポーネントは抽象度も高くあるべきで、安定度の高さが拡張の妨げになってはいけないと主張している。一方、安定度の低いコンポーネントは具体的なものであるべきだとしている。安定度が低いことによって、その内部の具体的なコードが変更しやすくなるからである。 (第14章 コンポーネントの結合より) こちらの例でいえば、安定度が最も高いinfraモジュールには インターフェースが最も多い割合で含まれているべき で、安定度が最も低いappモジュールには 実装が最も多い割合で含まれているべき ということになります。 ところで、上記の例だと、data:recipeやdata:netsuperには、uiから参照されるインターフェースと、その実装の両方が含まれていることになります。実装次第では、uiとdataで抽象度が逆転する可能性もあります。そのことを考えると、安定度・抽象度透過の原則(SAP)に従うためには下のようにモジュールの依存関係を整理したほうがいいかもしれません。 data:bridgeモジュールを導入してそこにuiから依存していたインターフェースを移動し、各dataモジュールではそのインターフェースを実装する形でdata:bridgeモジュールに依存します。 抽象度の計測 書籍ではモジュールの抽象度を次のような計算式で定義しています。 + Nc: モジュール内のクラス(抽象クラスとインターフェースも含む)の総数 + Na: モジュール内の抽象クラスとインターフェースの総数 + A: 抽象度。A=Na / Nc Aが0であれば一切抽象クラスやインターフェースが含まれない、最も抽象度が低いモジュールであることを表していて、Aが1であれば抽象クラスやインターフェースしか含まれない最も抽象度が高いモジュールであることを表します。 もっともKotlinではインターフェースに実装を持つことが出来る(デフォルト実装ですが)ことや、抽象クラスにも実装を持つことが出来ることも考えると、あくまで目安として参考にするのがよさそうです。 安定度と抽象度の関係 前述したやり方でモジュールの安定度と抽象度を計算することで、安定度と抽象度の関係を定量的に見ることが出来るようになりました。書籍では次のような図を使って安定度と抽象度の関係を整理しています。 横軸が安定度、右に行けば行くほどそのモジュールは安定しており、縦軸が抽象度、上に行けば行くほどそのモジュールの抽象度は高いものとなります。 この図にモジュールをプロットしていくとして、書籍では二つの「モジュールがプロットされるすべきではない」ゾーンと、「主系列」を定義しています。 苦痛ゾーン 図の左下のゾーンは抽象度が低く、かつ安定度が低いモジュールが含まれるゾーンです。 変更がしにくい、かつ抽象度が低いために実装の拡張もしにくい、そういったモジュールがここに含まれるので、そういったモジュールが多いマルチモジュール設計は変更に苦痛を伴いやすいでしょう。 スマートフォンアプリ開発ではどのようなモジュールがこのゾーンに含まれやすいかというと、例えば書籍でも例として上げられている具象ユーティリティライブラリーです。ほにゃららUtilクラスが作られ、アプリケーションの色々なところから呼び出される処理がそのほにゃららUtilクラスに追加されていく、というのは皆さんも心当たりがあるのではないでしょうか。こういったクラスはあまり変更されないのであれば苦痛を伴いませんが、良く変更される処理がその中に含まれているとその変更が予期しない形で他のモジュールに波及したり、あるいはどういった利用のされ方をされているか分からないので変更できなくなってしまうようなことが想定されます。 基本的には 一度作ったら変更されないようなモジュール(ロガー実装や基本的なデータ構造に対する処理)以外はここに含まれないのが理想的 でしょう。 無駄ゾーン 図の右上のゾーンは、抽象度が高く、かつ安定度が高いモジュールが含まれるゾーンです。 抽象度が高い、つまりインターフェースや抽象クラスが多く含まれているのにあまり参照されていない、このようなモジュールはそもそも必要性がない可能性が高いです。 もしこういったモジュールがプロジェクトに含まれているのを発見したら、 整理を検討した方が良い でしょう。 主系列 変動性の高いコンポーネントをこれらのゾーンからできるだけ遠ざけておくべきなのは明らかだ。両方のゾーンから最も離れた点を結ぶ軌跡は(1,0)と(0,1)をつなぐ直線になる。この直線を主系列と呼ぶことにする。 (第14章 コンポーネントの結合より) 苦痛ゾーンや無駄ゾーンから遠いところにある直線、(1, 0)と(0, 1)の間をつなぐ直線を書籍では主系列と呼んでおり、 主系列からの距離が近ければ近いほど、モジュールの安定度と抽象度が比例した関係にある ことになります。 主系列からの距離 モジュールがどの程度主系列から離れているかの指標として、書籍では以下の指標を定義しています。 D(Distance): 距離。D = | A(抽象度)+I(安定度)-1 | Dが0の場合、モジュールは主系列上にあることになり、Dが1の場合は最も主系列から遠い(苦痛ゾーンか無駄ゾーン)にあることになります。 マルチモジュール開発をする際は、 全てのモジュールが極力主系列上に近いところに配置されるように開発を進めていくべき でしょう。 クラシルのモジュール構成はどうか? さて、最後に簡単ですが、ここまでの内容を使って、クラシルのモジュール構成を分析してみたいと思います。以下は現行のクラシルのモジュール構成を簡略化した図になります。 クラシルではUIとデータの2層にアプリケーションをモジュール分割した上で、それぞれ機能ごとにモジュールを分割しています。 ui:baseとdata:baseはそれぞれインターフェース用のモジュールであり、UI層同士のアクセス、データ層同士のアクセス、UI層からデータ層へのアクセスはそれぞれui:baseとdata:baseを経由して行うようになっています。一方で、共通で利用する具象クラスもここに含まれています。 そしてui:xxxとdata:xxxに、それぞれui:baseとdata:baseに定義されたインターフェースの実装が含まれているという構造になっています。 図ではそれを矢印として表示すると煩雑になってしまうため省略していますが、common_modulesは基本的にどのモジュールからもアクセスできるようになっています。(なので図では依存しているモジュールの数は少ないが実は安定度MAX)なので、これらのモジュールが頻繁に変更されるようだと、設計を見直す必要がありそうです。 安定度に着目してみると、 app ファン・イン = 0 ファン・アウト = 14 I = 14 / (0 + 14) = 1 となり、極めて不安定なモジュールから依存グラフが始まり、 ui:recipe ファン・イン = 1 ファン・アウト = 3 I = 3 / (1 + 3) = 3/4 ui:base ファン・イン = 6 ファン・アウト = 3 I = 3 / (6 + 3) = 1/3 data:recipe ファン・イン = 1 ファン・アウト = 3 I = 3 / (1 + 3) = 3/4 data:base ファン・イン = 12 ファン・アウト = 2 I = 2 / (12 + 2) = 1/7 と安定度が推移しており、基本的に不安定なモジュールから安定したモジュールへ依存関係があることが分かります。 そして、ui:baseやdata:baseといった共通インターフェースを配置するモジュールが依存グラフの先にあることで、ある程度安定度と抽象度が比例していることが分かりますが、今後の改修でui:baseやdata:baseモジュールに具象クラスを多く含むようになってきてしまうと、プロジェクト全体の改修しにくさに繋がっていきそうです。実際、どうしても共通モジュールは肥大化しがちなので、適宜リファクタリングをすることでなるべく抽象度を下げていきたいものです。 今回は、クラシルのモジュール図の一部を使って安定度と抽象度の分析を行ってみました。皆さんのAndroidプロジェクトでも、是非やってみてはいかがでしょうか。 まとめ 全AndroidエンジニアはClean Architectureの第IV部を読んだ方がいいよ Gradleのモジュールは第IV部のコンポーネントを表現するのにうってつけの単位だよ モジュールは同じタイミング、同じ理由で変更されるものをまとめるといいよ どのくらいの粒度で分割すべきかはアーキテクトの判断次第だよ、バランスだよ モジュールは循環依存できないことを生かした設計を考えるといいよ モジュールの依存関係は変更されやすいものから変更されにくいものへ依存する方向で書くといいよ 依存するモジュールが多いモジュールは不安定で、依存するモジュールが少ないモジュールは安定していると考えると楽だよ モジュールの安定度とコードの抽象度が一致しているといいよ モジュールの安定度・抽象度を計測する便利な指標があるよ、簡単だから今日から使えるよ おわりに クラシルのAndroidチームでは、生産性の高いアーキテクチャを維持・アップデートしつつも楽しくプロダクト開発できる人、したい人を求めています。 今回の記事を読んで興味が出てきた方、是非お話ししましょう! 下記の採用リンクから応募いただいてもOKですし、Twitterから気軽にDM頂けてもOKです! dely.jp twitter.com
アバター
こんにちは、プロダクト開発本部SREチームの松嶋です。 delyのSREチームは、2020年末頃まで最大2人体制の少数で奮闘してきましたが、嬉しいことにこの1年でメンバーが4人と倍増しました。 それまでは、リソース不足であったため足元にある緊急度の高い課題を解決していくことがSREのメインイシューで、長期的に取り組んでいく必要のある改善業務に着手することが困難な状態でした。 しかし、SREのプラクティスを何も実践できていなかった訳ではなく、想定外の複雑さを減らし、今以上に増やさないための文化づくりを意識的にしてきたので、サービスの信頼性が大きく下がることはほとんどなく、アラート対応に追われる状況に陥ることは防げていたと思います。 実際どのように想定外の複雑さを減らす取り組みをしていたのかは、現CTOの井上が「SRE NEXT 2020」にて発表しているので、興味のある方はこちらの記事をご覧ください。 tech.dely.jp メンバーが増員したことでリソース不足からは解消されたのですが、本来SREとして何をすべきなのか役割が曖昧であること、またチームとして機能するための仕組みがないことが浮き彫りとなりました。 そこで本記事では、ここ1年で自分たちがサービスや組織に提供できる価値を改めて再考し、SREがチームとして効果的に機能するために現在まで取り組んできたことについて紹介したいと思います。 1. チームミッションの再考 最初に取り組んだのは、SREチームのミッションの再考です。 delyのバリューとミッションが更新された同時期にちょうどメンバーが4人になったため、SREは会社のバリューを体現していくためにチームとして何を成し遂げていくのかを議論しました。 delyのバリューは、 「Trade on」、「Deliver Passion & Happines」、「Good to Great」、「Heart to Heart」 の4つがあるのですが、これらのバリューに沿ってSREとして何を実現していくのかチーム内で話し合い、また開発部の他チームの意見も取り入れた上でミッションを決定しました。 delyが持つ熱量と幸せをできるだけ多くの人に届けられるように、プロダクトの価値最大化をシステム運用において実現する。 その過程において最善の選択が何であるかを考え、システムの設計と運用を改善し続ける。 このミッションには、何故私たちSREはシステムの設計と運用を改善していくのかの想いが込められています。SREは直接的にプロダクトの機能開発に貢献している訳ではないですが、信頼性と開発速度の2つの指標を追っているのは、ユーザーの満足度と事業の成功度を高めるためであり、その手段としてシステムの設計、運用を改善していることを日頃から意識していくことが重要だと考えています。したがって、会社として大切にしている「delyが持つ熱量と幸せをできるだけ多くの人に届けられる」、「プロダクトの価値最大化」という文言をミッションに入れることにしました。 ミッションを再定義することで、delyのSREらしさが可視化されたと思いますし、チームとしての共通認識が生まれ、足元が揃いやすくなったのではないかと感じています。 delyのバリューについて詳しく知りたい方は、こちらのカルチャーデックをご覧ください。 https://speakerdeck.com/delyinc/delyculture speakerdeck.com 2. 役割スコープの策定 続いて、ミッションに沿ったSREの役割スコープを定めました。 役割スコープとは、SREの責任範囲や業務内容を明確にするためのものです。 何故ミッションだけではなく役割スコープも定義したかというと、SREの業務は組織やフェーズによってやるべきことが変わってくるので、責任範囲を決めることで、SREメンバーの認識のずれを最小限にし、チーム間のコミュニケーションを取る上でもSREまたは開発者がボールを持ったほうが良いのか判断しやすくするためです。 SRE本の第1章には、GooleのSREの責任範囲について以下のように書かれています。 ワークフローの細かい部分、優先順位、日々の運用は SRE のチームによって異なっていますが、 サポートするサービスに対する一連の基本的な責任と、中核となる信条の尊重は、すべてのSRE チームに共通するものです。概して、SRE チームは、サービスの可用性、レイテンシ、パフォー マンス、効率性、変更管理、モニタリング、緊急対応、キャパシティプランニングに責任を負います。私たちは、SRE チームはどのように環境とやりとりすべきか、業務の規範を明文化しました。 この環境には、実働環境のみならず、プロダクト開発チーム、テスティングチーム、ユーザーなども含まれます。これらのルールや作業のプラクティスは、運用の作業ではなく、エンジニアリング作業に集中し続けるのに役立っています。 1.3 SRE の信条/Site Reliability Engineering しかし、delyとサービスの規模やフェーズも全く異なるため、Googleの考えを取り入れつつもdelyのSREが現状取り組んでいることを踏まえて役割スコープを11個定めました。 決定したdelySREの役割スコープは、以下のとおりです。 1. サービスのSLOを下回ることなく、変更の速度の最大化を追求する 2. モニタリング 3. 緊急対応 4. 変更管理 5. 需要の予測とキャパシティプランニング 6. パフォーマンス 7. インフラコストの管理 (AWS/GCPのコスト管理) 8. コンサル/オンボーディング (開発者へのインフラオンボーディング、サーバーサイド設計レビュー) 9. ローンチ調整 (新規サービス立ち上げ時のコンサルティング、新規サービスのインフラ設計・構築・運用) 10. サービスのセキュリティ (インフラ周りのセキュリティ対策、AWS/GCPのアカウント・権限管理) 11. サービスの復旧準備 (永続データのバックアップ設計・運用) ※項目7~11が、dely独自の役割スコープなので注釈をつけています。 SRE本でも言及されている項目も含まれていますが、delyのSREが今までやってきたこと、これからもやっていくべきことを考慮した上で、新しくスコープを定めた方が自分たちの業務範囲を認識しやすく、今まで以上に円滑なコミュニケーションや意思決定ができると考えたため、独自のスコープを定義しました。 3. 課題の洗い出し&優先度決め ミッション及び役割スコープが決定し、チーム内の共通認識が出来上がったところで次に取り組んだのは、現状システムが抱えている課題の洗い出しと各課題の優先度付けです。 SREが解決していくべき課題は大小様々ありますが、基本的に上述した役割スコープに紐づくものになっています。 また、課題が可視化されていても各課題の優先度が数値化されていないと、メンバーが着手しやすいものや影響度の小さい課題ばかり取り組んでしまう可能性があり、優先度の判断が個々に委ねられてしまうので、SREチームの価値を最大限発揮することができません。 そこで、課題の優先度を客観的な指標で決定できるように、 「信頼性」、「開発速度」、「運用効率化」、「工数」 の4つの指標を用いて優先度を算出する方式を採用しました。 「信頼性」、「開発速度」、「運用効率化」の3指標には3~4段階のウェイトを割り振り、それぞれを掛け合わあせたものを「工数」で割って算出した値が高いほど優先度が高い課題となり、その課題から取り組んでいくようにしました。 「優先度」=「信頼性」×「開発速度」×「運用効率化」/「工数」 各課題における「信頼性」、「開発速度」、「運用効率化」の重み付けは、以下のような重み付け表で定義しているので、課題に最も近いポイントを選択するようにしています。 # 信頼性 - サービスダウンやパフォーマンス悪化等、ユーザー体験が著しく損なわれる可能性が高い or 既にユーザーへ悪影響がでている ->4 - 信頼性を下げている原因となっているが、現段階ではユーザー体験が著しく損なわれる可能性が低い ->3 - 現状は問題として顕在化していないが、解消するとより信頼性の向上につながる ->2 - 信頼性の向上には影響がない ->1 # 開発速度 - 全体、または各Squadの機能開発速度を既に大きく妨げている原因となっている or 開発速度を著しく下げている可能性が高い ->4 - 開発速度を下げる要因の1つではあるが、現段階では致命的な問題になっている可能性が低い ->3 - 現状は問題として顕在化していないが、解消するとより開発速度の向上につながる ->2 - 開発速度の向上には影響がない ->1 # 運用効率化 - SREの運用業務の負荷が既に高く、自動化することで運用コストが大幅に削減できる ->3 - SREの運用業務の負荷がそこまで高くないが、自動化することで運用コストが削減できる ->2 - 運用作業の自動化によって運用コストの削減につながらない ->1 実際にSREチームで運用している課題表はこのようになっています。 SREチームの課題表 基本的に優先度が高い順に着手していくルールにはなっていますが、運用開始時点からルールで縛りすぎると柔軟に対応しづらくなってしまうので、多少の前後は許容することにしました。 また、システム自体も日々刻々と変化しており、課題も時間が経つにつれ優先度も変動があるはずなので、定期的に見直し会を開催し優先度をすり合わせたりと運用面も改善していっています。 4. 運用負荷の計測 皆さんご存知の通り、Googleでは手作業による運用業務の割合がSREが受け持つ全業務のうち50%以下に抑え、それ以外の時間は長期間のエンジニアリングプロジェクトのために時間を費やすことを目指しています。 「手作業、繰り返される、自動化が可能、戦術的、長期的な価値がない、サービスの成長に比例して増加する」 *1 という特徴を持つ作業はトイルと呼ばれており、エンジニアリング作業の進捗を妨げるものです。 delyのSREチームでも自分たちがプロジェクト業務に時間を割けているのかどうかの判断が肌感覚になっていたので、GoogleSREのこちらの記事を参考に計測、トラッキングしていくことにしました。 cloud.google.com Googleと異なっている点は、トイルだけではなく、プロジェクト業務(SRE課題の取り組み)とオーバーヘッド(MTG、1on1、評価等)以外のものは全て計測対象にしたことです。 何故ならトイル自体が明確にトイルである、トイルではないと二分することが難しく、スペクトラムとして考えるほうが良いので *2 、トイルのみを洗い出して計測するよりも運用業務全般を対象にしたほうが負荷が高いタスクが可視化されやすく、改善のためのアクションを取ることができると判断しました。 実際の計測方法は、1週間で運用業務に費やした時間と対応した回数を記録していき、1週間の勤務時間あたりどの程度運用業務に使っているか割合を出しています。 最近の運用負荷の変動と内訳を集計した結果は以下のとおりです。 SREチームの運用負荷の割合 運用タスク別集計結果 運用開始時は、ちょうどテレビ番組にてクラシルのレシピ紹介が大きく取り上げる機会が近かったこともあり、負荷対策に時間を使っていたため一時的に運用負荷が高い割合となっていましたが、レビューが一部のメンバーに集中していたり、開発者依頼の運用タスクが一部負担が大きく効率が悪かったので、この部分は改善に取り組んできました。 レビューに関しては、一部のメンバーに偏らないようランダムアサイン方式に変更したり、運用タスクに関しては自動化や開発者自身でも対応できる部分を増やすために環境整備を実施してきたので、運用負荷は徐々に減少しています。 5. SRE月報の実施 最後にSREの社内認知の拡大のために取り組んでいる内容について紹介します。 SREの存在価値を最大限発揮してしていくためには、他のチームやステークホルダーのサポートや理解が必要不可欠です。 しかし、現状delyのSREチームは、他のチームから見るとSREが普段どのようなことに取り組んでおり、何を改善しているのかが見えづらく、SREの役割を理解しづらいという問題があります。 SRE自体の認知が低い状態でSREのプラクティスを実践していっても、浸透しなかったり形骸化してしまいますし、まずは自分たちが普段どんなことに取り組み、何を改善して成果をあげているのかを可視化し、知ってもらうことが必要だと考えたので、システムの現在の状態や傾向、その月に発生した障害の話やSREメンバーが改善した項目について月ごとに共有していくことにしました。 もちろんこれだけでSREの認知度や理解度が上がる訳ではないので、今後他にも社内認知拡大のための取り組みを実施していく予定です。 最後に 今回は、SREチームが自分たちの価値を最大限発揮できるように取り組んできたことを5つ紹介しました。 淡々と今までやってきたことを書いていますが、現在に至るまで様々な紆余曲折がありました。 まだチームとして始動したばかりで発展途上だと思うので、これからも良いプロダクトを作っていくために、システム設計、運用を改善していきたいです。 私たちがチーム立ち上げ期にどのようなことに苦戦し、乗り越えてきたかなど少しでも興味を持っていただけたら、是非カジュアルにお話ししませんか。 meety.net https://meety.net/articles/t2--tr6k9sxi_jo meety.net delyでは全職種採用強化中です。気になる方は以下のリンクから募集職種一覧がご覧いただけます。 speakerdeck.com *1 : 5.1 トイルの定義/Site Reliability Engineering *2 : 6.3 トイルの分類学/The Site Reliability Workbook
アバター
こんにちは、エンジニアリングマネージャーのtakaoです。 こちらの記事はdely Tech Talk 「#1 dely 新CTO 井上崇嗣 / dely new CTO Takashi Inoue」 の文字起こしです。 anchor.fm 今回の配信では、CXOの坪田さん、コマース事業責任者の大竹さん、新CTOの井上さんの3名に、CTO交代に関するお話をしていただきました。 (細かい相づちや話し言葉など文字起こしの際に編集している箇所があります。話している3名の雰囲気などはPodcastを聞いていただいたほうが感じられると思うので、お時間がある方はぜひPodcastをお聞きください。) 自己紹介 坪田: 今日はdelyTechTalk第1回ということで、CTO交代について話したいと思います。僕はdelyでクラシルのプロダクトマネジメントをしている坪田なんですけど、今日は大竹さんと井上さんに来てもらって、CTO交代について話していきたいと思いますので、よろしくお願いします。早速CTO交代しましたということで、たけさんからバトンタッチした思いを語っていきますか。 大竹: まず知らない人もいるかもしれないので自己紹介すると、大竹雅登といいます。この会社は代表の堀江さんと一緒に共同創業して2014年からなのでもう7年半ぐらい経ちます。創業のときからずっとCTOとして自分でコードを最初はずっと書いて、特にクラシルのときは最初は自分1人しかいなかったので、1人でコードを書いたりしていました。その後は、開発組織が大きくなって組織マネジメントとかをやっていったという経緯があります。2018年以降はコマース事業部というところの事業責任者としてビジネス側を今見ています。そういった経緯なんですけども、今回9月1日付でCTOを交代して、この後紹介する井上さんが新CTOとしてdelyのCTOに就任したというような経緯になります。井上さんの紹介もしてもらっていいですか。 井上: 井上と申します。僕は2018年の5月に入社したんですが、delyは4社目で、新卒ではSIerに入社してて、そこから転職して2社経験しています。一つ目がゲームを作ってる会社で、ゲーム作ってたわけではなくて、開発者が楽をするためのプラットフォームみたいなところをやっていました。その後はWeb系のベンチャーに行って、そこでインフラエンジニアやってたというところで、delyはSREとして入社しました。そこからちょっとマネージャーとかいろいろしつつ、開発部の部長をしつつ今回CTOに交代させていただいたという感じです。 CTO譲りますの記事から2年半の経過〜なぜ井上さんがCTOに選ばれたのか 坪田: たけさんCTOを譲りますっていう宣言をしてからどれぐらい経ちました? 累計70億調達、1500万DL突破の「クラシル」のCTO、譲ります。|大竹雅登/dely|note 大竹: 2年ぐらいだったかな。クラシルのCTOを譲りますとブログを書いたのが2019年なんで、1月とかで2年半以上かかったかな。当時、もともと2019年時点で僕がコマース事業部の事業責任者になってビジネス側にもフォーカスするって決めてたので、CTO業を専任でやる人が必要ってのはその当時から、社内としても認識があって、探してたんですけど、実際あの記事を出してから30人ぐらいは連絡して一部の人は会ったりしてたんですけど、なかなかフィットする人はいなくて。delyにカルチャーフィットしてるとか、スキルが近いとか経験があるとかそういうのも全部フィットする人ってなかなかいなくて、すぐに交代っていうことには全然ならなかったんですけど、今回井上さんに決めたというか、交代するっていうふうにした理由にも繋がるんですけど、今クラシルってのは、リリースしてから5年半ぐらい経ったかな。アプリだと5年半ぐらい経って、結構成熟してきていて、今後もっと発展するためにはなくちゃいけないことってのがいくつかありますと。そのうちの1個が今後今やっていきたいことにも繋がるんですけど、データをしっかり活用し、かつリコメンドを強化していきたいと思ってですね、データパイプラインをしっかり整備してそれを基に、個人に合ったレシピとかコンテンツをパーソナライズしてお届けするっていうことをやっていかなくちゃいけなくて、これまでと比較して、技術というかテックの深いところまでちゃんと見れるCTOというか、見える人が必要なので、そこの部分を担ってもらいたい人ってのが適任なんじゃないかなと思ってるんですよ。井上さんは今何年でしたっけ入社して? 井上: もう3年半くらいですね。 大竹: 3年半ぐらいでSREでもちろんそのインフラとかバックエンドとかデータ回りとかってのは、もちろんそこに関してはプロフェッショナルですと、当然ながらかつこれまでエンジニアリングだけじゃなくて、開発部が今40人とかいる中で、そこの技術的なマネジメントとかピープルマネジメントとかっていうのをずっとやってたんでそこに対する信頼感が圧倒的にあると。外部で仮にスーパーマンみたいな人がいたとしても、結局チームからの信頼とか、この人が言うならそこに対してついていこうと思えるような人じゃないと、組織としてのチームとしてのパフォーマンスがでないじゃないですか。やっぱりそこは結局もう外部でとかじゃなくて、もう既に信頼感のある井上さんで、技術スキルも今やっていきたいことにマッチしているし、今後も信頼して任せられるっていう人なんだったら、井上さんが適任だよねっていうことを決めたという経緯ですね。 坪田: その信頼できるパートナーで技術もあるのが井上さんであると。これ流れめちゃくちゃ早かったですよね。そうしようって決めてから、何かその週中ぐらいに話して 大竹: 決めたのは、もうその週の中から3日とかで決めたというか、時間だともう1時間で決まったと思うんですけど 坪田: delyっぽい意思決定ですよね 大竹: そう、すぐ即決だったんですけど、でもそれってなんか、適当に決めたというよりかは、もう満場一致というか、delyの中の全員から見てもう井上さんだったらそこがいいよねと、そうなるべきだよねってことが決まったし、その後全体会議とかで発表したじゃないですか。もう全然何も違和感とかなく、結構あると思うんですよ、人によってはなんでこの人ってのはなるかもしんないですけどそこは全く違和感なく受け入れられたってのは、やっぱ適任だからなんじゃないかなと思ってます。 坪田: 素晴らしい。そんな、会話を受けて井上さんもちょっとメッセージを。CTOって突如発表されるのもあるんですけど、なんかもうこの人なら納得感があったみたいなところは、ある程度社内の雰囲気もあって、よかったですよね。 井上: そうですね。僕も僕なりにやるべきことをやってきたっていう感じはあって、別にCTOになりたいという気持ちでやってきたわけじゃなくてシンプルに今自分がやるべきこと、価値が最大化できるものは何なのかっていうのを考えてやってきたっていうのが、やってきたことかなっていうところはありますね。なので、やるべき人がいないところとか、宙に浮いちゃってるものとかなんだけど、やっぱやんないといけない部分みたいなのをやることが一番その会社にとって事業であったりプロダクトを良くすることであったりっていうところに、効くのであれば、やるしかないよねっていうところをやってきたっていう。 大竹: 僕その井上さんのスタンスと、CTOのやるべきこと一致してるなと思うもう1個が、他社とかも含めて結構成果を出しているCTO、あとうちの社外取になってる松本さん、LayerXで代表取締役CTOをやっている松本さんとかと話しててよく思うのは、技術だけじゃ絶対駄目なんですよ、技術力を持ってるとかエンジニアリング力が高いってのでは駄目で、結局経営者なんで俯瞰して、例えばチームのこととか、事業のこととかっていうのを俯瞰した上で、技術的なバックグラウンドを持っていてそちらに関しても正確な意思決定ができるっていう人がなるべきだなと思ってて、井上さんって最初もSREのスペシャリストとしてもちろん入社してるんですけど、何か必然か、井上さんのCapabilityが高いから、すごいハードルが高いプロジェクトとか抽象度の高い不確実性の高いプロジェクトがどんどん任されて、すごい大変だったと思うんですけど、そういうのをちゃんとこなすっていうか、全部あらゆる可能性とリスクとかを整理して、実行することができるのを3年間見てきたってのも1個大きな理由ですね。何かテクノロジーの能力だけで選んだわけでは全くないっていう状況です。 坪田: でもなんかそれはありますよね。今って会社も大きくなって人も増えてきて、松本さん理論のプロダクトマネジメント・プロジェクトマネジメント・ピープルマネジメント・テックマネジメントの中で少しずつ分業できるようになったじゃないですか。エンジニアはピープルマネジメントだと多分もう井上さんが今やってくれて、プロダクトはプロダクトで別であるみたいなところで、何かそれぞれの技術が生かせるようになってきたりとか、リソース投資できる状態になっていく中でのそのバトンタッチとして、何かいい感じになってきたのかなと思います。でも僕あれなんですよ。2年ぐらい前にdelyに入ったんですけど、井上さんその時リーダーだった?マネージャーじゃなかったっけ? 井上: 一応メンバーはいたっていう感じですね。 坪田: メンバーがいてリーダー職だけど井上さん全然マネージャー志向じゃなかったんですよ。なんか井上さんマネージャーにしますってなったときに、本当はもうやりたくないんですよみたいな話があって、なんかカフェで1on1したんだ。1年2年ぐらいだったらやりますって言ったんですよね。 井上: めっちゃなんか記憶曖昧ですけど、僕は完全に覚えていて、2年やってくれって言われましたね。 坪田: 最初は技術志向でSREを推進していきたい、プレーヤーとしてもやっていきたいけど、井上さんが一番適任だと思って1on1をして話始めたら、僕本来そっちをやりたいから2年やりますみたいなこと言って、2年経ったけどそのままずっとやっていただいてるみたいな感じですね。 大竹: 確かにマネージャーじゃなくてCTOになったという意味で2年の約束は守られてない 坪田: 井上さん的にその辺の考え方とかCTOをやると、技術的に突き抜けたいところから、少し後方支援の部分も含まれてくると思うんですけど、その辺の考え方とかはどうなんですか。 井上: CTOっていうのも抽象的ではありつつ、やっぱり誰がやるのかによって進め方とか方針とか変わってくるかなとは思っていて。最終的にどうあるべきなのかっていうところが揃ってれば、その山の登り方的なところは別に人それぞれやればいいのかなっていうのは思ってはいるので、僕は僕なりにやり方を考えて進めていて、その中で自分がやりたいことが満たせていけばそれでいいのかなとは思ってるので、別に何かCTOになったから今そのときにやりたいことが今できなくなったのかどうかっていうと、別に僕のやり方次第だなっていうところは思ってるんで、そこはいいのかなと思ってます。 大竹: 井上さんってやるまではちょっと保守的というか、「ちょっとやんない方がいいと思いますよ」って言うんですけど、やるとすごいExecuteするってのがあって、これ入社時からそうなんですよ。井上さんを採用するときに井上さんの前職が新宿の辺りにあったんすけど、そこに井上さんがOKっていうまで毎回行って毎回同じ話をするっていうのを3,4回して。井上さんも当時なんでこの人毎回同じ話するんだろうって思いましたよね。 井上: 全ての面接にたけさんはいるんですよ。 坪田: 新宿まで行ってたんですか? 大竹: 行ってました。何回か行ってその近くで、こうこうこうだから来てほしいですって何回も言って、わかりました持ち帰りますみたいな感じで毎回持ち帰られてもう1回言うっていうことをずっとやってたんですよ。 坪田: よく決めましたね。 井上: そうですね。やっぱりそのときはまだ組織も小さかったし、自分のスキルを活かせるなってその時SREとして入ってるわけだから、SREとして、自分のスキルが活かせそうだっていうところを思って入ってます 大竹: 当時何人ぐらいでしたっけdely? 井上: 10人ぐらいだと思いますね。エンジニアは10人ぐらいで、社員は5,60人ぐらい。 大竹: 今と比べたら全然、3分の1とか4分の1しかない規模なんで、その当時に入ってるんで、 坪田: でもそこからの信頼感の積み上げ、井上さん俯瞰力も高いじゃないですか。技術だけに特化してるんだけど、ありとあらゆることを何かアンテナ張ってたりしてるから、マネージメントやっぱ上手だし、でもあれですね、ちょっとこの話をすると時間が長くなりすぎちゃうから、ちょっと今後の新CTOの方針というか、サービスの方針も含めて、多分語らないと、時間があれですね。で、今ってなんか全社的にちょっと新しいことにチャレンジしてるっていうフェーズだと思っていてECもそうだし、クラシルもそうだし、他の事業もそうかなと思うんですけど、井上さんがCTOになって、データパイプラインをちゃんと整備していこうみたいな話も、背景とかもあると思うんですけど、そういうのを聞けたらいいなと思うんで、語っていただけるといいかなと思います。 CTOの交代に伴って組織や事業はどう変化していくのか 大竹: まだ本題の詳細については言えない部分が結構あるんですが、ものすごく仕込んでるんですよね。いろいろ変革期というか、クラシルってこれまでの5年間は、いわゆるレシピ動画としてのコアな価値みたいなのを磨き込んできたってとこがあるんすけど、さらにインフラとなるようなレベルにしたいなと思っててそこで1年ぐらい、仕込んで企画から実装から設計からテストからっていうのをずっとやって仕込んでる部分があって、そこに対して、 データをちゃんと活用するために、データパイプラインがあり、データのストレージがあり、それをリコメンドのアルゴリズムに反映するとかっていうのが必要になってくると、そこを今絶賛、採用だったり社内でのリソースの再配分だったりとか結構アグレッシブにしてるっていうのが状況ですよね 井上: そうっすね、やっぱり一般的なWebのアプリケーションだったりアプリを作るっていう話と、データを使ってどうのこうのするっていう話は、スキルセットがやっぱり違ってるっていうところが特徴的で、やっぱりデータを扱うっていうところのスペシャリティがないとちゃんとしたものが出来上がらなかったり、できたとしてもメンテができなくなったりみたいなことはよく起こるものではあるので、ちゃんとそこに対する開発なり考え方なりアーキテクチャなりみたいなところは、統一感を持ってやっていかなきゃいけないっていうところは間違いないなと思うんですね、これからこのdelyという会社が、そのデータを使ってどうこうするって話になったときに、それをなんか、ただ1人の人間がやるのかって言ったらそれは結構スケーラビリティに欠けるっていうところがあったり、そもそもそんなことできるのかっていう人がいるのかって話があったりして、難しさがやっぱり細分化するというか、役割を分けてやっていかなければいけないっていうところかなと思っていてただその役割を分けて採用はするんだけど、役割が分担されている中において、それをうまく遂行していくってのもまた難しさがあるみたいなところがあるので、それをうまく埋めつつ、ただしそのデータの部分、技術の部分をやってはいるんだけど、最終的にプロダクトを良くしていくって話とかユーザーに価値を与えるっていうところを一番目的とするっていうところは忘れずにやってくっていうところをやるっていうのは、今後やっていくべき部分かなっていうふうには思いますね。 大竹: 結構こういうデータとかアルゴリズムとかそういうところをやるっていうプロジェクトって長期になるじゃないすかどうしても。なので、ちゃんとやっていくぞっていう意思とあとやりきれる人がちゃんとリーダーシップを持ってやるってのがすごい重要だと思うんすよね。ポンとやろうと思っても、アサインがちゃんとされてなかったり、リーダーシップを持って推進する意思決定する人がいないとき、厳しいっていうか、フワッと終わっちゃうのが結構あるかなと思うんですけど、そこに関してはうちの場合そこに意思を持って今回の交代も井上さんがそこを名実ともにちゃんと引っ張っていくっていう人としてちゃんとアサインして、そこに権限ももちろんあるし、強い意思決定もできるような状態にして本腰入れてやっていくぞという経営的な意思決定とプロダクト的な意思決定と、あとそこに合わせた人材とをCTOというラベルをちゃんとアサインするというところまで、今セットでやってるっていう状況なんで、結構本腰入れてますっていうのはアピールしたいっすね。 井上: そうですね。結構やっぱり長期だし、作り変えるっていうのも簡単にはできない部分っていう話があって、どうしてもそこは誰かが意思を持つってのはすごく重要かなというふうに思っているので、そこを自分が担っていくっていうのが、手段はどうであれ、意思は絶やさないっていうところが重要かなと思ってます。 大竹: そうなんですよ 坪田: いやそれめっちゃ重要ですよね。なんか結構バズってるワードとかでそれこそ機械学習とかAIとかデータみたいなのって一瞬会社でやろうってなるけど、短期だと結果が出ないから、徐々にテンション下がっていって、意思が続かなくて、もう1年やってれば成果出たかもしれないけど、やめるみたいな会社って、結構多いと思うんですよ。それを今回、役職と権限とリソースがセットでできたっていうのは、多分その意思表明にもなるからその話もっと発信していったほうがいいですね。 大竹: こんだけやってる会社ないと思うんですよね。ちゃんと振り切ってやってるところはないと思います。 坪田: あとちゃんとC向けにデータがあるっていうのは結構重要かなって思ってて、やりたくても、データ量が少なくて実現できない会社って多いと思うんですけど、それで言うとC向けのサービスでこんだけ持ってるっていうのは、強みちゃ強みっすよね。 井上: そうっすね。ここは本当にいいところだなっていうのがあります。やっぱりプロダクトを良くするとか、ユーザーのためっていうところももちろんめちゃめちゃ重要でそこを本質的なところだとは思うんですけど、技術者としてみたいなところもしっかり考えていかなきゃいけないフェーズだなとは思っていてデータを扱う人として何を扱うのかみたいなところで、toC向けのユーザーのデータを使えるっていうのは、技術としても結構面白みのある部分なんじゃないかなと思っています。 大竹: 今クラシルはもう3200万ダウンロード累計超えてるんで、こんなサービスないですよ。僕ら社内にいるから普通に感じてるけど、3200万ですからね、それはなっすよ普通に考えれば、そういうサービスの変革期みたいなところなんで、 坪田: なんかあれですよね僕も結構周りの社外の人に言われたりするのは、もうクラシル5年経ってるからそんなやることないんじゃないかみたいなことを言われがちなんですけど、今結構サービスを新しく変えていこうという意志を持って、また新しいことやってるので、そういった方にぜひ来ていただきたいなと思うんですけど、どういう人がクラシルに来ていただきたい方なんですか。 delyにはこういう人に来てほしい 井上: delyの特徴でもあるしクラシルもTRILLもやってるってところでもあるんですけど、さっきも言ってる通り、ビジョンが明確で、本当にそこを達成するっていうことを考えてる会社だしそのためにバリューを作ったし、新しくしたしっていうところもあってちゃんとそのバリューに基づいて意思決定をしていくと、最終的にはユーザーに価値を与えるっていう風になってくるかなと思って、そこに共感してもらえる人。っていうのが前提かなという風に思ってます。それで、僕技術の話すごくしてますけど、やっぱり技術っていうところは、それを叶えるための手段になる部分かなというふうに思っているのでそこをちゃんと共感できる方。で、かつクラシルっていうサービスとか、TRILLってサービスを良くしていくことに共感してもらえる方なのであれば、もう満たしてると思うんで、それだけでいいかなと思う。 大竹: そうっすね、あと僕らのクラシルのサービスミッションって80億人に1日3回幸せを届けるってあったじゃないですか。だから、規模がまだ足りないわけです。それをもっと多くの人に伝えるためにはどうすればいいのかっていうことを考えるときに、もっときめ細かく各パーソナライズしなくちゃいけないとかっていう技術の方が必要になってくるので、そこもそういう意味でも結構変革期っていうのがあるんですね。ミッションに、結構忠実にその規模ももっと多くの人に使ってもらうってことを追い求めると、そういうところから発想の起点で必然的に必要になってくる技術力っていうのがあるというフェーズなんで、そういうサービス作っていきたい人ってのは、楽しいんじゃないかなと思いますね。 井上: そうですね、クラシルのデータを使ってっていうところで、本当にユーザー数が多いのでこれをいかに広げていくかっていうフェーズかなとクラシルにおいては。delyは結構TRILLとか、コマースとかもあるんで、フェーズがいろいろあるんすけど、これからやっていきたいことを実現しようと思っときに、すごい大容量のデータを使ってるとか、それを使って価値を生み出すということに共感してもらえる方っていうのは面白いんじゃないかな。 坪田: というのを今後、その辺は井上さんから発信してっていただけるわけですね。ツイッターを全然やらないという井上さん 大竹: 井上さんのTwitterをフォローしてください。ここまで聞いてる人。ここまで聞いた人は絶対興味あるはずだから、とりあえずフォローしてください。 twitter.com 井上: ちょっと情報発信が下手なんで。これからやっていかないといけない。 坪田: dely結構職人肌の人多いからね。信念あるし一生懸命やるけど、あんまり社外に発信しないっていう。井上さんそういうタイプだったと思うのでちょっとこれから発信者としての役割を担っていくというね。井上さんのキャラクターを話しておきますとですね、井上さんは任天堂が大好きすぎて、毎日任天堂のTシャツを着て出社してるんですよ、今も任天堂のTシャツ着てますね。 井上: これは実は違うんですよ。これUNDERTALEっていう、ちょっとたまたま任天堂じゃないです。一応Switchでできる。 大竹: 任天堂で覚えてもらっても大丈夫です 坪田: 井上さんが毎日いろんな任天堂のキャラクターというか、アイテムのTシャツ着てるじゃないすか。で井上さん全然Slackで絵文字使ってくれないから、俺めちゃくちゃそのTシャツの絵文字を追加してるんですけど、一向に使ってくれないです。 井上: 知ってはいるんですが、文字打ってるときに絵文字打とうっていうのが割り込んでこないんですよね。 坪田: いやでも、リアクションで絵文字使えるじゃないすか。で俺最近積極的にそういう絵文字を使ってるんすけど、井上さん一切そういうのしてくれないです。 大竹: 絶対やってください 井上: それはまずちょっと意識的にスタンプをいろいろなものを使っていくっていうのをやります 大竹: それもミッションで 坪田: なのでちょっとあれですよね、多分このPodcastが出るころにはたけさんがnoteを書いてくれると思うので、ちょっとそこから意思をバトンタッチして今後井上さんも技術的な発信をしていきつつ、ただそのサービスがどうなってくかみたいな話はECもそうだし、クラシルもそうだしそのデータパイプラインとかデータを使って何をするかっていうのは、多分このdely Tech Talkで今後語られていくんじゃないかなと思います。 大竹: はい。 坪田: そんな感じですかね。 大竹: こんな感じです。今仕込んでるやつとかも今後半年1年とかのスパンで出てくると思うんで楽しみにしてください。 坪田: はい。今日はありがとうございました。 大竹, 井上: ありがとうございました。 最後に delyでは全職種採用強化中です。以下のリンクから募集職種一覧がご覧頂けます。 dely.jp
アバター
TRILL開発部の石田です。 WWDC 2021で ShazamKit が発表され、楽曲認識アプリであるShazamのリソースを誰でも使えるようになりました。 今回はそのShazamKitの実装例と、Shazamで使われている楽曲認識のアルゴリズムであるAudio Fingerprintingについて紹介したいと思います。 ShazamKitについて スマートフォンなどのマイクから音楽を取り込み、その音楽が何かを教えてくれるサービスの代表格としてShazamがあります。 ShazamKitは、Shazamが持っている膨大な音楽のカタログと、音楽を認識するアルゴリズムを使うことができるライブラリです。 カタログ自体を作成することもでき、デベロッパーが用意した音源を使って独自のカタログを作り、それに対して録音した音声を認識させることもできます。 ShazamKitの利用にはXcode13が必要で、iOS15以降の端末でしか動作しません。 ちなみにShazamKitはAndroidでも利用可能です。 ShazamKitの実装 ShazamKitを使って、iPhoneのマイクから音楽を取り込み、その音楽が何かを表示するアプリの実装していきます。 まず、iPhoneのマイク利用の許可を得るためInfo.plistのNSMicrophoneUsageDescriptionを入力します。 またApple Developerのコンソールから、対象アプリにShazamKitの利用を有効にする必要があります。 まず楽曲の入力ですが、AVAudioEngineで受け取った音声入力を、SHSessionに流していきます。 具体的には以下のようなコードになります。 let audioEngine = AVAudioEngine() let session = SHSession() audioEngine.inputNode.installTap(onBus : 0 , bufferSize : 1024 , format : nil ) { (buffer, _) in session.matchStreamingBuffer(buffer, at : nil ) } try ? audioEngine.start() これによって入力された音声が自動的にサーバに送られ、結果が返ってきます。 結果はデリゲートメソッドで受け取ります。 楽曲が見つかった場合と見つからなかった場合のメソッドが用意されています。 注意しなければいけないのが、これらデリゲートメソッドはバックグラウンドスレッドで実行されるため、UIに変更を加える場合はメインスレッドで処理する必要があります。 func session (_ session : SHSession , didFind match : SHMatch ) { // 楽曲が見つかった場合に呼ばれる } func session (_ session : SHSession , didNotFindMatchFor signature : SHSignature , error : Error? ) { // 楽曲が見つからなかった場合に呼ばれる } 上記を用いてSwiftUIで簡単なアプリを実装をしてみました。 コードは以下のようになります。 import SwiftUI struct ContentView : View { @StateObject private var viewModel = ContentViewModel() var body : some View { VStack(alignment : .center, spacing : 10 ) { Spacer() AsyncImage(url : viewModel.artworkURL ) { image in image .resizable() .frame(width : 300 , height : 300 ) .scaledToFill() .cornerRadius( 10 ) } placeholder : { ProgressView() } Text(viewModel.title) .font(.title) .fontWeight(.bold) Text(viewModel.artist) .font(.title2) Spacer() Button(action : {viewModel.startFinding()}) { Text(viewModel.isFinding ? "Listening" : "Tap to Shazam" ) } .tint(.primary) .buttonStyle(.bordered) .controlSize(.large) Spacer() } } } import AVFoundation import ShazamKit import SwiftUI class ContentViewModel : NSObject , ObservableObject { @Published private ( set ) var isFinding = false @Published private ( set ) var title = "" @Published private ( set ) var artist = "" @Published private ( set ) var artworkURL = URL(string : "" ) private let audioEngine = AVAudioEngine() private let session = SHSession() override init () { super . init () session.delegate = self AVAudioSession.sharedInstance().requestRecordPermission { _ in } } func startFinding () { guard ! audioEngine.isRunning else { return } audioEngine.inputNode.installTap(onBus : 0 , bufferSize : 1024 , format : nil ) { (buffer, _) in self .session.matchStreamingBuffer(buffer, at : nil ) } try ? audioEngine.start() isFinding = true } private func stopFinding () { audioEngine.inputNode.removeTap(onBus : 0 ) audioEngine.stop() DispatchQueue.main.async { self .isFinding = false } } } extension ContentViewModel : SHSessionDelegate { func session (_ session : SHSession , didFind match : SHMatch ) { stopFinding() guard let items = match.mediaItems.first else { return } DispatchQueue.main.async { self .title = items.title ?? "" self .artist = items.artist ?? "" self .artworkURL = items.artworkURL } } func session (_ session : SHSession , didNotFindMatchFor signature : SHSignature , error : Error? ) { stopFinding() DispatchQueue.main.async { self .title = "not found" } } } これだけのコードで実際に楽曲を認識させることができます。 Audio Fingerprintingについて Shazamの楽曲認識アルゴリズムであるAudio Fingerprintingについて解説します。 詳しい内容は論文 ( An Industrial-Strength Audio Search Algorithm ) にまとまっているので、詳細はそちらを参照していただければと思います。 また、説明に用いる画像のいくつかは論文より引用していますので、画像の詳細を確認したい場合も元の論文を参照していただければと思います。 Shazamはカフェなどで流れている音楽を認識し、それが何の楽曲かを検出してくれるアプリです。 そのため、人の声や周囲の雑音など、楽曲以外の音声が入力に入っていても正しく楽曲を検出するような頑健なアルゴリズムが必要です。 音声は空気中を伝搬する波なので、楽曲認識ではその波自体のマッチングを行えば良いように思います。 しかし、CDなどから取り込んだ電子的な音源ならまだしも、環境音などノイズが乗っている音声は容易に波の形が変わってしまうので、カフェなどでの楽曲認識には向きません。 そこで、Audio Fingerprintingではスペクトログラムという表現を使います。下記の画像はWikipediaの Spectrogramの記事 より引用しています。 スペクトログラムは、横軸が時間、縦軸が周波数、色の濃淡で音の強さ (大きさ) を表現します。 例えばある曲をスペクトログラムに変換したとき、低い音がなっている場面ではスペクトログラムの下のほうが白くなり上のほうは黒くなり、逆に高い音がなっている場面では上が白で下が黒、というようになります。 楽曲をスペクトログラムに変換後、音の強さを用いてピークを検出します。 スペクトログラムでは時間、周波数、音の強さの3次元で表現されていたデータが、あるスレッショルドでピークを抽出することで時間と周波数の2次元の星座図のようなデータになりました。 Audio Fingerprintingでは、この星座図から任意の2点を選択し、それを特徴量として用います。 2点の選択は下図のように無数に行うことが出来ます。 星座図のある点1は横軸:時間 ( ) と縦軸:周波数 ( ) から と表現することができます。 同様に点2は と表現されます。 としたとき、点1と点2のペアを ] と表現することができます。 これがAudio Fingerprintingで用いる特徴量となります。 具体例を用いて説明すると、マイクで録音した10秒の楽曲があったとき、3秒の位置に1000Hzのピークが、4秒の位置に2000Hzのピークがあったとき、そのペアは [ 1000Hz : 2000Hz : 1秒 ] と表現できます。 何故 ] ではなく ] と表現しているかというと、カフェで流れている楽曲が元音源でいう何秒の位置のものか分からないため、ピークの位置関係 ( ) のみを特徴量として使っています。 この特徴量を用いて楽曲のデータベースに対してマッチングを行っていきます。 上図はマイクで録音した楽曲とデータベース内のとある楽曲とのマッチング例となります。 横軸はデータベースの楽曲の時間、縦軸はマイクで録音した楽曲の時間であり、図中の点は上記で抽出した特徴量がマッチした部分となります。 この図でいうと、例えば録音した楽曲 (縦軸) の17秒の位置とデータベースの楽曲 (横軸) の0秒の位置で、周波数とその位置関係を表す ] がマッチしたものがあったことになります。 この方法でマッチしたポイントが多い楽曲を正解と検出してもよいのですが、これだけでは精度がでません。 実際に正解の楽曲をマッチングさせた場合には下図のようになります。 中央付近にマッチしたポイントが一直線に並んでいる部分があります。 これは録音した楽曲の開始点が正解楽曲の40秒付近であり、そこから同じ時間経過に対して高い確率でマッチするため、一直線にポイントが並ぶことになります。 単純なマッチングの数ではなく、このような一直線にマッチするという特徴を使って、Shazamでは楽曲を検出しています。 以上がShazamの楽曲認識アルゴリズムであるAudio Fingerprintingの解説です。 繰り返しになりますが、より詳しい解説は 論文 にまとまっているので、興味のある方は参照していただければと思います。 まとめ WWDC2021で登場したShazamKitの実装例とAudio Fingerprintingの解説をしました。 何気なく利用しているShazamですが、背後にあるアルゴリズムを知ると、今後利用するときにスペクトログラムや一直線にマッチする結果を想像せずにはいられませんね。 delyではサービスをさらに大きくすべく、エンジニアを積極採用中です。 もし興味がありましたら、気軽にアクセスしていただければと思います。 dely.jp
アバター