TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

411

はじめに こんにちは、Retail Hub 事業部でエンジニアを務めている 羽馬 です。 この記事は、Vue.js 日本ユーザーグループ主催の Vue.js v-tokyo Meetup #21 で登壇した際の発表資料を元に、VueUse というライブラリを使って Vue.js 開発を効率化する方法をご紹介します。 登壇資料はこちら: speakerdeck.com VueUse とは VueUseは、Vue Composition APIのための包括的なユーティリティコレクションです。以下の特徴があります: 200以上の便利な関数を提供 Vue.jsアプリケーション開発の生産性を大幅に向上 ローカルストレージ、デバイス情報、スクロール位置、フォームバリデーションなど、幅広い機能をカバー 宣言的で再利用可能なコンポーザブル ライフサイクルフックの自動処理 必要な機能のみをインポート可能(tree-shaking対応) 高度にカスタマイズ可能なオプション vueuse.org VueUseを使用することで、複雑な機能を簡潔に実装でき、開発効率を劇的に向上させることができます。 VueUseが提供する主な機能 VueUseは非常に多くの機能を提供していますが、ここでは特に有用な機能をいくつか紹介します: 状態管理 useStorage : ローカルストレージやセッションストレージとリアクティブな状態を同期 useState : シンプルな状態管理 センサーと端末情報 useMouse : マウスの位置を追跡 useGeolocation : デバイスの位置情報を取得 useDeviceOrientation : デバイスの向きを検出 ブラウザ操作 useClipboard : クリップボードの操作 useDark : ダークモードの切り替え useFullscreen : フルスクリーンモードの制御 アニメーションとタイミング useInterval : 定期的な処理の実行 useTimeout : 遅延処理の実行 useTransition : スムーズな状態遷移 ネットワークとAPI useFetch : HTTPリクエストの簡易化 useWebSocket : WebSocketの操作 UI操作 useVModel : v-modelの簡易実装 useInfiniteScroll : 無限スクロールの実装 これらの機能を使用することで、一般的なWeb開発タスクを簡単に、そして効率的に実装することができます。 VueUseの具体的な使用例 VueUseの機能の中から、特に有用なものをいくつか抜粋して具体的に説明します: ローカルストレージの利用 ( useStorage ) ローカルストレージの利用 ( useStorage ) import { useStorage } from '@vueuse/core' const state = useStorage ( 'my-storage-key' , { count : 0 }) // stateを変更すると自動的にローカルストレージに保存される state . value . count ++ VueUseを使用しない場合: import { ref , watch } from 'vue' const state = ref ( JSON . parse ( localStorage . getItem ( 'my-storage-key' )) || { count : 0 }) watch ( state , ( newState ) => { localStorage . setItem ( 'my-storage-key' , JSON . stringify ( newState )) } , { deep : true }) // stateを変更する度に手動でwatchを設定する必要がある state . value . count ++ ダークモードの実装 ( useDark ) VueUseを使用する場合: import { useDark , useToggle } from '@vueuse/core' const isDark = useDark () const toggleDark = useToggle ( isDark ) // ダークモードの切り替えが簡単 toggleDark () VueUseを使用しない場合: import { ref , watch } from 'vue' const isDark = ref ( false ) const toggleDark = () => { isDark . value = ! isDark . value if ( isDark . value ) { document . documentElement . classList . add ( 'dark' ) } else { document . documentElement . classList . remove ( 'dark' ) } } watch ( isDark , ( newValue ) => { localStorage . setItem ( 'dark-mode' , newValue ) }) // 初期化時にローカルストレージから設定を読み込む isDark . value = JSON . parse ( localStorage . getItem ( 'dark-mode' ) || 'false' ) 無限スクロールの実装 ( useInfiniteScroll ) VueUseを使用する場合: import { useInfiniteScroll } from '@vueuse/core' const el = ref ( null ) const { arrivedState , reload } = useInfiniteScroll ( el , () => { // 新しいデータをロードする処理 }) VueUseを使用しない場合: import { ref , onMounted , onUnmounted } from 'vue' const el = ref ( null ) const isLoading = ref ( false ) const checkScroll = () => { if ( ! el . value ) return const { scrollTop , scrollHeight , clientHeight } = el . value if ( scrollTop + clientHeight >= scrollHeight - 50 && ! isLoading . value ) { isLoading . value = true // 新しいデータをロードする処理 // 処理が完了したらisLoading.value = falseにする } } onMounted (() => { el . value ?. addEventListener ( 'scroll' , checkScroll ) }) onUnmounted (() => { el . value ?. removeEventListener ( 'scroll' , checkScroll ) }) これらの例から分かるように、VueUseを使用することで、複雑な機能をより簡潔に、そして宣言的に実装できます。特に、ローカルストレージの同期やダークモードの実装など、頻繁に必要となる機能を簡単に実装できる点が大きな利点です。 一方で、VueUseを使用しない場合、より多くのボイラープレートコードが必要となり、ライフサイクルフックの管理やイベントリスナーの設定など、細かい実装の詳細に注意を払う必要があります。 ただし、プロジェクトの規模や要件によっては、外部ライブラリに依存せず、Vueの基本機能のみで実装することが適切な場合もあります。特に、使用する機能が限定的で、バンドルサイズを最小限に抑えたい場合などは、VueUseを使用しない選択肢も考慮に値します。 まとめ VueUseは、Vue.jsアプリケーション開発において非常に強力なツールです。以下に、VueUseを使用することの主な利点をまとめます: 開発効率の向上 : 複雑な機能を少ないコードで実装可能 頻繁に使用される機能が既に最適化されて提供されている コードの可読性と保守性の向上 : 宣言的なAPIにより、コードの意図が明確になる コンポーザブルの再利用が容易 学習曲線の緩和 : Vue.jsのベストプラクティスが組み込まれている 豊富なドキュメントとコミュニティサポート パフォーマンスの最適化 : ライフサイクルフックの自動管理 tree-shakingに対応し、必要な機能のみをバンドル可能 Vue.jsエコシステム互換性 : Vue.js 2、Vue 3、Nuxt.jsなど、様々な環境で使用可能 VueUseを活用することで、開発者はビジネスロジックの実装に集中でき、結果としてより高品質なアプリケーションを効率的に開発することが可能になります。ただし、プロジェクトの要件や規模に応じて、VueUseの使用を検討することが重要です。 今後のVue.js開発において、VueUseは重要なツールの一つとなることが期待されます。継続的な学習と実践を通じて、VueUseの可能性を最大限に活用していくことをお勧めします。
アバター
はじめに 背景 業務フロー 1. 分析の準備 KPIとログの設計 A/Bテストによる効果検証の準備 2. データの準備 エンジニアリング目線も加味しつつログ設計を最適化出来る データのニーズの変化に臨機応変に対応できる 3. データの分析 終わりに はじめに DELISH KITCHENでデータサイエンティストをやっている山西です。 今回は「データサイエンティストとしてプロダクト開発プロジェクトに積極関与した経験談」をお送りします。 背景 DELISH KITCHENをはじめとするプロダクトの開発/改善は、仮説検証サイクルによって日々推進されております。 仮説検証サイクルにより、各機能/改善のリリース後の効果の良し悪しが分析によって振り返られ、次の方策へと繋げられていきます。 本記事では、データサイエンティストの立場で、この仮説検証サイクルを支援するプロダクト改善フローを紹介します。 実践して学んだことや意識したこと等々もポエム調で織り交ぜていきます。 業務フロー プロダクトの仮説検証サイクルにおけるデータ職責としての業務フローは、 分析の準備 データの準備 分析および振り返り に大別できます。 以下、それぞれの業務感について簡単に紹介します。 1. 分析の準備 KPIとログの設計 プロダクトへの新機能追加/または改善を行う際、リリース後にその結果の良し悪しをKPI、特定の指標で観察することになります。 よって、 何を観測したいか →そのためにはどんな準備(分析設計/ユーザーの行動ログ)が必要か をPdM(プロダクトマネージャー)と連携しながら整理していきます。 その具体作業や心持ちは以下の記事に詳しくまとめています。 tech.every.tv 当たり障りの無い主張ではありますが、「目的、仮説をしっかり言語化し共通認識を持ったうえで具体(ログ、分析手法)を詰める」ことの大切さを、実践してみて改めて学ぶことが多々ありました。 ここが疎かになると、 KGIからズレたKPIを設定してしまう → 視点のすり合わせが出来ていない。アウトカムが意識出来ていない。 仕込んだログが宙に浮いたり一人歩きしたりする → ログが使われない、意図されない使われ方をしてしまう。 本来の目的とずれた分析をしてしまう → 統計、機械学習的にはリッチでも、ビジネスサイドから見たらSo What?な示唆になりがち 過剰な準備をしてしまう → ダッシュボード作ったけど見られない、使われない問題 など、苦い着地をしがちです。 企画時点から一蓮托生的にPdMと視点をあわせることが、より良い仮説検証サイクルを目指すため近道になると感じております。 A/Bテストによる効果検証の準備 プロダクトの開発/改善を振り返るための効果検証のアプローチとしては、A/Bテストを採用することが多いです。 過去にその推進の具体感や全体像についてそれぞれ紹介した記事があるので、よろしければご覧ください。 tech.every.tv tech.every.tv 2. データの準備 分析としての準備が出来たら、次はデータの準備に取り掛かります。 1.で策定した分析計画 の具現に向け、 ユーザーの行動ログ等のビッグデータ を目的に応じた形で利用可能な状態を作り出します。 そのために、 Spark, SQLを駆使しつつETLを実装 必要に応じて中間テーブルを実装 BIツール(ダッシュボード)による可視化、定期実行体制の構築 A/Bテストの設定 等々、いわゆるアナリティクスエンジニアリングの部分もデータ職責として一通り行います。 弊社は、データサイエンティストでも結構ETL等、データ処理をはじめとしたエンジニアリングのコードを書いている組織だと勝手に思っています。 ( データエンジニアと完全分業できるほど人員が多く無い」という裏事情も正直あったりはします。 ) これは個人のスペシャリスト志向、ジェネラリスト志向等々のキャリア感にも左右され賛否両論はあると思います。 が、個人的には「"データの成り立ち"や"ビッグデータとしての集計の勘所"を経験で裏打ちできる」ことで生まれるプラスの側面も大きいと考えております。 例えば以下のエピソードが挙げられます。 エンジニアリング目線も加味しつつログ設計を最適化出来る 前段の話に戻りますが、「1. 分析の準備」でログを設計する際には、 事業視点(観察したいKPIに対してどういうログが欲しいか) を考慮していました。 ここにさらに エンジニアリング視点(どういうログだと、効率的にETL, 集計管理できるか) を加味できるようになれば、双方の視点を踏まえたログ設計が可能になります。 データのニーズの変化に臨機応変に対応できる ビジネスサイドの要求は往往にして変化しがちです。 仮説に沿った分析、ログ設計計画を立てたとしても、「違う角度からデータを見たい」という要望は多々発生します。 こういう際、データエンジニアに頼らずとも「自前でデータの奥底にまである程度触ることができ、ETL手法や中間テーブルの持ち方等を再検討して新たなデータのニーズに対応できる」ことは、手の動かしやすさ、俊敏性に直結すると感じております。 3. データの分析 機能がリリースされ、データが観察可能な状態になったら、その経過を KPIの観察 、 BIツールの観察 等で見守ります。 そして、最終意思決定者であるPdMと共に結果や考察をまとめていきます。 仮説検証としては「1. 分析の準備」の内容の履行が主になりますが、必要に応じて追加の事後分析や探索的分析も行います。 例えば、「KPIの単純集計」や「シンプルな可視化」よりもさらに奥行きのある示唆(ユーザーニーズや、根底に潜むユーザークラスタなど)を得たい場合は、データと分析の特性に合った多変量解析やら機械学習手法を選定し実施します。 また、A/Bテストでは、解釈に癖のある「有意差」をどう評価するかなどなど、統計的な知見の解釈のサポートを行ったりもします。 そして、このようにして統計、機械学習の文脈で得られたデータ解釈を、PdM向けに還元することになります。 データの本質に着眼し、良い分析手法を選定し示唆を出すためのデータサイエンスのハードスキル面 その解釈結果をビジネスの言葉に翻訳し、PdMに還元するための言語化等々のソフトスキル面 など、意思決定を支援する計らいはさまざまな難しさはあるものの、新たな知見が生まれたり、成果に寄与したときは達成感があります。 終わりに 簡単ですが、プロダクト開発におけるデータサイエンティスト(アナリティクスエンジニアに近いかもしれません)の立ち回り例を紹介しました。 データ人材としてプロダクトの価値の増大に貢献できるよう、引き続き精進していきたいところであります。 似たような背景、課題感を抱えたどなたかに対して、この記事で何かしら響くものがあれば幸いです。
アバター
はじめに そもそもなぜメンテナンスが必要になってくるのか メンテの手順にどうやって再現性を持たせるか コード化して再現性を持たせる コマンドライン上で再現性を持たせる 事前に模擬メンテを行う アプリケーションコードの変更なしでメンテに追従できるようにする まとめ はじめに こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。 今回はバックエンド系を触っている人なら誰しもが一度は関わるであろうメンテナンス(以下メンテと省略します)の話をしていきたいと思います、 実際のメンテのための計画方法は以下の記事で紹介されていますので、気になる方はご覧ください。 tech.every.tv そもそもなぜメンテナンスが必要になってくるのか まずメンテが起きないように基本オンタイムでアプリケーションが更新されるように仕組みを作ることは大切です。 たとえば、CI/CD からブルーグリーンデプロイメントなどの手法を使って、アプリケーションの更新を自動化することで、サービス利用者にとっては透過的にアプリケーションが更新されることができます。 しかしアプリケーションが動いているインフラ周りの変更や、アプリケーションの大規模な仕様変更などが発生すると、どうしてもダウンタイムを伴うメンテナンスが必要になります。 メンテの手順にどうやって再現性を持たせるか サービスを止めるにあたって、メンテの手順を明確にしておくことが大切です。 しかしその手順は人間が行うものであるため、ヒューマンエラーが発生する可能性があります。 そのため、メンテ時には、アプリケーションの状態を元に戻すための手順を明確にしておくことが大切です。 例えば AWS の RDS でメンテンスを行う場合でも以下のような手順を踏みます。 (今回は例でインプレースアップグレードで話をします) バッチが走らないようにする アラートを一時的に無効にする サービスの停止を行う(ロードバランサーの切り替えて固定レスポンスを返す等) DB のバックアップを取る DB のアップデートを適用する アップデート後に動作確認を行う サービスを再開する(ロードバランサーの切り替えを元に戻す) アラートを有効にする バッチを再開する 必要であればバッチの再実行を行う ※実際に mysql8 化をした記事があるので気になる方はご覧ください。 tech.every.tv これらの手順だけでもかなりの数があり、毎回コンソール上から手動で実行するのはミスが発生しやすいです。 意図せぬ設定をしてどこで間違ったのかがわからないということが起こりがちです。 そのため、メンテ時には、アプリケーションの状態を元に戻すための手順を自動化出来るようにしておくことが大切です。 コード化して再現性を持たせる 例えば、Terraform を使って、インフラの状態をコードで管理することで、メンテ時にはコードを実行するだけでアプリケーションの状態を元に戻すことができます。 以下はロードバランサーのルールを変更するコードの例です。 こちらでは、ロードバランサーのリスナールールを変更して、メンテ中には固定レスポンスを返すようにしています。 メンテ解除時は変更差分を取り消して apply するだけで元に戻すことができます。 // メンテ用のルール resource "aws_alb_listener_rule" "server_maintenance_rule" { listener_arn = aws_alb_listener.lb_https.arn priority = 4 // priorityが競合しないように番号を調整しておく action { type = "fixed-response" fixed_response { content_type = "application/json" status_code = "503" message_body = jsonencode ( { message = "メンテナンス中です" } ) } } condition { host_header { values = [ "server.example.com" ] } } } // 通常のルール1 resource "aws_alb_listener_rule" "server_rule" { listener_arn = aws_alb_listener.lb_https.arn priority = 5 // 通常: 1 action { type = "forward" target_group_arn = var.server_tg_arn } condition { host_header { values = [ "server.example.com" ] } } } // 通常のルール2 resource "aws_alb_listener_rule" "dashboard_rule" { listener_arn = aws_alb_listener.lb_https.arn priority = 6 // 通常: 2 action { type = "forward" target_group_arn = var.dashboard_tg_arn } condition { host_header { values = [ "dashboard.example.com" ] } } } // 通常のルール3 resource "aws_alb_listener_rule" "web_rule" { listener_arn = aws_alb_listener.lb_https.arn priority = 7 // 通常: 3 action { type = "forward" target_group_arn = var.web_tg_arn } condition { host_header { values = [ "example.com" ] } } } コマンドライン上で再現性を持たせる また物によってはコード管理していないものもあると思います。 そこでは AWS CLI で事前にコマンドを作成しておくことで、メンテ時にはコマンドを実行するだけで適用することができます。 これは AWS CLI で RDS のインスタンスのエンジンバージョンを変更するコマンドの例です。 本来であればコンソール上でボタンを押して切り替えたり手で打つところがなくなるので、ヒューマンエラーを減らすことができます。 aws rds modify-db-instance \ --db-instance-identifier example-rds \ --apply-immediately \ --engine-version 8 . 0 . 35 \ --option-group-name default:mysql-8-0 \ --db-parameter-group-name mysql80-example-rds \ --allow-major-version-upgrade \ --enable-performance-insights 事前に模擬メンテを行う 実際に手順書を作成していざ本番だと、コマンド上のミスや考慮漏れなどが発生することがあります。 そのため可能であれば、開発環境や QA 環境等で事前に模擬でメンテを行うことで、本番でのミスを減らすことができます。 ただし本番とは異なる環境で行うため、実際のメンテ時には環境の違いを考慮して手順を確認することが大切です。 (実際にハマったものですが、DB アップデート時にインスタンスサイズの違いで開発環境では 15 分程度だったのものが本番では 30 分程度かかりました) そのため出来ることなら本番と近い環境で行うことが望ましいです。 アプリケーションコードの変更なしでメンテに追従できるようにする こちらは再現性の観点からは外れますが、アプリケーションコードの変更なしでメンテに追従すると、サービスへのデプロイをすることなく切り替えが瞬時に行えるようになります。 例えば API 通信をしているアプリケーション(クライアントアプリや web、dashboard など)で事前にメンテ用に特定レスポンス(503)の挙動を考慮しておくことで、メンテ時にはアプリケーションコードの変更なしでメンテに追従することができます。 const response = await fetch ( "https://api.example.com" ) ; if ( response . status === 503 ) { // メンテナンス中の場合の処理 // リダイレクトなり、画面に描画したり return; } // 移行通常の処理 まとめ ここまでメンテに関わってくることはそれなりにありましたが、メンテナンスはサービスを維持するために必要な作業です。 再現性を持たせたり手順を自動化することで、ヒューマンエラーを減らすことができ精神的にもとても楽になります。 完璧にすることは難しいですが、今後ともトラブルなくメンテをやりきれるよう普段から意識してサービス開発に取り組んでいきたいと思います。
アバター
はじめに こんにちは、Retail Hub 事業部でエンジニアを務めている 羽馬 です。 先日、Vue.js 日本ユーザーグループ主催の Vue.js v-tokyo Meetup #21 に登壇する貴重な機会をいただきました。本記事では、その経験を通じて得られた知見や、Vue.js 開発における効率化のヒントをご紹介します。 vuejs-meetup.connpass.com 登壇テーマ:VueUseで実現するVue.js開発の効率化 今回の登壇では、Vue.js 開発者にとって強力な味方となる「VueUse」というライブラリについて紹介しました。発表資料は以下からご覧いただけます speakerdeck.com VueUseとは VueUse は、Vue Composition API のための包括的なユーティリティコレクションです。200以上の便利な関数を提供し、Vue.js アプリケーション開発の生産性を大幅に向上させます。 vueuse.org なぜVueUseに注目したのか VueUseに注目した主な理由は以下の3点です 開発効率の劇的な向上 複雑な処理を数行のコードで実装可能 例: useLocalStorage でローカルストレージ操作が簡素化 コードの再利用性と保守性の向上 Composition API を活用したロジックのカプセル化 コンポーネント間での再利用が容易 Vue.js 開発スキルの向上 コミュニティで広く採用されているベストプラクティスの学習 ソースコードから Vue 3 と Composition API の効果的な使用法を学習 発表のハイライト 1. VueUse の特徴 200以上の多様なユーティリティ ローカルストレージ、デバイス情報、スクロール位置、フォームバリデーションなど幅広くカバー 2. 代表的な機能と使用例 // マウス座標の取得 const { x , y } = useMouse () // クリップボード操作 const { copy , copied } = useClipboard () // 非同期通信 const { data , error , loading } = useAxios ( 'https://api.com/data' ) 3. VueUse がもたらす価値 宣言的で再利用可能なコンポーザブル ライフサイクルフックの自動処理 必要な機能のみをインポート可能(tree-shaking対応) 高度にカスタマイズ可能なオプション 実践的な活用例 VueUseの実践的な活用例をいくつか紹介します レスポンシブデザインの実装 import { useMediaQuery } from '@vueuse/core' const isLargeScreen = useMediaQuery ( '(min-width: 1024px)' ) 無限スクロールの実装 import { useInfiniteScroll } from '@vueuse/core' const el = ref ( null ) const { arrivedState , reload } = useInfiniteScroll ( el , () => { // 新しいデータをロードする処理 }) ダークモードの実装 import { useDark , useToggle } from '@vueuse/core' const isDark = useDark () const toggleDark = useToggle ( isDark ) これらの例からわかるように、VueUseを使用することで、複雑な機能を簡潔に実装できます。 得られた学びと今後の展望 Vue.js v-tokyo Meetup #21 への参加を通じて、以下の点を再認識しました コミュニティの力:Vue.js コミュニティの活気と、知識共有の重要性 継続的な学習の必要性:常に新しいツールや手法を学び、実践することの大切さ 知見の共有:得た知識を社内外で共有し、フィードバックを得ることの価値 今後は、以下の取り組みを行っていきたいと考えています VueUseの社内導入と活用事例の蓄積 コミュニティイベントへの積極的な参加と貢献 社内勉強会やテックブログを通じた知見の共有 おわりに Vue.js v-tokyo Meetup #21 への登壇は、技術的な学びだけでなく、コミュニティとのつながりを深める貴重な機会となりました。イベントを運営してくださったスタッフの皆様、参加者の皆様に心より感謝申し上げます。 イベントの詳細なレポートについては、Vue.js 日本ユーザーグループの公式ブログ もご参照ください。 note.com 最後に、今回の経験を活かし、今後も Vue.js コミュニティへの貢献と、社内での知見共有を続けていきたいと思います。 今後も、このようなイベントや情報共有の場に積極的に参加し、Vue.js コミュニティの発展に少しでも貢献できるよう、努めてまいります。
アバター
はじめに こんにちは。DELISH KITCHEN開発部兼、CTO室 DevEnableグループの村上です。 エブリーでは今年から「社内外から憧れる開発組織へ」というのをミッションにDevEnableグループを設立し、社内活性化から広報・採用活動まで幅広い活動を現場のエンジニアが兼務しながら行っています。DevEnableの設立について詳細はCTOの今井がブログにしているのでぜひご覧ください。 tech.every.tv そのDevEnableの活動の中で今年はすでに Go Conference 2024 と Kotlin Fest 2024 への協賛を行いました。エブリーとしては初のスポンサー活動となったので、改めてこの半年を振り返っていきたいと思います。 なぜスポンサー活動を始めたのか スポンサー活動を始めたのは大きく以下2つの理由からでした。 エブリーという会社をより多くのエンジニアに知ってもらいたい 普段使っている技術のコミュニティへ貢献したい まず採用観点では、エブリーが提供しているサービスは知っていても会社自体は知らないという方も多く、まだまだ会社の認知度に課題がある状況でした。そんな中で、スポンサーとしての登壇やブース運営を通じて、多くのエンジニアとの接点を作りながらエブリーという会社を知ってもらえることへの期待は大きかったです。もちろんスポンサー活動自体、短期的にすぐに採用の成果が出るものではないですが、会社自体の認知度を上げることは転職活動を行うときの転職先の候補として挙げてもらいやすくなるので長期的な視点で見れば大切なことだと思います。 また採用だけではなく、会社としてはより技術コミュニティに貢献していきたいという思いがあります。それは私たちの普段の開発が様々なOSSや技術コミュニティの知見なしに行うことはできず、多くの恩恵を受けているからです。それをただ享受するだけではなく、エブリーとしても技術発信やスポンサー活動を行い、さらに技術コミュニティを盛り上げていくことはこの業界自体の活性化やより良いサービスが生まれていくことに繋がります。 こうした理由からエブリーでも普段使っている技術を中心に協賛するカンファレンスを年の初めに選定していきました。 スポンサー活動としての取り組み スポンサーブースの準備 開催の2ヶ月ほど前からスポンサーブースの企画などの準備をしました。特に今回は初の協賛となるので、エブリー自体がブースを出すのが初めての中で全て0から決めていく必要がありました。 1. テーブルクロス・バックパネル この2つはブースの印象を決める重要な役割となります。今回は広報、デザイナーと相談しながらエブリーが提供するDELISH KITCHENのサービスをテーマにしながら、カラーも含めたデザインを決めていきました。 テーブルクロスやバックパネルは多くの場合で一度作ったものを流用することが多くなると思います。カンファレンスによってブースの大きさも変わってくるので、机の大きさによって折り畳んでも違和感のないデザインにしたり、バックパネルもあえて2つに分けて、小さいブースでは片方のみを使うこともできるようにしました。 これらに加えて、運営Tシャツがあるとよりスポンサーとしてのまとまりも出て印象も強くなると思うのでおすすめです。 2. ノベルティ ブースにわざわざ足を運んでいただいた方に対して何か配れるノベルティがないか考えていきました。ノベルティを考える上で大事にしたのは以下の2点です。 エブリーらしさ、エブリーのサービスのファンになってもらえるようなもの なるべく嵩張らずにすぐに消費できるもの まずエブリーらしさでいうとエブリーが提供するDELISH KITCHENでは元々 キッチンツール の販売をしていたのもあったので、それを活用して抽選でプレゼントする企画を行いました。当日はDELISH KITCHENは使っていた方でもキッチンツールの存在を知ってもらえたり、これを使ってDELISH KITCHEN使ってみますという方もいてとてもいい機会となりました。 また、抽選とは別に全ての人に配れるものとして、コーヒーとクッキーを用意しました。オフラインでは多くの企業からノベルティをもらうのもあって、あまり大きいものだと邪魔になってしまう一方、カンファレンスでは長時間セッションを聞くので小腹が空いたりすることもあるので嵩張らずに消費できるものが良いと考えました。 ちなみにコーヒーは弊社CTOの今井がコーヒー好きというのもあって実は隠れたエブリーらしさがあったりします。いつかはCTOブレンドコーヒーをお披露目したいなとチームで企んでいます(笑) 最後にちょっとした失敗談からの学びとして、ノベルティを企画する際はそのカンファレンスのノベルティに関する注意事項もしっかりと確認しておきましょう。経験値的に個包装の飲食が問題ないと勝手に思って用意していたが、実は別のカンファレンスでは使えなかったということが弊社では発生してしまい急遽ノベルティを考えるということがありました。 3. ブースコンテンツの企画 ノベルティだけではなく、来場者とブースでどういったコミュニケーションを取っていくかも考えました。今回はいくつか候補をあげながら以下のようなテーマ別で企画を行いました。 会社・サービスを知ってもらう 実際にスーパーで設置しているサイネージでのDELISH KITCHENコンテンツの放映 アプリの紹介パネル カンファレンスにまつわる技術の会社内における活用状況を表すインフォグラフィックパネル 来場者同士のコミュニケーション カンファレンスにまつわる技術についてのアンケートボード 楽しんでもらえる参加型コンテンツ XフォローでのDELISH KITCHENツールのプレゼントキャンペーン まずは大前提としてスポンサーすることの目的が採用であったとしても採用色を全面に出すのはあまりおすすめできません。カンファレンスに参加するエンジニアはあくまでもその技術が好きでセッションを聞きにきています。私たちスポンサーはその熱をより盛り上げていくことがブースとしての役割の一つでもあると思っています。私たちはそれを前提に置いた上で今回はオフラインならではのスポンサーと参加者、そして参加者同士がコミュニケーションを取れるようにコンテンツを企画しました。 実際にアンケートボードやインフォグラフィックパネルをきっかけに多くのエンジニアと技術的な話が深くできたり、お互いのことが知れる機会を作ることができました。 エブリーブースにてKotlinのLinterに関するアンケートを行っています!✨ 現在はktlintが優勢です! ぜひブースにお立ち寄りください! #KotlinFest pic.twitter.com/wcVPHBj5vD — 株式会社エブリー 開発部 (@every_engineer) 2024年6月22日 x.com スポンサーセッションの準備 今回、エブリーではGo Conference2024にてスポンサーセッションをさせていただく機会をいただきました。ぜひ発表内容が気になる方はこちらをご覧ください。 speakerdeck.com 25分枠が与えられた中で、運営側で現場エンジニアへの登壇依頼と登壇内容のすり合わせを行いました。内容のすり合わせでは、会社としてこれまでの知見として溜まっていることを多少泥臭くても事例としてリストアップしていきながら登壇者とどういう切り口で進めていくかを会話していきました。基本的にこの準備段階では登壇者の方が大変ですが、私たち運営陣は今後も会社紹介スライドの準備や内容レビュー、発表練習などサポートできることは全力でやっていくスタンスを大切にしていきたいです。 カンファレンス当日の運営 当日はこれまで企画を進めてきた運営メンバーだけではなく、それぞれの技術カンファレンスに対して普段その技術を使っている現場のエンジニアにも声をかけて、参加してもらいました。 当日スタッフにはこれまでの企画背景を知ってもらうように事前に以下のような説明をしました。 ブース内容とその背景 当日のブース運営で意識して欲しいこと、注意点 各メンバーの役割 当日のシフトスケジュール ただこれは振り返ってみてですが、当日は予想外のハプニングがたくさん起きます。基本的な心得としてメンバーに共通認識として持って置いたほうがいいのはある程度それを想定して柔軟に動くことのように思います。実際に混雑予想も加味してシフトスケジュールを組んだが予想外の時間で混雑して急遽シフト外のメンバーがシフトに入ってくれたり、準備してた紙がなくなってチャット内で情報共有したりとそれぞれが主体的に動くことでなんとか乗り切ったことも多いです。 カンファレンス前後でコミュニティの熱をさらに盛り上げる スポンサー活動は当日のブース運営に注目が行きがちですが、スポンサーとしてはその前後でも技術コミュニティを盛り上げていくことも大事です。例えばエブリーでは今回このような取り組みを前後にやってみました。 カンファレンス前 社内のプロポーザル提出に向けてリストアップ、声かけを行う カウントダウンブログと題して当日までの数日間、関連する技術の知見を発信し続ける カンファレンス後 開催当日のカンファレンス参加レポート スポンサー同士でのアフターイベントの開催 カンファレンス前には社内外含めて、当日のカンファレンスに注目が集まるようにその技術に関する発信を強化し、カンファレンス後ではオフライン開催が増える中でどうしても参加できない方が多くなってしまうことが予想できたので、そういった方に向けてカンファレンスの盛り上がりを最速で届けるようなこともしました。 またアフターイベント的にスポンサー同士で技術イベントを開催して、カンファレンスきっかけでのコミュニティの輪を広げられるような取り組みにも初めて挑戦しました。結果として、カンファレンスとはまた違った一人一人との密なコミュニケーションをとることができたり、カンファレンスの感想をお互いに話せたりととてもいい取り組みだったので今後も定期的に続けていきたいです。 every.connpass.com every.connpass.com 反省点としては今回こうした取り組みの動き出しが遅かったことがあります。こうしたカンファレンス前後にも力を入れる場合、その設計も含めて2-3ヶ月前から動き出せると余裕を持って企画、進行ができ、より内容を充実させることができそうです。 半年間スポンサー活動をやってみて こうしたスポンサー活動自体、DevEnableグループだけが単独で動いただけでは到底実現することはできませんでした。専属のチームがいないからこそ、人事、広報、デザイナー、運営外のエンジニアに協力してもらいながら総動員で動けたからこそ実現することができたと思っています。 冒頭に話したように正直1~2回のスポンサー活動で短期的に何か大きな目に見える成果というのは感じにくく、そこは継続的に数年単位でやり続ける長期的な目線が大切になると思います。ただ今回初のスポンサー活動を行っていく中で、普段あまり外部のイベントに参加しないエンジニアも社内には少なくなかったですが、そうしたエンジニアからも参加するいい機会をもらえたという感想を言ってもらえたり、アフターイベントの取り組みで登壇機会を作ることで技術コミュニティに入っていく一歩目を支援できたりと社内のエンジニアの成長にも貢献できている実感がありました。実際にカンファレンスきっかけで技術更新が行われたりもしています。個人的には予想外にそうした採用、認知だけではなく、スポンサー活動を通じて、エブリーで働くエンジニアと技術コミュニティのつながりができ、そこから刺激をもらえる成長機会を作ることができることは気づきでした。 個人的にはこれからはこれをきっかけに社外の技術コミュニティと並行して、社内の技術コミュニティをもっと盛り上げていきながら、それぞれの分野でより知見がたまっていったり、発信が活発的に行われる状態を目指していきたいです。 最後に DevEnableグループではこうしたスポンサー活動以外にも社内活性化も含めて、幅広くエンジニア組織のための取り組みを強化しています。 DevEnableグループを面白そうと思った方や、そんな開発組織で働きたいと思った方はぜひお話しましょう! corp.every.tv
アバター
はじめに そもそも話題の背景 低下の要因たち 仕様変更に至るまでの経緯 計測方法の見直し テストファイルがあるものだけ抽出する(ホワイトリスト型) 除外したい pkg を名指しする(ブラックリスト型) まとめ はじめに こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。 最近はエルデンリングが再燃しています。 2024/06/18 に Go Conference 2024 の非公式イベントとして の Go BASH が開催されました。 andpad.connpass.com その際に go の ver 1.22 におけるテストカバレッジについて話をしてきたため、その内容を記事にまとめたものです。 speakerdeck.com そもそも話題の背景 go1.22 が出たタイミンで影響少なそうなリポジトリで試しにアップデートしてみたところ、CI を眺めているとカバレッジが大きく低下しているのを発見しました。 一瞬 go test -cover 壊れたのかと思いましたが、そうではなくリリースノートにも記載されている通り、カバレッジの計算方法が変わったことが原因でした。 tip.golang.org 内容をまとめると Go 1.22 以降、テストファイルのないパッケージでもカバレッジが表示される 関数がカバーされていない場合は 0.0%扱い 実行可能なコードが全くない場合は、「テストファイルがない」と報告される ex.) 構造体やインターフェースの定義のみで関数がない場合など のように、テストカバレッジの状況をより詳細に把握できるようになりました。 低下の要因たち テストを行っていない部分が影響を及ぼしており、それらを以下に列挙します。 自動生成系のもの エンドポイント生成(OpenAPI, GraphQL のジェネレータ) ORM 意図的に書いてない実装 テスト内容がテストコード作成時の労力に見合ってないもの 必要なテスト環境が複雑でチームやプロジェクト規模によっては Skip してるもの 自動生成系のものに関しては、提供されている pkg によってはテストコードの生成までしてくれるものもありますが、それでも全てのケースを網羅するのは難しいです。 また、意図的に書いてない実装は単純にテストをちゃんと書けばいいだけではありますが、プロダクトのフェーズやチームのレベル感次第となります。 一時しのぎとして 1.21 以前と同じ挙動にする場合は、テスト実行時に GOEXPERIMENT = nocoverageredesign を設定することで可能です。 GOEXPERIMENT =nocoverageredesign go test -cover ./... ただし、GOEXPERIMENT を指定すると予告なく使用できなくなる可能性があるため、別の対応策を考える必要があります。 仕様変更に至るまでの経緯 ここまでの話を踏まえて、なぜこのような仕様変更が行われたのか気になり、その原因となった issue を探してみました。 github.com 概要としてはカバレッジレポートの出力方式について改善の余地あるのでは?というものです。 議論自体は 2018/03/18 から行われており、当時は go1.10 時代です。 go1.10 のリリースノートにテストに関する変更が記載されており、その中で一度に複数パッケージのカバレッジが取れるようになったことが記載されています。 go.dev この変更の際に、カバレッジを計測できないテストがないものは、go1.21 と同じように計上されていないままでした。 そのため、カバレッジは計測範囲に対しての全体の割合を出したいという要望が出てきたようです。 中では以下のことが議論がされていました。 テストファイルのないパッケージについてカバレッジ(0%)を出力するべきか 出力した方が未テストの関数があることが分かりやすい 出力しないと総合カバレッジ率が不自然に下がる 総合カバレッジにテストファイルのないパッケージを含めるべきか 含めた方が未テストの関数が明確になる go test -cover と go test -coverprofile でカバレッジ出力を統一するべきか 統一しないとユーザーに混乱を招く可能性 統一すると要件によっては不自然な動作になる可能性 テストファイルがない=未テストなのか、それとも単にテストが書かれていないだけなのか 未テストと見なすべきか コードに応じて判断が分かれる可能性 [no test files]と 0.0%のどちらがユーザーにとってわかりやすいか 決定的な決め手はなかったようですが、やはり明示的に引数で対象とするパッケージを指定している以上レポートに含まれるべきという最初の要望が最終的に採用されました。 上記の話を踏まえて、以下の方針で計測方法を見直すことにしました。 意図的にテストないものをカバレッジ入れたくはない 注視したいのは自分たちが実装している部分 ビジネスロジック周り そもそも不要なものまで計測対象にしているのが良くない 自動生成系、意図的に書いてないところ 引数で対象 pkg は指定して、入れたくないものは除外しておく go test -cover ./… → go test -cover pkg1 pkg2 計測方法の見直し 基本方針をもとにホワイトリスト型とブラックリスト型の 2 つの方法を試してみました。 テストファイルがあるものだけ抽出する(ホワイトリスト型) 専用の抽出スクリプトから絞り込むようにしたものです。 この際 *_test.go があるディレクトリが対象とすれば自動生成系は巻き込まれません メリット これまでと同じような挙動に出来る デメリット コード全体の質を測るうえではテストがないと隠蔽されてしまう listup_test_pkg.sh #!/bin/bash # 現在のディレクトリからすべてのGoパッケージを検索 for pkg in $( go list ./... ) ; do # 各パッケージのディレクトリを取得 pkg_dir = $( go list -f ' {{.Dir}} ' $pkg) # *_test.goファイルがそのディレクトリに存在するか確認 if [[ $( ls $pkg_dir /*_test.go 2 > /dev/null ) ]] ; then # 存在する場合はパッケージ名を表示 echo $pkg fi done go test -cover $( ./listup_test_pkg.sh ) 除外したい pkg を名指しする(ブラックリスト型) 除外対象 pkg を一元管理して、テスト時に除外するようにしました。 管理は何かしらのファイルでできればいいので、yaml や json 等で記載します。 書き方によってはファイル名やディレクトリ構成に規則性があれば管理は容易となります (他の手として go test -cover の結果を grep -v で引き抜く手もありますが余計なテスト実行となるので採用しませんでした) メリット 除外したいものが明示的 今回の変更の背景を考えると現実的な落とし所になりそう デメリット 除外箇所の羅列が面倒で手間がかかる testcnf.yml ignore: - github.com/rymiyamoto/example-api/internal/app/ignore1 - github.com/rymiyamoto/example-api/internal/dashboard/ignore2 - github.com/rymiyamoto/example-api/internal/external/ignore3 exclude_patterns = $( yq e ' .ignore[] ' testcnf.yml | sed ' s/^/-e / ' | tr ' \n ' ' ' ) go test -cover $( go list ./... | grep -vE $( echo $exclude_patterns)) まとめ ここまで go1.22 移行後のテストカバレッジについて話してきましたが、その取り扱い方はプロダクトやチームによって異なると思います。 今回のように仕様変更による影響がある場合は、その変更の背景を知ることで今後の方針を決めるのに役立つはずです。 また、テストカバレッジはあくまで一つの指標であり、それが全てではないことを忘れずに、テストの質やカバレッジの意味を考えることが大切だと感じました。 今後も go のアップデートに合わせてプロダクトの品質向上に努めていきたいと思います。
アバター
目次 はじめに 勉強会の概要 LT枠1: Go x LLMで 新たなコード生成の可能性を探る (OPTiM 今枝) LT枠2: Go言語で行うメール解析 (every きょー。) LT枠3: プロダクトでどれくらいMELTしてますか? (OPTiM 坂井) LT枠4: slices/maps pkgを活用してオレオレ実装を撲滅したい (every ayaka.yoshida) まとめ はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 今回の記事では、先日OPTiMさんと共同で開催した勉強会についてご紹介したいと思います。 勉強会の概要 7月2日(火)に、弊社everyとOPTiMさんとの2社合同でのGoの勉強会 OPTiM × every Golang Developer Night を開催しました。 この勉強会は、OPTiMさんのご厚意により、OPTiMさんのオフィスで開催させていただきました。 受付では各社のノベルティも配られました。また、暑いなか参加上限に近い数の方にご参加いただき、勉強会も懇親会も盛況でした。 受付のノベルティ 会場の様子 勉強会中の風景 懇親会の様子 勉強会では、OPTiMさんとeveryからそれぞれメンバーが登壇し、Golangを利用したプロジェクトでの成功事例や課題克服にまつわるLTが行われました。 以下が当日のタイムスケジュールとなっています。 時間 内容 19:30 - 19:35 オープニング 19:35 - 19:40 OPTiM 会社紹介 19:40 - 19:45 every 会社紹介 19:45 - 20:00 LT枠1: Go x LLMで 新たなコード生成の可能性を探る (OPTiM 今枝) 20:00 - 20:15 LT枠2: Go言語で行うメール解析 (every きょー。) 20:15 - 20:30 LT枠3: プロダクトでどれくらいMELTしてますか? (OPTiM 坂井) 20:30 - 20:45 LT枠4: slices/maps pkgを活用してオレオレ実装を撲滅したい (every ayaka.yoshida) 20:45 - 21:00 質疑応答 21:00 - 21:10 クロージング 21:10 - 懇親会 LT枠1: Go x LLMで 新たなコード生成の可能性を探る (OPTiM 今枝) speakerdeck.com このセッションでは、Go言語のコード生成について紹介が行われました。 コード生成は、DX向上や品質・セキュリティ向上、パフォーマンス最適化、保守性・拡張性の向上、相互運用性・プラットフォーム互換性の向上などの目的で行われます。 oapi-codegen 1 のような api interface や templategen 2 のようなテンプレート生成など、 Go言語自体がコード/テンプレートの自動生成をサポートしているため、手軽にコード生成を行うことができます。 そこに、LLMによるコード生成も加わることで、より効率的なコード生成が可能になるという提案に関するお話でした。 コード生成ツールとして、 plandex 3 というGo言語で書かれたOSSも紹介されました。 コードを自動で生成できることで、開発効率が向上し、品質やセキュリティの向上にもつながるため、 LLMを織り交ぜたコード生成を上手く取り込むことで、より効率的な開発が目指せそうだと感じました。 LT枠2: Go言語で行うメール解析 (every きょー。) speakerdeck.com このセッションでは、Go言語を使ったメール解析について紹介が行われました。 メールのプロトコルの話から始まり、ヘッダーのようなメールの構成などの説明があり、実際にGo言語でメール解析を行う方法についても解説がありました。 メールのメッセージは、RFC2822 4 で規定されたメールプロトコルで構成されており、 メールヘッダーの情報を利用することで、トラブルシューティングやスパム検出、セキュリティ分析など様々な用途に活用できるようです。 Goではnet/mailパッケージ 5 が標準で提供されており、 メッセージからヘッダーやボディの情報を取得することで、様々なメール解析を行うことができるとのことでした。 発表中には実際のユースケースの紹介もあり、メール解析の活用方法についてのイメージをより具体的に持つことができました。 ちなみに、Goによるメール解析の実際のユースケースとして、きょー。が弊社everyのテックブログも書いているので、興味のある方はぜひご覧ください。 tech.every.tv LT枠3: プロダクトでどれくらいMELTしてますか? (OPTiM 坂井) (発表資料は公開準備中です。公開され次第追記します。) このセッションでは、MELT 6 という、モニタリング/オブザーバビリティに関する概念についての紹介が行われました。 MELTとは、システムレベルの懸念を理解するための「モニタリング」とアプリケーションレベルの懸念を理解するオブザーバビリティに関する考え方であり、 SaaSなどの発展で複雑化して管理者による制御が難しくなってきているシステムに対して、 Metrics/Events/Logs/Tracesを基本的な観測可能性のシグナルとして捉えることで、観測可能なシステムの開発ライフサイクルの実現を目指すための概念とのことでした。 OpenTelemetry 7 などを用いて、Goでどのように計装し可視化するのか、デモも交えて説明していただけました。 モニタリング/オブザーバビリティの考え方は、システムの問題の特定や解決を行うための重要な概念であり、 Go言語を用いた計装方法や可視化方法を学ぶことで、システムの理解を深めることができると感じました。 また、実際にデモを行っていただいたことで、どのように可視化されるのかが明確になり、ツールの良さをより実感することができました。 LT枠4: slices/maps pkgを活用してオレオレ実装を撲滅したい (every ayaka.yoshida) speakerdeck.com このセッションでは、Go言語のslices/mapsパッケージの活用方法について紹介が行われました。 Go1.21でslicesパッケージ 8 およびmapsパッケージ 9 が追加され、sliceやmapの操作をより簡潔に行うことができるようになりました。 これにより、以前まではforループなどを駆使して自前で実装していた処理を、公式提供のslices/mapsパッケージのメソッドに置き換えることができるようになったということでした。 実際にプロジェクトで使われているコードを新しいパッケージのメソッドに置き換えた例が提示され、コードがより簡潔で可読性の高いものになることが参加者も実感できたのではないかと思います。 Goでは、最低限の機能のみを提供することによるシンプルな言語設計を方針とされていますが、その影響で自前の実装が長大になるケースもあったかと思います。 しかし、slices/mapsのように、公式によるパッケージ提供のおかげで徐々にプログラムの記述が簡潔になってきているのではないかと感じました。 1.22以降でも、ループ変数のスコープの変更やイテレータなど、様々な改善が行われているため、今後もGo言語の進展に注目していきたいと思いました。 まとめ 今回の記事では、7月2日(火)にOPTiMさんと共同で開催した勉強会について紹介しました。 LT枠では、Go言語を使ったコード生成やメール解析、MELTを意識したGoによるシステムのmonitoring/observation、slices/mapsパッケージの活用について発表がありました。 それぞれのセッションを通じて、Go言語の新たな活用方法や機能、システムの可視化方法などを学ぶことができ、非常に有意義な時間を過ごすことができました。 最後に、このような機会を提供していただいたOPTiMさん、登壇者の皆様、参加者の皆様、本当にありがとうございました。お疲れ様でした。 最後まで読んでいただき、ありがとうございました。 oapi-codegen ↩ templategen ↩ plandex ↩ RFC2822 ↩ net/mailパッケージ ↩ New Relic | メトリクス、ログ、トレース、イベントとは?違いを解説 ↩ OpenTelemetry ↩ slicesパッケージ ↩ mapsパッケージ ↩
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 28日目の記事です。 はじめに DelishKitchenとヘルシカでバックエンドエンジニアをしているyoshikenです。 今回は新規アプリ開発の際に、Android/iOSアプリの課金処理(subscription)について awa/go-iapを使用して大変便利だったので、その紹介をしたいと思います。 awa/go-iap: go-iap verifies the purchase receipt via AppStore, GooglePlayStore, AmazonAppStore and Huawei HMS. Android/iOSの課金処理のフローについてはサイバーエージェントさんのテックブログで詳しく説明されているので、そちらを参照してください。(この記事を書いてくださった方がそのまま awa/go-iapの原型を作成されたかと 自動購読課金について【Android編】 | サイバーエージェント 公式エンジニアブログ 自動購読課金について【iOS編】 | サイバーエージェント 公式エンジニアブログ awa/go-iapとは awa/go-iapは、In-App Purchase (IAP) のサポートを提供するライブラリで、実際にawaでも使用されているので一定の信頼性があり、更新頻度も高く、以前自分がissueを立てた際は次の日には修正されていました。 offerDiscountType is missing in JWSTransactionDecodedPayload · Issue #266 · awa/go-iap また、今回はAndroid/iOSについての説明をしますが、他にもAmazon AppStoreやHuawei HMSといった決済方法に対応しています。 Android編 サーバー側でやることは 署名の検証 receipt問い合わせ acknowledgeの送信 となります 署名の検証 サイバーエージェントさんのテックブログ にも書かれていますが、送られてきたreceiptデータの検証に署名を利用します。 awa/go-iapを使用しない場合の実装はサイバーエージェントさんにお任せするとして… awa/go-iapのVerifySignature関数を使用することで、簡単に署名の検証ができます。 https://pkg.go.dev/github.com/awa/go-iap@v1.32.0/playstore#VerifySignature 以下に実装例を出します import ( "encoding/base64" "github.com/awa/go-iap/playstore" "yourapp/types" ) // Verify receiptが改竄されていないかを検証 // receiptData: レシートデータ、Androidから送られてくるのはbase64化されている // signature: レシートデータの署名 func Verify(receiptData, signature string ) ( bool , error ) { // base64化されているので戻す b, err := base64.StdEncoding.DecodeString(receiptData) if err != nil { return false , err } // レシートデータ自体が正しいか署名を検証 return playstore.VerifySignature(types.GooglePlayPublicKey, b, signature) } Androidから送られてくるreceiptデータがbase64化されているのでデコード処理が必要という意味で関数化しましたが、実際の処理は一行で終わるので無理に関数化しなくても良いかもしれません。 また、検証にはGooglePlayのPublicKeyが必要ですが、これはGooglePlayConsoleから取得できます。 subscription問い合わせ receipt自体の検証が完了したら、購入情報を取得するために再度GooglePlayに問い合わせを行います。 こちらの問い合わせもVerifySubscriptionV2関数を使用することで簡単に実装できます。 https://pkg.go.dev/github.com/awa/go-iap@v1.32.0/playstore#Client.VerifySubscriptionV2 import ( "context" "github.com/awa/go-iap/playstore" "google.golang.org/api/androidpublisher/v3" "github.com/avast/retry-go/v4" "yourapp/types" ) type PurchaseData struct { AutoRenewing bool `json:"autoRenewing"` OrderID string `json:"orderId"` PackageName string `json:"packageName"` ProductID string `json:"productId"` PurchaseTime int64 `json:"purchaseTime"` PurchaseState int64 `json:"purchaseState"` DeveloperPayload string `json:"developerPayload"` PurchaseToken string `json:"purchaseToken"` } func Verifyreceipt(ctx context.Context, receiptData, signature string ) (*androidpublisher.SubscriptionPurchaseV2, error ) {    // 先程のVerify関数で返り値にdecode済みのreceiptデータを追加しても良いかもしれません b, err := base64.StdEncoding.DecodeString(receiptData) if err != nil { return nil , err } var purchaseData PurchaseData err = json.Unmarshal(b, &purchaseData) if err != nil { return nil , err } request, err := playstore.New([] byte (types.GooglePlayServiceAccount)) if err != nil { return nil , err } var res *androidpublisher.SubscriptionPurchaseV2   // ちょくちょく5xxを返すことがあるのでリトライ処理を入れています err = retry.Do( func () error { res = &androidpublisher.SubscriptionPurchaseV2{} res, err = request.VerifySubscriptionV2(ctx, purchaseData.PackageName, purchaseData.PurchaseToken) if err != nil { return err } return nil }, retry.RetryIf( func (err error ) bool { // 任意のエラーハンドリング if strings.Contains(err.Error(), "Service Unavailable" ) { return true } }), ) if err != nil { return nil , err } return res, nil } acknowledgeの送信 署名の検証やらdb保存やら諸々が完了してレスポンスを返却するまえにGooglePlayに対してacknowledgeを送信して購入を承認する必要があります。 逆をいうと、acknowledgeを送信しないと購入が完了しないので、エラー時はacknowledgeさえ送信しないことに注意を払えばなんとかなります。 acknowledgeですが、こちらも関数が用意されており、 AcknowledgeSubscription を使用します。 import ( "context" "github.com/awa/go-iap/playstore" "google.golang.org/api/androidpublisher/v3" "yourapp/types" ) type PurchaseData struct { AutoRenewing bool `json:"autoRenewing"` OrderID string `json:"orderId"` PackageName string `json:"packageName"` ProductID string `json:"productId"` PurchaseTime int64 `json:"purchaseTime"` PurchaseState int64 `json:"purchaseState"` DeveloperPayload string `json:"developerPayload"` PurchaseToken string `json:"purchaseToken"` } func (u *UseCaseImpl) AckSub(ctx context.Context, purchaseReceipt PurchaseData) error { cl, err := playstore.New([] byte (types.GooglePlayServiceAccount)) if err != nil { return err } return cl.AcknowledgeSubscription(ctx, purchaseReceipt.PackageName, purchaseReceipt.ProductID, purchaseReceipt.PurchaseToken, &androidpublisher.SubscriptionPurchasesAcknowledgeRequest{}) } acknowledgeには冪等性があるので何度再送しても大丈夫です。 iOS編 iOSもAndroidと似たようなものですが、serverから見るとAndroidより簡単です。 実際にApplestoreに問い合わせが必要な部分はreceiptの検証のみです。 以下が実装例です import ( "context" "github.com/awa/go-iap/appstore" "github.com/avast/retry-go/v4" ) func VerifyReceipt(ctx context.Context, receiptData, password string ) (*appstore.IAPResponse, error ) { request := appstore.IAPRequest{ ReceiptData: receiptData, Password: password, } var res *appstore.IAPResponse err := retry.Do( func () error { res = &appstore.IAPResponse{} err := appstore.IAPClient.Verify(ctx, request, res) if err != nil { return err } err = appstore.HandleError(res.Status) if err != nil { return err } return nil }, retry.RetryIf( func (err error ) bool { // 任意のエラーハンドリング if strings.Contains(err.Error(), "Service Unavailable" ) { return true } }), ) if err != nil { return res, err } return res, nil } GooglePlayと同じく時折5xxを返すのでリトライ処理を入れています。 iOSはクライアント購入時にack相当はしているのでサーバー側でのacknowledgeは不要です。 おわりに 以上のように、awa/go-iapを使用することで、Android/iOSの課金の購入処理を簡単に実装することができます 今回例に上げたのはsubscriptionのみですが、単発購入の場合も似たような実装になるかと思います。 ただ、subscriptionの場合は購入後の処理のほうが複雑で、定期的な購入情報を問い合わせるか、通知( App Store Server Notifications / RTDN )を受けるかしたあとにそれぞれのプロダクトに合わせた処理をしていきます。(今回は購入処理だけです 自分が実装するさいにawa/go-iapの参考になるドキュメントが少なかったため、今回の記事を書きました。誰かのお役に立てれば幸いです。 参考 awa/go-iap: go-iap verifies the purchase receipt via AppStore, GooglePlayStore, AmazonAppStore and Huawei HMS. 自動購読課金について【Android編】 | サイバーエージェント 公式エンジニアブログ 自動購読課金について【iOS編】 | サイバーエージェント 公式エンジニアブログ
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 27日目の記事です。 目次 はじめに 背景と問題点 既存設計の問題 新しい設計方針 リファクタリングの準備 テストを書く リファクタリング作業 実装する テストする まとめ 終わりに はじめに DELISH KITCHENのiOSアプリ開発を担当している池田です。DELISH KITCHENでは皆様の料理体験がより良いものになるよう、日々新しい機能を追加しています。今回は「リアーキテクチャを支えるテスト駆動開発:効果的なリファクタリングの方法」について、実際の経験をもとにお伝えします。テスト駆動開発の重要性を改めて確認しながら、効果的なリファクタリングの方法を紹介します。 背景と問題点 DELISH KITCHENのiOSアプリは2016年のリリース以来、様々な機能を追加してきました。しかし、しっかりとした設計方針がないまま開発を続けてきたため、今後も継続的に機能を追加することが困難になっていました。そのため、一度まとまった時間を取り、リファクタリングを行うことにしました。 既存設計の問題 DELISH KITCHENのiOSアプリでは、SPMを用いたマルチモジュールで設計されています。現在は下図のようなモジュール構成になっています。 例えば、Networkingモジュールは通信に関する実装を含んでいますが、以下のような問題がありました: レスポンスそのものをアプリ全体で使い回しているため、通信を意識する必要のないUIモジュールがNetworkingモジュールに依存している。 レスポンスがサーバの返却するJSONをそのままの形でパースしたものであり、アプリで使いやすい形ではない。 新しい設計方針 上記の問題を解決するために、クリーンアーキテクチャを元にした設計に移行することにしました。クリーンアーキテクチャとは、ドメインモデルを中心とした設計であり、各層が独立して動作することを目指します。下図のような構成を目標にリファクタリングを進めていきます。 リファクタリングの準備 テストを書く リファクタリングを行う前に、まず既存の動作をテストすることが重要です。レスポンスの変換に対してテストを行うことで、変更後の動作を担保します。以下に、簡易的なデコードテストの実装例を示します: // テストコード例 class DecodableTests : XCTestCase { func testGetRecipeResponseDecoding () { if let _ : GetRecipeResponse = Self .decodeJSON(from : "GetRecipeResponse" ) { XCTAssert( true ) } } } extension DecodableTests { /// 共通のJSONデコードテストメソッド static func decodeJSON < T : Decodable > (from fileName : String ) -> T? { guard let fileURL = Bundle.module.url(forResource : fileName , withExtension : "json" ) else { XCTFail( "Failed to find file \( fileName ) .json" ) return nil } do { let data = try Data(contentsOf : fileURL ) let decodedObject = try JSONDecoder.decoder.decode(T. self , from : data ) return decodedObject } catch { XCTFail( "Decoding failed: \( error ) " ) return nil } } } このテストではOpenAPIのJSONレスポンスをJSONファイルとしてプロジェクトのローカルに配置し、そのJSONのデコードが失敗しないことをチェックしています。 リファクタリング作業 実装する 設計方針に従って少しずつリファクタリングを行います。今回は、NetworkingモジュールからModelを切り出し、サーバから取得したレスポンスをドメインモデルに変換するように変更します。以下に、リファクタリングのステップとコード例を示します: Modelモジュールを作り、アプリで利用しやすいドメインモデルを定義し直す。 Networkingモジュールではサーバから取得したレスポンスをドメインモデルへと変換する。 既存実装ではレスポンスとModelの型を一致させることで、ModelをCodableに準拠させ、JSONからの変換のコードの実装が不要でした。今回はModelをレスポンスに依存させるのではなく、レスポンスをModelに依存させるように関係を変更すること、レスポンスとModelの構造が異なることからJSONから変換する処理を実装する必要が出てきました。 // リファクタリング前のコード例 // Networkingモジュール struct Recipe : Codable { let id : Int let title : String let category : String // 以下略 } // リファクタリング後のコード例 // Modelモジュール struct Recipe { let id : Int let title : String let category : Category // 以下略 enum Category { case unknown case typeA case typeB } } // Networkingモジュール(将来的にはInfraモジュールへ変更予定) extension Recipe : Decodable { enum CodingKeys : String , CodingKey { case id case title case category // 以下略 } public init (from decoder : Decoder ) throws { let container = try decoder.container(keyedBy : CodingKeys.self ) let id = try container.decode(Int. self , forKey : .id) let title = try container.decode(String. self , forKey : .title) let category = try container.decode(Category. self , forKey : .category) // 以下略 self . init ( id : id , title : title , category : category // 以下略 ) } } extension Recipe.Category : Decodable { public init (from decoder : Decoder ) throws { let container = try decoder.singleValueContainer() let value = try container.decode(String. self ) switch value { case "typeA" : self = .typeA case "typeB" : self = .typeB default : self = .unknown } } } テストする ビルドできるコードが実装できたら、テストを実行します。全てのテストが成功すればここで終了ですが、変更内容が大きいため、いくつかのテストは失敗することが予想されます。失敗したテストの該当する実装を修正しながら、全てのテストが成功するまで続けます。 まとめ リファクタリングを実施するにあたり、テスト駆動開発の考え方を取り入れました。既存コードに対してテストを書いてからリファクタリングを行うことで、変更後のコードに問題がないことを確認でき、比較的大きな規模のリファクタリングでも安心して進めることができました。今後も継続的にリファクタリングを行い、リアーキテクチャを進め、開発しやすいコードを目指していきます。 終わりに テスト駆動開発は、効果的なリファクタリングを実現するための強力な手法です。テスト駆動開発の重要性を改めて認識し、日々の開発に取り入れることで、より健全で拡張性の高いアプリケーションを構築することができます。この記事が、皆さんの開発においても役立つことを願っています。
アバター
はじめに はじめまして!開発本部のデータ&AIチームに4月に新卒入社した蜜澤です。 最近Amazon QuickSightを使用してダッシュボード作成に励んでいるので、QuickSightにおけるインタラクティブなグラフの作り方を紹介しようと思います!中でも、割合系の指標に対してフィルターを適応するのに苦戦したので、そのあたりの作り方を説明します。 この記事は every Tech Blog Advent Calendar 2024(夏) 26日目の記事になります。 作成するグラフのイメージ 性別、年代、レシピ名を指定すると、CTRの折れ線グラフが表示される。 使用する模擬データ 実際のデータを使用してしまうとアレなので、今回は以下の模擬データを使用します。 それぞれのカラムの定義は以下の通りです。 date:日付(2024-04-01~2024-04-07) recipe:レシピ名(ハンバーグ、からあげ、生姜焼き) gender:性別(男性、女性) age:年代(10代、20代、30代、40代、50代、60代以上) click:レシピをクリックした回数(1~10の整数の乱数) impression:レシピが表示された回数(500~1000の整数の乱数) また、今回は「CTR=click / impression * 100」と定義します。 CTRカラムを作ることは可能ですが、フィルターをかける都合上、CTRはquicksight上で定義します。 使用するデータの注意点として、date 、gender、age、recipe全ての組み合わせごとにclick、impressionを集計したデータである必要があります。 このようなデータを使用することで「10代と20代の男性にハンバーグが表示された場合のCTR」としてフィルターをかけることができるようになります。 データセットを作成する 今回使用する模擬データをQuickSight上にアップロードします。 今回使用するデータはCSVファイルなのでファイルのアップロードを選択します。 ファイルをアップロードすると確認画面が出てくるので、想定通りのデータなら「次へ」を押します。 データの編集/プレビューを押して、データの型などを確認します。 問題がなければ、「保存して視覚化」を押します。 「保存して視覚化」を押すと、分析ページに飛びます。 グラフ作成 左上の「視覚化」を押すと、作成可能なビジュアルのアイコンが表示されるので、作成したいものを選択します。今回は「折れ線グラフ」を選択します X軸とY軸をどのカラムにするかを選択します。 使用しているデータセットに含まれるカラムが表示されているので、その中から必要なものを選んで、「X軸」と「値」にドラッグ&ドロップします。 X軸に「date」、値に「impression」をドラッグ&ドロップしてみると、、 日付ごとのimpressionの折れ線グラフが表示されます。 今回作りたいグラフはCTRのグラフなので、これからCTRを定義します。 カラム名の上にある「計算フィールド」を押します。 このような画面が表示されるので、「名前」と「定義」を入力します。 名前が「CTR」、定義は「sum({click}) / sum({impression}) * 100」としました。{カラム名}でカラムを計算フィールド内で指定できます。 ここで、「{click} / {impression} * 100」と記入すると、性別や年代を複数フィルターするときに正しく計算できなくなってしますので注意が必要です! 入力したら、「保存」を押します。 CTRが追加されたので、値に「CTR」をドラッグ&ドロップすると、日付ごとのCTRのグラフになりました! しかし、このままでは性別、年代、レシピ名を指定できないので、指定するために「パラメータ」と「フィルター」を作成します。 パラメータの作成 この後作成するフィルターで使用する値を転送できる名前付きの変数の役割を果たす「パラメータ」を作成します。 左上の「パラメータ」を押して、「追加」を押します。 このような画面が出てくるので、それぞれの項目を入力していきます。 まずは、性別のパラメータを作ります。名前は「gender」、データ型は「文字列」、値は「複数の値」、デフォルト値は「男性、女性」にします。名前とデフォルト値はお好みで変更していただいてOKです! 入力が終わったら「保存」を押します。 作成したパラメータが表示されます。 同じように年代とレシピ名のパラメータも作成します。 今回作成するダッシュボードは「各レシピ別の傾向を分析する」ユースケースを想定しています。 そのため、レシピ名は「複数の値」ではなく、「単一の値」を選択します。 フィルター作成 次に、作成したグラフに表示するデータを選別する役割を果たす「フィルター」を作成します。 先ほど作成したパラメータを使います。 左上の「フィルター」を押し、「追加」を押します。 まずは「gender」のフィルターを追加してみます。 「gender」を押すとフィルターが作成されます。 作成されたgenderフィルターの3点部分を押して、「編集」を押します。 編集画面が出てくるので、各項目設定します。 「フィルタータイプ」は「カスタムフィルター」を選択します。 「フィルター条件」は「次と等しい」を選択して、「パラメータを使用」のチェックボックスにチェックを入れます。 チェックを入れると、すべてのビジュアルとシートにフィルターを適用するか聞かれるので、用途に合わせて選択します。 今回はビジュアルを1つしか作らないので「いいえ」を選択します。 パラメータが選択できるようになりました。 性別のフィルターなので、先ほど作成した「genderパラメータ」を選択します。 最後に「適用」を押します。 同様のやり方で年代のフィルターである「age」とレシピ名のフィルターである「recipe」も作成します。 これでフィルターの編集は終了です。 コントロールの追加 次に、「コントロール」を作成します。 フィルターに使用しているパラメータの値はダッシュボードの編集者しか変更ができませんが、コントロールを追加することで閲覧者がパラメータを変更できるようになります。 左上の「パラメータ」を押して、コントロールを追加したいパラメータの3点部分を押します。 「コントロールを追加」を押します。 このような画面が表示されるので、各項目を入力します。 genderのコントロールなので 名前 は「性別」、 スタイル は「ドロップダウン - 複数選択」、 値 は「特定の値」、 特定の値を定義 は「男性」「女性」とします。 入力したら「保存」を押します。 グラフの上部にコントロールが表示されるようになりました。 ドロップダウン形式で「男性」、「女性」、「すべて選択」を選べるようになりました。 試しに、「男性」以外のチェックボックを外してみると、、 グラフの形が変わりました。 男性と女性を集計対象としたCTRのグラフから男性のみを対象としたCTRのグラフに変わリました。 同様にageのコントロールも作ります 「gender」と「age」とは少し違う作り方で「recipe」のコントロールも作ります。 「recipe」のコントロールは「レシピ名をテキストとして指定できるようにする」ことを目指します。 そのため、「テキストフィールド」のスタイルを選択します。 今回の例ではレシピ名が3種類しかないのでドロップダウン形式でも良いのですが、実際の運用を見越すとなると、多くの種類のレシピが対象になります。 そうなると、ドロップダウン形式では見通しが悪くなるため、直接レシピ名を指定する手段を採用しました。 「性別」と「年代」はドロップダウン形式、「レシピ名」のみテキストフィールド形式になっていることがわかります。 レシピ名に「ハンバーグ」と入力してみます。 グラフの形が変わりました。先ほどまでは全てのレシピ(ハンバーグ・からあげ・生姜焼き)を集計対象としたCTRのグラフでしたが、ハンバーグのみを対象としたグラフとして絞り込むことができました。 細かい調整 ここまでできたらほぼ完成です! 最後に細かいところをいじります。 デフォルトだと日付が見にくいので、表示形式を変更します。 「date」を押します。 このような画面が表示されるので、「形式」を押して、好きな表示形式を選択します。今回は「YYYY/MM/DD」にします。 dateの表示形式が変更されてスッキリしました! グラフのタイトルも変更します。 左上の「プロパティ」を押します。 画面右側にプロパティの編集画面が表示されます。 「ディスプレイ編集」内の「タイトル編集」の筆のようなアイコンを押します。 このような画面が表示されるので、好きなタイトルを入力します。 ここで、パラメータを押すとタイトルがパラメータの内容で動的に変更できるようになるので、「recipe」を押してみます。 パラメータが入力されました! パラメータの後ろに「のCTR」と入力し、「{入力したレシピ名}のCTR」というタイトルが表示されるようにしました。 レシピ名が「ハンバーグ」の時に、グラフのタイトルが「ハンバーグのCTR」になっています。 説明は省略しますが、X軸やY軸の名前も変更できます。 軸の名前をよしなに変更して、完成です!!! 使ってみる 男性20代と女性20代の生姜焼きのCTRを比較して、日付によって男性女性どちらのCTRが高くなるのかなどを見ることができます。 (今回は乱数を使ったデータなので示唆は得られませんが、、、) この使い方はあくまでも一例であり、色々な使い方ができると思います。 最後に 今回はAmazon QuickSightを使用して、インタラクティブな可視化を行いました。 今回紹介したビジュアル以外にも多くのビジュアルがあり、様々な可視化ができるので、これからも色々な可視化に挑戦していきたいと思います。 また、今回はデータの整形に関しては触れませんでしたが、QuickSightを使用する際にデータの形式はかなり悩むところなので、今後はデータ整形に関することも紹介できたらなと思います! この記事が何かの参考になれば幸いです。 最後まで読んでいただきありがとうござました!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 25 日目の記事です。 はじめに こんにちは、24 新卒として 4 月から入社し、DELISH KITCHEN 開発部でソフトウェアエンジニアをしている新谷です。 現在取り組んでいる業務で、画像を受け取って外部に送信する処理を行う API を作成する機会がありました。そこで学んだ、Go 言語における multipart/form-data を使った画像の取り扱い方について紹介します。 背景 開発した機能としては、クライアントがアップロードした画像を API サーバーで受け取り、外部の API に POST するというものです。開発した部分は以下の図で表すと、API サーバーの部分になります。 この処理を実現するためには、画像データの受信かつ送信を行う必要があります。今回は、画像を取り扱う際によく使われる multipart/form-data 形式を使用して画像を受け取り、外部 API に送信するように実装したので、その方法について紹介します。 multipart/form-data とは multipart/form-data は、Web ページのフォームからファイルをアップロードする際に使用されるデータ形式です。 multipart/form-data 形式のリクエストは、以下のような特徴があります。 リクエストボディが複数の部品(part)から構成される 各部品はヘッダーとボディから構成される 各部品は boundary で区切られる ヘッダーには Content-Disposition や Content-Type などが含まれる ボディにはファイルのデータが含まれる 以下は、 multipart/form-data 形式のリクエストの例です。 POST /post HTTP/1.1 Host: httpbin.org User-Agent: Go-http-client/1.1 Content-Length: 376 Content-Type: multipart/form-data; boundary=600fcf99bf89273352a59587b26bc07642bd bfc97ce30f8445ecc0c3873a Accept-Encoding: gzip --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a Content-Disposition: form-data; name="file"; filename="sample.txt" Content-Type: application/octet-stream this is dummy. --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a Content-Disposition: form-data; name="key1" value1 --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a-- ここでは、boundaryで区切られた 2 つの部品が含まれています。1 つ目の部品はファイルのデータを含み、2 つ目の部品はフォームデータを含んでいます。 実装 API Server での画像の受け取り方 本 API Server は、Go の echo フレームワークを使用しています。echo の c.FormFile を使用して画像を受け取る実装を以下に示します。 file, err := c.FormFile( "image_file" ) if err != nil { return types.ErrInvalidParameters } src, err := file.Open() if err != nil { return types.ErrInvalidParameters } defer src.Close() それぞれの処理について解説します。 c.FormFile("image_file") は、リクエストから"image_file"という名前のフィールドを取得します。先ほどの multipart/form-data 形式のリクエストの例では、 name="file" としていた部分に対応します。 image_fileフィールドから取得したfileにはmultipart.FileHeader型のファイル情報が格納され、ファイル名やファイルサイズなどの情報を取得できます。 次に、 file.Open() でファイルを開き、 src にファイルの中身を格納します。 file.Open() は、ファイルを開いてmultipart.File型のファイルオブジェクトを返します。multipart.Fileは以下のインターフェースを実装しています。 type File interface { io.Reader io.ReaderAt io.Seeker io.Closer } io.Reader 、 io.ReaderAt 、 io.Seeker 、 io.Closer のインターフェースを実装しているため、 io.ReadAll などを使用してファイルの中身を取得することもできます。 ファイルの中身を取得した後は、 defer src.Close() でファイルをクローズします。 外部 API への画像の POST 外部 API に画像を POST する際は、 multipart/form-data 形式でリクエストを送信する必要があります。Go 言語では、標準ライブラリの mime/multipart パッケージを使用してmultipart.Writerを作成し、リクエストボディを構築します。 body := & bytes.Buffer {} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile( "image_file" , file.Filename) if err != nil { return types.ErrInternalServer } _, err = io.Copy(part, src) if err != nil { return types.ErrInternalServer } err = writer.Close() if err != nil { return types.ErrInternalServer } req, err := http.NewRequest( "POST" , "https://example.com" , body) if err != nil { return types.ErrInternalServer } req.Header.Set( "Content-Type" , writer.FormDataContentType()) client := &http.Client{} resp, err := client.Do(req) if err != nil { return types.ErrInternalServer } defer resp.Body.Close() body にはリクエストボディを格納するバッファを作成します。 次に、 multipart.NewWriter(body) でマルチパート形式のリクエストボディを構築するmultipart.Writerオブジェクトを作成します。 writer.CreateFormFile("image_file", file.Filename) は、フォームフィールド名を"image_file"とし、ファイル名を file.Filename とするファイルパートを作成します。 そのファイルパートに、 io.Copy(part, src) でファイルの中身をコピーします。 writer.Close() をすることで、最後のboundaryを書き込みます。 その後は、 http.NewRequest でリクエストを作成し、 req.Header.Set("Content-Type", writer.FormDataContentType()) でリクエストヘッダーにContent-Typeを設定します。 writer.FormDataContentType() は、multipart.Writerのboundaryを含むContent-Typeを返します。 気をつけたこと 外部 API に画像を送信する際は、エラーが発生する可能性があるためリトライ処理を実装しました。 for tries < MaxLimit { // リクエスト構築 req, err = r.buildRequest(file, fileName, hurl) if err != nil { return nil , err } // リクエスト処理 body, err = r.doRequest(ctx, req) if err != nil { file.Seek( 0 , 0 ) tries++ continue } } リトライ処理で気を付けるべき点は、一度リクエストを送信すると画像ファイルが読み込まれた状態になるため、リトライ時にファイルのポインタを先頭に戻す必要がある点です。 ファイルポインタを戻さないと、リトライ時にファイルの中身が空になってしまいます。 multipart.File は io.Seeker インターフェースを実装しているため、ファイルポインタを先頭に戻すには、 file.Seek(0, 0) を使用することができます。 まとめ 今回は、Go 言語で multipart/form-data 形式を使って画像を受け取り、外部 API に送信する方法を紹介しました。echo フレームワークと mime/multipart パッケージを利用することで、画像の受け取りや送信が簡単に実装できます。しかし、まず multipart/form-data 形式のリクエストの仕組みを理解することが重要だと感じました。また、リトライ処理を実装する際には、リクエストボディを元の状態に戻すことを忘れないようにしましょう。
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 24 日目の記事です。 はじめに こんにちは。DELISH KITCHEN 開発部 RHRA グループ所属の池です。 RHRA グループでは主に小売向けプロダクトの開発を行なっています。 本記事では、RDS の EBS BurstBalance が枯渇してパフォーマンスが著しく低下した事例について、その調査過程で得られた知見を共有したいと思います。 背景 事象が発生した環境では、以下の構成の RDS インスタンスを使用していました。 MySQL 5.7.44 インスタンスタイプ: db.t3.medium ストレージタイプ: 汎用 SSD (gp2) ストレージサイズ: 30GiB この環境において、BurstBalance が急激に消費され始め、最終的に枯渇してししまい、データベースのパフォーマンスが著しく低下しました。 次の図は BurstBalance が枯渇した際のメトリクスです。 BurstBalance の枯渇 BurstBalance とは まず BurstBalance について理解を深めておく必要があります。 BurstBalance の仕様 BurstBalance は、Amazon EBS の汎用 SSD (gp2) ボリュームに適用される性能指標です。これは、ベースラインパフォーマンスを超えて一時的に高い IOPS を発揮できるクレジットの残量を表します。 gp2 ボリュームの場合、ボリュームサイズに応じて以下の特性を持ちます。 ベースラインパフォーマンス: 3 IOPS/GB、ただし最小 100 IOPS バーストパフォーマンス: 最大 3,000 IOPS 最大クレジット量: 5.4 million I/O クレジット IOPS がベースラインを超える場合にバーストが利用され、最大 3,000 IOPS まで発揮される BurstBalance が完全に枯渇した場合、IOPS はベースラインの値に制限されます。例えば、30GiB のボリュームの場合、ベースラインは 100 IOPS(3 IOPS/GB × 30GB = 90 IOPS ですが、最小値の 100 IOPS が適用されます)となり、BurstBalance 枯渇時はこの値に制限されます。 これらの仕様により、gp2 ボリュームは短期的な高 I/O 要求に対応できますが、継続的に高い I/O 要求がある場合、バーストバランスが徐々に減少し、最終的にベースラインパフォーマンスに制限されることになります。 消費量の計算式 BurstBalance の消費量は以下の式で概算できます 消費量 = (使用IOPS - ベースラインIOPS) × 秒数 例えば、30GiB のボリュームの場合 ベースラインIOPS = 最小量の100IOPS 1分間300 IOPSを維持した場合の消費量 = (300 - 100) × 60 = 12,000 IOクレジット より詳細な gp2 ボリュームの仕様と BurstBalance については、 AWS の公式ドキュメント を参照してください。 調査方法 問題が発生した際、以下の調査を行いました CloudWatch メトリクスの確認 BurstBalance, ReadIOPS, WriteIOPS 等のメトリクスを確認 RDS のエラーログの確認 スロークエリログの確認 Performance Insights の利用 問題のあるクエリの特定と実行計画の分析 調査結果 調査の結果、以下のことが判明しました。 BurstBalance は主に WriteIOPS の増加によって発生 I/O を大きく消費するクエリが存在 次の図は BurstBalance と WriteIOPS の相関を表すグラフになります。 青色が BurstBalance、オレンジ色が WriteIOPS を示しています。WriteIOPS が増加すると BurstBalane が減少することがわかります。 BurstBalance と WriteIOPS の相関 ここで仮に 1 時間平均の WriteIOPS を 600IOPS/second として、そのペースで消費し続けたと仮定した場合に、枯渇するまでの時間を概算で計算すると以下のように計算できます。 1時間あたりの消費量: (600 - 100) × 3600 = 1,800,000 IOPS/hour BurstBalanceを消費し切る時間: 5,400,000 / 1,800,000 = 3時間 また、問題のクエリは複雑な SELECT 文で、実行時に I/O が急上昇していました。 このクエリに対して実行計画を確認したところ、extra に Using temporary, Using filesort という記述があり、内部的なソートの実行と一時テーブルへの書き込みが発生していることがわかりました。 これらの結果から、複雑な SELECT 文の実行により、大量の一時テーブルへの書き込みが発生し、それが BurstBalance の急激な消費につながったと考えられます。 ただ、あるタイミングで突然このクエリが一時テーブルへの書き込みを大量に行うようになった要因については調査中になります。 可能性の一つとして、日々のデータの蓄積によりこのクエリで利用する一時テーブルの容量がオンメモリ内で処理できるメモリ制限を超えてしまい、ディスクに書き込まれるようになった可能性を推察しています。 この部分については引き続き調査中なので、結果がわかったら報告できればと思います。 対策 本事象について、以下の対策が考えられます。 問題のクエリのチューニング インデックスの見直し クエリの書き換え(サブクエリの削除、JOIN の最適化など) ストレージのアップグレード gp2 から gp3 への移行を検討(より予測可能なパフォーマンス特性) ストレージサイズの増加(ベースライン IOPS の向上) 一時テーブルのサイズ制限の調整 tmp_table_size と max_heap_table_size パラメータの調整 インスタンスタイプの見直し より高性能なインスタンスタイプへの移行 定期的なクエリパフォーマンスの監視と改善 システム監視の見直し CloudWatchAlert 等による BurstBalance の監視 Pingdom 等による外形監視 まとめ RDS における BurstBalance の枯渇は、予期せぬパフォーマンス低下を引き起こす可能性がある問題です。今回の事例から、以下の学びを得ることができました 複雑なクエリが I/O に与える影響の大きさ データベース設計とクエリ最適化の継続的な改善の必要性 定期的なパフォーマンスモニタリングの重要性 今回の知見が少しでも皆様の参考になれば幸いです。
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 23日目の記事です。 こんにちは 開発本部データ&AIチームでデータエンジニアを担当している塚田です。 今回は先日行われたAWS Summit Japan 2024に現地参加する時間が取れましたので、 昨日の「Kotlin Fest 2024 に ひよこスポンサー として参加してきました!」に引き続きイベントレポートですが、個人的に印象に残ったことや聴講したセッションについて触れたいと思います。 tech.every.tv セッション動画やセッション資料も期間限定で公開されているようなので、気になる方はそちらもご確認いただければと思います。 現地で参加したセッション(1日目のみ) どのセッションも満席で熱量の高さを感じました。 また、セッション内で「生成AI」という単語を聞くことがないと思うほど頻繁に耳にしました。 基調講演 AWSと創る次の時代 1時間30分の大ボリュームだったので詳細は省きますが、 やはりAIについて触れられることが多く「責任あるAI」が何回も出てきており その言葉からもあるようにAIが当たり前になり生活に溶け込んでいくようになっても 常に意識するべきことであることを再認識しました。 Amazon Q Businessの日本語対応 が発表され、使うことができるのを楽しみにしています! Dive deep on Amazon S3 S3をより深く理解するため3つの軸で詳しく説明がありました。 フロントエンドの理解 インデックスの理解 ストレージ層の理解 その他にも Amazon S3 Express One Zone や 誤削除に対して にも言及がありましが、ここでは3つの軸についてまとめたいと思います。 フロントエンドの理解 ピークのトラフィックは1PB/秒を超える規模となっている 利用者側でもできる緩和策がある マルチアップロード 独自実装せずにCommon Runtime(CRT)を利用しベストプラクティスに則る 新しいマウントポイントがいくつか出てきている S3マウントは新しいマウオンとオプションが出ているけれど、いまだにバッドプラクティス それなのに新しいマウントポイントが出てくる理由としては機械学習ニーズが大きい インデックスの理解 350兆のオブジェクトを管理しており、1億リクエスト/秒を処理している 利用状況とキャパシティを常に監視し適切にスケールを行っている プレフィックスごとにPUTとGETのリクエスト数の上限が設定されている それを超えると503を返却する キー配列によって負荷が変わる キー名のカーディなりティは左に寄せる キー名に日付を入れる際はできるだけ右側にする ストレージ層の理解 何百万ものハードドライブを利用して、エキサバイトのデータを保管している データは常に冗長化された環境で保管され保存時、保存後定期的にチェックを行なっている 感想 AWS re:Invent 2023でも同じような内容があったと記憶していますが、日本語で聞くことができ理解がより深まったと感じました。 冗長的にオブジェクト管理をしていたのは理解していましたが、どういう体系で管理されて利用できているかの理解を深めることができました。 S3 Express One Zone の利用場面がいまいちイメージでできていませんでしたが、機械学習などI/O性能が上がることでメリットを受けられるなど理解が進みました。 データ×生成 AI - 事例から学ぶビジネスインパクト創出の方程式 クラウドネイティブが進んでいる企業は生成AI活用も進んでいる傾向がある 活用が進んでいる企業では共通するポイントがある 顧客起点文化 小規模チーム 頻繁な実験 小規模チームでも大規模かつ機微なデータでも迅速に生成AIプロダクトが構築できる Amazon QやKnowledge bases for Amazon Bedrockを活用する 責任あるAIを作るためにも責任共有モデルに基づいた実現が必要になってくる 感想 活用が進んでいる企業に共通するポイントはなるほどと感じる部分が大きく責任あるAIを作っていくためにも上記に記載したもの以外もしっかり考えてアクションしていきたいと思いました。 EXPO セッション以外にも様々なブースがありましたので、少しだけ触れたいと思います。 Partner Solution Expo たくさんのパートナーブースがあり、盛り上がりがすごかったと感じました。 また、利用しているサービスのブースもありましたのでオフラインでの質問や相談ができたのがよかったです。 ご対応いただいた皆様ありがとうございます。 認定者ラウンジ 認定者ラウンジを利用させていただきましたが、利用するのにも多数の参加者が並んでおり資格取得者の多さに驚きを覚えました。 保持している資格によってステッカーがいただけたのですが、おまけでクッキーもいただきました。 あとで知りましたが、AWS Certified Data Engineer - Associateを持っているとクッキーももらえるようになっていたようです。 まとめ たくさんのインプット、知識の更新をすることができ、インプットしたことを業務やプロダクトに還元できるようにしていきたいと強く思える場になりました。 生成AIなど進歩が早い中で頻繁に実験を繰り返し開発する文化をより強くしていきたいと考えています。 1日目最初のセッションが始まる30分前には多数のサインがありました!盛り上がりがすごい!
アバター
はじめに こんにちは、株式会社 エブリー DevEnableグループです。 先日のGo Conference 2024に引き続き、本日、約5年ぶりのオフライン開催となったKotlin Fest 2024にひよこスポンサーとして参加してきました! Kotlin Fest運営の皆様および参加された皆様、お疲れ様でした! 早速参加レポートをさせていただきます。 www.kotlinfest.dev 5年ぶりのオフライン開催 Kotlin Fest 2024は今回5年ぶりのオフライン開催となりました。会場はベルサール渋谷ファーストの2Fを貸し切って、2つのセッションルームと1つのスポンサーブース兼フリースペースという形の大きな会場での開催でした。 スポンサーブースの紹介 エブリーは8年前からDELISH KITCHENアプリでKotlinを採用しています。Kotlin採用に対する振り返りを当時採用したCTO自らが綴っているのでご覧ください。 tech.every.tv いつもKotlinコミュニティの恩恵を受けている我々もコミュニティのさらなる盛り上がりに貢献していきたく、スポンサーとして協賛させていただき、ブースも出展しました! ブース エブリーでは、弊社が提供するDELISH KITCHENのサービスをイメージしてブースの雰囲気を作っています。今回も多くの方からDELISH KITCHENを使っていますとの声をかけていただき、実際に使っていただいている方の声を聞ける貴重な機会で僕たち開発者もパワーをもらえました。 ノベルティ 今回は以下のようなノベルティを用意させていただきました。 クリアファイル 会社とサービスのステッカー DELISH KITCHENグッズ DELISH KITCHENグッズに関してはXフォローでの抽選プレゼントキャンペーンを行いました。DELISH KITCHENグッズに関してはたくさんの商品があるのですが、その中でも人気のある商品を中心に5つ準備させていただきました。 用意してた全てのグッズがなくなるほど好評で多くの方に参加していただけました! アンケート 今回、アンケートでは『KotlinのLinter、なにを使ってる?』と題して回答をしてもらいました。また、シールの色でKotlinをAndroid開発で使っているか、サーバーサイドで使っているかがひと目でわかるような工夫もあり、参加者が楽しんでいただけるような内容を考えました。回答いただいた多くの皆様、ありがとうございました! 最終結果はこちら...! Androidエンジニア 👑1位: ktlint 2位: Android Lint 3位: detekt サーバーサイドエンジニア 👑1位: detekt 2位: ktlint 3位: その他(Konsist, CheckStyle) 全体ではktlintが1位となり、枠をはみ出すほど多くの方の回答が集まっていました。一方でサーバーサイドエンジニアの中では、ktlintとdetektを使っている方の割合がほぼ同じ僅差という面白い結果となっています。 ブースに来てくださる方はAndroidエンジニアの方が割合としては多いですが、サーバーサイドでKotlinを活用している方も多くいらっしゃり、両プラットフォーム上でのKotlinの盛り上がりを肌で感じました。 各社スポンサーブースの様子 スポンサーブースは、2つのセッションルーム間で開催され、オープニング前やセッション間での休憩中に多くの人で賑わっていました。こういった光景を見られるのもオフラインならではです。 各社のブースもそれぞれの会社の特色がノベルティや出し物から出ており、スタンプラリーをしながら楽しませてもらいました。 特にエムスリーさんのブースではKotlinのモチーフである『トリ』に関連して、『エンジニアトリ診断』を受けることができ、Kotlinや開発に関するいくつかの質問に答えることでトリタイプが診断されます。僕の結果は...『はやぶさ』タイプでした! セッションの紹介 今回発表されたセッションの中から気になったものをいくつかまとめさせていただきました。 パフォーマンスと可読性を両立:KotlinのCollection関数をマスター 発表者: Masayuki Sudaさん fortee.jp こちらのセッションでは、Kotlin の Collection 関数を有効活用する方法を紹介されていました。 Collection 関数の中には変換を行う Map や Zip、フィルタリングを行う filter や partition、グループ化を行う groupBy、部分取得を行う slice など多種多様な関数が用意されていますが、一つ一つ具体的なソースコードと処理結果を説明してくださいました。 その中で、+ 演算子や - 演算子を扱い要素の追加や削除を行う場合は新しいリストが生成されるためメモリが消費しやすい、windowed を使うと計算量が多くなり、パフォーマンスに影響が出るなど、細かい課題や問題点まで説明してくださっていた点が印象的でした。 また最後に Collection、Sequence、for の3 つでパフォーマンスの最適化の観点で、 Collection … 非常に優秀。即時評価を行い、すべての要素を評価する仕組みで、リストが大きい場合はパフォーマンスに難有り。 Sequence … 遅延評価を行う仕組みで、大規模なコレクションに対して複数の処理を行う場合に向いている。 for … 最速。 といったまとめをしてくださっていました。プログラムを作成するうえではパフォーマンスは重要なため、常に説明してくださった観点は意識しようと改めて感じました。 なお、この記事内では一部の関数のみ抜粋して紹介しましたが、Collection 関数自体はまだまだ多く提供されており、使ったことがないものも多々あるため、これを期に勉強し直したいと強く感じました。 withContextってスレッド切り替え以外にも使えるって知ってた? 発表者: T45Kさん fortee.jp こちらのセッションでは、withContext をスレッドの切り替え以外でも使うことができる、という内容を紹介されていました。 Coroutines では API の処理は withContext(Dispatchers.IO)、計算量が多い処理は withContext(Dispatchers.Default)、UI 関連の処理は withContext(Dispatchers.Main) と、スレッドの切り替えのために使用することがあり、私自身このためだけに使用するものと認識していましたが、実際は CoroutineContext を切り替えるものであり、スレッドを切り替える以外の用途で利用されるものと紹介され、しっかりと理解していないまま利用していると反省するきっかけになりました。 なお、コンパイル時に Context の検査が行われる、Coroutines でなくとも使用ができる Context Parameter というものが Kotlin 2.1 で導入予定となっているそうで、これから注目して追っていきたいと思います。 KotlinのLinterまなびなおし2024 発表者 nyafunta9858さん fortee.jp こちらのセッションでは、KotlinのLinter導入へのモチベーションや各Linterについての特徴について解説されていました。 アーキテクチャやコードルールをLinterを用いてプロジェクト内で統一するモチベーションの一つとして、メンバーの入れ替わりが挙げられていました。ルールを統一することで、メンバーの入れ替わりに影響されずコードの品質を担保できます。 everyはエンジニアが様々なプロダクトを経験することができる環境ですので、Linterの運用は強力な武器になると最認識しました。 またブースではどのLinterを使用しているかアンケートを取りました。結果としてはktlintが多く、次点でdetectでした。この発表やアンケート結果を弊社プロダクトに活かしていければと思います。 Jetpack Compose: 効果的なComposable関数のAPI設計 発表者: haru067さん fortee.jp こちらのセッションでは、主にComposable関数の引数の注意点とテクニックについて紹介されていました。 引数はそのコンポーネントの再利用性や拡張性を考えて書く必要があり、その中で有効なテクニックとして以下が紹介されていました。 stateはコンポーネント内で閉じてもよいなら閉じる。他コンポーネントと状態を共有したい場合は親に委ねる。 引数はパフォーマンスの観点から基本的にフラットに記述する。データクラスを用いると、他画面との兼ね合いで無駄な再描画を引き起こす可能性がある。引数が多くなった場合はクラスへ切り分けることを考慮する。 デフォルト引数は安易に設定しないように気をつけ、値をつど考えることが有益な場合は設定しないようにする。 など開発の際に誰もがどうしようか考える内容に触れられており、大変面白かったです。 特に引数をフラットに書く話は、パフォーマンスに直結するので気をつけようと思います。 最後に 最後になりますが、Kotlin Fest 2024の運営の皆さん、カンファレンスの運営をしていただき本当にありがとうございました! また、参加者の皆さん、カンファレンスへの参加お疲れ様でした。 弊社も当日多くのエンジニアが参加し、セッションを聞きながらKotlinコミュニティから刺激を受けるいい機会となりました。今後もイベントやこういったスポンサー活動を通じてKotlinコミュニティに貢献していきたいと思います! 今回参加できなかった皆様もぜひ来年は参加してみてください。 エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 21 日目の記事です。 はじめに こんにちは、エブリーでCTOをしている今井です。 Kotlin Fest 2024 の開催がいよいよ明日に迫ってきました。 エブリーでは8年前にDELISHKITCHENのアプリを作り始めた時からKotlinを使っており、 今回KotlinFestを通じて、Kotlinコミュニティに貢献できることを嬉しく思っております。 このブログでは、Kotlinを採用した当時の状況や、採用して良かったことに関して振り返ってみたいと思います。 Kotlinの採用 FirstCommitのスクリーンショット 改めてコミットを見たところ、Kotlinを初めて導入したのはなんと2016年のFirstCommitからでした。 当時のバージョンは1.0.3で、その時はまだGoogleの正式なサポートもまだ発表される前 1 で、 Androidアプリ開発においてまだまだJavaが主流。かつこんなにKotlinが急激に浸透するとは思ってもいませんでした。 また、開発を進めるにあたってもKotlinの導入事例はとても少なく、ちょっとしたことでも調べるのに苦労したことを覚えています。 当時のコードを振り返ると、たくさん拡張関数作って、ActivityやFragmentをシュッと書きたいという 自分なりにKotlinらしく書くことを模索した形跡がありました。 FragmentUtil.kt fun SupportFragment.inflate(layoutResId: Int , inflater: LayoutInflater?, container: ViewGroup?): View? = inflater?.inflate(layoutResId, container, false ) fun Fragment.inflate(layoutResId: Int , inflater: LayoutInflater?, container: ViewGroup?): View? = inflater?.inflate(layoutResId, container, false ) MainFragment.kt class MainFragment : AbstractFragment() { companion object { @JvmStatic fun newInstance() = MainFragment() } override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflate(R.layout.fragment_main, inflater, container) } これ自体は今思うとまだまだ未熟な書き方も多く恥ずかしい部分も多いですが、 Kotlinを採用したことでの恩恵も多く受けたと思います。 Kotlinの恩恵 Null-Safety 一番の恩恵は確実にNull-Safetyにありました。 当時Androidは自分一人での開発だったためレビューもしてもらえない環境かつ、 その時点での自分のAndroid開発経験も半年ほどと未熟だった中で、 リリース当初からCrashFreeRateが99.9%という高いスコアを出せていたのは、 Kotlinを採用していなければ実現できなかったと思います。 シンプルで短く書ける また、シンタックスがシンプルで短く書けることも初期の開発速度に寄与していたと思います。 上記で紹介した拡張関数やLambdaなどは使えるところは意地でも使うくらい、ハマってました。 Null-Safetyではどうしてもその中身のチェックに記述が増えがちなところあると思いますが、 SmartCastなどにより、書きやすさを維持したままより安全なコードが書けるも良かったです。 書いていて楽しい Kotlin自体の機能ではないですが、上記の恩恵などから、書いていて楽しかったというのも、 今思うと恩恵として大きかったと思います。まだまだ未知なものを自分の手で開拓していく感じも面白かったです。 まとめ エブリーではAndroidアプリの開発当初からKotlinを採用し、Kotlinに支えられてきました。 同様に弊社のあらゆるエンジニアは多くの技術やオープンソースに支えられています。 これらを盛り上げる技術コミュニティに支援をすることは、会社にとってもの技術投資でもあると考えています。 今回のKotlinFestをはじめ、今後も技術コミュニティにも積極的に支援していきたいと考えております! 余談: フルKotlinのアプリでストア総合一位はDELISHKITCHENが初? Kotlinが採用されたGoogleI/O 2017には現地で参加していたのですが、 ちょうどそのとき日本のストアで総合1位になってました。 今日本で1位のアプリはフルKotlinだというのをGooglerに話したところ、 フルKotlinとか聞いたことない / 多分ランキング初めてなんじゃないか? と言っていただいたので、個人的には勝手に弊社のアプリが初めてだと思ってますw GoogleI/O 2017の時のストアのスクリーンショット GoogleI/O 2017で正式サポートが発表 された ↩
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 20 日目の記事です。 はじめに こんにちは、DELISH KITCHEN でクライアントエンジニアを担当している岡田です。 今回は KSP 化について執筆させていただきます。 概要 Kotlin でより効率的な開発を行う上で、アノテーション処理は欠かせない要素です。アノテーションを利用することで、定型的なコードを自動生成したり、コンパイル時にコードの検証を行ったりすることができます。 Kotlin のアノテーション処理といえば、従来は kapt が主流です。しかし、近年ではより高速でKotlinの言語機能を活かせる KSP が主流になりつつあります。 この記事では、kapt と KSP について調べ、 Android プロジェクトの KSP 化を検討するにあたって必要な情報を記述します。 kapt について kapt (Kotlin Annotation Processing Tool) を使用すると、 Java アノテーションプロセッサを使用して Kotlin コードを処理できます。 Room などの Android 開発でよく使用される、多くのライブラリと連携しています。 しかし、 Kotlin ファイルから Java アノテーションプロセッサが読み取れるようにするためには Java スタブを生成する必要があります。 実際に生成された Java スタブ は build/generated/source/kapt/ で確認することができます。 例えば以下は実際に DELISH KITCHEN で Hilt によって生成される、 ArticleListActivity のインスタンスに依存性を注入するためのコードです。 ArticleListActivity_GeneratedInjector.java @OriginatingElement( topLevelClass = ArticleListActivity.class ) @GeneratedEntryPoint @InstallIn(ActivityComponent.class) public interface ArticleListActivity_GeneratedInjector { void injectArticleListActivity(ArticleListActivity articleListActivity); } .*_GeneratedInjector.java は @AndroidEntryPoint や @Inject など、依存性を注入する必要がある全箇所に対して生成されます。 この例のような Java スタブが、 Hilt に関してのみでもプロジェクト内で相当な数生成されています。 Java スタブの生成は高コストのオペレーションのため、処理速度が遅く、ビルド速度に大きく影響してしまうのです。 また 2024/6/20 時点で kapt はメンテナンスモード に入っています。 「既に新しい機能が実装されることはない」と明記されており、 KSP の使用が推奨されています。 KSP がサポートされていないライブラリを用いたいなどの理由がない限りは、 kapt ではなく KSP を使用する方が良いでしょう。 KSP について KSP (Kotlin Symbol Processing) は、 Kotlin コードを直接処理するためのアノテーション処理ツールです。 kapt とは異なり、 Kotlin コンパイラのプラグインとして動作するため、 Java コードへの変換が不要です。 つまり kapt のように Java スタブを生成する必要がなくなり、処理速度が大幅に上昇し、ビルド速度も速くなります。 Googleが公開しているGithub によると、「 kapt と比較すると、 KSP を使用するアノテーションプロセッサは最大 2 倍高速に実行できる」そうです。 また Kotlin の公式ドキュメント では、コード生成に 7.5 倍ほどのパフォーマンス差があることも確認されています。 For performance evaluation, we implemented a simplified version of Glide in KSP to make it generate code for the Tachiyomi project. While the total Kotlin compilation time of the project is 21.55 seconds on our test device, it took 8.67 seconds for kapt to generate the code, and it took 1.15 seconds for our KSP implementation to generate the code. 仕組みとして、 Kotlin プログラムを Kotlin の文法に沿ってシンボルレベルでモデル化しているそうです。 KSP は Kotlin プログラムの構造を、クラスや関数などの構成要素単位で処理することができます。 Kotlin プログラムの構造をより抽象化された形で捉えて処理することで、アノテーション処理などのタスクを効率的に行います。 ただし、 if 文や for ループなどの制御構文にはアクセスできず、細かい制御フローの解析には向いていません。 kapt から KSP へ移行する方法は、 公式ドキュメント を参照ください。 また KSP の最新のリリースは こちら から確認できます。 今後のロードマップ には、マルチプラットフォーム対応やパフォーマンス改善も挙げられています。 実際の速度比較 では実際に KSP 化すると、 Kotlin のタスクはどの程度速度が上がるのでしょうか。 今回は簡単に、 Room でデータを Insert するだけの簡単なアプリを作成して比較しました。 測定方法は Android Studio の Build > Clean Project 実行後、 Build > Rebuild Project を実行し、 Build Analyzer にて測定しました。 データはそれぞれ 10 回ずつ測定し、画像は平均に一番近いデータを添付しています。 kapt KSP 1.9s 1.4s 結果として、およそ 0.5s ほど KSP の方が早かったです。 kapt で生成された Java スタブとして、以下のファイルが確認できました。 AppDatabase_Impl.java UserDao_Impl.java これらは Room の Database と Dao 関連のスタブです。たった 2 ファイルの生成ですが、如実な差を確認することができました。 Room や Hilt を導入しているプロジェクトでは、大幅な速度上昇が見込めそうです。 KSP の注意点 kapt と KSP の併用する際は注意が必要です。 Room などの主要なライブラリは KSP に対応していますが、 KSP に対応していないライブラリも存在します。 こちら で現在サポートされている主要ライブラリを確認することが可能です。 例えば 2024/6/20 時点で、 Auto Factory は KSP をサポートしていません。 またプロジェクトのライブラリ更新が滞っており、そちらを先に対応しないと KSP 化できないという場合も想定できます。 プロジェクトでこれらのような KSP に未対応のライブラリを導入している場合は、一部のみを KSP 化し、 kapt と併用する形になるでしょう。 しかし kapt と KSP の併用は、ビルド時間が増加することがあります。 これは同じアノテーションプロセッサを kapt と KSP の両方で実行する場合に処理が重複してしまうためです。 高速なアノテーション処理を期待して KSP を導入したのに、逆に処理が遅くなるという事態に陥ります。 まとめ この記事では、 Kotlin のアノテーション処理ツールである kapt と KSP について記述しました。 kapt は Java のアノテーションプロセッサを Kotlin で利用できるツールですが、 Javaスタブ の生成が必要なためビルド時間が長くなるという欠点があります。 また、現在メンテナンスモードに入っているため、今後 KSP への移行が推奨されています。 KSP は Kotlin コードを直接処理できるため、 kapt よりも高速にアノテーション処理を行うことができます。 公式ドキュメントによると、 kapt と比較して最大2倍、コード生成では 7.5 倍ほどのパフォーマンス差があるそうです。 KSP 化を進める際は、 KSP に対応していないライブラリが存在することに注意が必要です。 そのようなライブラリを使用している場合は、 kapt と KSP を併用する必要がありますが、ビルド時間が増加する可能性があるため注意が必要です。 おわりに Kotlin Fest 2024 まで、あと 2 日! https://www.kotlinfest.dev/ 株式会社エブリー は、ひよこスポンサー として Kotlin Fest 2024 に参加します。 ぜひ、ブースでお会いしましょう!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 19 日目の記事です。 はじめに こんにちは、DELISH KITCHEN でクライアントエンジニアを担当している kikuchi です。 Kotlin Fest 2024 の開催が近づいてきましたので、今回は折角の機会ですので Kotlin に関わる話として DELISH KITCHEN で一部の処理を LiveData から Kotlin Coroutines Flow に移行した話をまとめてみたいと思います。 移行を考えた背景 現状 DELISH KITCHEN ではアーキテクチャに MVVM (Model View ViewModel) を採用しており、ViewModel で更新された LiveData を View が observe するという一般的な実装となっていますが、今回 Flow を調査する過程で オペレータ (map など) が使用できる Null 安全性が保証される 既に通信周りを Coroutines で実装していたため、Flow に移行しやすい ワンショット通知を無理やり LiveData で実現していた箇所を適切な方法に修正ができる (SharedFlow の利用) 新しい技術の習得 というメリットを感じ、今回新規で実装する箇所から徐々に Flow を採用することを決断しました。 特に新しい技術の習得というのはエンジニアにとって成長に繋がる良い機会ですので、積極的に取り入れたいと考えました。 LiveData と Flow の違いについて 早速ですが、本項目では LiveData と Flow の違いを細かくまとめていきます。 LiveData について LiveData とは Android Jetpack の一部であり、Android のライフサイクルに対応した監視ができる仕組みです。 監視のタイミングですが、オブザーバーのライフサイクルの状態が STARTED か RESUMED の場合のみ LiveData の変更通知を受け取ることができます。 オブザーバーを Activity や Fragment のライフサイクルと紐づけている場合、onStarted や onResume の場合 (画面がアクティブな状態) でのみ通知を受け取ることができ、 onPause や onStop の場合 (画面が非アクティブな状態) では通知を受け取ることができません。 また observe の処理さえ定義しておけば自動的に変更通知を受け取れるようになります。 つまりは実装上で明示的にオブザーバーを開始・終了する必要は無く、また画面がアクティブであるかの判定も不要ということになります。 上記をふまえて、オブザーバーを Activity のライフサイクルに紐づけてデータを監視する簡単な実装例を載せたいと思います。 // ViewModel 側の実装 class TestViewModel { private val _testData = MutableLiveData<Int>() val testData: LiveData<Int> = _testData fun updateTestData() { _testData.postValue(1) ...① } } // View 側の実装 class TestActivity : AppCompatActivity { private val viewModel: TestViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { viewModel.testData.observe(this) { data -> ...② data ?: return@observe ...③ // 監視しているデータの更新通知を受け取ったら実行する処理 updateView(data) } } } ViewModel 内部では更新可能な MutableLiveData を更新し、連動して外部に公開している LiveData も更新されるように実装することで、 ① のように ViewModel 内で値を更新すると、② の observe の処理が自動的に発火する挙動となります。 実装例を見て分かる通り、明示的にライフサイクルに応じて監視を開始・終了する処理はしていません。 ここで 1 つ注意点があり、LiveData は Java で実装されているため Null 安全性は保証されておらず、Null が設定されてしまう可能性があるため、 念の為 ③ のような Null チェックが必要となります。 Flow について Flow とは Kotlin Coroutines 上で非同期にデータを取り扱うための仕組みです。 Flow には常に最新の値を保持する StateFlow と、replay パラメータで設定した数分の過去の値を保持できる SharedFlow が存在しますが、 今回は StateFlow を利用してオブザーバーを Activity のライフサイクルに紐づけてデータを監視する実装例を載せたいと思います。 // ViewModel 側の実装 class TestViewModel { // MutableStateFlow では初期値が必要 private val _testData = MutableStateFlow<Int>(0) val testData: StateFlow<Int> = _testData fun updateTestData() { _testData.value = 1 ...① } } // View 側の実装 class TestActivity : AppCompatActivity { private val viewModel: TestViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { ...② repeatOnLifecycle(Lifecycle.State.STARTED) { ...③ viewModel.testData.collect { data -> ...④ // 監視しているデータの更新通知を受け取ったら実行する処理 updateView(data) } } } } } LiveData と同様、ViewModel 内部では更新可能な MutableStateFlow を更新し、連動して外部に公開している StateFlow も更新されるように実装することで、 ① のように ViewModel 内で値を更新すると、オブザーバー側で通知を受け取ることができます。 ただし LiveData と異なる点は、② のように必ず lifecycleScope.launch のスコープ内であることを明示する必要があり、③ のようにどのライフサイクルで通知を受け取るか 明示する必要があります。実装例だと画面がアクティブな場合に受け取れるよう repeatOnLifecycle(Lifecycle.State.STARTED) を指定しています。 (上記 ② と ③ の指定をすることで、LiveData のケースと同じ契機で更新通知を受け取ることができます) データを受け取る処理は ④ のように collect で受け取ります。 Flow は LiveData と違い Kotlin で実装されているため Null 安全性が保証されており、Null チェックは不要となります。 また、具体的な例は割愛しますが Flow はオペレータを使用できるため、直接 Flow 型のデータを操作する場合は filter や map などを使用することができます。 こちらは LiveData に無い Flow の強みと言えます。 LiveData と Flow の比較 今回は簡単な実装例のみ載せましたが、ほぼ同じような挙動にすることが可能だと分かりました。 ただ細かい部分で違いがありましたので、違いを一覧でまとめてみたいと思います。 LiveData Flow 実装の複雑さ 低 やや複雑 Null 安全性の保証 されていない (が実装でカバーできる) されている オペレーター使用可否 使用不可 使用可能 監視タイミングの管理コスト 低 (Android のライフサイクルと連動) 中 (実装で明示する必要あり) KMP での使用可否 不可 可能 移行にあたって苦労した点 LiveData と同じ用途で実装しようとするとどうしてもコードの記述量が増えてしまいましたが、今回は紐づけるライフサイクルを変更する必要が無いため、 以下のように拡張関数を定義することでコードの煩雑さを解消しました。 // 拡張関数 fun <T> Flow<T>.observe(viewLifecycleOwner: LifecycleOwner, action: (T) -> Unit) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { collect { action(it) } } } } // オブザーバー側 class TestActivity : AppCompatActivity { override fun onCreate(savedInstanceState: Bundle?) { viewModel.testData.observe(this) { data -> updateView(data) } } } また、移行当初は Android Jetpack Lifecycle ライブラリで使用されており、かつ現在は Deprecated となっている lifecycleScope.launchWhenStarted で定義していたことで、 オブザーバー側が STOPPED となってもコルーチンがキャンセルされず停止された状態のままとなっており、リソースが浪費されている問題が発生していました。 こちらは前述した viewLifecycleOwner.repeatOnLifecycle を使用することで、意図通りオブザーバー側が STARTED 状態になる度に実行し、STOPPED になる度にキャンセルされる 挙動となったため回避できたのですが、Deprecated は常に意識して日々改修していく重要さを改めて痛感しました。 まとめ LiveData は Android のライフサイクルが考慮されているなど Android アプリに対して最適化された作りとなっており、対して Flow はオペレータの使用が可能である、 Null 安全性が保証されているなど、より Kotlin の恩恵を受けることができるため、どちらを採用する場合でもメリットがあります。 そのため開発するアプリの規模や実現したい機能、学習コストなどからどちらを採用するか検討すると良いかと考えています。 近年サーバーサイド Kotlin の導入事例も増えてきていますので、アプリエンジニアでもサーバー開発をすることを見越して Kotlin の標準 API である Flow を採用する、 という考え方もあるかもしれません。 エブリーではこれからも職種にとらわれずいろいろな事に挑戦できる環境を作って行きたいと考えています。 今回紹介した内容が少しでも皆さまのお役に立てれば幸いです。 おわりに Kotlin Fest 2024 まで、あと 3 日! https://www.kotlinfest.dev/ 株式会社エブリー は、ひよこスポンサー として Kotlin Fest 2024 に参加します。 ぜひ、ブースでお会いしましょう!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) の18日目の記事です DELISH KITCHEN 開発部で小売様向き合いで主にアプリ開発をしている野口です。 Flutterエンジニアをしておりますが、直近インフラやサーバーサイドもやらせていただいております。 今回は事業譲渡されたネットスーパーアプリでインフラで使用しているFJcloud-V(旧ニフクラ)で抱えていた問題を一部AWSに置き換えることで解決した話について紹介します。 課題 課題①  SSL証明書更新対応に工数が掛かる SSL証明書はFJcloud-Vで管理しており、SSL証明書の更新には以下のフローが必要になります。(やりとりが大変だとわかっていただければいいので内容は理解しなくてOKです。) 小売様の承認 小売様それぞれがドメインを管理しており、whois 情報内のメールアドレス所持者(=小売様内の特定の部署または人)による承認が必要 メールの内容に沿って承認手続きを行なってもらう エンジニアが、FJcloud-Vの管理画面から更新情報を入力し、証明書更新申請を行う 小売様に証明書更新の承認をしていただく メールの内容に沿って承認手続きを行なってもらう エンジニアが証明書更新が成功したことを確認し、ロードバランサーに紐づくSSLアクセラレーター更新する 小売様とやりとりしながら更新しなければいけないですし、有効期限ギリギリの場合は小売様を急かすことになり、最悪有効期限が過ぎてしまいサイトを一時閉鎖しないといけないリスクがあります。 また、現在弊社ではネットスーパーアプリを十数社の小売様に提供しているため、この対応を小売様分で行わないといけないのでとても工数が掛かってしまいます。 課題② FJcloud-Vのロードバランサーが小売様ごとに個別に建てられていて費用がかさんでいる FJcloud-Vのロードバランサーはいくつか種類があるのですが、ネットスーパーで使用しているものはロードバランサー(L4)になります。 https://pfs.nifcloud.com/service/lb.htm ロードバランサー(L4)は複数ドメインへの対応ができなく、小売様ごとに個別に建てられているため、十数小売様分のロードバランサー費用が掛かっています。 解決方法 課題① 前提としてドメインは小売様のものですが、DNSはエブリー管理なのでDNSレコードの作成をエブリーが行うことは可能です。 なので、DNSレコードを作成し、 ALB のDNSの検証を行うことで小売様の承認手続きが不要になります。 また、既存のFJcloud-VのロードバランサーはSSLの終端になっており、この構造を保つために、ロードバランサーをAWSの ALB に移行し、証明書を ACM に移行することで解決できます。 課題② ALBは小売様ごとではなく、ネットスーパー全体のロードバランサーを作ることで解決できます。 つまり、ネットスーパー全体のALBとACMで証明書を小売様ごとに作成し、ALBに各小売様用の証明書を紐づけることで解決しました。 構成図はこのようになります。 Route53では複数の小売様のドメインを管理し、ACMから取得した各小売様用の証明書をALBに設定しています。 FJcloud-VのネットワークとVPCは Site-to-Site VPN で疎通できるようにしてあります。 やったこと 以下にはTerraformの実装を記載しています。 FJcloud-Vのサーバーとロードバランサーが疎通できるようにターゲットグループを指定する FJcloud-Vのipに向くターゲットグループを作成します。 # ターゲットグループを作成 resource "aws_lb_target_group" "nifcloud_server" { name = "nifcloud-server" port = 80 protocol = "HTTP" target_type = "ip" } # FJcloud-Vに向くように設定 resource "aws_lb_target_group_attachment" "nifcloud_server" { target_group_arn = aws_lb_target_group.nifcloud_server.arn target_id = & { 指定したいIP } } ALBを作成してターゲットグループを紐づけを行う # ALB作成 resource "aws_lb" "server" { name = "server" internal = false load_balancer_type = "application" } # ターゲットグループで設定したFJcloud-VのIPと通信するようにする resource "aws_lb_listener" "server_https" { load_balancer_arn = aws_lb.server.arn port = 443 protocol = "HTTPS" default_action { type = "forward" target_group_arn = aws_lb_target_group.nifcloud_server.arn } } Route 53のホストゾーンと小売様用のalias レコード (ALB に向ける) を作成する # 小売様Aのホストゾーンの作成 resource "aws_route53_zone" "martA" { name = "martA.example.com" } # 小売様Aのaliasレコードを作成 resource "aws_route53_record" "martA_a" { zone_id = aws_route53_zone.martA.zone_id name = "martA.example.com" type = "A" } DNS (FJcloud-V管理) を Route53 に向ける DNSはFJcloud-Vで管理されているので、FJcloud-VからRoute53に向くようにします。 以下の記事のようにRoute53で作成したmartAのホストゾーンのNSレコードをドメインレジストラのネームサーバに設定をすることでFJcloud-VからRoute53に切り替えることができます。 https://dev.classmethod.jp/articles/route53-domain-onamae/ Route53に検証用のCNAMEレコードを登録し、ACMで証明書を発行する # 小売様Aの証明書を発行 resource "aws_acm_certificate" "martA_cert" { domain_name = local.martA_domain_name validation_method = "DNS" } # Route53に小売様Aの検証用のCNAMEレコードを登録 resource "aws_route53_record" "martA_cert" { for_each = { for dvo in aws_acm_certificate.martA_cert.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type } } allow_overwrite = true name = each.value.name records = [ each.value.record ] ttl = local.cname_default_ttl type = each.value.type zone_id = aws_route53_zone.martA.zone_id } # 小売様Aの証明書の検証 resource "aws_acm_certificate_validation" "martA_cert" { certificate_arn = aws_acm_certificate.martA_cert.arn validation_record_fqdns = [ for record in aws_route53_record.martA_cert : record.fqdn ] } 証明書の検証が通ったらALBに証明書の紐付けを行う resource "aws_lb_listener_certificate" "server_martA" { listener_arn = aws_lb_listener.server_https.arn certificate_arn = aws_acm_certificate.martA_cert.arn } 作成したALBに対して上記のようにすることで1つのALBに複数の証明書を紐づけることができます。 まとめ 今回の対応で全ての小売様に適用すれば、十数小売様のロードバランサーを1つにすることができます。 FJcloud-Vのロードバランサーが、1ヶ月あたり1万円で1年で12万、小売様が15と仮定すると 12 ✖️ 15で 180万円掛かっていたところ https://pfs.nifcloud.com/price_extax/network.htm#load ALBはざっくり概算で1ヶ月あたり5000円で1年で6万円になるので 180 - 6 = 174でだいたい150万円くらいはサーバー費用を浮かせることができます。 また、初めてインフラの構築を行いましたが、クライアントのお仕事と違って、連携するサービスが多く気にしないといけないことが多岐に渡るなと感じました。 ご覧いただきありがとうございました。
アバター
こちらは every Tech Blog Advent Calendar 2024(夏) 17日目の記事になります。 こんにちは。 開発本部のデータ&AIチームでデータサイエンティストをしている古濵です。 変わらずML周辺の開発をもりもりしています。 今回は、DatabricksのFeature Storeについて検証した内容を共有します。 Databricks Model Servingについても検証記事をまとめていますので、ぜひご覧ください。 tech.every.tv 背景 現状、DELISH KITCHENを中心に、ML開発を進めており、今後のML開発のagility向上を見据えて、Feature Storeを導入することを検討しています。 DatabricksではFeature Storeを使う理由を、下記のようにまとめています。 参考 特徴量ストアとは |Databricks on AWS 発見性 :Databricks ワークスペースからアクセスできるFeature Store UIでは、既存の特徴量を参照および検索できます。 リネージ :Databricksで特徴量テーブルを作成すると、特徴量テーブルの作成に使用されたデータソースが保存され、アクセスできるようになります。特徴量テーブルの各機能について、その機能を使用するモデル、ノートブック、ジョブ、エンドポイントにアクセスすることもできます。 モデルのスコアリングやサービングとの統合 :Feature Storeの特徴量を使用してモデルをトレーニングする場合、モデルは特徴量メタデータと一緒にパッケージ化されます。モデルをバッチスコアリングまたはオンライン推論に使用すると、Feature Storeから自動的に特徴量が取得されます。呼び出し側はこれらの特徴量について知る必要はありませんし、特徴量を検索または結合して新しいデータをスコアリングするロジックを組み込む必要もありません。これにより、モデルのデプロイメントや更新が容易になります。 ポイントインタイムのルックアップ :Feature Storeは、特定の時点での正確性を必要とする時系列およびイベントベースのユースケースをサポートします。 上記含めて、DatabricksのFeature Storeに関するドキュメントを読んだ際、以下のような疑問を持ちました。 「Feature Store使わずに、Deltaテーブルから学習/推論すれば良いのでは?」 このように思った理由は、一般的なFeature Storeを使うメリットを、Unity CatalogとDeltaテーブルの機能でカバーできているのではないかと思ったからです(Unity Catalogに関しては こちら をご確認ください)。 これらを踏まえて、Feature Storeを 使う場合 と 使わない場合 の比較を実施しました。 検証した内容の共有の前に、前提条件となる弊社のDatabricksを活用したML開発状況をサマリます。 ML開発はスモールスタート中で、モデル開発は1人もしくは2人 バッチ推論がメインで、推論結果をサーバーのRedis(ElastiCache for Redis)にデプロイする運用 DatabricksのUnity Catalogは最近使えるようになったばかり Feature Storeを使う場合と使わない場合の比較 Basic example for Feature Engineering in Unity Catalog という、Databricksが提供しているサンプルノートブック(ワインの品質を予測するモデル作成例)を使って、Feature Storeを 使う場合 と 使わない場合 の比較を行いました。 事前準備 実行環境はDatabricks 13.3LTS Runtimeを使用し、ノートブックの最初に以下を実行しています。 %pip install databricks-feature-engineering dbutils.library.restartPython() サンプルノートブックのうち、 使う場合 と 使わない場合 のどちらでも使用するコードを以下に定義します。 細かい説明はしませんが、 wine_qualityのサンプルデータセット を準備し、Unity CatalogとFeatureEnginneringClientを使えるようにしています。 import pandas as pd from pyspark.sql.functions import monotonically_increasing_id, expr, rand from databricks.feature_engineering import FeatureEngineeringClient, FeatureLookup import mlflow import mlflow.sklearn from mlflow.tracking.client import MlflowClient from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score raw_data = spark.read.load( "/databricks-datasets/wine-quality/winequality-red.csv" , format = "csv" , sep= ";" , inferSchema= "true" , header= "true" ) def addIdColumn (dataframe, id_column_name): """Add id column to dataframe""" columns = dataframe.columns new_df = dataframe.withColumn(id_column_name, monotonically_increasing_id()) return new_df[[id_column_name] + columns] def renameColumns (df): """Rename columns to be compatible with Feature Engineering in UC""" renamed_df = df for column in df.columns: renamed_df = renamed_df.withColumnRenamed(column, column.replace( " " , "_" )) return renamed_df # Run functions renamed_df = renameColumns(raw_data) df = addIdColumn(renamed_df, "wine_id" ) # Drop target column ("quality") as it is not included in the feature table features_df = df.drop( "quality" ) display(features_df) 下記コードにはreal_time_measurementという特徴量の集計があります。 これが具体的にどんな特徴量なのか想像し難いですが、今回議論したい内容ではないため、Feature Storeに保存されていない特徴量という認識で進めます。 ## inference_data_df includes wine_id (primary key), quality (prediction target), and a real time feature inference_data_df = df.select( "wine_id" , "quality" , ( 10 * rand()).alias( "real_time_measurement" )) display(inference_data_df) spark.sql( "USE CATALOG uc_sandbox" ) spark.sql( "USE SCHEMA naoki_furuhama" ) table_name = "wine_db" fe = FeatureEngineeringClient() 特徴量テーブルの作成 Feature Storeを 使う場合 fe.create_table( name=table_name, primary_keys=[ "wine_id" ], df=features_df, schema=features_df.schema, ) Feature Storeを 使わない場合 features_df.write \ .mode( "overwrite" ) \ .saveAsTable(table_name) Feature Storeに特徴量テーブルとして保存する場合はFeatureEnginneringClientのcreate_tableメソッドを使います。 テーブルを作成して、スキーマを見るという観点だけはどちらも同じように見ることができます。 違いとして、Feature Storeを使う場合はprimary_keyの指定が必須で、primaly_keyを意味する PK がカラムの横に表示されているのがわかるかと思います。 なお、使わない場合で保存したとしても、Unity Catalog内のDeltaテーブルであれば、ALTER TABLEすることで、特徴量テーブルにすることもできます。 参考 Unity Catalogでの特徴量エンジニアリング | Databricks on AWS 特徴量テーブルの更新 特徴量テーブルに新しくカラムを追加する際のコードを対比してみます。 ## Modify the dataframe containing the features so2_cols = [ "free_sulfur_dioxide" , "total_sulfur_dioxide" ] new_features_df = (features_df.withColumn( "average_so2" , expr( "+" .join(so2_cols)) / 2 )) Feature Storeを 使う場合 fe.write_table( name=table_name, df=new_features_df, mode= "merge" ) Feature Storeを 使わない場合 new_features_df.write \ .mode( "overwrite" ) \ .option( "mergeSchema" , "true" ) \ .saveAsTable(table_name) 特徴量テーブルの更新も作成と大きくは変わりません。 Feature Storeを使う場合は、write_tableメソッドを使って特徴量テーブルを更新します。 使わない場合は、mergeSchemaオプションを指定することでカラムが増えても問題なくDeltaテーブルを更新できます。 学習データの作成 Feature Storeを 使う場合 def load_data (table_name, lookup_key): # In the FeatureLookup, if you do not provide the `feature_names` parameter, all features except primary keys are returned model_feature_lookups = [ FeatureLookup( table_name=table_name, lookup_key=lookup_key ) ] # fe.create_training_set looks up features in model_feature_lookups that match the primary key from inference_data_df training_set = fe.create_training_set( df=inference_data_df, feature_lookups=model_feature_lookups, label= "quality" , exclude_columns= "wine_id" ) training_pd = training_set.load_df().toPandas() # Create train and test datasets X = training_pd.drop( "quality" , axis= 1 ) y = training_pd[ "quality" ] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size= 0.2 , random_state= 42 ) return X_train, X_test, y_train, y_test, training_set # Create the train and test datasets X_train, X_test, y_train, y_test, training_set = load_data(table_name, "wine_id" ) Feature Storeを 使わない場合 def load_data () training_pd = inference_data_df.join( features_df, on=[ "wine_id" ], how= "left" ).toPandas() # Create train and test datasets X = training_pd.drop( "quality" , axis= 1 ) y = training_pd[ "quality" ] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size= 0.2 , random_state= 42 ) return X_train, X_test, y_train, y_test # Create the train and test datasets X_train, X_test, y_train, y_test = load_data() Feature Storeを使う場合は、FeatureLoookup機能を使ってFeature Storeに保存している特徴量テーブルを引っ張ってきます。 サンプルコードではFeatureLookupは1つしか指定していませんが、複数指定することで様々な特徴量テーブルを結合することが可能です。 そして、create_training_setメソッドを使って、起点となる(primary_keyやlabel等を含んだ)データと特徴量テーブルを結合して学習データを作成することができます。 使わない場合は、Deltaテーブルから特徴量テーブルを読み込んで、推論データと結合して学習データを作成します。 特徴量テーブル1つであれば、こちらの方がシンプルで可読性も高いですが、特徴量テーブルが増えるとleft joinを繰り返し記述することになります。 モデルの学習 検証用のモデルを準備します。 # Configure MLflow client to access models in Unity Catalog mlflow.set_registry_uri( "databricks-uc" ) model_name = "uc_sandbox.naoki_furuhama.wine_model" client = MlflowClient() try : client.delete_registered_model(model_name) # Delete the model if already created except : None Feature Storeを 使う場合 # Disable MLflow autologging and instead log the model using Feature Engineering in UC mlflow.sklearn.autolog(log_models= False ) def train_model (X_train, X_test, y_train, y_test, training_set, fe): ## fit and log model with mlflow.start_run() as run: rf = RandomForestRegressor(max_depth= 3 , n_estimators= 20 , random_state= 42 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) mlflow.log_metric( "test_mse" , mean_squared_error(y_test, y_pred)) mlflow.log_metric( "test_r2_score" , r2_score(y_test, y_pred)) fe.log_model( model=rf, artifact_path= "wine_quality_prediction" , flavor=mlflow.sklearn, training_set=training_set, registered_model_name=model_name, ) train_model(X_train, X_test, y_train, y_test, training_set, fe) Feature Storeを 使わない場合 mlflow.sklearn.autolog(log_models= True ) def train_model (X_train, X_test, y_train, y_test): ## fit and log model with mlflow.start_run() as run: rf = RandomForestRegressor(max_depth= 3 , n_estimators= 20 , random_state= 42 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) mlflow.log_metric( "test_mse" , mean_squared_error(y_test, y_pred)) mlflow.log_metric( "test_r2_score" , r2_score(y_test, y_pred)) mlflow.register_model( model_uri= "runs:/abcdefghijklmnopqrstuvwxyz123456/model" , name=model_name ) train_model(X_train, X_test, y_train, y_test) モデルの学習はFeature Storeを使う場合と使わない場合で、FeatureEnginneringClientを使うかmlflowを使うかの違いがあります。 また、Feature Storeを使う場合は、特徴量テーブルとモデルを紐づけるリネージが作成できることが確認できます。 これにより、一目でモデルがどんな特徴量から学習されたかを見ることができ、過去の実験の振り返りをもとにした改善が容易になります。 バッチ推論 登録済みのモデルのうち、最新のモデルを取得する関数を定義します。 # Helper function def get_latest_model_version (model_name): latest_version = 1 mlflow_client = MlflowClient() for mv in mlflow_client.search_model_versions(f "name=" {model_name} "" ): version_int = int (mv.version) if version_int > latest_version: latest_version = version_int return latest_version Feature Storeを 使う場合 ## For simplicity, this example uses inference_data_df as input data for prediction batch_input_df = inference_data_df.drop( "quality" ) # Drop the label column latest_model_version = get_latest_model_version(model_name) predictions_df = fe.score_batch( model_uri=f "models:/{model_name}/{latest_model_version}" , df=batch_input_df ) display(predictions_df[ "wine_id" , "prediction" ]) Feature Storeを 使わない場合 batch_input_df = inference_data_df.join( features_df, on=[ "wine_id" ], how= "left" ).drop( "quality" ) # Drop the label column latest_model_version = get_latest_model_version(model_name) from pyspark.sql.functions import struct, col # Load model as a Spark UDF. Override result_type if the model does not return double values. loaded_model = mlflow.pyfunc.spark_udf( spark, model_uri=f 'models:/wine_model_uc/{latest_model_version}' , result_type= 'double' ) # Predict on a Spark DataFrame. batch_input_df.withColumn( "prediction" , loaded_model(struct(* map (col, batch_input_df.columns)))) predictions_df = batch_input_df display(predictions_df[ "wine_id" , "prediction" ]) バッチ推論もFeature Storeを使う場合と使わない場合で、FeatureEnginneringClientを使うかmlflowを使うかの違いがあります。Feature Storeを使う場合はfe.score_batchメソッドを使い、使わない場合はmlflow.pyfunc.spark_udfメソッドを使って推論します。 また、大きな違いとして、Feature Storeを使う場合は、推論時に起点となる(primary_keyやlabel等を含んだ)データを渡せば、 特徴量をデータとして渡さずに推論することができます 。 これは、FeatureEnginneringClientがメタデータをもとに学習時に使用した特徴量を自動的に取得してくれるからだと思われます。 メリット・デメリット Feature Storeを使う場合と使わない場合で、以下の実装上の比較をしました。 特徴量テーブルの作成 特徴量テーブルの更新 学習データの作成 モデルの学習 バッチ推論 では、実際にどのようなメリット・デメリットがあるかまとめます。 メリット リネージ機能 Feature Storeを使う場合は、特徴量テーブルとモデルを紐づけることができるため、モデルがどんな特徴量から学習されたかを一目で確認できる。 特徴量の自動取得 Feature Storeを使う場合は、推論時に起点となるデータを渡せばFeature Storeがよしなに特徴量を参照してくれるため、明示的に特徴量をデータとして渡さずとも推論できる。 デメリット 可読性 FeatureLoopup機能を使った特徴量の結合は、暗黙的にleft joinしてるなど、FeatureEnginneringClientについて理解がないと、どんな処理をしているわからず、可読性が低い。 学習コスト 特徴量の自動取得な便利な側面はあるが、上記の可読性のデメリットも含めてFeatureEnginneringClientの書き方に慣れるまでの学習コストがかかる。 振り返り Databricks側が、Feature Store機能の使用を推奨している理由は冒頭で述べた通りです。 それぞれの観点で、弊社のMLのスモールスタートフェーズにおいてFeature Storeを使う理由があるか考えると、以下のようになりました。 発見性 ◯ 特徴量に関しては、Unity Catalog内のDeltaテーブルでもUnity Catalog Explorerでも参照できるため、Feature Storeを使わなくても検索やスキーマ等を確認できる。 対して、Unity Catalog外のDeltaテーブルであれば、上記ができないためFeature Storeを使えると嬉しい。 リネージ ◯ Feature Storeを使うことで特徴量テーブルとモデル間でのリネージ機能があるため、Feature Storeを使えると嬉しい。 モデルのスコアリングやサービングとの統合 △ モデルのスコアリングに関しては、バッチ推論時に特徴量を自動取得してくれる機能があるため、Feature Storeを使えると嬉しい。 MLのスモールスタートフェーズにおいて、サービングに関してはスコープ外であり、Feature Storeが使えなくてもよい。 ポイントインタイムのルックアップ ✗ MLのスモールスタートフェーズにおいて、特定の時点での正確性を必要とする時系列およびイベントベースのユースケースはスコープ外であり、Feature Storeが使えなくてもよい。 所感 個人的なDatabricksのFeature Storeに対する疑問が、今回の検証で解消されました。 特に 発見性 と リネージ に関して、これからML開発を進めていき、たくさんの実験をしていく中で大いに助けになってくれる機能だと感じました。 FeatureEnginneringClientの学習コストや可読性に関して懸念を述べましたが、今回の検証にあたりドキュメントを読み込んでいく中で整理できたかなと思います。 これから新しくMLのモデル開発するメンバーを見据えた整備も順次進めていければなと思います。 Feature Storeを導入することで、MLのモデル開発のagility向上に繋がることは間違いないと感じました。 今実装しているいくつかの特徴量から順にFeature Storeに移行していくことで、Feature Storeの恩恵を受けられるML環境の構築を目指していきたいと思います。
アバター
この記事は、 every Tech Blog Advent Calendar 2024(夏) の16日目の記事です。 はじめまして、データストラテジストのoyabuです。 N1分析、色んなメリットがあるので頼る場面が多いのですがN1分析時に注意していることを書いてみます。 N1分析のpros/cons ここでのN1分析は、1人のユーザーをアクションログ単位で分析することを指します。インタビューなどは含みません。 そういったN1分析のpros/consは以下であると考えています。 pros ユーザーの解像度が上がる 楽しい cons 適当にみちゃうと時間の無駄 楽しいので時間が溶ける なので、基本的にはある程度N1分析する目的とユーザーのあたりを付けることが大事だと思っています。 方針を決める N1分析のおすすめはまずユーザーをよしなにマッピングして対象のユーザーをどこに移動させたいか。を考えたうえで、そのギャップをみて施策を考案する方針です。 たった1人のユーザーであるN1の動きを意思決定の根拠とするのはちょっとつらいです。が、施策を決めるうえでのヒントを集めていくのに使うのはそれなりに筋がよいとも思ってます。 なので、だいたいの方針は以下な感じになると思います。 適当にユーザーを色んな切り口でマッピングする 良さげなユーザー群があれば、その人達を特定の指標でスコアリングしてグラデーションをみる 一貫性のあるスコアがみえれば、まずもっともスコアの高いユーザーを見てみる だいたいやたらすごい動きをしているので、次点にいるユーザーを抽出してその人のN1も見てみる 比較して、次点にいるユーザーにどういった行動をして欲しいかを考える 適当なデータで見ていきます。 ユーザーをマッピングしてみる 弊社が持っているレシピデータやレシートデータなど、実データを使ってしまうとアレなので 簡単のためにそれっぽいデータをスプレッドシート上に用意して、ざっくりした分析の流れをご説明していきます。 用意したN1用のログデータはこんな感じです。 仮にレシピの閲覧数とレシート登録数をユーザーごとに集計してマッピングする方針を考えてみます。 まずはユーザー単位にそれぞれを集計して、 クロス集計してみます。 レシピの閲覧数とレシート登録数はそれなりに相関してそうな雰囲気がします。 たとえばもっとユーザーさんにレシート登録して欲しい。。!という目的があった場合、ここでいうAさんとBさんのN1を見比べてみると良さそうです。 N1分析してみる Aさんをみてみます。こんな感じです。 なんとなくレシピみた後にレシート登録する動きが多そうです。 次にBさんをみてみます。こんな感じです。 Aさんのような動きはなさそうです。 アプリ上でレシピを見るタッチポイントはそれなりに多い人なので、仮に行動フローがレシピサイトを見る->買い物にいく。とAさんの行動フローと似ていた場合、ひょっとしてアプリ上でレシピみたあとにレシートの登録のお願いなんかしてみるともっと登録してくれるのでは??と施策のヒントが出てきます。 まとめ ほんとはビニングとか必要になってくると思うのでもっと複雑な工程にはなるのですが、頑張って簡単にしました。 よいN1ライフをー
アバター