TECH PLAY

株式会社エブリー

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

402

はじめに Gson について Gson の課題 1. Null 安全が破壊されるリスク 2. デフォルト引数が無視される Kotlin Serialization について 具体的な修正内容 1. Data Class の書き換え 2. Retrofit の Converter の置き換え まとめと今後の課題 はじめに こんにちは、株式会社エブリーで Android アプリ開発を担当している岡田です。 弊社が提供する デリッシュキッチン の Android アプリでは、アプリの堅牢性向上とモダンな開発体験のための選択として、JSON パーサーを従来の Gson から Kotlin Serialization への移行を検討しています。 今回は弊社で行なっているイベント「挑戦WEEK」にて、Gson から Kotlin Serialization への移行を、Android のコードベース変更に限定して挑戦してみました。こちらについて、少しお話しさせていただければと思います。 弊社の挑戦WEEKの取り組みについては以下の記事をご覧ください! tech.every.tv Gson について Android アプリの開発において、API との通信で受け取った JSON をデータクラスに変換する「JSON パース」は避けては通れない実装です。 デリッシュキッチン の Android アプリでは、長らく JSON パーサーとして Google 製の「Gson」を利用してきました。Gson は非常に歴史が長く、Android アプリ開発の黎明期からデファクトスタンダードとして広く使われており、Retrofit などのネットワークライブラリとも標準で連携しやすいという特徴があります。 長年アプリの通信基盤を支えてくれた Gson ですが、プロジェクトのフル Kotlin 化が進み、よりモダンな言語仕様を活用していく中で、実は Android アプリを開発する上でいくつかの大きな課題を抱えるようになっていました。 Gson の課題 Java 時代には非常に優秀だった Gson ですが、Kotlin で構成された現代のアプリにおいては、Kotlin の強みである言語仕様とコンフリクトを起こすケースが目立つようになってきました。 1. Null 安全が破壊されるリスク Gson は内部でリフレクション( sun.misc.Unsafe など)を用いてインスタンスを生成します。そのため、Kotlin のデータクラスでプロパティを「非 Null( String など)」で定義していても、サーバーから返ってくる JSON 側にそのキーが存在しない場合、Gson は強制的に null を代入してしまいます。 これにより、Kotlin コンパイラが保証しているはずの「Null 安全」がランタイムで破壊され、アプリの思わぬところで NullPointerException を引き起こす原因となっていました。 2. デフォルト引数が無視される Kotlin のデータクラスでは val isPremium: Boolean = false のようにデフォルト引数を設定できます。しかし、Gson はコンストラクタを経由せずにインスタンスを生成することがあるため、JSON に該当のキーが含まれていない場合、このデフォルト値が適用されません。結果として、意図しない型の初期値( Int なら 0 、参照型なら null )が入ってしまうという問題がありました。 これらの挙動は、開発者が意図しない「不正な状態を持ったインスタンス」がアプリ内を回遊することを意味しており、結果として予期せぬクラッシュの温床になり得ます。 Kotlin Serialization について 最終的に、これらの課題を根本から解決するために、Kotlin 公式が提供しているシリアライズライブラリ「Kotlin Serialization( kotlinx.serialization )」へ移行を検討しています。 Kotlin Serialization は、コンパイル時にシリアライズ・デシリアライズのためのコードを自動生成する仕組みを持っています。実行時に重いリフレクションを行わないため、非常にモダンで Kotlin ライクな設計となっています。 このライブラリへ切り替えることで、以下のような大きな恩恵を受けることができます。 厳格な Null 安全の保証 非 Null として定義したプロパティに対して JSON に値が存在しない場合、強制的に Null を入れるのではなく、パース時に明確に例外( SerializationException )を投げてくれます。これにより、不正なデータによる後続処理でのクラッシュを防ぐことができます。 デフォルト値の完全なサポート JSON にキーが存在しない場合、Kotlin 側で定義したデフォルト引数が正しく適用されます。 パフォーマンス向上とアプリサイズ削減 リフレクションに依存しないため、パース速度が向上します。また、ProGuard/R8 による最適化とも相性が良く、アプリのバイナリサイズの削減にも繋がります。 具体的な修正内容 実際に Gson から Kotlin Serialization へ移行するにあたり、行った具体的な修正内容をご紹介します。 1. Data Class の書き換え Gson の @SerializedName アノテーションを、Kotlin Serialization の @SerialName に変更し、クラスに @Serializable アノテーションを付与します。 【従来の Gson での実装】 data class UserResponse( @SerializedName ( "id" ) val id: Long , @SerializedName ( "user_name" ) val userName: String , @SerializedName ( "profile_image_url" ) val profileImageUrl: String ? ) 【新しい Kotlin Serialization での実装】 @Serializable data class UserResponse( @SerialName ( "id" ) val id: Long , @SerialName ( "user_name" ) val userName: String , // サーバーからキーが送られてこない可能性がある場合はデフォルト値を設定 @SerialName ( "profile_image_url" ) val profileImageUrl: String ? = null , @SerialName ( "is_premium" ) val isPremium: Boolean = false ) 2. Retrofit の Converter の置き換え API 通信に Retrofit を使用しているため、Gson の ConverterFactory を Kotlin Serialization 用のものへ差し替えました。 この際、サーバーからのレスポンスにおいて、アプリ側で定義していない未知のキーが含まれていてもパースエラーにならないよう、 ignoreUnknownKeys = true を設定しています。 // Json パーサーの設定 val json = Json { ignoreUnknownKeys = true // 未知のキーを無視する coerceInputValues = true // null が来た場合にデフォルト値があればフォールバックする } val contentType = "application/json" .toMediaType() val retrofit = Retrofit.Builder() .baseUrl( "https://api.example.com/" ) // GsonConverterFactory.create() からの置き換え .addConverterFactory(json.asConverterFactory(contentType)) .build() 主にこれらの修正を、API レスポンスを受け取る全てのデータクラスと Retrofit クライアントに対して適用し、段階的に移行を進めました。 また他にも com.google.gson.internal.bind.util.ISO8601Utils を利用している箇所や、 JsonUtil という Android アプリ側で Json を扱う際に使用するクラスの修正など、細かい修正も行いました。 総差分ファイル数はおよそ 500 ファイルと、大規模な改修になりました。 まとめと今後の課題 今回の改修で JSON パーサーを Kotlin Serialization に移行したことにより、Kotlin の言語仕様に沿った厳格な型安全性が担保されます。Android のコードベース上での堅牢性は大きく向上しました。 しかし、ライブラリが「厳格」になったからこそ直面する新たな課題もあります。 それは、 サーバーからのレスポンス仕様(スキーマ)の正確な把握 です。 Gson の時代は「JSON にキーがなくても、とりあえず Null を入れてクラッシュさせない」という緩さがありました。しかしこれからは、非 Null プロパティのキーが JSON に存在しなければ、即座にパース失敗となってしまいます。 これを防ぐためには、以下のような対応をサービス全体で意識していく必要があります。 サーバーレスポンスで Null が返る、またはキーが省略される可能性のあるフィールドには、適切な Nullable 定義やデフォルト値を設定する クラッシュログを監視し、パースエラーが発生した場合は迅速にデータクラスの定義をチューニングする サーバーサイドのエンジニアと密に連携し、API 仕様書とクライアント実装の乖離をなくす デリッシュキッチンは歴史のあるサービスですから、型安全に API レスポンスをパースするには、この辺りの見直しは避けて通れません。 時間と根気がいる作業にはなりますが、徐々にでも整備できればと思います。 もしまだ Gson を利用している方で、「データクラスの Null 安全が担保できずに困っている」「原因不明の NullPointerException に悩まされている」と感じているなら、一度 JSON パーサーの移行を検討してみてはいかがでしょうか。 今後も、Kotlin の厳格な型安全性を武器に、より品質が高く安定した デリッシュキッチン をユーザーの皆様にお届けできるよう、改善を続けていきます。
アバター
はじめに こんにちは。開発本部 開発1部 デリッシュリサーチチームの 江﨑 です。 本記事では、これまでHive Metastore上のDeltaテーブルで管理していたデリッシュリサーチ用データ(約40テーブル)をUnity Catalogへ移行したプロジェクトの全体像を、インフラ整備からAthena連携・Databricks Managed MCP活用まで紹介します。 はじめに 背景:なぜ Unity Catalog に移行したか 課題 1:テーブルスキーマが「コードを読まないと分からない」 課題 2:データリネージを Mermaid で管理していた Unity Catalog とは マネージドテーブル vs 外部テーブル 移行手順の全体像 Step 1:インフラ整備 IAMロールの設定 Catalogの作成 Step 2:Unity Catalog テーブルの作成 移行スクリプトの流れ Step 3:既存 ETL コードの変更 主な変更:テーブル参照パスの書き換え Step 4:Athena から Unity Catalog のデータを参照する 実際に行った変更 Step 5:Quick Suite のデータセット移行 移行結果 Before / After Before(Hive Metastore 時代) After(Unity Catalog 移行後) Databricks Managed MCP Databricks Managed MCP とは DBSQL MCP:ETL 開発が変わる 具体的な開発体験 まとめ 背景:なぜ Unity Catalog に移行したか 移行前、デリッシュリサーチではDatabricksのHive Metastore上で約40個のDeltaテーブルを運用していました。その中で、運用上の課題が積み重なってきていました。 課題 1:テーブルスキーマが「コードを読まないと分からない」 「このテーブルに user_id カラムってあったっけ?」という確認をするたびに、Notebookを開いて display(spark.table("schema.table")) を実行するか、ETLコードを読み返す必要がありました。テーブルが増えるほど、この手間もかさんでいきます。 課題 2:データリネージを Mermaid で管理していた テーブル間の依存関係(「このテーブルはどのテーブルから作られているか」)をMermaidのコードで手作業管理していましたが、40テーブルを超えると複雑すぎてメンテナンスが限界になり放置されていました。テーブルを追加するたびにMermaidを手で更新する運用は、明らかにスケールしません。 これらの課題を解消するためにUnity Catalogへの移行を決めました。 Unity Catalog とは Unity CatalogはDatabricksの統合データガバナンスソリューションです。Hive Metastoreとの最大の違いは、 三層の名前空間(Catalog > Schema > Table) を持つ点です。 Catalog(例: marketing_research) └── Schema(例: search) └── Table(例: search_count) Hive Metastoreでは schema.table の二層構造でしたが、Unity Catalogでは catalog.schema.table の三層になります。この変更によって、チームやプロジェクトを跨いだデータの整理がしやすくなります。 主な機能は以下の3点です: データカタログ :テーブルのスキーマ・カラムの説明文をUI上で管理・参照できる データリネージ :データの流れ(どのテーブルがどのテーブルを参照しているか)を自動追跡・可視化 アクセス制御 :行・列レベルの細粒度なセキュリティ設定 Unity Catalogのオブジェクト階層。今回の移行ではCatalog > Schema > Tableの三層構造を利用。(出典: Databricks公式ドキュメント ) Unity CatalogのUI。テーブルを選択するとカラム名・データ型・コメントを一覧で確認できる。 マネージドテーブル vs 外部テーブル Unity Catalogへの移行を検討するとき、決めなければならないのが テーブルタイプ です。 観点 マネージドテーブル 外部テーブル データ保管場所 Unity Catalogが管理するパス 任意のストレージパス(S3など) S3パスの形式 自動生成されたID形式になる 任意のS3パスを指定できる テーブル削除時 データも削除される データは残る Databricksの推奨 ほとんどのユースケース 既存ストレージとの互換性が必要な場合 Databricksはマネージドテーブルを推奨していますが、本プロジェクトでは 外部テーブル を選択しました。 その理由は、データ参照構成にあります。デリッシュリサーチではダッシュボードを Amazon Quick Suite(以下Quick Suite) → Athena → Glue Crawler → S3 という構成で構築しています。Glue Crawlerはクロール先のS3パスを読み取ってテーブル名を付与します。 ここで問題になるのが、マネージドテーブルのS3パス形式です。マネージドテーブルに移行すると、S3パスは s3://unity-catalog-metastore/__unitystorage/... のような __unitystorage 配下のシステム管理ディレクトリ(ランダム生成IDを含むパス) に配置されます。Glue CrawlerはS3プレフィックス/フォルダ名ベースでテーブル名を付けるため、Athena側のテーブル名が人間可読でない名前になり、現実的な運用が難しくなります。 外部テーブルであればS3パスを s3://<バケット名>/table_name のようにテーブル名ベースで保持できるため、Athena上のテーブル名が人間にとって分かりやすい名前のままになり、移行コストを抑えつつ今後の管理も楽になります。 移行手順の全体像 移行は以下の5ステップで実施しました。 Step 1 :インフラ整備(IAM・Catalogの作成) Step 2 :Unity Catalogテーブルの作成 Step 3 :既存ETLコードの変更(テーブル参照パスの更新) Step 4 :AthenaからUnity Catalogのデータを参照できるようにする Step 5 :Quick Suiteのデータセット移行 Step 1:インフラ整備 Unity Catalogを有効化するにあたって、AWS・Databricks側でいくつかの設定が必要でした。 IAMロールの設定 DatabricksがS3バケットにアクセスするためのIAMロールと、それをDatabricks側に登録するストレージクレデンシャルを新規作成しました。インフラ変更はTerraformで管理しています。概念的には以下のような構成です。 resource "aws_iam_role" "databricks_unity_catalog" { name = "<ロール名>" assume_role_policy = jsonencode ( { Statement = [{ Effect = "Allow" Principal = { AWS = "arn:aws:iam::<Databricks の AWS アカウント ID>:role/unity-catalog-prod-role" } Action = "sts:AssumeRole" Condition = { StringEquals = { "sts:ExternalId" = <databricks_sts_external_id> } } }] } ) } Catalogの作成 Unity Catalogを利用するには、Databricks上にCatalogを作成する必要があります。Catalog作成時のmanaged storage location指定は任意ですが、今回は運用上の理由から指定のS3バケットをmanaged storage locationとして指定しました。 参照: Unity Catalog の Catalog を作成する(Databricks 公式ドキュメント) Step 2:Unity Catalog テーブルの作成 インフラが整ったら、既存のHive MetastoreテーブルのデータをUnity Catalogの外部テーブルとして再作成します。旧パスから schema/table というパス構成に移行するため、CTAS(CREATE TABLE AS SELECT)を使いました。 移行スクリプトの流れ 旧パスのDeltaテーブルを SELECT * で読み取る schema/table 形式のパスにDelta形式で書き込みながらUnity Catalogの外部テーブルを作成 # 旧 Delta からデータをコピーしながら UC 外部テーブルを作成 spark.sql(f """ CREATE TABLE IF NOT EXISTS {catalog}.{schema}.{table_name} USING DELTA LOCATION '{new_s3_path}' AS SELECT * FROM delta.`{old_s3_path}` """ ) Step 3:既存 ETL コードの変更 主な変更:テーブル参照パスの書き換え Hive MetastoreではS3パスを直接指定してデータを読み書きしていましたが、Unity Catalogでは catalog.schema.table の三層構造で参照するよう書き換えました。 # Before(Hive Metastore):S3 パスを直接指定 df = spark.read.format( "delta" ).load( "s3://path/to/delta" ) df.write.format( "delta" ).save( "s3://path/to/delta" ) # After(Unity Catalog):カタログ名で参照 df = spark.table( "catalog.schema.table" ) df.write.saveAsTable( "catalog.schema.table" ) Step 4:Athena から Unity Catalog のデータを参照する 前述の通り、Quick Suiteのデータソースは Athena → Glue Crawler → S3 という構成です。Unity Catalogに移行しても、この構成を維持する必要があります。 実際に行った変更 Glue Crawlerのクロール先を、Unity Catalog外部テーブルのS3パスに変更しました。 Before: 旧 S3 バケット(Hive Metastore 用)→ Glue Crawler → Athena After: Unity Catalog 外部テーブルの S3 バケット → Glue Crawler → Athena 具体的な変更内容: Glue Crawlerのクロール先S3パスをUnity Catalog外部テーブルのパスに変更 GlueのIAMポリシーにS3バケットへのアクセス権限を追加 Terraformで管理しているIAMポリシーとGlueリソースを更新し、 terraform apply 後にGlueコンソールからクローラーを手動実行してテーブルが正しく作成されることを確認しました。 Step 5:Quick Suite のデータセット移行 Step 4でGlue → Athena側の変更が完了したら、Quick Suiteのデータセット参照先を新しいAthenaテーブルに切り替えます。 変更自体はQuick SuiteのUIから接続設定を変更するだけで完結しますが、切り替え前に テーブルスキーマの互換性確認 が重要です。カラム名・データ型が一致していないと、ダッシュボードの集計が壊れます。 移行結果 Before / After 移行の成果をBefore / Afterでまとめます。 Before(Hive Metastore 時代) 項目 状態 テーブルスキーマ確認 Notebookを実行するかコードを読む必要がある データリネージ Mermaidで手作業管理 After(Unity Catalog 移行後) 項目 状態 テーブルスキーマ確認 Unity CatalogのUIでカラム一覧・データ型・説明文を即時参照 データリネージ UI上で自動生成・可視化(Mermaidでの手作業管理が不要に) 特にデータリネージの自動可視化は、Mermaidの維持コストをゼロにしてくれる大きな恩恵でした。Unity CatalogにETLジョブが書き込むと、どのテーブルがどのテーブルから作られているかが自動でグラフとして記録されていきます。 Unity Catalogのデータリネージ画面。どのテーブルがどのテーブルから作られているかが自動でグラフ表示される。 Databricks Managed MCP Unity Catalogへの移行をきっかけに、Databricks Managed MCPも使えるようになったので紹介します。 Databricks Managed MCP とは Databricks Managed MCPとは、DatabricksがホストするMCP(Model Context Protocol)サーバーです。Claude CodeなどのAIエージェントからDatabricksのリソースをツールとして呼び出せるようにする仕組みです。 Databricks Managed MCPはUnity Catalogとの統合を前提に設計 されています。今回の移行後、実際に使えるようになりました。 DBSQL MCP:ETL 開発が変わる Databricks Managed MCPの中でも、ETL開発が中心のデリッシュリサーチにとって特に相性が良さそうだと感じたのが DBSQL MCP です。これはClaude CodeやCursorのMCPとして設定することで、ETL開発中にSQLをその場で実行・確認できるようになるツールです。 提供されるツール: execute_sql_read_only :テーブルの内容・カラム定義・データ分布をその場で確認 execute_sql :SQLの実行 poll_sql_result :長時間クエリの結果をポーリング ※ ツール名や提供機能はアップデートで変更される可能性があります。上記は執筆時点(2026年3月6日)で使用できるツールです。 具体的な開発体験 ETLコードを書きながら、Claude Codeに対して次のような依頼ができるようになります: 「 catalog.schema.table のカラム構成を確認して」 「 product_id カラムのユニーク数を調べて」 「このテーブルとJOINするテーブルのスキーマを見せて」 DBSQL MCPを使うことでCursorやVS CodeなどのエディタからDatabricksのテーブルの中身・スキーマをリアルタイムで確認しながらコーディングができます。 なお、DBSQL MCP以外にもUnity Catalog functionsやGenie spaceなども使えるようになっています。 参照: Databricks Managed MCP 公式ドキュメント MCPの詳細な設定方法や活用例については、 こちらの記事 も参照してください。 まとめ 約40テーブルのHive Metastore → Unity Catalog移行を通じて得た主な成果を3点でまとめます。 データカタログとデータリネージの整備 :Unity CatalogのUIでスキーマ情報を参照でき、データリネージも自動管理されるため、「コードを読まないと分からない」問題が解消されました 外部テーブルで構成互換性を維持 :Athena連携(Glue Crawler)がある場合は外部テーブルを選ぶことで、既存のBI構成に影響を与えずに移行できました Databricks Managed MCPが使えるようになった :ETL開発中にエディタから出ずにテーブル情報を確認できるようになり、開発体験が向上しました 今後は、Unity Catalogへの移行によって使えるようになった機能の検証・活用をしていきたいと考えています。 Unity Catalogへの移行を検討している方の参考になれば幸いです。
アバター
こんにちは @kyo です! 2026年2月21日に開催された Go Conference mini in Sendai 2026 にて、「GoとWasmでつくる軽量ブラウザUI」というタイトルで登壇させていただきました。この記事では、発表中にいただいたフィードバックについて深掘りをして得られた知見をご共有できたらと思います。 フィードバック: 「 (*js.Value).Call は遅いので、 bind したうえで Invoke するといいですよ」 from Hajime Hoshi さん、Go製ゲームエンジン Ebitengine の作者 発表スライド speakerdeck.com 背景 Go の syscall/js パッケージでは、JS のメソッドを呼び出す方法が2つあります。 方法 Go コード 特徴 Call document.Call("getElementById", "myDiv") シンプルだが毎回オーバーヘッドあり bind + Invoke getElementById.Invoke("myDiv") 初期化が必要だが高速 Call が遅い理由 前提知識: Go Wasm の仕組み Go で書いた Wasm コードがブラウザの JS を呼び出すとき、直接呼べるわけではありません。 間に Wasm メモリ と wasm_exec.js (Go 公式提供の橋渡しスクリプト)を挟んでやりとりします。 Wasm メモリ(Linear Memory)とは? Wasm メモリは WebAssembly の仕様で定義された WebAssembly.Memory オブジェクトで、 実体は Go(Wasm)と JavaScript の 両方からアクセスできる巨大なバイト配列 ( ArrayBuffer )です。 「リニアメモリ(Linear Memory)」とも呼ばれます。 developer.mozilla.org wasmbyexample.dev 普通、Go と JS はお互いの変数を直接見ることができませんが、 この共有のバイト配列を「伝言板」のように使うことで、データをやりとりできます。 例: document.Call("getElementById", "myDiv") の場合 Go 側が "getElementById" という文字列をバイト列に変換して Wasm メモリに書き込む JS 側( wasm_exec.js )が Wasm メモリからそのバイト列を読み出す TextDecoder で JS の文字列に変換する(= loadString() ) その文字列を使って document["getElementById"] を探す(= Reflect.get() ) 見つけた関数を実行する Invoke が速い理由は、このステップ 1〜4 を丸ごとスキップできるからです。 事前に関数への参照を取得しておけば、Wasm メモリを経由した文字列のやりとりが不要になります。 Call の処理の流れ Go 側で document.Call("getElementById", "myDiv") を呼ぶと、 wasm_exec.js の以下のコードが実行されます: // wasm_exec.js "syscall/js.valueCall" : ( sp ) => { sp >>>= 0 ; try { const v = loadValue ( sp + 8 ) ; // ① オブジェクトを取得(例: document) const m = Reflect . get ( v , loadString ( sp + 16 )) ; // ② ここが遅い(後述) const args = loadSliceOfValues ( sp + 32 ) ; // ③ 引数を取得(例: "myDiv") const result = Reflect . apply ( m , v , args ) ; // ④ 関数を実行 sp = this. _inst . exports . getsp () >>> 0 ; storeValue ( sp + 56 , result ) ; // ⑤ 結果をメモリに書き戻す this. mem . setUint8 ( sp + 64 , 1 ) ; // ⑥ 成功フラグ } catch ( err ) { // エラー処理... } } , ② が遅い理由には二つの原因があります const m = Reflect . get ( v , loadString ( sp + 16 )) ; // ^^^^^^^^^^^^^^^^^^ ← (A) 文字列デコード // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^← (B) プロパティ検索 (A) loadString() — 文字列デコード const loadString = ( addr ) => { const saddr = getInt64 ( addr + 0 ) ; // Wasmメモリ上の文字列の開始位置 const len = getInt64 ( addr + 8 ) ; // 文字列の長さ(バイト数) return decoder . decode ( // TextDecoder で バイト列 → JS文字列に変換 new DataView ( this. _inst . exports . mem . buffer , saddr , len ) , ) ; } ; Go が Wasm メモリに書き込んだバイト列を、 TextDecoder を使って JavaScript の文字列 "getElementById" に変換しています。 この処理では毎回 new DataView の生成と decoder.decode() が走っています。 (B) Reflect.get() — プロパティ検索 補足: プロパティとプロパティ検索とは? JavaScript のオブジェクトは、名前(キー)と値のペアの集まり です。 この「名前と値のペア」1つ1つを プロパティ と呼びます。 // document オブジェクトのイメージ(実際はもっと多い) document = { "getElementById" : function ( ... ) { ... } , // ← プロパティ "createElement" : function ( ... ) { ... } , // ← プロパティ "querySelector" : function ( ... ) { ... } , // ← プロパティ "title" : "My Page" , // ← プロパティ // ... 他にも数百のプロパティがある } ; プロパティ検索 とは、この中から名前を指定して値を探す処理です。 Go でいえば `map[string]any` から `map["getElementById"]` でキーを探すのに近いイメージです。 // プロパティ検索の例(どれも同じ意味) document .getElementById ; // ドット記法 document [ "getElementById" ] ; // ブラケット記法 Reflect . get ( document , "getElementById" ) ; // Reflect API(wasm_exec.js が使う方法) Reflect . get ( v , "getElementById" ) ; // これは実質的に v["getElementById"] と同じ // = document オブジェクトから "getElementById" という名前の関数を探す JavaScript のオブジェクトからプロパティ名で関数を検索します。 ここの処理でも毎回この探索処理が走ります。 Invoke の処理の流れ 一方、 getElementById.Invoke("myDiv") を呼ぶと // wasm_exec.js "syscall/js.valueInvoke" : ( sp ) => { sp >>>= 0 ; try { const v = loadValue ( sp + 8 ) ; // ① 関数そのものを取得(文字列ではない) const args = loadSliceOfValues ( sp + 16 ) ; // ② 引数を取得 const result = Reflect . apply ( v , undefined , args ) ; // ③ 関数を直接実行 sp = this. _inst . exports . getsp () >>> 0 ; storeValue ( sp + 40 , result ) ; // ④ 結果をメモリに書き戻す this. mem . setUint8 ( sp + 48 , 1 ) ; // ⑤ 成功フラグ } catch ( err ) { // エラー処理... } } , Call との違い loadString() がない → 文字列デコードが不要 Reflect.get() がない → プロパティ検索が不要 v はすでに関数への参照なので、 Reflect.apply() で直接呼ぶだけ 処理の違いまとめ Call の処理: Go → [メソッド名をメモリに書く] → JS: loadString() → Reflect.get() → Reflect.apply() ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~ ~~~~~~~~~~~~~ 毎回発生するオーバーヘッド 文字列デコード プロパティ検索 Invoke の処理: Go → JS: Reflect.apply() 図解 1. Call パターン(毎回のオーバーヘッド) 2. bind + Invoke パターン(初回のみオーバーヘッド) 3. 処理ステップの比較 4. bind が必要な理由 JS ではメソッドをオブジェクトから切り離すと this コンテキストが失われます。 bind で this を固定しないと Invoke 時にエラーになります。 コード例 遅いパターン(毎回 Call ) document := js.Global().Get( "document" ) for i := 0 ; i < 1000 ; i++ { // 毎回: 文字列書き込み → デコード → プロパティ検索 → 実行 element := document.Call( "getElementById" , "myElement" ) element.Call( "setAttribute" , "data-index" , i) } 速いパターン( bind + Invoke ) document := js.Global().Get( "document" ) // 初期化: bind で this を固定 getElementById := document.Get( "getElementById" ).Call( "bind" , document) for i := 0 ; i < 1000 ; i++ { // 毎回: 関数実行のみ(文字列処理・プロパティ検索なし) element := getElementById.Invoke( "myElement" ) // ... } 実用的なパターン: よく使うメソッドをまとめて事前バインド var ( document = js.Global().Get( "document" ) getElementById = document.Get( "getElementById" ).Call( "bind" , document) createElement = document.Get( "createElement" ).Call( "bind" , document) querySelector = document.Get( "querySelector" ).Call( "bind" , document) consoleLog = js.Global().Get( "console" ).Get( "log" ).Call( "bind" , js.Global().Get( "console" )) ) func getElement(id string ) js.Value { return getElementById.Invoke(id) } func newElement(tag string ) js.Value { return createElement.Invoke(tag) } オーバーヘッド比較表 処理 Call bind + Invoke 文字列の Wasm メモリ書き込み 毎回 初回のみ TextDecoder によるデコード 毎回 初回のみ Reflect.get (プロパティ検索) 毎回 初回のみ Reflect.apply (関数呼び出し) 毎回 毎回 makeArgSlices + storeArgs 毎回 毎回 ベンチマーク結果(10,000回呼び出し) 各メソッドを計測した実測結果 DOM操作(実際のJS API) JS API自体の実行コストが含まれるため、相対的な差は小さい。 // Call パターン document.Call( "getElementById" , "myElement" ) // bind+Invoke パターン getElementById := document.Get( "getElementById" ).Call( "bind" , document) getElementById.Invoke( "myElement" ) 対象メソッド Call (ms) bind+Invoke (ms) 差分 速度比 document.getElementById 48.7 46.6 +2.1 ms 1.05倍 console.log 68.3 59.3 +9.0 ms 1.15倍 element.setAttribute 26.8 25.8 +1.0 ms 1.04倍 DOM操作自体のコストが大きいため、Call と bind+Invoke の差は 3〜15% 程度に留まる。 純粋なオーバーヘッド検証 JS側の処理コストを排除し、 Call 固有のオーバーヘッドを可視化。 空の関数(何もしない関数) JS側に何もしない関数を用意し、呼び出しオーバーヘッドだけを測定。 // Call パターン: 毎回「文字列デコード → プロパティ検索 → 関数実行」 noopObj.Call( "noop" ) // bind+Invoke パターン: 事前バインド済みなので「関数実行」のみ noop := noopObj.Get( "noop" ).Call( "bind" , noopObj) noop.Invoke() 対象 Call (ms) bind+Invoke (ms) 差分 速度比 noop 1.90 0.40 +1.50 ms 4.76倍 JS側の処理コストがないため、 Call 固有のオーバーヘッド(文字列デコード + プロパティ検索)が約4〜5倍の差としてはっきり現れる。 メソッド名の長さによる影響 Call は毎回メソッド名を文字列デコードするため、名前が長いほどコストが増えるか検証。 // 短いメソッド名(1文字) obj.Call( "a" ) // 長いメソッド名(30文字) obj.Call( "abcdefghijklmnopqrstuvwxyz1234" ) // bind+Invoke はどちらも同じ(事前バインド済み) fn := obj.Get( "a" ).Call( "bind" , obj) fn.Invoke() 対象 Call (ms) bind+Invoke (ms) 差分 速度比 メソッド名 "a"(1文字) 1.80 0.50 +1.30 ms 3.61倍 メソッド名 "abcdefghij...1234"(30文字) 2.10 0.60 +1.50 ms 3.50倍 メソッド名の長さはほぼ影響しない。 TextDecoder のコストは小さく、Go↔JS間の境界越え自体( valueCall のスタック操作 + Reflect.get )の方がはるかに大きい。(メソッドをたくさん増やしたらもっと差が出るかも) いつ使い分けるか シナリオ 推奨 理由 高頻度呼び出し(60fps 描画、大量 DOM 操作) bind + Invoke オーバーヘッド削減の効果が大きい 低頻度呼び出し(ボタンクリック等) Call でOK 可読性を優先、パフォーマンス差は体感できない 同じメソッドをループで繰り返し呼ぶ bind + Invoke 最もメリットが出るケース まとめ Call は毎回「文字列の Wasm メモリ書き込み → TextDecoder によるデコード → Reflect.get によるプロパティ検索」という3つのオーバーヘッドが発生する bind + Invoke は事前に関数参照を取得・固定しておくことで、これらのオーバーヘッドをすべてスキップし、 Reflect.apply で直接関数を実行できる 純粋なオーバーヘッド比較では約4〜5倍の差があり、高頻度呼び出し(描画ループや大量DOM操作)では効果が大きい 一方、DOM操作自体のコストが大きい場面では差は数%程度に留まるため、低頻度の呼び出しでは Call のシンプルさを優先して良さそう よく使うメソッドを var でまとめて事前バインドしておくのが実用的なパターン 最後に Go Conference mini in Sendai で Hajime Hoshi さんからいただいた「 Call は遅いので bind + Invoke がいいですよ」というフィードバックは、最初は「そういうテクニックがあるんだな」程度の理解でした。しかし実際に wasm_exec.js のソースコードを読んでみると、 Call が遅い理由は単なる「関数呼び出しの方法の違い」ではなく、Go と JavaScript という2つの異なるランタイムが Wasm メモリという共有バイト配列を介してやりとりする仕組みそのものに起因していることがわかりました。 普段 Go を書いているだけでは意識しない「文字列がバイト列として Wasm メモリに書き込まれ、JS 側で TextDecoder によってデコードされる」という一連の流れを知ったことで、Go Wasm が裏側でどれだけの処理をしているのかを実感できました。と同時に、 wasm_exec.js がたった1つのファイルで Go と JS の橋渡しをすべて担っていることに、改めてすごさを感じました。 カンファレンスでのたった一言のフィードバックが、ここまで深い学びにつながるとは思っていませんでした。発表して、フィードバックをもらって、それを深掘りする——このサイクルの価値を改めて実感しています。 参考 golang/go#32591 — syscall/js: performance considerations golang/go#39740 — syscall/js: increase performance of Call, Invoke, and New golang/go#44006 — syscall/js: remove Wrapper type to avoid extreme allocations Go syscall/js ソースコード Go wasm_exec.js ソースコード
アバター
はじめに こんにちは。リテールハブ開発部小売アプリチームの池です。 エンジニアチームのマネージャーになってから、気づけば1年半が経ちました。 この1年半を振り返ると、悩みながら行動を続けてきた時間でした。マネージャーとしてどう行動すべきか日々悩みながら試行錯誤し、周りの支援を借りつつ、自分なりにこれだと思うことを試しては失敗を重ね、走り続けてきました。その中で意識していたのは、ただ失敗を繰り返すだけではなく、そこから得られる学びを積み重ねて次に活かすことです。 この記事では、マネージャーが日々何を考え、どんな判断をしているのかを共有したいと思います。また、失敗談が中心にはなりますが、同時にマネージャーという仕事は「人やチームと向き合う仕事」であり、多くの魅力とやりがいがあることも伝えられたらと思っています。マネージャーに興味があるエンジニアの方、同じ立場で悩んでいるマネージャーの方、あるいはマネージャーが何を考えているか知りたいチームメンバーの方にとって、少しでも参考になれば幸いです。 今回は、自身の振り返りも兼ねて、その中でも特に強く心に残っているマネージャーとしての意思決定に関する学びをピックアップして振り返ります。 背景 私がマネージャーになった当時のチーム状況を説明します。 1年半前の当時、私のチームでは以下の業務を並行して進めていました。 自社でゼロから新規開発している小売アプリの開発 事業譲渡で引き継いだ5つの小売アプリの運用保守 エンジニアは私を含めて3名、デザイナー1名、PdM1名の計5名を中心に、上長の支援を得ながらそれらの業務をこなす必要がありました。 引き継いだアプリそれぞれの全容は十分に把握できておらず、わからないことだらけです。コードから仕組みを読み解きながら運用保守、顧客からの問い合わせ・要望に対応する日々でした。チームも発足して間もなく体制が整っておらず、PdMも入れ替わりで着任したばかり。そこに新規開発も並行して進めていて、カオスな状態でした。 そんな中で、私は初めてマネージャーを担うことになりました。 失敗①:チームの方針を明確に示さなかった 一つ目の失敗は、マネージャーとして方針を明確に示すことの重要性と、それがチームに与える影響の大きさを理解していなかったことです。 その当時、少ない人数のチームで新規開発と既存運用が同時に走り、毎日やることが尽きません。私自身もプレイヤーとして動かないと回りませんでした。「まずは足元の開発を回すこと」が最優先で、開発や調整、障害対応と、自身も一人のプレイヤーとして目の前のタスクを一つずつ処理し、なんとか回すことに必死でした。 一方で、私はマネージャーになったばかりです。方針を示すことの大事さを理解していませんでした。どこを目指すのか、何を優先するのか。そういったことを言語化するという発想自体が薄かったのだと思います。 そして、方針を明確に示さずに全てをうまく回そうという意識のまま、大きな対策を打たずに走り続けていました。その結果、チームはどこに向かっていいかわからない状態になっていきました。 重要でないことに時間を使ってしまう 自律的な判断が難しくなる 方針がないと、迷いながら働くことになります。技術負債をどこまで許容するのか、属人化をどこまで受け入れるのか、ドメイン理解にどれだけ時間をかけるのか、作り込みすぎないラインはどこか、各々の判断のズレが積み重なりチームはさらに忙しくなっていきました。 結果的に「全部をそのままやる」ということが暗黙の方針となり、当然ながら、すべてが中途半端になり目標達成も遠のきます。メンバーの不満も溜まり、私自身も時間で解決しようと夜遅くまで働くことが増えました。疲弊するばかりで状況は良くなりません。 方針が全てを解決するわけではないですが、方針を示さなかったからこそ、余計な忙しさを生んでいたのだと思います。 学び 忙しくても、方針を示すことだけは省いてはいけません。 何を最優先にするのか 何を後回しにするのか どこまでやれば十分か このような方針があるだけで、チームは「何に時間を使うべきか」を考えられるようになります。 今は、「やること/やらないこと」を明確にすることを意識し、忙しい時ほど立ち止まるようにしています。 失敗②:チームを見ずに手法を当てはめた 2つ目の失敗は、解決策から入ってしまったことです。 マネージャーに役割が変わると求められるスキルは変わり、人やチームを動かすスキルが必要になります。しかし当時の私は、その変化を十分に受け止めきれていませんでした。マネージャーとしての理解も引き出しもなく、何をすればいいのかわからない。そんな状態です。 わからないなら学ぶしかないと思い、本や記事を読み、過去の自身の成功体験や他社の成功事例に答えを求め、「これが正解だろう」と思ったものをチームに適用しました。 その一つがスクラム開発の導入です。自分の過去の経験からスクラムをやることでチームが良い方向に進むと、どこかで信じていました。マネジメントに自信が持てない中で、実績のある手法を頼ろうとしていたのだと思います。 スクラム自体は良い手法ですが、そのときのチームのフェーズや状況には合っていませんでした。本来私がやるべきだったのはプロセス改善ではなく、チームの課題を見つけてどう解決するかを考えることです。 スクラムをうまく運用できなかったことにも問題はありますが、チームの課題を見ずに形式的に導入しても効果は限定的になります。その結果、重要ではない会議や作業、議論が増えていきました。 たまたまチームの問題とスクラムの手法がマッチしていた箇所では効果が出たものの、全体としては納得感も高まらず、次第に形骸化して空回りしていき、最終的にはスクラムをやめる判断をしました。 失敗の原因は手法そのものではなく、チームを見ていなかったことでした。 学び まずやるべきことは、手法を探すことではなく、チームの状態を観察して明らかにすることでした。 何が一番のボトルネックなのか どこにエネルギーを割くべきなのか メンバーは何に困っているのか それらを言語化した上で解決策を考えるべきでした。 チームはそれぞれ、プロダクトのフェーズや事業状況も、メンバーの性格・スキルも異なります。当然、課題やボトルネックもチームごとに違います。同じ状況のチームは存在しません。だからこそ、マネジメントにどのチームにも当てはまる画一的な手法はありません。 特に、ベンチャー企業の新規事業で限られたリソースと期間で目標を達成する必要がある環境を踏まえると、何を優先し何を捨てるかの判断は大きく変わってきます。そのためにも、まずはチームを観察し、置かれた状況を把握した上で、行動を考える必要がありました。 一方で、考え過ぎて動けなくなるのもまた問題です。すべてを理解してから動くことはできません。実際には、軽く試し、軽く失敗し、そこから学ぶことも多くあります。行動してみて初めて見えてくる課題もあれば、後から納得感がついてくるケースもあります。 今は、観察しながら動き、チームの反応を見て調整することを意識しています。 失敗③:一度決めた方針を続け過ぎてしまった 3つ目の失敗は方針を見直さなかったことです。 失敗①②を経て、私はチームの状況を見て方針や行動の意思決定を意識するようになりました。 たとえば、初期フェーズで作るものがある程度決まっている状況では、ドメイン理解を一定に留めることや属人化を許容するという判断をしました。その時点では合理的な判断だったと思います。 しかし問題は、その方針を見直すべきタイミングで見直さなかったことでした。 プロダクトの状況は変化し、チームの構成も変わり、メンバーも成長しているのにもかかわらず、その判断だけが更新されないままになっていました。 その結果、以下のような影響が徐々に現れてきました。 技術的な判断の拠り所が持てない場面が増えていく 「自分たちが作っているものは本当に価値があるのか」という空気がチームに漂い始める 属人化が固定して急な休みが取りづらくなる アラート対応の担当者が偏る 変化の兆しには気づいていましたが、対策を打てていない自分もいました。方針を決めた後の運用ができていなかったのです。 学び 方針にもメンテナンスが必要です。定期的に見直さないと現実との乖離が大きくなっていきます。 チームが不健全な状態になっていないか チームの熱量や納得感は下がっていないか このような、出ていたはずのシグナルをしっかりと見逃さず、短いサイクルで方針が実状に合っているかを問い続ける必要があります。 方針は一度決めて終わりではなく、状況の変化に合わせて更新し続けるものだと学びました。 おわりに 本記事では、マネージャーとしての意思決定に関する3つの失敗を振り返ってみました。実際にはもっと多くの失敗をしています。 大事なことは、失敗しないことではないと思っています。 マネージャーの仕事に正解はないと、この1年半で実感しました。完璧な判断を下し続けることはできません。それでも、打席に立ち続けることはできます。迷いながらでも決める。うまくいかなければ振り返って次に活かす。その繰り返しで前に進めると思っています。 一方で、失敗ばかりを書いてきましたが、マネージャーの仕事にはそれ以上のやりがいがあると感じています。一人では到底成し遂げられないことをチームで実現できたときの達成感。エンジニアとは異なる視点やスキルが求められる中で、自分自身が成長していく実感。そして何より、メンバーの成長や変化に向き合いながら、チームが前に進んでいく過程を間近で見られることの面白さ。マネージャーの醍醐味だと思っています。 まだまだ未熟ですが、これからも打席に立ち続け、学び続けていければと思います。
アバター
はじめに こんにちは。リテールハブ開発部の清水です。 先日 SRE Kaigi 2026 に参加してきまして、私の中でSRE熱がかなり高まっています。 私たちはDatadogをオブザーバビリティ基盤として使用しているのですが、私自身はDatadogをまともに触った経験がありませんでした。 Datadogの画面を開くと左のメニューだけでも大量の項目があって、何ができるのか把握すること自体に大きなハードルを感じていました。 そのような中で、Datadog Learning Centerというものを知りました。無料でハンズオン形式の学習ができるとのことだったので、実際にやってみることにしました。 Datadog Learning Centerとは Datadog Learning Center は、Datadogが公式に提供している無料のオンライン学習プラットフォームです。 ブラウザ上でハンズオン形式でDatadogの各機能を実際に操作して学ぶことができます。 コースはGetting Started、APM、Logs、Kubernetes、Securityなどのカテゴリに分類されていて、初心者向けの入門コースから特定のケースにフォーカスしたコースまで幅広く用意されています。 学習環境について ラボという学習環境が提供されるので、ブラウザさえあれば学習できる形になっていました。 ラボの画面では左半分にターミナル、右半分に学習教材が表示されます。 学習用のDatadogアカウントが自動作成されて、IDとパスワードがターミナル上に表示される仕組みです。 これをDatadogのログイン画面に入力すれば、学習用のアカウントで実際のDatadog環境を自由に触ることができます。 自分の本番環境を壊す心配がないので、気軽に色々試せるのが良いところです。 なお、コンテンツはすべて英語です。英語が苦手な方にとってはややハードルが高いかもしれません。 最初に取り組んだコースについて Datadog Learning Centerの コース一覧画面 を開くと多数のコースがずらりと並んでおり、圧倒されます。 画面上部に LEARNING PATHS というリンクがあるので、ここを開くと目的別に整理された学習順序が紹介されています。 私はこの画面で紹介されている Core Skills Learning Path から始めました。 以下の6コースで構成されており、Datadogの基本的な操作スキルを身につけるための入門パスです。 Datadog Quick Start Tagging Best Practices Getting Started with Metrics Getting Started with Monitors Introduction to Dashboards Getting Started with Notebooks 所要時間 じっくり内容を確認しながら進めて、各コースで30分~1時間程度、合計4時間で終わりました。 学習した感想 Datadogに対する恐怖感が薄れた 一番大きな収穫は、Datadogでざっくりどんなことができるのかわかったことです。 学習前はDatadogの画面を開いても、左メニューに並ぶ項目が何を意味しているのかわからず、触ること自体に抵抗がありました。 学習後は、メトリクス、ダッシュボード、モニター、ログ、APM、トレース、Software Catalog、ノートブックといった用語がそれぞれ何を指しているのか、なんとなく掴めるようになりました。 正直なところ、すべてを完璧に理解できたわけではありません。ただ、「何がわからないかがわからない」状態から「各機能の役割はわかった上で、詳細はこれから深掘りしていけばいい」という状態になれたのは大きな進歩でした。 今回学習したCore Skills Learning Pathはよく使う部分をざっと紹介してもらうような内容となっており、他のコースで実践的な内容を学んでいけるのではないかと思います。 学習用Datadogアカウントの中身を見るだけで勉強になった 学習用Datadogアカウントには、架空のWebサービスを題材として本番さながらの設定やデータが最初から用意されています。 AIに「これはどんな機能ですか?」と会話しながら各画面を見ていくと各種機能について素早く理解が進むと感じました。 特にダッシュボードの作り方がすぐ真似できる要素があって参考になりました。 元々、私たちはテナントごとに別々のダッシュボードを用意していて、それぞれのダッシュボードの中でも各種リソース状況がフラットに並んでいるだけの状態でした。 学習用Datadogアカウントのダッシュボードから以下の内容を真似することにしました: ダッシュボードを全テナント共通にして、テンプレート変数で切り替える 画面上部にはリンク集とMonitor Summaryを設置する 画面要素をGroupウィジェットでグループ分けして表示する 改善前と見比べると、情報の整理や視認性がだいぶ向上したのではないかと思います。同じようにダッシュボードの構成に悩んでいる方の参考になれば幸いです。 おわりに Core Skills Learning Pathは、Datadogを初めて触る人にとって良いスタート地点でした。4時間程度の投資で、Datadogの主要機能の全体像と基本操作を一通り学ぶことができます。 Learning Centerにはこのパス以外にも、さまざまな機能に特化したコースが多数用意されています。今後も引き続き他のコースに取り組み、Datadogの活用の幅を広げていきたいと考えています。 Datadogを使っているけれどちゃんと学んだことがないという方は、ぜひCore Skills Learning Pathから始めてみてはいかがでしょうか。
アバター
はじめに こんにちは、リテールハブ開発部でバックエンドエンジニアをしているホシと申します。 現在、Laravel などを利用しながら小売アプリ開発に取り組んでいます。 先日、サービスのリリースに伴い、旧サービスの外部システムから当社のMySQL DBへユーザーデータ移行を行う機会がありました。 ただ今回、今まで行ったデータ移行と大きく違うのは、ユーザーの個人情報を含んだデータ移行でした。 データ移行自体はこれまでも経験していましたが、個人情報を含む移行は前提が異なり、多くの学びと反省点がありました。 そこで本記事では以下の点についてお話できればと思います。 実データを自由に扱えない状況での事前準備の進め方 リハーサルの重要性と、実施できる場合の考え方 今回採用した移行方式と「変換できたのに正しく入っていない」問題と確認点 移行当日に意識しておくべき判断と心構え 次回のデータ移行に向けた改善点 それぞれの中で経験したことや反省点を記載しながら、今後に向けた改善点などを記載しています。 また、個別の移行内容というよりは、データ移行を進める際の準備・検証・判断の考え方に焦点を当てています。 1. 実データを自由に扱えない状況での事前準備の進め方 個人情報を含むデータは「自由に触れない」 個人情報が含まれる場合の制約 開発環境・検証環境に簡単に持ち込めない ログ出力、スクリーンショット、共有方法にも制限がある データ配置・保管場所にも慎重な判断が必要 事前確認が十分にできない 「本番で初めて見るデータ」が発生しがち など通常とは異なる制約がある中で行う必要がありました。 この部分をもっと考慮した上で、事前準備が必要であったところは大きな反省点でした。 「できないから仕方ない」で済ませると危険 事前にできることはできる限りやり、仕様書・IF定義だけで満足しないことが大事です。 とはいえ対応できる範囲も限界はあるので以下の点を特に注意できればと思っています。 NULL / 空文字 / 不正値の扱いは各項目でどうなっているのかを明確にする。 桁数・文字種・フォーマットのばらつきがあることを前提とした考慮をする。 仕様上はOKでも、実データでは違う可能性を考える。 (ここは正直難しいですが、データパターンを確認し、できる限り考慮したいです。) あるあるの話ですが、通常は仕様に基づいて対応するのが当たり前ですが、 こういった大量データになるとおかしなデータはほぼ混ざっているのが逆に普通かと思っています。 2. リハーサルの重要性と、実施できる場合の考え方 今回は事前にリハーサルが実施できる状況でした。 しかし、以下の点でしか検証を行えていませんでした。 データ移行手順が問題ないこと(スムーズに移行作業が完了できる) 個人情報データの取り扱いに問題がないこと データ変換がエラーなく行われ、正常にDBへ投入できること(想定通りのフォーマットであること) 実際に移行したデータを使用して簡単な動作確認を実施し問題ないこと 上記はどれもリハーサルで行う必要な項目で、どれも大事なのですが、 ただ、本番データを想定した観点としては以下の点でもっと時間をかけて行うべきでした。 DBに入った内容に想定していないデータが入っていないか データパターンを各項目で洗い出し、システム上問題ない値であること 想定していないパターンの場合、どう対処するかを事前に明確にすること しかし、これも個人情報のデータとなると、あまり時間をかけてのリハーサルや ローカルに保存して後ほど詳細に確認もしにくいのもあり、簡単な確認で済ませてしまっていました。 元データを最大限に利用する 一番確実なのはやはり元データを使用できることです。 当日のデータ移行の検証という意味では、これを利用した検証が一番確実だと思います。 リハーサル当日だけで出来ない部分は、別日に詳細にできればと良いと思います。 しかし、今回のような個人情報データは自由に扱えないことを理由にうまく利用できていなかったのも反省点です。 いかにここでそのファイルを活用したダミーデータを作れるかも非常に有効だったのではと思っています。 3. 今回採用した移行方式と「変換できたのに正しく入っていない」問題と確認点 どうやって移行を行おうとしたか 今回は以下の方法で行いました。 移行元データ ↓ phpでデータを読み込み、MySQLのLoadData用TSVファイルにコンバートするコードを作成 ↓ Load DataでTSVデータをDBへ投入 ↓ データ移行完了 特別な処理は特になく、シンプルな変換処理でした。 「変換できたのに正しく入っていない」問題と確認点 コンバートまでは特に問題なく変換できて、件数・エラーチェックは行えていましたが、 以下の点が見落としポイントでした。 データ投入できていて、かつ全件「自動変換」など発生なく投入できているか? Load時にWarningなどが発生していないか?(投入はできていても実は正常に入っていないケースがある) カラム定義上は問題なく入っているが、値が想定していないものではないか? Load Data はデータ投入時に非常に便利ですが、上記の点をしっかりと考慮した上で使用しないとデータ移行時の確認内容としては不十分になってしまいます。 特に日付はNULLの場合と「0000-00-00」で入る場合で全く挙動が異なります。 MySQLでは「0000-00-00」はNULLではなく「値」として扱われます。 そのためアプリケーション側では未設定ではなく異常値として存在してしまうことになり、 日付計算・バリデーション・ORM変換で後から問題を引き起こす可能性があるので特に注意が必要です。 今回の移行処理の責務を分離して考える 本来は以下のようにそれぞれのフェーズの責務を明確にして、対応をしていく必要がありました。 フェーズ 役割 保証すること 保証できないこと コンバーター 生データをLoad Data用形式へ変換 TSV構造・文字コード・基本的な値変換 DBがどう解釈するか、業務的な正しさ Load Data DBへ高速に一括投入 指定件数の取り込み、物理的な格納 型変換の影響、暗黙変換、Warningの発生 DB格納後の確認 実データの妥当性検証 業務的に正しい値であることの確認 この確認を行わないと移行成功とは言えない 特に今回は、Load Data の部分とDB格納後の確認が混在した確認になっていたと反省しています。 実際のデータパターンの検証例 パターンは膨大にあるようで、実はシンプルなものをいくつか用意しておくだけでも違うと思っています。 -- 不正日付確認 SELECT COUNT (*) FROM users WHERE birthday = ' 0000-00-00 ' ; -- NULL発生率確認 SELECT COUNT (*), SUM (email IS NULL ) FROM users; -- 想定外形式確認(都道府県以外のパターン確認) SELECT prefecture, COUNT (*) FROM users GROUP BY prefecture; AIを活用したパターン洗い出し・事前テストの試み 今回、AIをうまく使いこなせていなかった点も反省点だったと思っています。 まさにAIをうまく活用できる点として、 前述したデータパターンを様々な観点で出すことができる 個人情報部分は適当なパターンデータに変換して検証することもできる 当時は本番データをそのまま使用できないことで、ちょっとしたサンプルデータ、パターンデータで行い、 十分な事前検証も不足していました。 4. 移行当日に意識しておくべき判断と心構え 今までいくつか事前準備と慎重な検証が必要と記載をしていますが、 当日は必ず想定外が起きると思って計画を立てた方が良いかと思っています。 特に膨大なデータや内容が多いほど顕著になるかと思います。 以下の点に注意して当日は望めるようにしておきたいです。 1つ1つの作業完了条件を明確にしておくこと。 ロジックや仕様変更をその場でする修正は原則しないこと。 移行を中止、延期する、または後日対応でOKかなどの判断が当日できる人がいること。 移行がもし出来ない場合のリカバリ方法があること。 また、時間も想定しているよりも掛かってしまうことが多いと思います。 なるべく余裕をもった計画にしたいです。 今回は投入データの件数確認でさえ、すぐに終わるつもりが件数の正解を事前に用意していなかったため、 非常に大きく時間をかけてしまったのも今回の反省点でした。(完了条件が明確になっていない) 5. 次回のデータ移行に向けた改善点 今回の経験から以下の点を意識し改善できるようにしていきたいです。 一連の手順、動作確認は事前リハーサルでできる限り行うこと。 本番当日に向けた事前準備はしっかり時間を確保して対応する。(当日は必ず何か起きる前提) 当日の作業の完了基準の明確化(データ件数、エラー有無、動作確認など) Load Dataなどデータ投入後の部分が一番大事かつ明確にする部分であること。 仕様通りに作成しても、不正データは必ずある前提で行い、それらも考慮すること。 個人情報を扱うデータの場合は、同等のダミーデータを用意できると検証時に非常に有効。 検証パターン、データ生成などはAIも活用し、検証精度やコスト減に役立てる。 6. まとめ 通常、データ移行は当日一度きりの作業で、 同じ内容で再度実施するケースは少なく、個別のノウハウは蓄積しにくいところもありますが、 共通の考え方、対応として、 事前準備の重要性 検証設計の明確化 当日の判断基準 何かあった時のリカバリ方法 これらを意識することで、当日の障害確率は下げられると思っています。 改めて今回を通じて、データ移行は技術作業というより、検証設計と判断設計の仕事だったと感じています。 正直、完璧に準備をして全てスムーズに完了させることはなかなか難しいところですが、 もし今後データ移行などの作業をする際に本記事が少しでも参考になれば幸いです。 最後までお読みいただきありがとうございました。
アバター
Go の JSON Schema ライブラリたちはどのように JSON Schema を表現しているか 目次 はじめに JSON Schema について ライブラリごとの JSON Schemaの表現の比較 google/jsonschema-go invopop/jsonschema santhosh-tekuri/jsonschema ianlancetaylor/jsonschema 横断比較 まとめ はじめに こんにちは、開発本部開発1部の あかがわまさとも です。 2026年2月21日に開催された Go Conference mini in Sendai 2026 にて、「google/jsonschema-goのこれまでとこれから」というタイトルで登壇させていただきました。本記事では、調査の過程で行った、Go の JSON Schema ライブラリたちが、それぞれどのように JSON Schema を表現しているかの比較について述べます。 当日の発表では、JSON Schemaのおさらいから、 google/jsonschema-go の登場背景、機能についてお話ししました。よければご覧ください。 JSON Schema について JSON Schema は、JSONデータの構造や型、制約を定義するための言語です。JSON Schema それ自体もJSONで書かれます。 { " type ": " object ", " properties ": { " name ": { " type ": " string " } , " age ": { " type ": " integer ", " minimum ": 0 } } , " required ": [ " name " ] } 上に挙げた例はシンプルに見えますが、JSON Schema を Go のデータ構造で表現しようとすると、いくつかの難しさがあります。 キーワードの多さ : JSON Schema のキーワードは type 、 properties 、 items 、 minimum 、 maximum 、 pattern 、 allOf 、 anyOf 、 $ref など非常に多岐にわたります。これらをまとめて Go の型、例えば構造体として表現するには、多くのフィールドを用意する必要があります。そして、付随する機能たちの実装も比例して大きくなります。 バージョン間のキーワード差分 : JSON Schema にはいくつかのドラフトバージョンがあり、現在は Draft 2020-12 が最新です。バージョン間ではキーワード名やセマンティクスが変わることがあります。例えば、スキーマ定義の格納先は Draft 7 では definitions ですが Draft 2020-12 では $defs です。タプルのバリデーションは Draft 7 では items (配列形式)ですが Draft 2020-12 では prefixItems に変わりました。同じデータ構造で複数バージョンをどう扱うかは設計上の判断が分かれるところです。 $ref による参照 : JSON Schema ではスキーマ同士が $ref で参照し合います。この参照を Go のデータ構造上でどう保持するかも、ライブラリの設計思想が現れるポイントです。 ライブラリごとの JSON Schemaの表現の比較 今回は著名さと自分の興味から、以下の4つを比較対象としました。 google/jsonschema-go reference 元々 Go の公式 MCP SDK 内で作られていた、google が開発している JSON Schema ライブラリ JSON Schema バージョンは Draft 2020-12, Draft 7 をサポート invopop/jsonschema reference Inference(Goの型からのスキーマ生成)に強みを持つ JSON Schema バージョンは Draft 2020-12 をサポート santhosh-tekuri/jsonschema reference 2017年にv1がリリースされるなど歴史が長く、現在v6まで出ている JSON Schema バージョンは最新を含むほぼ全てをサポート ianlancetaylor/jsonschema reference Go の 元コアコミッターである、Ian Lance Taylor 氏が実装したライブラリ JSON Schema バージョンは Draft 2020-12, Draft 2019-09, Draft 7 をサポート では、各ライブラリを見ていきましょう。 google/jsonschema-go 約70個のフィールドを持つ Schema 構造体を使用しています。JSON Schema のキーワードが1対1でフィールドにマッピングされており、 Extra map[string]any で非標準のキーワードも格納できます。 type Schema struct { Type string Properties map [ string ]*Schema Items *Schema Required [] string Minimum * float64 Maximum * float64 // ... 約70フィールド Extra map [ string ]any } Draft 7 と Draft 2020-12 のバージョン差分は、単一の構造体の中にバージョン固有のフィールドを両方持つことで吸収しています。例えば、Draft 7 の definitions と Draft 2020-12 の $defs に対応する Definitions と Defs が共存しています。 type Schema struct { // draft 7 Definitions map [ string ]*Schema // "definitions" ItemsArray []*Schema // "items" (配列形式) DependencySchemas map [ string ]*Schema // "dependencies" (スキーマ) // draft 2020-12 Defs map [ string ]*Schema // "$defs" PrefixItems []*Schema // "prefixItems" DependentSchemas map [ string ]*Schema // "dependentSchemas" // ... } Marshal/Unmarshal 時に $schema の値からドラフトを判定し、適切なフィールドへの振り分けやキーワードの出し分けを行います。 素朴かつ全てをサポートした巨大な構造体なので、構造体リテラルでの手動構築、 json.Unmarshal によるパース、 For[T]() による Go の型からの推論と、スキーマの構築方法を幅広くサポートしています。 $ref の解決は明示的な Resolve() メソッドで行い、スキーマのバリデーションにも対応しています。 invopop/jsonschema 約60個のフィールドを持つ Schema 構造体を採用しています。 Properties には orderedmap.OrderedMap[string, *Schema] を使用しており、順序も含めて管理することができます。 type Schema struct { Type string `json:"type,omitempty"` Properties *orderedmap.OrderedMap[ string , *Schema] `json:"properties,omitempty"` Items *Schema `json:"items,omitempty"` Required [] string `json:"required,omitempty"` // ... 約60フィールド Extras map [ string ]any `json:"-"` } Draft 2020-12 のみをサポートしており、バージョン差分の吸収は行いません。フィールドは $defs 、 prefixItems など 2020-12 のキーワードに対応しています。 google/jsonschema-go と同じく素朴な構造体ですが、フィールドの型に違いがあります。数値制約( minimum など)には *float64 ではなく json.Number を、整数値キーワード( maxLength 、 minLength 、 maxItems など)には *int ではなく *uint64 を使用しており、JSON の元表現の保持や、負の数の型レベルでの排除を意識した設計です。 一方で、Draft 2020-12 のキーワードでも 現時点では unevaluatedItems 、 unevaluatedProperties 、 $dynamicAnchor 、 $vocabulary には対応していません。 こちらはスキーマ 生成 に特化したライブラリです。主要な構築方法は Reflector を使った Go の型からの推論で、構造体タグで title 、 minLength 、 enum などを細かく制御できます。一方、 $ref の解決やバリデーション機能は提供していません。 santhosh-tekuri/jsonschema Schema 構造体を使います。他のライブラリと異なる点が2つあります。 1つ目は、数値制約に *big.Rat を使っており、浮動小数点誤差を回避していることです。2つ目は、 $ref が文字列ではなく、コンパイル時に解決された *Schema ポインタとして保持されることです。 Compiler が JSON をパースしてスキーマを構築する際に、参照の解決も同時に行われます。 type Schema struct { Ref *Schema // 解決済みの直接ポインタ Types *Types Minimum *big.Rat Maximum *big.Rat Properties map [ string ]*Schema // ... } バージョン差分は Draft 型で管理されます。各 Draft はキーワードの集合やサブスキーマの場所、 $id のフィールド名(Draft 4 は id 、Draft 6 以降は $id )などをまとめて管理しており、Draft 4 から Draft 2020-12 まで、ほぼすべてのバージョンをサポートしています。 Compiler が $schema キーワードからドラフトを自動検出し、異なるドラフトのスキーマを混在させることも可能です。 var ( Draft4 *Draft Draft6 *Draft Draft7 *Draft Draft2019 *Draft // draft 2019-09 Draft2020 *Draft // draft 2020-12 ) スキーマの構築は Compiler 経由のみで、プログラマティックな組み立てや Go の型からの推論は提供されていません。バリデーションがこのライブラリの主機能で、JSON Schema 仕様に準拠した複数のエラー出力フォーマットに対応しています。 ianlancetaylor/jsonschema 他の3つとは全く異なるアプローチを取っています。固定フィールドの構造体でも map[string]any でもなく、キーワードと型付き値のペアのリストで表現します。 type Schema struct { Parts []Part } type Part struct { Keyword *Keyword Value PartValue // 12種類の型付きユニオン } PartValue は PartString 、 PartInt 、 PartSchema 、 PartSchemas など12種類の型からなる型付きユニオンです。 この設計はバージョン差分の吸収と密接に結びついています。ドラフトごとに別の Go パッケージが用意され、各パッケージが独自の Vocabulary を登録します。 Vocabulary はドラフトの名前、 $schema の URI、キーワードの集合、参照解決関数などをまとめた型です。 type Vocabulary struct { Name string // "draft2020-12" など Schema string // $schema の URI Keywords map [ string ]*Keyword // キーワード名 → 定義 Resolve func (*Schema, *ResolveOpts) error Cmp func ( string , string ) int } type Keyword struct { Name string ArgType ArgType // PartString, PartSchema など期待する値の型 Validate func (arg PartValue, instance any, state *ValidationState) error } 各ドラフトパッケージは init() で自身の Vocabulary をグローバルレジストリに登録します。キーワードは JSON の定義ファイルからコード生成されるため、ドラフト間の条件分岐が一切ありません。スキーマの構造がバージョンに依存せず、異なるドラフトバージョンは単に異なるキーワードセットを登録するだけでよくなります。ただし、結局キーワードセットは巨大になるので、そう言う意味では他のとあまり変わらない、とも言えるかもしれません。 // ドラフトごとに別パッケージ import "github.com/ianlancetaylor/jsonschema/draft202012" import "github.com/ianlancetaylor/jsonschema/draft7" 内部表現がキーワードリストであるため、構築 API も独特です。構造体リテラルではなく、ドラフトバージョン別パッケージが提供する API を使って Builder パターンでスキーマを組み立てます。JSON のパースや Infer[T]() による Go の型からの推論にも対応しています。 $ref の解決はアンマーシャル時に自動で行われ、バリデーションもサポートされています。 横断比較 ライブラリ 内部表現 スキーマ構築 バージョン差分 参照解決 バリデーション google/jsonschema-go struct(両バージョンのフィールド共存) struct リテラル / JSON / For[T]() 単一structに両バージョン 明示的 Resolve() あり invopop/jsonschema struct struct リテラル / JSON / Reflector (主力) Draft 2020-12 のみ 生成のみ なし santhosh-tekuri/jsonschema struct( *big.Rat ) Compiler 経由のみ Draft 型 + 自動検出 コンパイル時にポインタ解決 あり(主機能) ianlancetaylor/jsonschema []Part Builder / JSON / Infer[T]() ドラフト別パッケージ アンマーシャル時に自動 あり まとめ 4つのライブラリを比較してみると、JSON Schema の表現方法にはいくつかのアプローチがあることがわかります。 google/jsonschema-go と invopop/jsonschema は、JSON Schema のキーワードを構造体のフィールドに直接マッピングする素朴なアプローチです。google/jsonschema-go は両バージョンのフィールドを共存させてバージョン差分を吸収し、invopop/jsonschema は Draft 2020-12 に絞ることでシンプルさを保っています santhosh-tekuri/jsonschema も構造体ベースですが、コンパイル時の $ref のポインタ解決など、バリデーション用途に最適化された表現を選んでいます ianlancetaylor/jsonschema は、キーワードとペアのリストという独自の表現で、バージョン差分をデータ構造レベルで吸収する大胆な設計です 同じ「Go で JSON Schema を扱う」という目的でも、内部表現の選択がバージョン対応や API 設計にまで影響を及ぼしていることが面白かったです。この記事を通して、Go で JSON Schemaを扱うことに興味を持っていただければ幸いです。
アバター
はじめに こんにちは!デリッシュキッチンで主にバックエンドの開発を担当している秋山です。 最近GitHub公式ブログで発表された GitHub Agentic Workflows というツールを知り、使い心地が気になったので試してみました。本記事では、CI/CDパイプラインにAIエージェントを組み込んで、テスト失敗時の原因調査からIssue作成までを自動化するワークフローを試しに構築した体験を紹介します。 目次 はじめに GitHub Agentic Workflowsとは 今回試してみたこと セットアップ 実際に作ったもの テスト対象のGoコード GitHub Agentic Workflowsを使ったワークフロー定義 コンパイル 結果 GitHub Agentic Workflowsのセキュリティ設計 Safe Outputs 脅威検出(Threat Detection) 豊富なデザインパターン まとめ ご参考 GitHub Agentic Workflowsとは GitHub Agentic Workflows は、ワークフローを自然言語で定義し、コーディングエージェントに実行させる仕組みです。従来のGitHub ActionsではYAMLでワークフローを記述していましたが、GitHub Agentic Workflowsでは マークダウン形式 で自然言語を使ってワークフローを定義できます。定義したマークダウンは gh aw compile コマンドでGitHub Actions用のYAMLにコンパイルされ、GitHub Actions上で実行されます。 主な特徴は以下の通りです。 マークダウンベースのワークフロー定義 : YAMLの代わりにマークダウンで自動化を記述。直感的で柔軟 選択可能なAIエンジン : GitHub Copilot、Claude、OpenAI Codexなど、使用するコーディングエージェントをワークフローごとに選択可能 セキュリティ重視の設計 : デフォルトで読み取り専用権限。書き込み操作には明示的な許可が必要。さらに脅威検出による自動セキュリティ分析も実行される ※Agentic Workflowsは2026年2月現在 テクニカルプレビュー段階 にあり、今後大きな変更が入る可能性があります。 今回試してみたこと GitHub Agentic Workflowsを使って、 CI上の自動テスト失敗時に原因を調査し、Issueを作成してくれるワークフロー を構築しました。Claude Codeに手伝ってもらいつつ、初回ワークフロー実行まで約30分で完了しました。 セットアップ まずは拡張機能をインストールし、initで初期設定を行います。 # Agentic Workflows拡張機能のインストール gh extension install github/gh-aw # Agentic Workflowsのワークフローの初期化 gh aw init 実際に作ったもの テスト対象のGoコード まず、検証のために 意図的にデータレースのリスクがあるGoコード を用意しました。 // cache.go package main type Cache struct { data map [ string ] int } func NewCache() *Cache { return &Cache{data: make ( map [ string ] int )} } func (c *Cache) Set(key string , val int ) { c.data[key] = val } func (c *Cache) Get(key string ) int { return c.data[key] } // Increment increments the value for key by 1. func (c *Cache) Increment(key string ) { c.data[key]++ } sync.Mutex を使わず map[string]int に直接アクセスしているため、並行アクセス時にデータレースが発生します。 また、テストでは100個のgoroutineから同時にIncrementを呼び出します。 // cache_test.go func TestCacheConcurrentIncrement(t *testing.T) { c := NewCache() c.Set( "counter" , 0 ) var wg sync.WaitGroup const workers = 100 for i := 0 ; i < workers; i++ { wg.Add( 1 ) go func () { defer wg.Done() c.Increment( "counter" ) }() } wg.Wait() got := c.Get( "counter" ) if got != workers { t.Errorf( "expected counter=%d, got %d (lost %d increments due to race)" , workers, got, workers-got) } } 自動テストワークフロー( test.yml )の定義です。 # .github/workflows/test.yml name : Tests on : push : branches : [ "**" ] pull_request : jobs : test : runs-on : ubuntu-latest steps : - uses : actions/checkout@v4 - uses : actions/setup-go@v5 with : go-version : "1.23" - name : Run tests (race detector + repeat) run : go test -v -race -count=5 -timeout 120s ./... GitHub Agentic Workflowsを使ったワークフロー定義 続いてGitHub Agentic Workflowsを使ったワークフローの定義です。 定義は マークダウン で書きます。 Front Matterでワークフローの細かい設定ができます(engineなどの部分です)。 メインの内容に、自然言語でワークフローの定義をしています。今回は英語で書いてますが、日本語でも問題ないと思います。 --- engine: copilot on: workflow_run: workflows: ["Tests"] types: [completed] branches: - main permissions: contents: read actions: read safe-outputs: create-issue: ~ --- # CI Doctor Automatically investigate failures in the "Tests" workflow and create a GitHub Issue with a diagnostic report. ## Instructions When triggered, check if the "Tests" workflow run that just completed has a conclusion of `failure`. If it succeeded, do nothing. For failed runs: 1. **Fetch the workflow run logs** for the failed "Tests" run using the GitHub Actions API. 2. **Classify the failure type** by scanning the logs: - `DATA RACE` → **Race condition** - `too long` or `took too long` → **Time-dependent flakiness** - `panic:` → **Panic / crash** - `FAIL` with a specific test name → **Assertion failure** 3. **Identify the affected tests** by extracting lines matching: - `--- FAIL: TestXxx` - `FAIL\tgithub.com/...` 4. **Extract the root cause evidence** — copy the relevant log lines verbatim as a code block. 5. **Create a GitHub Issue** with the following structure: **Title**: `CI Failure: <TestName> — <failure type>` **Body**: - Summary - Failed Test(s) - Failure Type - Log Evidence - Root Cause Analysis - Suggested Fix Add labels: `bug`, `flaky-test`. Front Matterの設定について補足します。 engine: copilot でAIエンジンを指定。Copilot以外にもClaudeやCodexを選択でき、使用するモデルの指定も可能( エンジン設定リファレンス ) workflow_run トリガーでTestsワークフローの完了を監視 permissions で contents: read と actions: read のみに権限を限定 safe-outputs に create-issue を設定し、Issue作成のみを許可。それ以外の書き込み操作は行えない コンパイル GitHub Agentic Workflowsのワークフロー( .md )は、 gh aw compile コマンドで .lock.yml にコンパイルされます。 gh aw compile 検証ではci-doctorという名前でワークフローを作っていたので、 .github/workflows/ci-doctor.lock.yml が生成されました。このYAMLファイルが実際にGitHub Actionsで実行されるワークフローのようです。 これでワークフローの準備は完了です。 結果 Testsワークフローが失敗すると、定義したワークフローが自動的に起動しました。 GitHub Agentic Workflowsで定義したワークフロー また、以下のようなIssueが作成されました。 自動作成されたissue bug と flaky-test ラベルも自動で付与されています。 また、問題の説明や具体的な修正案まで提示してくれました。 今回は簡単な例でしたが、より複雑なケースでどうなるのかは今後確認していきたいです。 GitHub Agentic Workflowsのセキュリティ設計 AIエージェントにCI/CDパイプライン上で作業させるとなると、気になるのがセキュリティです。GitHub Agentic Workflowsはこの点について手厚い設計がされています。 Safe Outputs GitHub Agentic Workflowsのワークフローは デフォルトで読み取り専用権限 で実行されます。書き込み操作(Issue作成、PR作成など)を行うには safe-outputs で明示的に許可する必要があります。 safe-outputs : create-issue : ~ # Issue作成を許可 create-pull-request : ~ # PR作成を許可 ワークフローごとに許可する操作を限定できるため、「このワークフローはIssue作成だけ」「このワークフローはPR作成まで」といった細かい権限管理が可能です。 脅威検出(Threat Detection) safe outputsが設定されている場合、GitHub Agentic Workflowsは自動的に 脅威検出 を実行します。これはエージェントの出力が実際にGitHubに書き込まれる前に、セキュリティ分析を行う追加のレイヤーです。デフォルトでは有効になっています。 検出フローは以下の順序で動作します。 エージェントジョブ (読み取り専用権限で実行) 脅威検出ジョブ (エージェント出力のセキュリティ分析) Safe Outputsジョブ (安全確認後に書き込み権限で実行) プロンプトインジェクション、シークレットの漏洩、悪意のある変更を検出します。 脅威が検出された場合、ワークフローは失敗し、safe outputsジョブはブロックされます。 詳細は 脅威検出リファレンス を参照してください。 豊富なデザインパターン GitHub Agentic Workflowsのリファレンスには具体的なデザインパターン/使用例がいくつも載っています。 その中で僕が気になったのは MultiRepoOpsパターン という、複数リポジトリを横断した自動化パターンです。 例えば、PRD(プロダクト要求仕様書)を別リポジトリで管理しているケースでは、PRDの変更に連動して開発リポ側にIssueを作成したり、実装状況をPRDリポ側にフィードバックしたりといった連携ができるようになるのではないかと思いました。 まとめ GitHub Agentic Workflowsを使って、ワークフローを作成しました。 リファレンスには今回紹介した他にも細かい設定やユースケースが書かれています。 気になった方はぜひ 公式ドキュメント を見て、試してみてください。 ご参考 GitHub Agentic Workflowsを発表 - リポジトリの自動化を実現 - GitHubブログ Blog | GitHub Agentic Workflows About Workflows | GitHub Agentic Workflows GitHub - github/gh-aw: GitHub Agentic Workflows
アバター
開発2部の内原です。 Goは静的型付けで事前コンパイルされる言語なので、WebAssembly(WASM)にコンパイルしておけば、JavaScriptのJust-In-Time(JIT)コンパイルより速度的に有利であるように思えます。 なんとなくGoをWASMにすればJSより速くなるくらいのふわっとした認識でいましたが、果たしてどのような実装でも速くなるのかそうでないのか、速くなるとしたらどれくらいの差が出るのか、という疑問を持ったので調べてみました。 そこで、いくつかのアルゴリズムで実際にベンチマークを取って検証してみましたが、アルゴリズムの特性によって結果が様々であることがわかりました。 事前準備 実行環境 MacOS 26.2 Go 1.25 Node.js v25 Chrome (144) Go WASM のビルドと関数公開 Go側の関数公開は以下のように js.FuncOf でラップしてグローバルに登録します。 import "syscall/js" func main() { js.Global().Set( "goAdd" , js.FuncOf( func (this js.Value, args []js.Value) any { n1 := args[ 0 ].Int() n2 := args[ 1 ].Int() return add(n) })) select {} } Go側では syscall/js パッケージを使って関数をグローバルに公開し、以下のコマンドでWASMバイナリをビルドします。 $ GOOS=js GOARCH=wasm go build -o main.wasm main.go $ ls -lh main.wasm -rwxr-xr-x@ 1 uchihara staff 2.1M Feb 11 16:00 main.wasm 生成されるWASMバイナリのサイズは約2MBです。Goランタイムが含まれるため、それなりのサイズになります。 JS側では WebAssembly.instantiateStreaming でWASMをロードし、 go.run(instance) を呼ぶと、上記で登録した関数がグローバルから呼び出せるようになります。 const go = new Go () ; const { instance } = await WebAssembly . instantiateStreaming ( fetch ( "main.wasm" ) , go . importObject ) ; go . run ( instance ) ; const r = goAdd ( 1 , 2 ) ; 計測方法 ブラウザ版とCLI版(Node.js)の両方で計測(ただし一部を除いて性能差はさほど出なかった) 各テストは複数回計測の平均を採用 ベンチマーク関数は以下です。 function bench ( fn , args , iters ) { const times = [] ; for ( let i = 0 ; i < iters ; i ++ ) { const start = performance . now () ; fn ( ... args ) ; const end = performance . now () ; times . push ( end - start ) ; } return times . reduce (( a , b ) => a + b , 0 ) / times . length ; } フィボナッチ関数 まずはシンプルなフィボナッチ数列の計算で比較しました。その際、関数呼び出しのオーバーヘッドが性能に影響を与える可能性があると考えたため、再帰版とループ版の2パターンで確認します。 Go側とJS側でほぼ同一のロジックを実装しています。 func goFibRecursive(n int ) int { if n <= 1 { return n } return goFibRecursive(n- 1 ) + goFibRecursive(n- 2 ) } func goFibIterative(n int ) int { if n <= 1 { return n } a, b := 0 , 1 for i := 2 ; i <= n; i++ { a, b = b, a+b } return b } function jsFibRecursive ( n ) { if ( n <= 1 ) return n ; return jsFibRecursive ( n - 1 ) + jsFibRecursive ( n - 2 ) ; } function jsFibIterative ( n ) { if ( n <= 1 ) return n ; let a = 0 , b = 1 ; for ( let i = 2 ; i <= n ; i ++ ) { [ a , b ] = [ b , a + b ] ; } return b ; } 再帰版 fibRecursive(40) 実装 実行時間 倍率 JavaScript 660ms 1.0x Go WASM 1,560ms 2.4x ループ版 fibIterative(10000000) 実装 実行時間 倍率 JavaScript 9ms 1.0x Go WASM 15ms 1.7x CLI版だとどちらのパターンでもJSのほうが高速という結果になりました。 ただブラウザ版だとGo WASMのほうが3倍ほど速くなっていました。JSエンジンの最適化による差分かもしれません。 原因分析 フィボナッチ計算は計算自体が軽量で、関数呼び出しのオーバーヘッドが支配的になります。JITコンパイラはこの種のシンプルな数値計算を最適化している可能性が考えられます。 どうやら「WASMにすれば速くなる」という単純な話でもなさそうです。 行列乗算 計算量をもう増やせば差が出るかもと考えたので、比較的計算量の大きい512x512の行列乗算で試してみました。 Go/JS双方でikjループ順を使い、同一の決定的データを生成して計算しています。 Go側は []float64 スライスを使い、JS側では Float64Array を使っています。 func goMatMul() { n := 512 a := make ([] float64 , n*n) b := make ([] float64 , n*n) for i := 0 ; i < n*n; i++ { a[i] = float64 (i% 97 ) * 0.01 b[i] = float64 (i% 89 ) * 0.01 } c := make ([] float64 , n*n) for i := 0 ; i < n; i++ { for k := 0 ; k < n; k++ { aik := a[i*n+k] for j := 0 ; j < n; j++ { c[i*n+j] += aik * b[k*n+j] } } } sum := 0.0 for _, v := range c { sum += v } return sum } function jsMatMul () { const n = 512 ; const a = new Float64Array ( n * n ) ; const b = new Float64Array ( n * n ) ; for ( let i = 0 ; i < n * n ; i ++ ) { a [ i ] = ( i % 97 ) * 0 . 01 ; b [ i ] = ( i % 89 ) * 0 . 01 ; } const c = new Float64Array ( n * n ) ; for ( let i = 0 ; i < n ; i ++ ) { for ( let k = 0 ; k < n ; k ++ ) { const aik = a [ i * n + k ] ; for ( let j = 0 ; j < n ; j ++ ) { c [ i * n + j ] += aik * b [ k * n + j ] ; } } } let sum = 0 ; for ( let i = 0 ; i < n * n ; i ++ ) sum += c [ i ] ; return sum ; } 実装 実行時間 倍率 JavaScript 190ms 1.0x Go WASM 208ms 1.1x 差は縮まりましたが、まだ若干JSが優勢です。 原因分析 JS側ではTypedArrayに対して最適化が行われている可能性があります。 またGo WASM側では以下箇所がオーバーヘッドになっている可能性があります。 WASMではSIMD命令を十分に活用できない? Goのスライスにおけるbounds checkのコストがある? 行列乗算は計算量が大きいためJS-WASM境界のオーバーヘッドは相対的に小さくなりますが、依然としてJSが有利でした。 SHA-256 計算量をもっと増やすと変化が出てくるかもと考えたので、SHA-256関数を利用することにします。 その際、JSによる純粋な実装よりネイティブAPIによる実装のほうが効率的である可能性が高いと考えたため、SubtleCrypto:digest()も比較対象に含めました。 ただ、SubtleCrypto:digest()は非同期関数であり、ベンチ時に同期的に呼び出しを行う必要がある点に注意が必要でした。 チェインハッシュ 小さなデータのハッシュ結果を次のハッシュの入力にする、という処理を10万回繰り返しました。 実装 実行時間 倍率 Go WASM 41ms 1.0x JS 純粋実装 114ms 2.8x SubtleCrypto 200ms 4.9x Go WASMがJS純粋実装の約2.8倍速く、最速という結果になりました。また、SubtleCryptoはさらに遅いという結果になりました。 Go WASMが速い理由 SHA-256はビット演算・整数演算が中心のアルゴリズムで、Goのコンパイル済みWASMコードが有利な立場だったと言えそうです。また、10万回のハッシュ計算を1回の関数呼び出しでWASM内で完結させている点で、呼び出しオーバーヘッドの影響がほぼなく、効率的だったと考えられます。 js.Global().Set( "goSHA256" , js.FuncOf( func (this js.Value, args []js.Value) any { data := [] byte (args[ 0 ].String()) iterations := args[ 1 ].Int() h := sha256.Sum256(data) for i := 1 ; i < iterations; i++ { h = sha256.Sum256(h[:]) } return hex.EncodeToString(h[:]) })) JS-WASM境界を跨ぐのは最初の呼び出しと結果の返却の1往復だけで、ループ全体がWASM内で実行されます。これにより crypto/sha256 標準ライブラリの実装がそのまま適用されます。 フィボナッチではJS側から関数を1回呼ぶという意味では同じでしたが、計算自体が軽いためランタイムオーバーヘッドが目立ちました。SHA-256チェインでは1回の呼び出しの中で重い計算を行うため、オーバーヘッドが相対的に無視できるようになります。 SubtleCryptoが遅い理由 ネイティブAPIの crypto.subtle.digest() が最も遅い結果となりました。 async function jsSHA256Subtle ( str , iterations ) { const encoder = new TextEncoder () ; let data = encoder . encode ( str ) ; data = new Uint8Array ( await crypto . subtle . digest ( "SHA-256" , data )) ; for ( let i = 1 ; i < iterations ; i ++ ) { data = new Uint8Array ( await crypto . subtle . digest ( "SHA-256" , data )) ; } return Array . from ( data , b => b . toString ( 16 ) . padStart ( 2 , "0" )) . join ( "" ) ; } SubtleCrypto はasync APIのみを提供しているため、10万回のチェインハッシュでは毎回Promise生成 → microtask enqueue → await復帰を繰り返します。ハッシュ計算自体よりも非同期ディスパッチのコストが支配的になっているようです。 巨大バッファハッシュ SubtleCryptoは大きなデータを一括処理するケースで優位であることが予想されます。64MBのバッファを1回だけハッシュする形式に変更して計測しました。 async function jsSHA256BulkSubtle ( size ) { const data = new Uint8Array ( size ) ; for ( let i = 0 ; i < size ; i ++ ) data [ i ] = i % 251 ; const hash = new Uint8Array ( await crypto . subtle . digest ( "SHA-256" , data )) ; return Array . from ( hash , b => b . toString ( 16 ) . padStart ( 2 , "0" )) . join ( "" ) ; } 実装 実行時間 倍率 SubtleCrypto 350ms 1.0x Go WASM 430ms 1.2x JS 純粋実装 980ms 2.8x 非同期呼び出しは1回だけなのでネイティブの速度がそのまま活かされ、SubtleCryptoが最速という結果になりました。 対して、GO WASM版にはなんらかのオーバーヘッドが存在しているのか、もしくはダイジェスト関数実装における性能差があるのかもしれません。 SubtleCryptoは大きなデータを一括処理する用途向きであり、小さなデータを繰り返しハッシュするような用途には向いていなさそう、ということがわかりました。 おまけ 計測中に興味深い現象を発見しました。Mac Chrome(144.0.7559.110)で、DevToolsを開いた状態では閉じた状態に比べてGo WASMの性能が低下するというものです。 テスト Go WASM(DevTools閉) Go WASM(DevTools開) 劣化率 再帰 fib(40) 1,550ms 3,150ms 2.0x 行列乗算 512x512 203ms 453ms 2.2x SHA-256 チェイン 41ms 136ms 3.3x SHA-256 64MB 428ms 1,461ms 3.4x 一方、JS側にはほとんど影響がありませんでした。 原因 DevToolsを開くと、Chromeが内部的にChrome DevTools Protocol(CDP)の Debugger.enable() を発行するようです。これによりWASMバイトコードにデバッグ用コード(ブレークポイント判定等)を挿入するため、WASMの実行速度が大幅に低下しますが、一方JSのJITコードには同等の影響があまり発生しないようです。 WASMのベンチマーク時はDevToolsを閉じた状態で行う、またはDevToolsを開いている場合にはDebuggerを無効化した状態で行う必要があることが分かりました。 まとめ Go WASMがJSより優位なのは、1回の関数呼び出しで大量の計算をWASM内で完結させるパターン(SHA-256チェインなど) 逆に、関数呼び出しが頻繁・計算が軽い場合は、JS JITが有利(フィボナッチなど) SubtleCryptoなどのasync APIは呼び出し回数に注意が必要。大バッファの一括処理なら効果的 WASMのベンチマーク時はDevToolsを閉じるorDebugger無効化。そうしないと2〜3倍の劣化が発生する 「GoをWASMにすればJSより速くなる」、というのは条件次第で真偽いずれもあり得ることが分かりました。 JS-WASM境界を跨ぐ回数を最小化し、WASM内でまとまった計算を完結させる設計にすることが重要そうです。 WASMの導入を検討する際は、対象のアルゴリズムがこのパターンに合致するかを事前に見極める必要があります。
アバター
こんにちは、エブリーでデリッシュキッチンの開発を主に担当している塚田です。 WebやAPIを運用する中で、セキュリティ強化は継続的な課題の一つです。 今回は、AWS WAF (Web Application Firewall) を導入する場合のアーキテクチャ選定と、そこで直面した技術的な検討事項について紹介します。 特に、 「CloudFront -> ALB -> ECS」という標準的な構成において、「WAFをどこに適用するか(Edge vs Regional)」 という議論にフォーカスします。 はじめに 今回は、静的コンテンツの配信効率化と負荷分散のために、以下のような構成をとっている前提で検討します。 また、セキュリティグループやネットワークACLによる防御や、アプリケーション側で行える基本的な攻撃への対処(SQLインジェクションやXSSなど)は実現できているものとします。 User -> CloudFront -> ALB (Application Load Balancer) -> ECS (Amazon ECS) そこで最初の岐路となったのが、 「CloudFront(Edge)にWAFを適用するか、ALB(Regional)に適用するか」 という問題です。 CloudFront vs ALB どちらにWAFを置くか? AWS WAFは、CloudFrontとALBのどちらにもアタッチ可能です。それぞれのメリット・デメリットを整理し、検討を行いました。 比較項目 CloudFront (Edge) に適用 ALB (Regional) に適用 防御範囲 全リクエストをエッジでブロック キャッシュミスし、オリジンに到達したリクエストのみブロック コスト 高い (全リクエストが課金対象) 安い (オリジン到達分のみ課金対象) オリジン負荷 攻撃リクエストがオリジンに届かないため負荷減 攻撃リクエストもALBまでは到達する 直接アクセス対策 ALBへ直接アクセスされるとWAFを回避される ALB自体を守るため、直接アクセスでもWAFが機能する IP制限 クライアントIPで直接制限可能 X-Forwarded-For ヘッダーの参照が必要 単純に以下の観点で検討した場合ALBの方が有利であると感じました。 圧倒的なコストメリット メディアサービスの特性上、画像や動画などの静的リクエスト数が膨大です。 CloudFront側でWAFを有効にすると、静的リソースへの正常なアクセスも含めた「全リクエスト」に対してWAFの料金(Web ACL使用料 + リクエスト数課金)が発生してしまいます。 ALB側であれば、CloudFrontでキャッシュアウトした「動的処理が必要なリクエスト」のみが検査対象となるため、コストを大幅に最適化できます。 防御対象の絞り込み 本当に守りたいのは、データベース接続やビジネスロジックを持つECS上のアプリケーションです。静的ファイルへのリクエストを除外した、純粋なアプリケーションリクエストのみを検査対象とすることで、運用時のログ分析ノイズも減らせると判断しました。 技術的検討事項と実装のポイント 有利であると感じたALB側にWAFを適用する場合ですが、いくつか特有の技術的課題が発生します。 1.クライアントIPの識別(IP制限) ALBにWAFを適用する場合、WAFが見る「送信元IPアドレス」はCloudFrontのIPレンジになってしまいます。攻撃者のIPや、社内からのアクセス許可(ホワイトリスト)をIPベースで行う場合、そのままでは機能しません。 対策: AWS WAFのルール設定において、IP判定に X-Forwarded-For ヘッダーを使用するように構成することで、IP制限を実現。 方法: AWS WAFの設定(コンソールやTerraform等)で、IPセットの一致条件を「IP address in header」とし、ヘッダー名に X-Forwarded-For を指定します。 ※ X-Forwarded-For は改ざん可能なヘッダーであることに注意が必要です。 2.AWSマネージドルール(IPレピュテーション等)の制約 ここが大きな注意点ですが、AWSが提供する AWSManagedRulesAmazonIpReputationList などのIPアドレスベースのマネージドルールは、基本的に「送信元IPアドレス」を検査対象とします。 ALB上のWAFから見ると送信元はすべてCloudFrontのIPとなるため、これらのルールはクライアントのIP(攻撃者)に対して正しく機能しません(CloudFrontのIPを評価してしまいます)。 SQLインジェクションやXSSなど「アプリケーション脆弱性への攻撃(リクエストの中身)」への防御やレートリミットでの制限を最優先とする場合は、この制約を許容できるかもしれません。 しかし、AWS提供のIPレピュテーションリストによる防御が必須要件である場合は、コスト増を許容してでもCloudFront側にWAFを適用する必要があります。 3.CloudFront以外からのアクセス遮断(WAF回避の防止) ALBにWAFがあるため、攻撃者がCloudFrontを経由せずにALBのDNS名に直接アクセスしてきた場合でも、WAF自体は機能します。しかし、キャッシュを介さない不必要な負荷を避けるため、ALBへのアクセスはCloudFront経由のみに限定すべきです。 対策: ALBのセキュリティグループ(SG)で、インバウンドルールを 「 CloudFrontのManaged Prefix List からのHTTPSのみ許可」 に設定しました。 これにより、攻撃者がALBのIPやDNSを特定して直接攻撃を仕掛けてきても、ネットワーク層で遮断されます。WAF以前の段階で不正アクセスを防ぐ重要な設定となります。 4.静的・動的コンテンツの分離によるコスト最適化 ALBレイヤーでの適用と同様の効果(コスト削減)を得るための別のアプローチとして、CloudFrontディストリビューションを静的コンテンツ用と動的コンテンツ用に分割する構成も有効です。 静的コンテンツ用CloudFront: WAFを適用しない(または安価なルールのみ)。常にキャッシュさせる 動的コンテンツ用CloudFront: WAFを適用する このように構成することで、エッジ(CloudFront)での防御メリットを享受しつつ、検査対象を動的リクエスト(ALBと同等のアクセス量)に絞り込んでコストを最適化できます。ただし、ドメイン設計やフロントエンドの実装変更が必要になるため、対応に必要なコストと防御したい内容を天秤にかけて検討すると良いでしょう。 実際に導入する際の運用考慮事項 実際にWAFを本番環境へ導入する場合には、技術的な設定だけでなく、誤検知(False Positive)への運用フローなどの考慮も必要です。 この点については、過去の記事でも詳しく紹介していますので、ぜひ参考にしてください。 tech.every.tv tech.every.tv まとめ 今回のWAF導入では、「どこで守るか」と「コスト最適化」という観点で検討を行いました。 CloudFront (Edge): 全リクエスト防御、DDoS対策重視、設定がGlobal ALB (Regional): コスト最適化(キャッシュ済みリクエスト除外)、アプリ保護重視 構成図だけ見れば「前段(Edge)で止めるのがベストプラクティス」とされることが多いですが、実際のリクエスト量や守るべき対象のリスク許容度を計算すると、他の方法も検討の余地があります。 今後も、サービスの成長に合わせて、セキュリティとパフォーマンス、そしてコストの最適なバランスを追求していきたいと思います。
アバター
Flutter3.38アップグレードにおけるiOSとAndroidの影響範囲 背景 Flutter3.38アップグレードの手順 パッケージのバージョン依存関係調整 依存関係の解消によって副次的問題が発生 1. Dart SDK バージョン 2. Ferryエコシステム(GraphQLクライアント) 3. Freezed(コード生成) 4. Firebase(iOS 13対応のため2.x系を使用) 5. その他の重要な更新 6. Isar Plus(データベース)モデル修正 解決した依存関係の競合 競合1: isar_db_generator 競合2: intl バージョン 競合3: source_gen バージョン 競合4: build バージョン 競合5: gql_exec バージョン 競合6: ferry バージョン 競合7: web パッケージ(iOS 13対応のための調整) 競合8: Firebase iOS 15要件 参考リンク 既存コードの改修作業 1. isar_plus 移行に伴うimport文の変更 2. isar_plus 移行に伴うPodfileの変更 3. NDKバージョンの明示的指定 4. Android app の namespace(AGP 8.x 必須) 5. サブプロジェクトへの namespace 注入 6. BuildConfig の有効化 7. Freezed 3.x マイグレーション(全モデルに abstract 修飾子) 8. retrofit / ParseErrorLogger 対応 9. Android 旧埋め込み (PluginRegistry.Registrar) 削除対応 10. iOS CocoaPods更新 Flutter バージョン別 カート追加時パフォーマンス比較レポート 比較サマリー 結論 主な改善要因(推測) 今後の検討 まとめ 1. テスト 2. iOS 11-12ユーザーへの対応検討 最後に  こんにちは、開発本部 開発2部 RetailHUB NetSuperグループに所属するホーク🦅アイ👁️です。 背景  現在提供しているネットスーパーアプリはFlutter+Dartで実装しております。一方で、昨年2025年11月1日までに対応が必要であったGoogleからの メモリの16KBページサイズをサポートせよ という通知を延長申請して2026年5月31日まで対応を保留にしていました。2026年2月現在、この延長期限も近づいてきていることを理由にFlutterのバージョンを3.38系にアップグレードすることになりました( こちら の公式ブログにも3.38から標準対応したとあります)。 Flutter3.38アップグレードの手順  通常、Flutterのバージョンをアップグレードする際の流れとしては、以下の2点を気にする必要があり実際にそのプロセスを踏まないといけないことも少なからずあります。 パッケージ依存関係の解消 既存ソースコードで改修  今回、対象のソースコードで利用しているFlutterバージョンは3.24.1と古めのバージョンであるためバージョン差分が大きく、2点とも対応が発生しました。以下にその2点の対応について詳細を記していきます。 パッケージのバージョン依存関係調整  目的はあくまでAndroidアプリの16KBページサイズに対応することなのでAndroidアプリのビルドをまず意識した以下のバージョンを調整しました。 Flutter,Dartのバージョンを上げる Flutter3.38.9、Dart3.10.8にしました DevTools 2.51.1 Gradleのバージョンを上げる 8.7にしました AGPのバージョンを上げる 8.5.1にしました Kotlinのバージョンを上げる 2.0.21にしました NDKバージョンを明示指定 29.0.14206865にしました その他利用ライブラリの対応バージョンをtrial and errorで上げる 現状のバージョンのままでビルドしてみてエラーが出たら上げるを繰り返しました 依存関係の解消によって副次的問題が発生  今回、Androidアプリのためにアップグレードをするので必要なライブラリパッケージも必要最低限のものだけを最小限でアップグレードすることを心がけたのですが、意図せずiPhoneアプリ側の方にも影響を及ぼしてしまうことが判明しました。具体的には以下のことが発生しました。 iOSの最小メジャーバージョン番号が12から13以上に引き上げ  直接的な引き上げ条件は、 Flutter公式 によれば、swiftコードを利用する場合とありますがFlutter3.38自体がそれに該当するということ( 参考ページ )でした。というわけで、必然的にFlutter3.38にしないといけない場合はiOS最小対応バージョンも13以上になるということでした。  最終的には以下に挙げるバージョン対応で一旦、すべての依存関係の競合を解決し、 pub get 、コード生成、iOS CocoaPodsインストールがすべて成功しました。 1. Dart SDK バージョン 変更 : >=3.7.0 <4.0.0 → >=3.8.0 <4.0.0 理由 : json_serializable と freezed の最新版が要求 2. Ferryエコシステム(GraphQLクライアント) パッケージ 最終バージョン 理由 ferry ^0.16.1 ferry_generator 0.12.0+3との互換性 ferry_generator ^0.12.0+3 build 4.0対応 build_runner ^2.10.3 build 4.0対応 gql_code_builder_serializers ^0.1.0 ferry_generator 0.12.0+で必須 gql_exec ^1.0.0 ferry_generator依存関係 gql_http_link ^1.0.0 gql_execとの互換性 gql_transform_link ^1.0.0 gql_execとの互換性 3. Freezed(コード生成) パッケージ 最終バージョン 理由 freezed ^3.2.5 build 4.0対応 freezed_annotation ^3.0.0 freezed 3.x対応 4. Firebase(iOS 13対応のため2.x系を使用) パッケージ 最終バージョン 理由 firebase_core ^2.32.0 iOS 13サポート(3.x+はiOS 13必須) firebase_analytics ^10.10.7 firebase_core 2.x互換 firebase_crashlytics ^3.5.7 firebase_core 2.x互換 firebase_messaging ^14.7.10 firebase_core 2.x互換 & webパッケージ互換 firebase_remote_config ^4.4.7 firebase_core 2.x互換 firebase_app_installations ^0.2.5+7 firebase_core 2.x互換 Firebase iOS SDK : 10.25.0(iOS 13+をサポート) 5. その他の重要な更新 パッケージ 最終バージョン 理由 intl ^0.20.2 flutter_localizations要求 retrofit_generator ^10.2.1 source_gen 4.0対応 json_annotation ^4.9.0 json_serializable要求(dependenciesに追加) 6. Isar Plus(データベース)モデル修正 isar_plus v4のAPI変更に対応: 変更後(isar_plus v4スタイル): @collection class EventLog { EventLog({required this .id}); final int id; // Auto-increment id (isar_plus v4) late String data; } 主な変更点: @Collection() (大文字)→ @collection (小文字) Id id = Isar.autoIncrement → final int id とコンストラクタで受け取る 実際のauto-increment IDは isar.collection.autoIncrement() で生成 解決した依存関係の競合 競合1: isar_db_generator 問題 : isar_db_generator パッケージが存在しない 解決 : isar_db は元の isar_generator を使用することを確認 競合2: intl バージョン 問題 : flutter_localizations が intl 0.20.2 を要求 解決 : intl を ^0.20.2 に更新 競合3: source_gen バージョン 問題 : isar_plus が source_gen ^4.0.2 を要求、 retrofit_generator ^8.1.0 が source_gen ^1.3.0 を要求 解決 : retrofit_generator を ^10.2.1 に更新(source_gen 4.0対応版) 競合4: build バージョン 問題 : build_runner >=2.9.0 が build ^4.0.0 を要求、 freezed ^2.x が build ^2.3.1 を要求 解決 : freezed を ^3.2.5 に更新(build 4.0対応版) 競合5: gql_exec バージョン 問題 : gql_transform_link が古い gql_exec を要求、 ferry_generator 0.12.0+3 が gql_exec ^1.0.0 を要求 解決 : gql_exec 、 gql_http_link 、 gql_transform_link をすべて1.x系に更新 競合6: ferry バージョン 問題 : ferry ^0.14.2+1 が ferry_exec ^0.3.1 を要求、 ferry_generator 0.12.0+3 が ferry_exec ^0.7.0 を要求 解決 : ferry を ^0.16.1 に更新 競合7: web パッケージ(iOS 13対応のための調整) 問題 : isar_plus が web ^1.1.0 を要求、Firebase 4.x系が web ^0.5.1 を要求 解決 : Firebaseパッケージを2.x系にダウングレード(iOS 13サポートのため) 競合8: Firebase iOS 15要件 問題 : Firebase 4.x系(firebase_core 4.0+)はiOS 15を最小要件とする 解決 : Firebase 2.x系(firebase_core 2.32.0)を使用してiOS 13をサポート 参考リンク isar_plus v4 ドキュメント ferry_generator changelog freezed 3.0 migration Firebase Flutter changelog firebase_core 3.0.0 breaking changes 既存コードの改修作業  前述にある依存パッケージライブラリをアップグレードすることで破壊的変更が発生してしまった全箇所をエラーがなくなるまで対応していくという作業も相当数発生しました。 1. isar_plus 移行に伴うimport文の変更   package:isar/isar.dart → package:isar_plus/isar_plus.dart に変更 2. isar_plus 移行に伴うPodfileの変更 ios/Podfile で isar_flutter_libs → isar_plus_flutter_libs に変更 3. NDKバージョンの明示的指定 追加 : ndkVersion "29.0.14206865" (安定版最新。r28+ で 16KB アライメント対応のため r29 を使用) ファイル : app/build.gradle 理由 : NDK r28以上で16KBアライメントがデフォルト対応。固定していた 28.0.12674087 ではなく、安定版最新 29.0.14206865 を推奨 4. Android app の namespace(AGP 8.x 必須) 追加 : namespace "tv.every.fresh" ファイル : app/build.gradle 理由 : AGP 8.x ではモジュールに namespace の指定が必須。未指定だと「Namespace not specified」でビルド失敗する。 5. サブプロジェクトへの namespace 注入 追加 : Android library プラグインで namespace 未指定のサブプロジェクトに、 AndroidManifest.xml の package を namespace として設定 ファイル : build.gradle (root) 理由 : AGP 8.x では全モジュールに namespace 必須。古いパッケージは namespace 未指定のため「Namespace not specified」でビルド失敗する。pub cache は編集しないため、root の subprojects.plugins.withId("com.android.library") で manifest の package を注入する。 6. BuildConfig の有効化 追加 : 全 Android サブプロジェクトで buildFeatures.buildConfig true ファイル : build.gradle (root) 理由 : AGP 8.x では BuildConfig がデフォルト無効。custom BuildConfig を使うパッケージがあったため「defaultConfig contains custom BuildConfig fields, but the feature is disabled」でビルド失敗する。root の subprojects.afterEvaluate で全モジュールに有効化する。 7. Freezed 3.x マイグレーション(全モデルに abstract 修飾子) 対象 : @freezed を付けた全てのモデルクラス 対応 : Freezed 3.0 マイグレーションガイドに従い、 全てのクラス定義に abstract 修飾子を追加 変更例 : // 変更前 @freezed class Shop with _$Shop {...} // 変更後 @freezed abstract class Shop with _$Shop {...} 理由 : Dart 3.10 コンパイラ下で non_abstract_class_inherits_abstract_member エラーを解消するため。Freezed 3.x の生成コード(mixin の抽象メンバー)と互換させるには、公開クラスを abstract class にすることが必要。 実施方法 : 上記のとおり、該当する全モデルファイルで class → abstract class に手動で置換。 検証 : 本対応後に iOSビルド成功し、iOS Simulator(26.2)でアプリ起動を確認済。 8. retrofit / ParseErrorLogger 対応 対象 : retrofit パッケージをimportしているREST APIを実装したdartファイル(ex. rest_api.dart) 現象 : 旧 retrofit 4.1.0 + retrofit_generator 10.2.1 で生成したコードが型 ParseErrorLogger を参照するが、 package:retrofit/http.dart のみの import では参照できずコンパイルエラーになる 対応 : retrofit : ^4.1.0 → ^4.9.2 に更新(Dart 3.8 対応バージョン、ParseErrorLogger は package:retrofit/retrofit.dart で提供) rest_api.dart : import 'package:retrofit/retrofit.dart'; を追加し、生成コード(part ファイル)から ParseErrorLogger を参照可能にする。 package:retrofit/http.dart は retrofit.dart に含まれるため削除可 結果 : build_runner 再生成後も手動修正不要でビルド可能 9. Android 旧埋め込み (PluginRegistry.Registrar) 削除対応 Flutter 3.38 では v1 Android 埋め込み API( PluginRegistry.Registrar / registerWith )が削除されている。以下のパッケージを更新済み。 path_provider : ^2.1.3 → ^2.1.5 (path_provider_android 2.2.5+ を要求し、v1 削除済み) shared_preferences : ^2.2.3 → ^2.3.4 (shared_preferences_android 2.2.3+ で v1 削除済み) url_launcher_android : ^6.0.38 → >=6.3.3 <6.3.27 (6.3.3 で v1 削除。6.3.27+ は androidx.browser 1.9.0 が AGP 8.9.1 を要求するため 6.3.26 以下に制限) compileSdkVersion : 34 → 36(path_provider_android 等が SDK 36 を要求。 app/build.gradle ) 10. iOS CocoaPods更新 iOS最小デプロイメントターゲットをiOS 13.0にFixしました。 platform :ios , ' 13.0 ' config.build_settings[ ' IPHONEOS_DEPLOYMENT_TARGET ' ] = ' 13.0 ' Flutter バージョン別 カート追加時パフォーマンス比較レポート  新しいバージョンになったのでそれだけでどれだけ既存アプリのパフォーマンスにも影響を及ぼしたか気になったのでFlutter DevToolsでプロファイリングしてTimeline Eventsを3.24.1と3.38.9でベンチマーク比較してみました。  以下は、 同じカート追加アクション で取得したDevToolsの計測データを用い、 Flutter 3.24.1 と Flutter 3.38.9 のパフォーマンスを比較したレポートです。 項目 Flutter 3.24.1 Flutter 3.38.9 総フレーム数 96 107 比較サマリー 指標 Flutter 3.24.1 Flutter 3.38.9 差分 傾向 平均FPS 19.8 fps 48.3 fps +28.5 fps ✅ 大幅改善 平均フレーム時間 50.47 ms 20.71 ms -29.76 ms ✅ 約59%短縮 平均ビルド時間 7.23 ms 1.25 ms -5.98 ms ✅ 約83%短縮 平均ラスター時間 26.86 ms 15.32 ms -11.54 ms ✅ 約43%短縮 平均Vsyncオーバーヘッド 5.96 ms 1.46 ms -4.50 ms ✅ 約75%短縮 最大フレーム時間 274.89 ms 85.51 ms -189.38 ms ✅ 約69%短縮 最大ビルド時間 115.16 ms 18.66 ms -96.50 ms ✅ 約84%短縮 最大ラスター時間 145.56 ms 41.46 ms -104.10 ms ✅ 約72%短縮 Janky率 100.0% 88.8% -11.2pt ✅ 改善 重度Jank率 (>33ms) 38.5% 4.7% -33.8pt ✅ 大幅改善 結論 Flutter 3.38.9 は 3.24.1 と比較して、カート追加時のパフォーマンスが全体的に大きく改善しています。 平均FPSが 19.8 → 48.3 と約2.4倍になり、体感の滑らかさが向上しています。 ビルド時間・ラスター時間・Vsyncオーバーヘッドのいずれも短縮。 最大フレーム時間は 274.89ms → 85.51ms(約69%短縮) と、改善が確認できます。 重度Jank率は 38.5% → 4.7% と約1/8に減少。 主な改善要因(推測) Flutterエンジン・Skiaの最適化 ビルドパイプラインの効率化(ビルド時間の大幅短縮) Vsyncまわりのオーバーヘッド低減 今後の検討 3.38.9 時点でも Janky率 88.8%、平均FPS 48.3 であり、60fps目標にはまだ余裕があります。 ラスターが主なボトルネックのため、画像最適化・RepaintBoundary・Clip削減などの施策を続けると、さらに改善の余地があります。 まとめ  本ブログでは、Android15以降でサポートされている16KBページサイズに対応するためFlutterのバージョンを3.38系にアップグレードすると既存アプリにどのような影響を及ぼすことになるかについてお話ししました。  一旦は、両OSともビルドが成功してアプリ起動までは確認が取れたのでこれから5月31日まであまり日がないですが以下のようなThe next stepsに基づいて進めていく予定であることをお知らせして結びとさせていただきます。 1. テスト 単体テストの実行 統合テストの実行 手動テスト(特にデータベース操作とGraphQL操作) 2. iOS 11-12ユーザーへの対応検討 アプリストアで古いバージョンを継続提供 ユーザーへの事前通知 段階的な移行計画 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv
アバター
はじめに デリッシュキッチンの 鈴木 です。 UX 体験向上のために Web フロントエンドのパフォーマンスを計測することもあるでしょう。その際に、計測結果をその都度サーバーへ送信すると、ネットワーク通信やシリアライズ処理が増え、画面描画やユーザー操作の体感に影響しやすくなります。これは避けなければなりません。 そこで実運用では、計測データをいったんメモリ上のバッファに溜め、一定間隔または一定件数でまとめて送信するバッチ送信が一般的です。しかしこの方式では、ページ遷移やタブクローズが起きた時点でバッファに未送信データが残っていると、送信開始前に失われたり、送信中の通信が中断されたりして欠損が起きる可能性があります。 さて、この問題にどう対処するべきなのでしょうか?今回は、ページ終了時に未送信データをできるだけ取りこぼさないために、パフォーマンス計測で使用される OpenTelemetry JS がどのように設計・実装して問題に対処しているかを、コードを手がかりに整理していきます。 課題: ページ遷移・クローズ時にデータが欠損する 送信完了前に通信が中断される問題 たとえばユーザーがボタンをクリックしてから画面遷移が完了するまでの時間(E2E レイテンシ)を計測する場合、まず正常系では次の流れになります。 ユーザー操作(計測開始) 処理完了(計測終了 → データ確定) 計測データをメモリ上のバッファに保存する 一定間隔または一定件数で、バッファの内容をまとめて送信する ここで問題になるのは、3 と 4 のあいだ、または 4 の途中にページ遷移やタブクローズが割り込むケースです。この時以下の問題が起こり得ます。 バッファに未送信データが残っていると欠損する 送信中の通信がページ終了により中断されることがある 一般的な非同期通信(fetch / XMLHttpRequest)は、ページ終了に伴ってブラウザ側で中断されることがあります。その結果、バッファ内に残っていたデータや送信途中のデータが Collector に届かず、データ欠損が起きます。 以下は、欠損が起きる典型的な流れを、シーケンス図として表したものです。 Fig 1: バッチ送信における正常系と、ページ終了割り込みによる欠損パターン(シーケンス図) ※ 図中の Normal は正常系、残り 2 つはページ終了が割り込むことで欠損が起こり得るケースです。 OpenTelemetry の対策 この問題に対して OpenTelemetry JS は、Web 標準 API を活用した 2 つのアプローチを組み合わせ、タブクローズ時の送信成功率を高めています。 検知とトリガー: ページ終了の直前に発火するイベントを検知し、バッファに残っているデータの送信をただちに開始する 送信継続: ページ終了後も送信が完了しやすい Web API に委譲する 全体像をレイヤに分けると次のようになります。 Fig 2: OpenTelemetry JS による二段構えの送信設計(検知とトリガー/送信継続) それでは、この 2 つの対策が具体的にどのコードで実現されているかを追っていきたいと思います。 実装詳細をコードで追う フェーズ1: 【検知】ページライフサイクルイベントの監視 通常、スパン(計測データ)はパフォーマンスへの影響を抑えるため、一定数をバッファに溜めてからまとめて送信(バッチ処理)します。しかし、ページ終了時に通常の周期的な送信タイミングを待っていると、その前にページが破棄されてしまった場合にスパンが失われる可能性があります。 そこで BatchSpanProcessor は、ページが終了する兆候を示すイベントを監視し、発火したら forceFlush() を呼んで今ある分を即座に送る方針を取ります。 該当コード( BatchSpanProcessor.ts ) private onInit(config?: BatchSpanProcessorBrowserConfig): void { if ( config?.disableAutoFlushOnDocumentHide ! == true && typeof document !== 'undefined' ) { this ._visibilityChangeListener = () => { if ( document .visibilityState === 'hidden' ) { this .forceFlush(). catch ( error => { globalErrorHandler(error); } ); } } ; this ._pageHideListener = () => { this .forceFlush(). catch ( error => { globalErrorHandler(error); } ); } ; document . addEventListener ( 'visibilitychange' , this ._visibilityChangeListener); // use 'pagehide' event as a fallback for Safari; see // https://bugs.webkit.org/show_bug.cgi?id=116769 document . addEventListener ( 'pagehide' , this ._pageHideListener); } } visibilitychange : document.visibilityState === 'hidden' になった瞬間を検知 pagehide : Safari 向けのフォールバック(コメントにもある通り) これらのイベントが発火すると forceFlush() が呼ばれ、バッファ内のスパンがエクスポート処理へ回されます。 該当コード( BatchSpanProcessorBase.ts ) forceFlush(): Promise < void > { if ( this ._shutdownOnce.isCalled) { return this ._shutdownOnce.promise; } return this ._flushAll(); } ここで重要なのは、 forceFlush() 自体は非同期であり、送信完了までページの終了を止められるわけではない点です。JavaScript には送信が完了するまでページ遷移を確実に止めるための一般的な仕組みがありません。したがって、検知してただちに送信を開始しても、なお送信中にページが閉じてしまう可能性は残ります。その穴を埋めるのが次のフェーズです。 フェーズ2: 【送信継続】ページ終了後も通信を継続しやすい API の活用 ページ終了後も送信を完遂するには、ページの寿命とネットワークリクエストの寿命を切り離せる API が必要です。OpenTelemetry JS の Transport 層は、状況に応じて次の 2 つを使い分けます。 navigator.sendBeacon() (ヘッダ不要の場合) fetch(..., { keepalive: true }) (認証などでヘッダが必要な場合) sendBeacon はページ終了時の送信継続に適した API ですが、リクエストにカスタム HTTP ヘッダー(例: Authorization )を付与できないという制約があります。そのため、認証等でヘッダーが必要なケースでは fetch を使う必要があり、ページ終了後も送信継続が期待できるよう keepalive: true を併用する設計になります。 選択肢A: navigator.sendBeacon() sendBeacon は、ページアンロード時の送信を想定して設計された API です。ノンブロッキングで送信を開始でき、ページが閉じた後もブラウザが送信継続を試みます。ただし、 sendBeacon の戻り値は送信完了を保証するものではなく、あくまでブラウザが送信処理の開始(キュー投入)を受け付けたかどうかの成否に近い点には注意が必要です。つまり、 sendBeacon を使っても確実に届くわけではなく、ページ終了時の到達率を上げるためのベストエフォートな手段だと捉えるのが正確です。 該当コード( send-beacon-transport.ts ) async send(data: Uint8Array ): Promise < ExportResponse > { const blobType = ( await this ._params. headers ()) [ 'Content-Type' ] ; return new Promise < ExportResponse >( resolve => { if ( navigator . sendBeacon ( this ._params. url , new Blob ( [ data ] , { type : blobType } ) ) ) { // no way to signal retry, treat everything as success diag.debug( 'SendBeacon success' ); resolve( { status : 'success' } ); } else { resolve( { status : 'failure' , error : new Error ( 'SendBeacon failed' ), } ); } } ); } 選択肢B: fetch の keepalive: true 認証ヘッダーが必要な場合は fetch を使いますが、ポイントは keepalive: true を付けることです。これにより、ページの破棄後もリクエストが一定の条件で継続されることが期待できます。 該当コード( fetch-transport.ts ) const isBrowserEnvironment = !!globalThis. location ; const url = new URL ( this ._parameters. url ); const response = await fetch (url. href , { method : 'POST' , headers : await this ._parameters. headers (), body : data, signal : abortController.signal, keepalive : isBrowserEnvironment, mode : isBrowserEnvironment ? globalThis. location ?. origin === url. origin ? 'same-origin' : 'cors' : 'no-cors' , } ); OpenTelemetry JS は、ブラウザ環境であることを検知した場合に自動で keepalive: true を付与するため、利用者側で特別な設定をしなくてもページ終了時に強い送信経路を取りやすい設計になっています。 なお keepalive (および sendBeacon )は、実装上おおむね 数十KB(典型的には約 64KiB 前後)の送信サイズ上限に当たりやすく、バッチが肥大化すると送信に失敗する可能性があります。したがって実運用では、ページ終了時のフラッシュ対象を未送信すべてにするのではなく、イベントを小さく保つ、分割する、重要度で間引くといった設計上の工夫も合わせて検討すると安全です。 まとめ: タブクローズ時のデータ損失はどこまで回避できるか OpenTelemetry JS は、ブラウザ仕様の範囲内で ベストエフォートにデータ欠損を減らす設計を取っています。 検知( BatchSpanProcessor ): ページが非表示・終了に向かうイベントを検知し、即座に forceFlush() を起動する 継続(Transport): sendBeacon または fetch(keepalive) を用い、ページ破棄後の通信継続をブラウザに委譲する この二段構えにより、タブクローズ時のデータ到達率は現実的に大きく改善します。一方で、開発者が理解しておくべき限界もあります。 開発者が知っておくべきポイント 設定不要で動く: 既定設定の範囲で、この仕組みは動作する 完全な保証ではない: ブラウザのクラッシュ、ネットワーク断、OS 側の強制終了などでは失敗し得る サイズ上限の影響がある: keepalive には送信サイズの上限があり、上限を超えると送信に失敗する可能性がある 認証環境でも破綻しにくい: 認証ヘッダーが必要な場合は fetch(keepalive) が選択されるため、現代のブラウザでは一定の実用性が期待できる 以上が、OpenTelemetry JS がページ終了時に欠損しやすい計測データを守るために採用している設計の要点です。 おわりに 今回 Opentelemetry JS のコードを追ってみましたが、かなり SDK レベルで頑張ってくれている印象がありますね。 ただ、SDK だけで完全に永続性を担保することは難しいので、どうしても失いたくないデータがある場合は、ブラウザのストレージ(IndexedDB など)にいったん永続化し、Service Worker などを用いてバックグラウンドで再送する、といった設計も選択肢になります。OpenTelemetry JS の仕組みはあくまでベストエフォートであるため、要件に応じて永続化を組み合わせるとより堅牢になるでしょう。
アバター
タイトル 目次 はじめに SRE Kaigi 2026 とは? 参加レポート 生成AI時代にこそ求められるSRE SRE とプロダクトエンジニアは何故分断されてしまうのか 開発チームが信頼性向上のためにできること: 医療SaaS企業を支える共通基盤の挑戦 おわりに はじめに こんにちは。2025年4月にソフトウェアエンジニアとして新卒入社した 黒髙 です。普段は デリッシュキッチン の開発に携わっています。 2026年1月31日(土)に中野セントラルパーク カンファレンスで開催された SRE Kaigi 2026 に参加してきました。本記事では、特に印象に残ったセッションをご紹介します。 SRE Kaigi 2026 とは? 2026.srekaigi.net SRE Kaigi は、Site Reliability Engineering(SRE)コミュニティの活性化と技術的な交流を促進することを目的としたカンファレンスです。第2回となる今回は「Challenge SRE!」をテーマに掲げ、SREを前に進めるための挑戦を応援することを目指して開催されました。 弊社にはSREエンジニアや専任のSREチームは存在せず、プロダクトごとの開発チームがそれに近い役割を担っています。私自身もその組織の一員として知見を高めたく、「SREの考え方を開発チームとしてどう取り入れるか」という視点で参加しました。 入場時には様々なノベルティをいただきましたが、『わかばちゃんと学ぶSRE』という冊子は、初めてSREに触れる私にとって理解の助けになりました。 ノベルティ 会場には3つのセッションルームに加え、多様なスポンサーブースや書籍販売コーナーがありました。さらにマッサージブースや屋台での軽食提供、コーヒー提供などバラエティ溢れる企画があり、一日を通してとても賑わっていました。 参加レポート 生成AI時代にこそ求められるSRE 発表者: 山口能迪 さん speakerdeck.com AWSの山口能迪さんによる、AI時代におけるSREの価値を再定義するセッションでした。「AIがコードや設定を書く時代に、SREは不要になるのか?」という問いに対して、「SREの重要性は、かつてないほど高まっている」と明確に答える内容でした。 セッションでは「AIは組織の能力を増幅するアンプである」という表現が使われていました。優れた組織はより強化され、課題のある組織は弱点を増幅させる。開発速度が上がる一方で、システムの不安定さや変更失敗率も増大しうるということです。その上で、SREの責務を「AIの爆発的な生産性を、カオスではなく、持続可能なユーザー価値へと変換する」と定義していたのが印象的でした。 また、SREがAIにもたらす価値は「コンテキスト」と「ガードレール」という2つの軸で整理されていました。 コンテキストとは、AIがより良く動作するための下地のことです。LLMは学習時点の一般的な情報しか知らないため、システム固有の情報はコンテキストとして与える必要があります。具体的にはオブザーバビリティによるテレメトリーの収集や、Infrastructure as Codeによる設定のコード化、ポストモーテムの整備などが挙げられていました。 一方、ガードレールはAIの失敗を予防・回復するための保険です。AIが生成したコードに存在しないライブラリが含まれるリスクへの対策としてのサプライチェーンセキュリティや、組織ポリシー違反を防ぐPolicy as Code、SLOに基づいた自動ロールバックなどが紹介されていました。 SRE自体がAI開発の文脈の中心にいる印象はあまりありませんが、AIが当たり前のように受け入れられ開発プロセスが成熟した今だからこそ、次のフェーズとしてSREを一連のデリバリーパイプラインに適切に組み込めるかどうかが今後の鍵になると実感しました。 SRE とプロダクトエンジニアは何故分断されてしまうのか 発表者: 渡邉美希パウラ さん speakerdeck.com ワンキャリアの渡邉美希パウラさんによる、SREチームとプロダクトチームの間に生じる「分断」の構造と解消アプローチについてのセッションでした。渡邉さん自身がSREチームからフロントエンドエンジニアへ異動した経験を持ち、両方の立場を知る当事者として語られていたのが印象的でした。 セッションでは、分断を引き起こす構造的要因として「受発注関係の固定化」「目指すベクトルのズレ」「1対多が生む情報の非対称性」の3つが挙げられていました。SREが横断的に複数プロダクトを担当する体制では、「パフォーマンス改善はSREの仕事」という意識が生まれやすく、受発注関係が固定化してしまう問題があります。また、プロダクトチームは「価値提供・スピード」、SREは「信頼性・安定」を重視するため、本来は同じユーザー起点であるはずなのに対立構造を生みやすいとも言えます。こうした分断は仲の良し悪しではなく、役割分担が生む必然的な帰結として整理されていました。私たちの組織にはSREこそいないものの、「過度な役割分担が心理的な壁になる」「受発注関係が固定化する」という現象はありありと想像できました。 解決アプローチとしては「バウンダリー・スパニング」- 境界を意図的になくし、人や情報を繋ぐことで価値を創造するというリーダーシップ理論を参考に、Reflecting(反射)、Mobilizing(結集)、Transforming(変形)という3つのステップで実践されていました。具体的には、インフラ変更のリポジトリをプロダクトチーム側に統合して境界線を排除したり、SLOを両チーム共通の評価指標として定例で議論したり、チーム間で人材を異動させたりといった施策が紹介されていました。 当事者として経験しながら、同時に観察者として構造を見抜いているような視点の鋭さに感銘を受けました。「足並みが揃いづらい」という課題感を、再現性のあるフレームワークで整理し、フェーズに分けてアクションを起こしていることも学び深い点でした。最後に「結局は全員が視座高くオーナーシップを持てば分断は問題にならない」と締められていましたが、シンプルながら本質を捉えた言葉であると感じ、自分自身も心がけていきたいと思いました。 開発チームが信頼性向上のためにできること: 医療SaaS企業を支える共通基盤の挑戦 発表者: kosui さん talks.kosui.me カケハシのkosuiさんによる、医療SaaS企業の認証権限基盤チームが信頼性向上に取り組んだ事例紹介でした。キーメッセージは「Embedded SRE不在でも、開発チームが設計を"自分ごと"として運用し続けることで信頼性は向上できる」というもので、自分たちの状況にも通じる内容でした。 医療SaaSは患者情報という極めて機密性の高いデータを扱うため、コンプライアンス、高可用性、トレーサビリティ、テナント分離といった厳しい品質要求があります。セッションでは、小規模チームがこれらの要求を満たすために採用した具体的な設計として、DBレベルでテナントを強制分離する行レベルセキュリティ(RLS)、過去データへの即座のアクセスを可能にするDelta Lakeのタイムトラベル機能、「何が起きたか」を完全に記録するドメインイベント、強い整合性と独立デプロイを両立するサービスベースアーキテクチャなど、詳細な設計内容が紹介されていました。これらの導入により、障害時の原因特定が2〜3時間から30分以内に短縮されるなど、具体的な成果も示されていました。 SREの役割や組織についてのセッションが多かった今回ですが、その中での具体的なアーキテクチャ設計の話は新鮮で聞き応えのあるものでした。ドメインごとに異なる品質要求を、限られた人的・時間的リソースの中でどう満たすか。その試行錯誤やトレードオフの判断は興味深く、同時に設計一つがプロダクトの今後を大きく左右する責任の重さも感じました。自分が設計に携わる際にも、SRE的な観点と「設計意図の浸透」を意識していきたいと思います。 おわりに AI時代におけるSREの再定義、チーム間の分断を防ぐ組織設計、開発チーム自身が信頼性を担うアーキテクチャ設計と、切り口はそれぞれ異なりますが、共通して感じたのは「信頼性は誰かに任せるものではなく、自分ごととして向き合うもの」というメッセージでした。 SRE Kaigi 2026全体を通して、SREという領域の幅広さと奥深さを知ることができました。また、「受発注関係の固定化」「目指すベクトルのズレ」「情報の非対称性」といった構造的な課題感は他のセッションでも度々取り上げられており、多くの組織で共通していることを実感しました。 個人的には、山口さんのセッションで語られた「AIの爆発的な生産性を、カオスではなく、持続可能なユーザー価値へと変換する」という言葉が印象に残っています。開発速度の向上も、最終的にはユーザーへの安定した価値提供があってこそ意味を持つ。その視点を忘れずに、これからは開発ライフサイクル全体の改善にも寄与していきたいと感じました。
アバター
はじめに こんにちは、リテールハブ開発部の杉森です。 近年、AIを活用した開発ツールが急速に普及しています。私たちのチームでも積極的にAIツールを導入し、要件定義でのユーザーストーリー作成、設計ドキュメントの生成、コードの自動補完、テストコードの生成など、各開発フェーズの作業効率化を図ってきました。 しかし、個々の作業は確かに早くなっているのに、プロダクト開発フロー全体を見ると期待したほどの生産性向上を実感できないという課題に直面しました。 本記事では、この課題に対するアプローチとして導入を検討しているAI-DLC(AI-Driven Development Lifecycle)について紹介します。 AI-DLCとは AI-DLCは、AWSが提唱するAIネイティブな開発方法論です。方法論のホワイトペーパーで理論的な枠組みが定義されており、これを実装するためのワークフローがaidlc-workflowsとしてGitHub上で公開されています。 AWSの公式ブログでは、現在のAI活用における2つのアンチパターンが指摘されています。 AI-Assisted: 人間が設計を主導し、AIはコード補完など狭い範囲の支援にとどまる。生産性向上は限定的で、AIの能力を十分に引き出せない AI-Managed: 複雑な問題をAIに丸投げし、自律的にすべてを解決することを期待する。出発点が曖昧なためAIが多くの仮定を立て、プロトタイプ以外ではほぼ機能しない AI-DLCは、これらのアンチパターンに対するアプローチとして設計されています。AIが作業計画の作成やタスク分解を主導し、人間がその内容を検証・承認し、AIが承認された計画に基づいて実行するというサイクルで、開発ライフサイクル全体を進めます。 AI駆動開発ライフサイクル(AWS公式ブログ) aidlc-workflows(GitHub) 従来の開発手法との違い AI-DLCは、既存の開発手法にAIを後付けするのではなく、AIを前提とした開発プロセスをゼロから設計しています。ホワイトペーパーでは、以下の設計思想が示されています。 AIが会話を主導する: 従来は人間がAIに指示を出していたが、AI-DLCではAIがタスク分解や提案を行い、人間は承認・判断に集中する Intent / Unit / Bolt: ビジネス目標(Intent)を作業単位(Unit)に分解し、数時間〜数日の短いサイクル(Bolt)で実装を回す。ScrumのSprintに近いが、サイクルが短い 各ステップで人間がチェックする: AIの出力を段階ごとに検証し、誤りを早期に検出する。ホワイトペーパーでは「損失関数のように機能する」と表現されている 設計技法を方法論に組み込む: ScrumやKanbanがチームに委ねていたDDD等の設計技法を、方法論の一部として標準化する aidlc-workflowsの設計原則 aidlc-workflowsは、上記の設計思想を実装するにあたり、以下の5つの設計原則に基づいています。 原則 説明 No Duplication 設定やルールを一箇所で管理し、重複を排除する Methodology First 特定のツールに縛られず、方法論そのものを軸にする Reproducible ルールを明文化し、使うAIモデルが変わっても結果がぶれないようにする Agnostic IDE・エージェント・モデルを問わず動作する Human in the Loop 重要な判断には必ず人間の承認を挟む 3フェーズ構成 AI-DLCは、以下の3つのフェーズで構成されています。 INCEPTION PHASE: WHATとWHYの決定 CONSTRUCTION PHASE: HOWの実装 OPERATIONS PHASE: デプロイと監視(aidlc-workflows上は未実装) INCEPTION PHASE 「何を作るか(WHAT)」「なぜ作るか(WHY)」を決定するフェーズです。方法論のホワイトペーパーでは「Mob Elaboration」というプラクティスとして定義されており、共有画面を使ってチーム全体でAIの質問と提案を検証します。AIがビジネス意図(Intent)を明確化する質問を投げかけ、ユーザーストーリー、非機能要件、リスク記述を生成し、凝集度の高い作業単位(Unit)へ分割します。 aidlc-workflowsでは、このフェーズが以下のステージに細分化されています。 ステージ 説明 Workspace Detection プロジェクトの状態を分析(新規/既存の判定) Reverse Engineering 既存コードベースの理解(Brownfieldの場合) Requirements Analysis 要件の収集と整理 User Stories ユーザーストーリーの作成 Workflow Planning 実行計画の策定 Application Design アプリケーション設計 Units Generation 作業単位への分割 CONSTRUCTION PHASE 「どう作るか(HOW)」を決定し、実際にコードを生成するフェーズです。方法論のホワイトペーパーでは、Domain Design(ビジネスロジックのドメインモデリング)→ Logical Design(非機能要件を含むアーキテクチャ設計)→ Code & Unit Tests(コードとテストの生成)→ Deployment Units(デプロイ可能な成果物の構築)という流れで進みます。「Mob Construction」でチームがリアルタイムで技術的決定とアーキテクチャの選択を行います。 aidlc-workflowsでは、このフェーズが以下のステージに細分化されています。 ステージ 説明 Functional Design 機能設計(ユニットごと) NFR Requirements/Design 非機能要件の設計 Infrastructure Design インフラ設計 Code Generation コード生成 Build and Test ビルドとテスト OPERATIONS PHASE デプロイと監視を担当するフェーズです。方法論としては定義されていますが、aidlc-workflowsには含まれておらず、将来的にワークフローが追加される予定です。 対応プラットフォーム 公式では以下のプラットフォームがサポートされています。 Kiro CLI Amazon Q Developer IDE plugin Kiro IDE(Coming Soon) 試してみる 導入の背景 私たちのチームの課題は、まさにAI-Assistedパターンに該当します。AIを個々の作業の効率化には活用できているものの、生産性向上は限定的にとどまっていました。 私たちのチームでは、Kiro CLIをすぐに使える環境ではなかったため、Claude Code向けにカスタマイズして使用しました。AI-DLCはツールに依存しない設計を謳っているため、ルールファイルを調整すれば他のAIツールでも問題なく適用できると考えています。 Claude Code向けのカスタマイズ 以下のようにClaude Code向けにカスタマイズしました。 1. カスタムコマンド(スキル)の作成 .claude/commands/aidlc.md にワークフロー定義を配置し、 /aidlc コマンドで起動できるようにしました。 .claude/ ├── commands/ │ ├── aidlc.md # メインワークフロー │ ├── aidlc-pr.md # PR作成用 │ └── aidlc-archive.md # アーカイブ用 └── aidlc-rule-details/ ├── common/ # 共通ルール ├── inception/ # INCEPTIONフェーズ ├── construction/ # CONSTRUCTIONフェーズ └── operations/ # OPERATIONSフェーズ 2. ルールファイルの分割 各ステージの詳細指示を .claude/aidlc-rule-details/ 以下に分割配置しています。これにより、AIが必要なタイミングで必要なルールのみを読み込み、コンテキストを効率的に使用できます。 クーポン機能を題材にした検証 AI-DLCの有効性を検証するため、小売向けアプリのクーポン機能開発を題材に検証を実施しました。 検証概要 対象システム: Flutter + Laravel + Vue.js + Goで構成されたマルチプラットフォームアプリ 題材: ポイント後付けクーポンと即時値引きクーポンの2種類 検証範囲: モバイルアプリ、管理画面、バックエンドAPI、バッチ処理 チーム構成: PdM1人+エンジニア2人 検証の進め方 「クーポン機能を追加したい」というビジネス意図(Intent)を起点に、AI-DLCのフェーズに沿って進めました。 INCEPTION PHASE(3人で実施): AIが要件を深掘りする質問を投げかけ、ユーザーストーリーや非機能要件を生成。PdMとエンジニアがその内容を検証・修正し、作業単位(Unit)に分割 CONSTRUCTION PHASE(1人で実施): Unitごとにドメイン設計、コード生成、テスト生成を実施。各ステージでAIの出力を確認し、承認・修正を繰り返した 今回は検証目的だったこともあり、各フェーズ半日ずつの計1日で実際に動くものまで作成できました。Inceptionフェーズの要件・設計をより作り込み、Constructionフェーズではガードレールの整備やAIが自律的に改善できる体制を組むことで、さらに短縮できる余地があると感じています。 実際の様子 AIからの深掘り質問(Inceptionフェーズ) Requirements Analysisでは、AIが要件の曖昧な部分を選択肢付きで質問してきます。以下はその一例です。 AI : クーポン利用状態の管理について確認させてください。ユーザーがクーポンを「利用」した後の状態管理はどうなりますか? A) 1回利用したら即座に使用済みになる(再利用不可) B) 有効期限内であれば何度でも利用可能 C) クーポンごとに利用回数を設定可能(1回、3回、無制限など) AI : クーポンの種類と適用範囲について確認させてください。 A) 全ての種類が両方のクーポンタイプで使用可能 B) クーポンタイプごとに使用可能な種類が決まっている このように、AIが仕様の選択肢を提示し、人間が判断するというサイクルでRequirements Analysisが進みます。初回の質問10問、追加の深掘り質問6問を経て、要件定義ドキュメントが生成されました。 人間による設計修正(Inceptionフェーズ) Application Designでは、AIが設計の選択肢を提示し、人間が判断するケースがありました。 人間 : アクティブユーザーではないユーザーにもレコードが作成されてしまいませんか? AI : 2つの選択肢があります。 A) クーポン公開時に全ユーザー分のuser_couponsレコードを作成 B) クーポン利用開始時にのみuser_couponsレコードを作成 人間 : B 生成されたユーザーストーリー(Inceptionフェーズ) User Storiesでは、管理者向け6件、会員ユーザー向け6件、システム向け1件の計13件が生成されました。以下はその一部です。 US-01: クーポン新規作成 As a 管理者 I want to 管理画面から新しいクーポンを作成したい So that 会員ユーザーに対してキャンペーンを提供できる Acceptance Criteria: クーポンタイプ(ポイント後付け/即時値引き)を選択できる クーポン名と説明文を入力できる 有効期限(開始日・終了日)を設定できる 対象店舗を選択できる 生成されたコード(Constructionフェーズ) Constructionフェーズでは、Unitごとにドメイン設計 → コード生成 → テスト生成が進みます。最終的に以下の規模のコードが生成されました。 Unit 対象 生成ファイル数 主な成果物 Backend Laravel 51ファイル Enum, Model, Migration, Service, Controller, Test Dashboard Vue.js 16ファイル Composable, Component, Schema, Page Mobile Flutter 38ファイル Entity, Repository, Provider, Widget, Page わかったこと / 今後の展望 良いと感じた点 実際にAI-DLCを触ってみて、以下の点が良いと感じました。 Human in the Loopの実現: AIが実行し、人間が監視するという関係性が明確。各ステージで人間の承認が必要なため、重要な意思決定は人間がコントロールできる コンテキストの保存と再開: aidlc-state.md でプロジェクトの状態を追跡しているため、セッションが途切れても前回の続きから再開できる ドキュメント化による追跡可能性: audit.md にすべてのやり取りが記録されるため、なぜその決定をしたのかを後から追跡できる 適応的なワークフロー: プロジェクトの複雑さに応じて、実行するステージが自動的に調整される 試した上で見つかった課題 Inception前の準備の必要性 今回「クーポン機能を追加したい」というリクエストからInceptionを開始しましたが、背景知識や「なぜこの機能が必要なのか」がアウトプットに反映されにくいことがわかりました。また、要件の解像度が低い状態でInceptionを始めると、議論が発散しやすくなります AI-DLCのInceptionに入る前に、ビジネス背景や目的を整理するステップが必要だと感じました。 仕様とAI実装のギャップ Inceptionフェーズで仕様を決め切った上でも、以下の2つの問題が発生しました。 仕様の記載漏れ: Inceptionフェーズで決めた仕様に漏れがあり、Constructionフェーズで初めて気づくケース。例えば、APIレスポンスのラッパー形式やお気に入り店舗のパラメータなど、実装段階で判明した考慮漏れがありました 仕様通りに実装されない: 仕様として記載されているにもかかわらず、AIが異なる実装をするケース。例えば、既存の認証方式と異なるパターンで実装したり、既存のアーキテクチャパターンに従わない実装が生成されることがありました 前者は要件定義やアプリケーション設計の精度を上げていく必要があります。今回検証だったので細部まで確認できていないところがありました。そのため、実業務に導入した場合はよりこの部分に時間を使うべきだと思いました。 後者はモデルの進化を待ちつつ、コンテキストの渡し方の工夫や、実装が仕様に準拠しているかを監査するサブエージェントの整備など、ガードレールを張っていくことが必要だと感じました。 コンテキスト管理の課題 AIツール固有の課題として、コンテキスト管理の難しさがあります。実装フェーズではコードの読み書きが多く発生するため、auto-compact(コンテキストの自動圧縮)が頻発しました。その結果、audit.mdへの書き込みが不安定になったり、要件定義ファイルへの指摘を繰り返してもアウトプットに反映されないことがありました。 対策として、コンテキストの使用量を抑えるためにルールファイルを分割して必要なタイミングでのみ読み込む方式にしたり、サブエージェントを活用して処理を分散させるなどの工夫が必要です。 レビュー負荷への対応 AIのアウトプット量が増えることで、人間のレビュー負荷が増大するという課題があります。この課題に対しては、以下のアプローチを検討しています。 レビューを軽減するプロセスの構築: 自動テストやLintの活用 AIの出力品質を上げる工夫: プロンプトの改善、ルールの整備 段階的なレビュー: 各ステージでの承認による分散 これらの最適解は、チームやプロダクトによって異なるため、継続的に改善していく必要があります。 最後に AI-DLCは、AIを活用した開発における「ボトルネックを特定し、解消していく」ためのフレームワークとして有望だと感じています。 今回見えてきた課題はAI-DLCのフレームワーク自体の問題ではなく、AIと人間が協働する上で必然的に発生する問題です。今後も継続的に活用しながら、チームに最適な形にカスタマイズしていきたいと考えています。
アバター
はじめに こんにちは。開発部でiOSエンジニアをしている野口です。 ヘルシカiOSアプリの開発を担当しており、アプリ内にはすでに「ヘルシカ」をはじめとしたキャラクターが実装済みです。 これらのキャラクターを生かしてよりユーザーに愛着を持っていただけるようにするため、アニメーションを導入したいと考えています。 Riveとは Riveとは、Webやアプリ、ゲーム向けの「インタラクティブなアニメーション」を作成・実装するためのデザインツールです。 Riveについて調べたところ、アニメーションの作成コストが低く、パフォーマンスがいいという記事を何記事か拝見し、導入を検討しました。 Riveの導入を断念した理由 Zombie Objectの発生 Riveの検証の際に、Zombieが発生していることがわかりました。(画像はDebug Memory Graphを使っています) Zombie とは、通常であれば削除されるはずの不要なオブジェクトが、何らかの理由でメモリに残り続けてしまう状態のことです。 つまり、プログラムのどこからも参照されていないにもかかわらず、メモリを占有し続けている状態です。この状態でアプリを使うと、思わぬクラッシュが発生するリスクがあります。 なお、XcodeのデバッグビルドでZombie Objectsをオンにしていたところ、アプリがクラッシュしてしまい、Zombieが発生していることが判明しました。 Zombie Objectsを有効にするには、XcodeでプロジェクトのScheme設定から[Product] > [Scheme] > [Edit Scheme]を選択し、[Run] > [Diagnostics]タブ内の"Enable Zombie Objects"にチェックを入れます。 Duolingoをはじめ他の企業が使用しているため、実際はこのZombieはそこまで問題ないのかもしれませんが、どうしてもRiveでないとできないことがある場合でない限りは使わないほうが良いと判断し、今回は導入をやめました。 Riveのissueを見るとクラッシュの報告はいくつか上がっており、まだ不安定なパッケージなのかもしれません。 代替アニメーション形式の検討 Riveの導入を断念したため、代替となるアニメーション形式を検討しました。 Riveでのアニメーションは断念しましたが、Riveでのアニメーション作成はAfter Effectsを使う場合と比べてデザイナーの学習コストが低いため、アニメーションの作成自体はRiveで行います。その上で、iOSアプリへの組み込み方法として以下の形式を検討しました。 GIF CPU: 低 メモリ: 多 懸念点: 24fpsで実験したところカクつきを感じるが、フレーム数を増やすとパフォーマンスに影響が出る可能性あり APNG CPU: ほぼ0 メモリ: 多 懸念点: 透過する場合はパフォーマンスが悪くなる可能性があるらしい(要調査)、Riveから直接出力できず、変換用のアプリケーション(After Effects)を使用する必要がある MP4 CPU: 再生時に上昇 メモリ: 少 懸念点: Riveから直接出力できず、変換用のアプリケーション(After Effects)を使用する必要がある。また、デザイナーがAfter Effectsに慣れていないため、作成時の学習コストや工数がAPNGよりもかかるらしい 以下の画像の通り、MP4は動画再生時に一瞬CPUが上がり、再生中はほぼ0になります。 Rive(参考) CPU: 高 メモリ: 中 懸念点: 開発側でZombie問題あり 検証結果まとめ 検証結果を以下の表にまとめました。 先ほどの項目にデザイン工数を追加しています。 Riveでアニメーションを作成した際のデザイン工数を1として、各形式のデザイン工数を比較しています。工数はデザイナーの主観です。 GIF : Riveから直接出力できるため、デザイン工数は1 APNG・MP4 : Riveから直接出力できず、変換用のアプリケーションを使用する必要がある。 項目 GIF APNG MP4 Rive CPU 低 ほぼ0 再生時に上昇 高 メモリ 多 多 少 中 懸念点 24fpsでカクつきあり 透過時の性能低下(要調査) After Effects学習コスト Zombie問題 デザイン工数 1 1.2 1.3 1 結論:APNGを採用 エンジニアとデザイナーが協議した結果、 APNGを採用 することにしました。 APNGを選んだ理由 CPU負荷がほぼ0で、パフォーマンスへの影響が最小限 GIFと比較して 滑らかなアニメーション が可能 Riveで作成したアニメーションを変換する必要はあるがMP4よりもわかりやすいため、デザイナーの工数は低いらしい 透過が必要になった場合にも対応可能(要調査) 他形式を採用しなかった理由 GIF - 検証時点のフレームレート(24fps)では若干カクつきを感じる - 他形式と同様のパフォーマンスを出すには、CPUやメモリ使用量が増える MP4 - デザイナーがAfter Effectsに慣れていないため、作成時の学習コストや工数がAPNGよりもかかるらしい - 透過ができない Rive - アニメーションの動きは一番綺麗だが、他形式との差は誤差の範囲 - Zombieが発生しており、Xcodeビルドではクラッシュする(TestFlight配布では問題なし) - 「Riveでないとできない」という理由がなければ使わない方がいいと判断 終わりに 今回はRiveの導入を見送り、APNGを採用することにしました。 Riveはアニメーションの動きが綺麗で魅力的なツールです。しかし、iOS版ではZombie Objectの発生によるクラッシュリスクがあり、現時点では安定性に不安が残ります。今後のアップデートでこの問題が解消されれば、再度導入を検討したいと思います。 アニメーション形式の選定は、パフォーマンス・開発工数・デザイン工数など、さまざまな観点から総合的に判断する必要があります。この記事が同じような課題を抱えている方の参考になれば幸いです。
アバター
はじめに こんにちは! 開発1部デリッシュキッチンの蜜澤です。 現在はデリッシュリサーチという、食トレンド分析ツールの開発を行っています。 本記事では、デリッシュリサーチで提供するデータの品質担保をするために行なったことを紹介させていただきます。 データ品質担保の必要性 デリッシュリサーチは食トレンドを分析するために、ダッシュボードで様々なデータを提供しています。 データが間違っていると、誤った意思決定につながるおそれがあるため、データの正確性に細心の注意を払う必要があります。 また、提供するデータが多岐にわたるため、テーブルの依存関係が複雑になっていきつつあり、放置しておくと集計のロジックを間違えてしまう可能性や、似たようなデータで整合性が保たれない可能性があります。 実際のER図を一部抜粋すると以下のようになっており、結構複雑です。 複雑な集計の中でも安心してサービスをご利用していただくために、データの品質の担保には特に注力しています。 実際に取り組んだこと 以下の2つの取り組みを行いました。 テーブル作成処理の単体テストの作成 元となる検索ログデータの増減に対するアラート作成 1つずつ詳細を紹介させていただきます。 テーブル作成処理の単体テストの作成 前述のER図を見てわかるようにテーブル数が多く、ETLが複雑になってしまうだけではなく、各テーブル作成の処理でも複雑なことをしているため、各テーブル作成の処理ごとに単体テストを行うようにして、テーブルごとに品質を担保できるようにしました。 入力データと期待される出力データを作成し、入力データを使用して実際のテーブル作成処理を行い、出力されたデータと期待される出力データを比較し、完全に一致しているかどうかを確認します。 具体例を用いて、どのようにテストを行なったのか紹介します。 ETLの作成はdatabricksを使用しています。 処理内容 検索ログデータから指定した期間(2024-01-01~2025-12-31)のデータを抽出し、検索ワードカラムの前後のスペースを削除する。 ※今回は簡単な例にしていますが、実際にはもっと複雑な処理を行なっています。 コードの実装例 実際に作成したテストのコードを簡略化したものを紹介します。 期待される出力データを作成して、データフレームに格納します。 最終的にデータフレームが一致しているかのテストを行うため、全てのカラムでソートを行なっています。 columns = [ 'event_date' , 'user_id' , 'search_word' ] expected_data = [ ( '2024-01-01' , 2 , 'キャベツ' ), ( '2024-01-01' , 5 , 'キャベツ' ), ( '2024-01-01' , 6 , 'キャベツ' ), ( '2024-01-01' , 7 , 'キャベツ' ), ( '2024-01-01' , 8 , 'キャベツ' ), ( '2024-01-01' , 9 , 'キャベツ' ), ( '2024-01-01' , 10 , 'キャベツ' ), ( '2024-01-01' , 11 , 'キャベツ 豚肉' ), ( '2024-01-01' , 12 , 'キャベツ 豚肉' ), ( '2025-12-31' , 3 , 'キャベツ' ) ] expected_df = pd.DataFrame(expected_data, columns=columns).sort_values([ 'event_date' , 'user_id' , 'search_word' ], ascending=[ True , True , True ]).reset_index(drop= True ) 入力データ(検索ログデータ)を作成します。 以下の項目を確認できるように作成します。 2024-01-01~2025-12-31のデータのみが抽出されるか 前後のスペースが削除されるか 前後ではない場所にスペースが入っている場合に削除されないか columns = [ 'event_date' , 'user_id' , 'search_word' ] input_data = [ # 期待通りの期間のデータが入るかの確認 ( '2023-12-31' , 1 , 'キャベツ' ), ( '2024-01-01' , 2 , 'キャベツ' ), ( '2025-12-31' , 3 , 'キャベツ' ), ( '2026-01-01' , 4 , 'キャベツ' ), # スペース削除の確認 ( '2024-01-01' , 5 , ' キャベツ' ), ( '2024-01-01' , 6 , 'キャベツ ' ), ( '2024-01-01' , 7 , ' キャベツ' ), ( '2024-01-01' , 8 , 'キャベツ ' ), ( '2024-01-01' , 9 , ' キャベツ ' ), ( '2024-01-01' , 10 , ' キャベツ ' ), ( '2024-01-01' , 11 , 'キャベツ 豚肉' ), ( '2024-01-01' , 12 , ' キャベツ 豚肉 ' ) ] input_df = pd.DataFrame(input_data, columns=columns) spark_input_df = spark.createDataFrame(input_df).createOrReplaceTempView( "spark_input_df" ) 作成した入力データを開発環境のDeltaテーブルへ書き込みます。 input_data = spark.sql((f """ SELECT event_date, user_id, search_word FROM spark_input_df """ )) input_data \ .write \ .format( "delta" ) \ .mode( "overwrite" ) \ .partitionBy( "event_date" ) \ .save(delta_table_path/search_log) 実際のETLの処理を開発環境で実行します。 処理内容の詳細は後述します。 args = { "env" : "dev" , "date" : "2025-12-31" } dbutils.notebook.run( "../01_SearchLog" , 0 , args) 上記の処理で書き込まれたデータを読み込み、データフレームに格納します。 データフレームの完全一致比較を行うため、全てのカラムでソートしています。 output = spark.sql((f """ SELECT event_date, user_id, search_word FROM delta_table_path/search_data ORDER BY event_date, user_id, search_word """ )) output_df = output.toPandas() 出力データと期待出力データを比較します。 def assert_output_equals_expected (output_df: pd.DataFrame, expected_df: pd.DataFrame): output_df = output_df.reset_index(drop= True ) try : assert_frame_equal(output_df, expected_df) print ( "データフレームが完全に一致しています。" ) except AssertionError as e: dbutils.notebook.exit(f "データフレームが一致しませんでした: \n {e}" ) assert_output_equals_expected(output_df, expected_df) 実際に実行した01_Searchの処理の内容はこちらになります。 dbutils.widgets.text( "env" , "dev" ) dbutils.widgets.text( "date" , "yyyy-MM-dd" ) end_date = dbutils.widgets.get( "date" ) env = dbutils.widgets.get( "env" ) search_data = spark.sql(f """ select event_date, time, user_id, regexp_replace(search_word, '^[ \\ u0020 \\ u3000]+|[ \\ u0020 \\ u3000]+$', '') as search_word --前後の半角スペースと全角スペースを削除 from delta_table_path/search_log where event_date >= '2024-01-01' and event_date <= '{end_date}' """ ) search_data \ .write \ .format( "delta" ) \ .mode( "overwrite" ) \ .partitionBy( "event_date" ) \ .save(delta_table_path/search_data) 課題・改善点 1つの処理を変更したことで、他の処理にも影響が及ぶ可能性があるため、基本的には全ての処理に対してテストを実行するのですが、量が多いため、現状だと全て完了するまでに1時間半ほどかかってしまいます。 依存関係を整理して、必要最低限のもののみテストを実施するようにすれば改善できるとは思います。 元となるログデータの増減に対するアラート作成 テーブル作成処理の単体テストの作成によって、デリッシュリサーチのために作成するテーブルのデータの品質は担保されるようになりますが、元の検索ログ自体に不具合が起きた場合には対処することができません。 例えば、検索ログデータのETLが遅延して、件数が正常ではなかった場合は、今回紹介した単体テストのみでは、対処することができません。 そこで、元の検索ログデータを使用して、検索ログ数の前週比を毎日集計して、閾値を上回る増加率・減少率だった場合にアラートを出す仕組みを作成しました。 アラートはPdMの人でも触れるようにするために、redashで作成しました。 以下のようなクエリを作成して、alert_flag=1となるレコードがあった場合にアラートを出すようにしました。 アラート用のSQL 実際にはユーザー属性ごとのログ数も確認しますが、今回は全体のログ数のみを確認するクエリを紹介します。 WITH search_log AS ( SELECT event_date, user_id, search_word FROM search_log WHERE event_date >= DATE_SUB(FROM_UTC_TIMESTAMP( CURRENT_DATE (), ' Asia/Tokyo ' ), 8 ) AND event_date < FROM_UTC_TIMESTAMP( CURRENT_DATE (), ' Asia/Tokyo ' ) ), daily_count AS ( SELECT event_date, COUNT (*) AS count FROM search_log GROUP BY event_date ) growth_rate AS ( SELECT event_date, count AS current_count, LAG( count , 7 ) OVER ( ORDER BY event_date) AS prev_week_count, ROUND (( count - LAG( count , 7 ) OVER ( ORDER BY event_date)) * 100.0 / LAG( count , 7 ) OVER ( ORDER BY event_date), 2 ) AS growth_rate FROM daily_count ) SELECT event_date, current_count, prev_week_count, CASE WHEN growth_rate < -20 OR growth_rate > 20 THEN 1 ELSE 0 END AS alert_flag FROM growth_rate 課題・改善点 現状は過去のデータを元に1年のうち年n回程度発生する増加率・減少率をalert_flagの閾値に設定していますが、このnが決め打ちになってしまうので、適切な閾値を設定するのが難しいです。 閾値を厳しくし過ぎると本当に問題が起きている時に気付けず、閾値を緩くしすぎてしまうと頻繁にアラートが鳴り信憑性がなくなってしまうので、運用していく中でちょうど良い閾値を見つけていきたいです。 まとめ データの品質担保をするために取り組んだことについて紹介させていただきました。 課題や、レビューとテスト作成にかなりの時間がかかるといった辛い点はありますが、データの品質担保ができているため、実施してよかったと思っています! 同じような課題を持っている方の参考になれたら幸いです。 最後まで読んでいただきありがとうございました。
アバター
はじめに 開発本部でデリッシュキッチンアプリ課金ユーザー向けの開発を担当している hond です! 2025年6月から社内勉強会の一つとして開催している「AIツールを活用した開発効率化勉強会」が開催から半年かつ現状の参加メンバーで一周したので、そもそもどのような勉強会だったのかやアンケートの結果からどのような成果が得られたのかについて振り返ろうと思います。 AIツールを活用した開発効率化勉強会 現在エブリーの開発部では、入社時に振り分けられる勉強会グループで開催する定期的な勉強会と勉強会のテーマに興味があるメンバーが集まって行う任意の勉強会があります。今回紹介する「AIツールを活用した開発効率化勉強会」は後者にあたり、AIに興味があるメンバー18人ほどが集まり開催しているものになります。 勉強会の目的 勉強会の目的は以下の3点として運営しました。 AIに関するインプット意欲を向上 AIツールをとりあえず試すマインドの向上 実務の中で活用していく意欲向上 2025年4月に 開発部とPdMにCursorが導入されました 。当時はCursor以外にもClaude CodeやClineなど多様な選択肢があり、AIツールは進化が早く個人でのキャッチアップには限界がありました。 そこで勉強会では、参加メンバーの多角的な視点でいろいろなツールや活用方法を吸収できる場にしたいと考えました。 具体的には、ハンズオン形式で実際に使ってみることや、他のメンバーから便利なユースケースを共有してもらうことで、活用の促進とユースケースの増加を狙いました。 また、新しいツールの導入には一定のハードルがあるため、「とりあえず試す」ことでそのハードルを緩和することも意識しました。 実施形式 実施形式は以下のように設計しました。 隔週1回で開催 発表者は参加者で順番に行う 事例紹介形式&ハンズオン形式 導入してみたけどうまくいかなかった例、うまく導入できていない例紹介タイムを設ける Cursorに限らずClaude CodeやRoo Code、ClineなどもOK 目的にある「とりあえず試す」を実現するため、勉強会ではなるべくハンズオン形式を採用するよう設計しました。 基礎的な内容よりも「〇〇をしたら△△になる」といった具体的なユースケースにフォーカスすることで、実務への応用をイメージしやすくしています。 また、うまくいった事例だけでなく、うまくいかなかった事例も積極的に共有してもらうようにしました。変化が激しく簡単に正解に辿りつけるフェーズではないと感じていたので失敗例を参加メンバーで考察することで、より深い理解に繋げることを狙っています。 Coding Agentにはそれぞれ良し悪しがあるため、ツールを絞らずそれぞれの長所や短所を学び、自分が使っているツールにどう活かせるか考えられる場にしました。 発表内容 半年間で行われた発表テーマは以下の通りです。 発表日 テーマ 6/16 MCP使い倒してコンテキストスイッチ最小限に 6/30 Cursor × iOS開発私はこうやってます 6/30 A/Bテストの実験設定をcursorに任せてみる 7/14 Claude Codeは言語化ムズイがいい感じという話 7/14 MCPサーバーを自作してみる 7/28 KiroでSpec駆動開発 7/28 Claude Codeのサブエージェント 8/25 CodeRabbitについて調べてみた 8/25 LLMで爆速論文検索 9/8 spec-kitを使ってみよう 9/8 コンテキストエンジニアリング 10/20 Amazon Bedrock AgentCoreでエージェント開発を加速させよう 11/17 いろいろあるよ!AWS MCP Servers 11/17 コンテキストエンジニアリングについて真面目に考える 12/1 Google Workspace Flows アルファ版 12/1 AIカンパニーをつくって遊んでみる 12/15 AWS Transform Custom Kiroやspec-kitをはじめとする開発体験を向上するツールを試す発表や、普段業務で使っているClaude CodeやCodeRabbitについての深掘りなど、幅広いジャンルの発表が行われました。 Amazon Bedrock AgentCoreを実際に参加者全員で作ってみるハンズオン形式の回もとても好評でした。 発表テーマを振り返ると、改めてAIが開発に与えるインパクトの大きさを感じます。同時に、いろいろなツールが出過ぎて手探りな期間だったなとも思います。 これだけのテーマを個人でキャッチアップするのは難しいので、勉強会という形で吸収できてよかったです。 アンケート結果 勉強会の振り返りとして、参加メンバーにアンケートを実施しました。14人から回答があり、以下の3項目については5段階評価で回答してもらいました。また、いくつか自由記述を設けて今後の改善に向けた意見を募りました。 5段階評価については下記の結果が得られました。 項目 1 2 3 4 5 総合満足度 0人 0人 3人 7人 4人 AIツール活用の参考になったか 0人 0人 3人 5人 6人 開発効率化の助けになったか 0人 1人 2人 9人 2人 全体的に高評価でしたが、特に「AIツール活用の参考になったか」に関してはNPS 21.5%という高い数字を出すことができました。 自由記述でのポジティブな意見としては、「自分ではキャッチアップしきれない部分について情報をインプットすることができた」や「色々なAIツールがある中でどのようなものがあるのかを知るとっかかりになったのが良かった」といった声が上がりました。どちらも勉強会を開催するにあたって目的にしていた部分が反映できたことが確認できる意見でした。 一方で、改善点として「回を重ねるごとに難易度が上がっていき、発表ネタを作るのが大変になっていた」という意見が複数見られました。確かに、勉強会を開始した当初と比較すると新しいツールや大きな開発体験の変化を伴うことは減ってきたのでテーマ的に一歩深掘りしたものが必要になっていました。あくまで、任意の勉強会なので負担にならないようある程度コントロールできるとよかったのではないかと思っています。 まとめ 他の勉強会と比較してデファクトスタンダードが出ていなかったりとても変化が激しい期間の勉強会の運営だったので、参加メンバーが満足いくものに設計できるか不安でしたが満足度もそれぞれ目的としていた指標に対しても高い評価が得られた結果となりよかったです。 ハンズオン形式に関しては発表者教材を準備しないといけないので負荷になっていたとは思いますが、聞いた内容を確実にアウトプットする機会や勉強会中にメンバー同士で議論できる場の提供になりプラスに働いたのではないかと思っています。 今後はより各チームのメンバーのユースケースの共有を活性化することで、よりお互いに刺激し合える勉強会を設計していきたいと思います。 社内勉強会の設計や運営の参考になったら幸いです!
アバター
はじめに デリッシュキッチンのiOSアプリを開発している成田です。 デリッシュキッチンではデザイン管理にFigmaを利用し、実装時にはDev Mode MCPサーバーを活用して精度を高めています。しかし、実際にビルドして確認してみると、レイアウト崩れが生じたりで期待するUIになっておらず、手動での「スクショ撮影→Cursorへ添付→指示→確認」という反復作業が発生していました。 この課題を少しでも解決するため、XCUITestによるスクリーンショット自動取得とCursorを組み合わせたUI自己改善ワークフローを構築しました。 概要 今回自動化したのは主に以下の2つです: スクリーンショットの取得 : UI実装後に、自動的にアプリをビルド、画面をナビゲーションしてスクリーンショットを取得 画像の読み込み : スクリーンショット取得後、画像を読み込んで分析し、レイアウト崩れなどを検出して実装を修正する 実現したワークフロー: UI実装依頼 ↓ AIがUIコードを生成 ↓ xcodebuildでビルド・UITestの実行 ↓ UITestが画面をナビゲーションしてスクリーンショット取得 ↓ スクリーンショットパスを一時ファイルに保存 ↓ 画像を読み込んで分析 ↓ 必要に応じてレイアウト崩れなどを自動修正 手順 このワークフローを実現するために必要な設定や実装手順は以下の通りです。 1. XCUITestの実装 XCUITestの標準API( XCUIApplication の screenshot() メソッド)を使用してスクリーンショットを取得します。 実装すべき内容は以下の通りです: スクリーンショット取得テスト : 画面をナビゲーションしてスクリーンショットを取得するテストメソッドを実装します。このテストメソッド内で以下の2つのヘルパーを使用します スクリーンショットヘルパー : XCUIApplication の screenshot() メソッドを使用してスクリーンショットをファイルに保存するヘルパークラスを実装します UI要素待機ヘルパー : UI要素が表示されるまで待機するユーティリティを実装します。XCUITestには自動待機機能がありますが、ネットワーク通信や非同期処理による画面更新の遅延がある場合などには、明示的な待機処理が必要です。これにより、テストの安定性を確保できます これらのファイルをUIテストのターゲットに追加します。 2. スクリーンショット取得スクリプトの作成 スクリーンショット取得は、 xcodebuild コマンドを使ってコマンドラインからビルド・テストを実行することで実現します。 スクリプト( scripts/cursor-screenshot.sh )で実装する処理は以下の通りです: 画面名の受け取りと環境変数の設定 : スクリプトは引数として画面名を受け取り、環境変数として設定します。この環境変数はUITestに渡され、どの画面にナビゲーションするかを決定します アプリのビルド : xcodebuild build でアプリをビルドします UIテストの実行 : xcodebuild test で DynamicScreenshotTests.testTakeScreenshotForScreen を実行します。UITestは環境変数を参照して、画面名に応じて適切な画面にナビゲーションしてからスクリーンショットを取得します スクリーンショットファイルの検索とパスの保存 : テスト実行後、最新のスクリーンショットファイルを検索し、パスを /tmp/cursor_screenshot_info.txt に保存します。このファイルを参照してスクリーンショットを読み込むようにします 3. Cursorのルール設定 - .cursor/rules/my-custom-rule.mdc にUI実装後の自動ワークフローのルールを追加 - UI実装後のワークフロー - UIを新規実装または大幅に変更した後は、必ず以下を実行すること: - 大幅な変更の定義: - 大幅な変更の定義: レイアウト、UIコンポーネント、View構造、スタイルなど、レイアウトや見た目に影響を与える変更 1. 当該の画面に対して`scripts/cursor-screenshot.sh`を実行 2. 取得した画像をもとに元のデザインと比較してレイアウト崩れなどがあれば修正する 3. 修正後、再ビルドして確認 4. 必要に応じて、上記のステップ(1-3)を繰り返す(最大n回まで) これによって期待するワークフローが実現できました。 おわりに UIを実装した後は、手動でビルドして確認し、レイアウト崩れなどを発見したら修正依頼を投げるという作業を繰り返す必要がありました。 XCUITestの仕組みを使ったワークフローによってUI実装から自己改善までを自動化することができるようになったので、プロンプトで修正依頼を投げる量を最小限に留めることができるようになりました。 とはいえ、UITestは壊れやすくワークフローの途中でこけることも度々起こりうるので、その辺はデメリットかなと思います。
アバター
目次 はじめに 注意事項 Echo v5の主な変更点 Echo v4からv5への移行しながら変更点を確認する バージョン更新とecho.Contextの変更 Routerのカスタマイズ (Interface + DefaultRouter) StartConfigを用いたサーバー起動 デフォルトロガーがslog.Loggerに変更 レスポンス情報の取得方法 (UnwrapResponse) URLパラメータの埋め込み方法変更 echo.POSTのような http.MethodXXX のヘルパーが廃止 まとめ はじめに こんにちは、開発本部開発1部トモニテグループのエンジニアの パンダム/rymiyamoto です。 2026/01/18 より Echo v5 がリリースされました 🎉 弊社プロダクトの多くが依存しているフレームワークなだけに、最新バージョンへの移行パスをいち早く探っておきたいところ。 さっそく、主要な変更点や所感をレポートします。 Echoを使って開発している方も多いと思いますので、その手助けになればと思います。 github.com 注意事項 本記事はEcho v4の基本的な知識がある読者を対象としています。 また執筆は2026/01/22時点のもので、Go 1.25.6をベースにしています。 リリース直後のv5には、今後も破壊的な変更が加わる余地が残されています。そのため公式からは、商用利用については 2026/3/31まで待機すること が推奨されている点に注意が必要です。 (ちなみにv4のサポートは2026/12/31までです) 公式からの引用↓ v5.0.0 was release on 2026-01-18. v4 will be supported with security updates and bug fixes until 2026-12-31. Until 2026-03-31, any critical issues requiring breaking API changes will be addressed, even if this violates semantic versioning. If you are using Echo in a production environment, it is recommended to wait until after 2026-03-31 before upgrading. Echo v5の主な変更点 今回の変更点をまとめてみると、主に以下の5点に集約されます。 【破壊的変更】 echo.Context がインターフェースから構造体に変更 将来的な機能追加を容易にする ための大きな設計変更です 【新機能】Router のインターフェース化による拡張性の向上 独自のルーター実装が可能になり、 正規表現ルーティング なども導入しやすくなります 【仕様変更】サーバー起動設定の構造体化 ( StartConfig ) アドレスやタイムアウト設定が構造体に集約され、 設定の可読性が向上 します 【破壊的変更】Go標準ライブラリ log/slog をネイティブサポート サードパーティ製ライブラリなしで、 モダンな構造化ログ が扱えるようになります 【破壊的変更】APIの一貫性向上とメソッド整理 全体的なシグネチャが見直され、より 洗練された開発体験 を提供します 概要からみてこれまでv4の期間が長かったのもあり大きな進歩だなと感じました。 今までは標準で対応しづらい部分はサードパーティを利用していたので、ログ周りが自分としては一番ありがたいです。 他にも設定周りが統一化されているのでよりコードの可読性が向上しそうな予感がしますね。 もちろん他にもいくつかAPIの変更点がありますが、詳細については実際にv4で動いているコードの修正をしながら気づいた部分を紹介していきます! (紹介しきれていない部分は下記のドキュメントを読んでみてください) github.com Echo v4からv5への移行しながら変更点を確認する バージョン更新とecho.Contextの変更 公式に記載されている通りの方法でバージョン更新や最低限の修正を行うことができます。 # v4 から v5 への移行 go get github.com/labstack/ echo /v5 # サードパーティの更新(対応されているサードパーティ系のライブラリは更新) go get github.com/labstack/echo-contrib go get github.com/labstack/echo-jwt/v5 # 一括置換 # echo.Context -> *echo.Context find . -type f -name " *.go " -exec sed -i ' s/ echo.Context/ *echo.Context/g ' {} + # echo/v4 -> echo/v5 find . -type f -name " *.go " -exec sed -i ' s/echo\/v4/echo\/v5/g ' {} + github.com 実際のコードの変更は以下のようになり、大多数の変更箇所になるかと思います。 // v4 func MyHandler(c echo.Context) error { return c.JSON( 200 , map [ string ] string { "hello" : "world" }) } // v5 func MyHandler(c *echo.Context) error { return c.JSON( 200 , map [ string ] string { "hello" : "world" }) } これだけだと型定義が変わっただけか〜となりますが、この変更による恩恵は新機能追加時に破壊的な変更にならないようにするための大事な変更と言えます。 まずecho.Contextがインターフェースだった場合、将来的に新しい機能(メソッド)を追加すると、そのインターフェースを実装しているすべてのコード(自作のContextラッパーやモックなど)がコンパイルエラーになります。 これは「破壊的変更」にあたるため、次のメジャーバージョンアップ(v6~)まで機能追加がしにくくなります。 構造体( *echo.Context )に変更することで、本体に新しいメソッドを追加しても、既存の利用側のコードはそのまま動作します。 これにより、v5の期間中にマイナーアップデートで便利な新機能をどんどん追加できるようになります。 Routerのカスタマイズ (Interface + DefaultRouter) Routerのインターフェース化により、独自のルーター実装が可能になりました。 これにより、正規表現ルーティングや、特定のアプリケーションでは不要な機能(ワイルドカードや 405 Method Not Allowed のハンドリングなど)を削ぎ落とし、より効率的なルーティング処理を実現できるようになるのではないかと思います。 まだこれからだとは思いますが、 RouterConfig が拡張されていくことにより様々な恩恵を受けやすくなりそうです。 // v4 type Router struct { ... } func NewRouter(e *Echo) *Router func (r *Router) Add(method, path string , h HandlerFunc) func (r *Router) Find(method, path string , c Context) func (r *Router) Reverse(name string , params ... interface {}) string func (r *Router) Routes() []*Route // v5 type Router interface { Add(routable Route) (RouteInfo, error ) Remove(method string , path string ) error Routes() Routes Route(c *Context) HandlerFunc } type DefaultRouter struct { ... } func NewRouter(config RouterConfig) *DefaultRouter func NewConcurrentRouter(r Router) Router // NEW type RouterConfig struct { NotFoundHandler HandlerFunc MethodNotAllowedHandler HandlerFunc OptionsMethodHandler HandlerFunc AllowOverwritingRoute bool UnescapePathParamValues bool UseEscapedPathForMatching bool } github.com StartConfigを用いたサーバー起動 新しく追加された echo.StartConfig 構造体を使用する形式でもサーバーを起動することができます。 もちろんこれまで通りの echo.Start を使用することもできますが、アドレス指定が引数ではなく設定から指定できるのは使いやすそうです。 以下は StartConfig を使用した場合の例です。 // v5: echo.StartConfig を使用する func main() { e := echo.New() e.Use(middleware.RequestLogger()) e.GET( "/" , func (c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!" ) }) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() sc := echo.StartConfig{ Address: ":1323" , GracefulTimeout: 10 * time.Second, } if err := sc.Start(ctx, e); err != nil { log.Fatal(err) } } $ go run server.go { " time " : " 2026-01-20T23:28:54.719092+09:00 " , " level " : " INFO " , " msg " : " Echo (v5.0.0). High performance, minimalist Go web framework https://echo.labstack.com " , " version " : " 5.0.0 " } { " time " : " 2026-01-20T23:28:54.719326+09:00 " , " level " : " INFO " , " msg " : " http(s) server started " , " address " : " [::]:1323 " } { " time " : " 2026-01-20T23:29:11.2848+09:00 " , " level " : " INFO " , " msg " : " REQUEST " , " method " : " GET " , " uri " : " / " , " status " :200, " latency " :3000, " host " : " localhost:1323 " , " bytes_in " : "" , " bytes_out " :13, " user_agent " : " curl/8.7.1 " , " remote_ip " : " ::1 " , " request_id " : "" } github.com デフォルトロガーがslog.Loggerに変更 これまではGoの標準のログを少し拡張した シンプルなloggingパッケージ を使っていましたが、slogを使うようになり、ログの設定でそのままslogの設定が利用できるようになりました。 これによりこれまで slog-echo のようなサードパーティを使ってslog利用していたように拡張する必要がなくなり、ロギング設定がより直感的になって個人的には一番嬉しいポイントになりました。 // v4 type Echo struct { Logger Logger // Custom interface with Print, Debug, Info, etc. } // v5 type Echo struct { Logger slog.Logger // slog.Logger interface } 実際にslogを使ってログ設定をカスタマイズする場合は以下のようになります。 skipper := func (c *echo.Context) bool { return c.Request().URL.Path == "/health" } e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ Skipper: skipper, // ヘルスチェックのリクエストはログに含めない LogStatus: true , LogURI: true , LogError: true , HandleError: true , // エラーをグローバルエラーハンドラに転送し、適切なステータスコードを決定できるようにします。 LogValuesFunc: func (c *echo.Context, v middleware.RequestLoggerValues) error { if v.Error == nil { logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST" , slog.String( "uri" , v.URI), slog.Int( "status" , v.Status), ) } else { logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR" , slog.String( "uri" , v.URI), slog.Int( "status" , v.Status), slog.String( "err" , v.Error.Error()), ) } return nil }, })) // start server... echo.labstack.com レスポンス情報の取得方法 (UnwrapResponse) ミドルウェア等でレスポンスの書き込みサイズやステータスコードを参照する場合、これまでは c.Response() のフィールドに直接アクセスしていましたが、v5では echo.UnwrapResponse ヘルパーを使用する必要があるので修正が必要になります。 // v5 resp, err := echo.UnwrapResponse(c.Response()) if err == nil { // resp.Size <-- 書き込まれたバイト数 // resp.Status <-- ステータスコード // resp.Committed <-- クライアントに送信済みかどうか fmt.Printf( "Status: %d, Size: %d \n " , resp.Status, resp.Size) } github.com この変更に伴う修正には正直、かなり手こずりました。いざv5の変更を当ててビルドしてみると、あちこちでヘルパーがエラーを吐き出しレスポンスの中身を見ている処理の見直しがいるのかなと疑問に思い、一体どうすれば…と右往左往する羽目になりました 😇 しかし、APIの変更点を見直したり実際のv5のコードを追ってみるなどの試行錯誤の末にこの UnwrapResponse の存在に気づきました この変更には最初こそ戸惑いましたが、 val, err := func() というGo言語の標準的なエラーハンドリングパターンに統一されたことで、変更の意図が腹落ちしました。 v4までではフィールド直接参照は手軽な反面、内部実装への依存度が高く、将来的な変更に弱い側面があります。 今回ヘルパー関数を経由する形になったことで、内部構造が隠蔽され、仮に取得に失敗してもエラーとして安全に検知できる(堅牢性が高まる)ようになっています。 また、このアプローチはGo1.20で標準ライブラリに追加された http.ResponseController の設計思想とも通じる部分があり、EchoがGoのモダンな作法に追従しようとしている姿勢がうかがえます。手間は増えましたが、長期的な保守性を高めるための洗練された変更と言えそうです。 URLパラメータの埋め込み方法変更 テストのとき、URL内にリソースのIDを埋め込んで実行する際によく使用していたコードが変更されています。 v5のほうが1行でまとまり直感的に書けるようになっており、これまでのコードよりも可読性が向上しています。 // v4 c.SetParamNames( "id" ) c.SetParamValues( "1" ) // v5 c.SetPathValues(echo.PathValues{{Name: "id" , Value: "1" }}) github.com こちらもぱっと調べて出てこなかったので実際に修正してみて気づいた部分になりました。 echo.POSTのような http.MethodXXX のヘルパーが廃止 これまでHTTPメソッドのヘルパーがあったのですが、v5では廃止されています。 そのため、 echo.POST などのヘルパーを http.MethodPost のように書き換える必要があります。 ( 何故かここはドキュメントにもAPI変更にも記載がないところでした ) 追記(2026/01/29) 公式のドキュメントの方にも記載されました github.com 以下がv4の時のコードです。 // HTTP methods // NOTE: Deprecated, please use the stdlib constants directly instead. const ( CONNECT = http.MethodConnect DELETE = http.MethodDelete GET = http.MethodGet HEAD = http.MethodHead OPTIONS = http.MethodOptions PATCH = http.MethodPatch POST = http.MethodPost // PROPFIND = "PROPFIND" PUT = http.MethodPut TRACE = http.MethodTrace ) github.com 追記(2026/01/29) 公式のドキュメントの方にも記載されました github.com まとめ 今回は Echo v5 がリリースされたので触れてみました。 v5の新しい機能に触れてみて、その利便性やコードの読みやすさが格段に向上しているのを肌で感じました。これは開発体験を大きく変える可能性を秘めていると思います。 実際に社内のプロダクトで実験しながらアップデート内容に触れてみましたが、試した結果、まだまだ修正が必要な箇所が多く、エラーの解消や動作検証に膨大な時間を費やしました。 正直なところ、完全に商用利用できる状態にするには、まだ多くの修正と検証が必要だと痛感しています。 この道のりは決して楽ではありませんが、v5がもたらす開発体験の向上を考えると、乗り越える価値は十分にあると感じています。 ガッツリとした変更ありつつもv4での記法がすべて変わるわけではないので、地道に修正していく必要があります。 まだまだリリースして日が浅く、公式含めサードパーティ系も更新が活発になってくるかと思うので最新の情報においていかれないように食らいついていこうと思います。 最後まで読んでいただきありがとうございました!
アバター
目次 はじめに エンジニア内定者研修について エンジニア内定者研修の概要 エンジニア内定者研修の目的 エンジニア内定者研修カリキュラム 前回からの改善点 ターミナルおよび Git/GitHub の基礎・プログラム基礎 ネットワーク/インフラ基礎 DB 研修 Web 基礎・Web アプリケーション開発基礎 AI開発基礎 受講者のフィードバック おわりに はじめに こんにちは。 開発本部開発1部トモニテ開発部所属の庄司( @ktanonymous )です。 弊社では、内定者向けとしては2回目となる内定者研修を2026年新卒のエンジニア内定者向けに実施し、 2025年中に全ての講義を完了しました。 今回の記事では、その内容について紹介したいと思います。 エンジニア内定者研修について 弊社では昨年度、エンジニア向けの内定者研修を初めて実施しました。 (昨年度の内定者研修の詳細については以下の記事をご覧ください) tech.every.tv 昨年度の研修では、入社後の新卒研修をよりスムーズに進められるよう、受講者の知識のベースラインを揃えることを目的として実施しました。 今年度も引き続き同様の目的のもと、昨年度の経験やフィードバックを活かしてカリキュラムを改善し、2回目の内定者研修を実施しました。 エンジニア内定者研修の概要 ここでは、エンジニア内定者研修の目的やカリキュラム、それぞれの講義概要について紹介したいと思います。 エンジニア内定者研修の目的 先述の通り、内定者研修では来年度入社する新卒のエンジニアメンバーが、入社後の研修を通じてよりスムーズに開発組織にジョインできるように、 ベースとなる基礎知識を学べる機会を提供することを主な目的としています。 具体的には、以下のような目的と方針を設定しました。 目的 入社前に基本的な技術や知識をキャッチアップする環境を提供する 方針 入社前に身に着けてほしい技術や知識のキャッチアップをサポートする 基礎知識を早期にキャッチアップすることで入社後の研修・オンボーディングをよりスムーズに進められるようになる 上記の目的と方針を踏まえ、4月に入社した新卒メンバーが中心となり資料の作成から当日の講義までを担当してもらいました。 最終的な資料としては社内メンバーのレビューを経て内容を担保するようにしています。 エンジニア内定者研修カリキュラム 今回の研修では以下のテーマで講義を行いました。 各回 90 分を目安に、2 週間に 1 回程度のペースで実施しました。 今回は、昨今のAI技術の発展に伴う開発環境の変化を踏まえて、AI開発基礎を新たなテーマとして取り入れました。 ターミナルおよび Git/GitHub の基礎・プログラム基礎 ネットワーク/インフラ基礎 DB 研修 Web 基礎・Web アプリケーション開発基礎 AI開発基礎 また、遠方から参加する方もいるため、全ての講義はオンラインで実施して録画を残すようにしました。 さらに、今回は講義で使用した資料を以下のリンクの speakerdeck に公開しています。 講義資料だけでなくイベントの登壇などで使用した資料などもご確認いただけますので、この機会にぜひご覧ください。 speakerdeck.com 前回からの改善点 前回の研修では受講者から以下のようなフィードバックがありました。 エンジニアとして知っておいた方が良いことを知ることができた 開催時期に対してスケジュールがタイトに感じられた また、運営面では担当の負荷が偏っていたり講義の準備のスケジュール感に対するフィードバックがありました。 これらのフィードバックを踏まえ、今回の研修では以下のような変更を取り入れました。 テーマ設定は昨年度の良さを引き継ぎつつ、関連性の高かったテーマを統合するとともに、昨今の開発環境の変化を踏まえてAI開発基礎を新たなテーマとして取り入れました。 約半年間の研修スケジュールで間延びしないように2025年内に全ての講義を完了するようにしました。 担当者の負荷が偏らないようにテーマごとの割り振りを均等に調整し、サポート体制なども明確になるように整備しました。 ターミナルおよび Git/GitHub の基礎・プログラム基礎 speakerdeck.com ターミナルおよび Git/GitHub の基礎・プログラム基礎の講義では、2024年度の「ターミナルおよび Git/GitHub の基礎」と「プログラム基礎」を統合し、 CLI(ターミナル)やチームでの開発を行うにあたり弊社でも利用している Git/GitHub の基本的な使い方、およびプログラムの基本的な構造やデータ構造、アルゴリズムについて学びました。 具体的には以下のトピックを取り上げました。 Linux コマンド Git とは? 木構造 配列とリスト ハッシュ ソート 探索 ネットワーク/インフラ基礎 speakerdeck.com ネットワーク/インフラ基礎の講義では、OSI 参照モデルを中心に、 ネットワークやインフラの基礎知識について学びました。 具体的には以下のトピックを取り上げました。 プロトコル TCP/IPとOSI参照モデル 関連するAWSリソース DB 研修 speakerdeck.com DB 研修では、DB の基本概念やバックエンド/データ系それぞれの視点での利用について学びました。 具体的には以下のトピックを取り上げました。 「データ」の種類と構造 SQLによるデータ操作 正規化・インデックス データ基盤の概要 Web 基礎・Web アプリケーション開発基礎 speakerdeck.com Web 基礎・Web アプリケーション開発基礎の講義では、2024年度の「Web 基礎」と「Web アプリケーション開発基礎」を統合し、 API や Web アプリケーションの基本構成や仕組み、バックエンド/フロントエンドそれぞれの役割、アーキテクチャやテスト、コーディング時に意識することなど、 組織/チームでの開発に携わるうえで重要となってくる考え方について学びました。 具体的には以下のトピックを取り上げました。 Web アプリケーションの構成要素 ブラウザが表示するHTMLの取得元について 外部データソースを利用した動的レンダリングについて フロントエンドとバックエンドの役割 開発に必要な知識 アーキテクチャ テスト CI/CD AI開発基礎 speakerdeck.com AI開発基礎の講義では、AIの概要やAIを活用した開発について学びました。 具体的には以下のトピックを取り上げました。 AIの基礎知識 AIの定義と分類 言語モデル(LLM)の仕組み トークン化とコンテキスト長 埋め込み(Embeddings) LLMの要素技術 プロンプトエンジニアリング 推論時のプロンプト手法 RAG(検索拡張生成)技術 LLMの拡張・統合技術 コンテキストエンジニアリング 開発ツールと活用事例の紹介 受講者のフィードバック 研修の改善のために、受講者からのフィードバックをアンケートで収集しており、その中でも以下のようなポジティブな意見が見受けられました。 図解や具体例(例:デリッシュキッチンだとどうか)が豊富だったため、とてもイメージしやすかった 昨今のAIによる開発環境の変化や具体的なモデルの構造などについて振り返ることができてよかった 基本的なことが丁寧に解説されていて分かりやすかった 一方で、「内容のボリュームに対して時間がタイトに感じられた」「実例や背景をより詳細に説明してほしい」といった意見もあり、 テーマに対して取り扱う内容の範囲を適切に設定する難しさを改めて実感しました。 得られたフィードバックを踏まえ、今後の研修運営をさらにブラッシュアップしていきたいと考えています。 講義風景① 講義風景② おわりに 今回の記事では、エンジニア内定者向けの研修についてご紹介いたしました。 内定者研修を通じて、今後入社するエンジニアのメンバーが入社後のオンボーディングをよりスムーズに進められるようにサポートすることができたと考えております。 また、研修の企画・運営に携わった若手メンバーにとっても、 知識の整理や研修の主要メンバーとしての新たなチャレンジの機会となり、貴重な成長の場にできたと感じています。 今回のような取り組みを含めて、今後もエンジニアの成長を支援する取り組みを続けて発信していきたいと思います。 最後まで読んでいただき、ありがとうございました。
アバター