TECH PLAY

株式会社エブリー

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

394

開発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による開発環境の変化や具体的なモデルの構造などについて振り返ることができてよかった 基本的なことが丁寧に解説されていて分かりやすかった 一方で、「内容のボリュームに対して時間がタイトに感じられた」「実例や背景をより詳細に説明してほしい」といった意見もあり、 テーマに対して取り扱う内容の範囲を適切に設定する難しさを改めて実感しました。 得られたフィードバックを踏まえ、今後の研修運営をさらにブラッシュアップしていきたいと考えています。 講義風景① 講義風景② おわりに 今回の記事では、エンジニア内定者向けの研修についてご紹介いたしました。 内定者研修を通じて、今後入社するエンジニアのメンバーが入社後のオンボーディングをよりスムーズに進められるようにサポートすることができたと考えております。 また、研修の企画・運営に携わった若手メンバーにとっても、 知識の整理や研修の主要メンバーとしての新たなチャレンジの機会となり、貴重な成長の場にできたと感じています。 今回のような取り組みを含めて、今後もエンジニアの成長を支援する取り組みを続けて発信していきたいと思います。 最後まで読んでいただき、ありがとうございました。
アバター
目次 はじめに セッション紹介 zerobus Ingest Agent Bricks [企業セッション] イオンにおけるマルチエージェントシステムの開発 まとめ 最後に はじめに 2025年11月28日に開催された「Databricks DATA + AI WORLD TOUR」に参加させていただきました。 今回は参加レポートとして、セッションの感想をお届けします! dataaisummit.databricks.com セッション紹介 zerobus Ingest 開発1部の吉田です。 Databricks Sessionで紹介されたzerobus ingestについてまとめます。 zerobus ingestは、クライアントからDatabricksへデータを直接ストリーミングできるマネージドサービスです。 従来のような複雑なメッセージバス(KafkaやKinesisなど)を介することなく、レコード単位でのデータ取り込みを実現します。 docs.databricks.com 中間インフラを排除できるため、運用管理の手間とコストが削減できます。 エブリーでは現在、サイネージ端末やWebアプリケーションのログ収集のために専用のインフラを構築・運用しています。 このインフラをzerobus ingestで置き換えることができるか、検証を進めていく予定です。 Agent Bricks こんにちは! 開発1部デリッシュキッチンの蜜澤です。 僕からは、Databricks Sessionで紹介されたAgent Bricksについてまとめます! Agent BricksはノーコードでAIエージェントの構築、評価、最適化できる機能になっています! AIエージェントを作成する際の下記のような課題に簡単に対応することができます。 調整すべき項目が多すぎる 評価するのが難しい コストと品質のバランスを取るのが難しい Agent Bricksではタスクを選び、エージェントの役割をざっくり指定するだけで自動でエージェントを作成してくれるので、調整するべき項目が少なくなっているようです。 ドキュメントを元に質問に答える「ナレッジアシスタント」や、要約・分類などのカスタムテキスト変換を行う「カスタムLMM」、Genieスペースとエージェントを統合する「マルチエージェントスーパーバイザー」などのユースケースがあるようです。 エージェントの評価に関しては、自動でベンチマークを作成し、エージェントを自動で最適化できるようです。 また、good/badのような評価しかできないツールもありますが、Agent Learning from Human Feedback(ALHF)によって、自然言語での指示に基づき、システムを自動調整することもできます。 コストと品質に関しては、「コスト最適化」モデルと「品質最適化」モデルのどちらかを選択することができ、多くの場合は両立も可能なため、自前で構築するよりも高品質かつ低コストなエージェントを作成できることが多いようです。 Agent Bricksを使用してAIエージェントを作成すると、自前で作成する際に課題となる点を簡単に解決し、気軽にAIエージェントを作成できるのが魅力だと感じました。 デモパートでは、マルチエージェントスーパーバイザーを使用して、複数のエージェントを簡単にまとめる方法をご紹介いただきました。 PowerPointやWord形式のドキュメントと役割を与えられたナレッジアシスタントと、テーブルを読み取るためのGenieスペースを登録することで、ユーザーからの質問に対して、ナレッジを元に必要な情報を考え、必要な情報をGenieがクエリを作成しテーブルから取得するというマルチエージェントが簡単に作成できていました。 個人的に特に便利だなと感じたのは、ナレッジを元に回答できないパターンの質問に関しては、ガイドラインを登録し回答を準備しておくことが簡単にできることでした。 2025年11月時点ではasia-northeast1リージョンではAgent Bricksはまだ使用できないのですが、使用できるようになるのがとても楽しみになりました! [企業セッション] イオンにおけるマルチエージェントシステムの開発 開発1部の岩﨑です。私からはイオン様におけるマルチエージェントシステムの開発について紹介します。 イオン様ではマルチエージェントシステムを「業務特化型エージェント」と「顧客向けAIエージェント」に大別して紹介されていました。 中でも業務特化型エージェントでは、自然言語の質問に基づいたクエリを実行して情報抽出するエージェントが紹介されました。 具体的には、データ抽出エージェントや可視化エージェントといったそれぞれのタスクに対するエージェントがあり、それをまとめるマルチエージェントスーパーバイザーをユーザとLLMとのインターフェースとしておくような構造となっています。 これによってクエリの生成から可視化、整合性の評価まで一気通貫で実行するようなマルチエージェントシステムを構築しています。 このエージェントはコンテキストが曖昧だった場合は推測で答えるのではなく、ユーザに聞き返すような仕組みとなっています。 また大規模なクエリをAIが実行しないように、システムリソースを大量に消費するクエリが生成された場合にはエージェントが主体となって実行しない意思決定が行われるようになっていたり、クエリをキャッシュする戦略など大規模データを扱っている企業ならではの工夫点などもみられました。 クエリの実行はDatabricks GenieとUnity Catalogを使用してAPIとして公開することでシームレスかつ低レイテンシで連携することができます。 また、Unity Catalogはガバナンスを意識した設計になっているためAIエージェントによる不正なデータアクセスなどの心配もありません。 さらにエージェントを作って終わりではなく、Genieへのリクエストを収集、フィードバックの作成、最適化のループを回すことによって精度改善にも取り組んでいるそうです。 他にも色々なエージェントの工夫点が紹介されており、非常に有意義なセッションでした。 まとめ 今回のDatabricks DATA + AI WORLD TOURでは、zerobus IngestやAgent Bricksといった機能の紹介から企業様の活用事例まで、幅広いセッションを聴講することができました。 AIエージェントに関するセッションが多く、ノーコードでのエージェント構築から大規模な業務システムへの適用までAIエージェントの活用が急速に広がっていました。 今回得られた知見を活かし、Databricksの機能の検証やAIエージェントの活用を進めていきたいと思います。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
AppleとLINEのネイティブ認証をつくる(サーバー編) この記事は every Tech Blog Advent Calendar 2025 の 29 日目の記事です。 前提 アプリ側のAppleとLINEのネイティブ認証実装 Apple LINE IDトークンと nonce サーバー側の実装 Appleの細かいポイント Bundle ID と Service ID LINEの細かいポイント API IDトークンの検証アルゴリズム 最後に 参考資料 こんにちは!開発1部で食事管理アプリ ヘルシカ のサーバーサイドの開発をしている 赤川 です。約1ヶ月にわたって続いたアドベントカレンダーも最終日となりました。 本記事では、ヘルシカiOS で Apple と LINE のネイティブ認証を導入した経験をもとに、ネイティブ認証のサーバー側の実装と周辺知識、実装時の細かいポイントについてお話しします。 iOS側の実装については、昨日公開の AppleとLINEのネイティブ認証をつくる(iOS編) をご覧ください。 tech.every.tv 前提 ヘルシカではもともと Web アプリベースの認証方法が採用されており、アプリ内で WebView が開きそこでIDやパスワードを入力して サインアップ / サインイン を行います。以下、AppleやLINEのIDプロバイダーを外部IdP、ヘルシカの認証サーバーを単に認証サーバーと呼ぶことにします。認証サーバーではOpenID Connectに則った実装がされており、おおまかには以下のような流れです。 ログインボタンを押す アプリ内のWebViewで外部IdP(AppleやLINE)の入力画面が開く 入力成功後、外部IdPから認証サーバーにコールバック 認証サーバーが外部IdPとToken Exchangeを行い、認証サーバーが外部IdPのトークンを受け取る 外部IdPのトークンの検証に成功した後、ヘルシカAPIサーバー用のトークンを発行し、安全にClientに渡す アプリはほぼWebViewを開くだけで、IdPとメインでやり取りするのはサーバーであることがわかります。 上記の形だと、例えばヘルシカにWebアプリが増えた、という場合でも同じエンドポイントが使え、認証サーバーの追加実装なしに拡張が可能なのがメリットと言えます。しかし、パスワードなどの入力は面倒ですし、忘れがちです。そこで、ユーザーにより簡単にサインアップ・サインインをしてもらえるように、Appleなら顔認証などiOSネイティブな方法、LINEならLINEアプリが開いてワンタップで認証できる方法を新しく採用することになりました。 Appleの認証画面 LINEの認証画面 アプリ側のAppleとLINEのネイティブ認証実装 (サーバー編)とタイトルに入れましたが、サーバー側の設計をするためにはまずアプリ側でネイティブ認証を実装する方法を知らなければなりません。AppleではASAuthorizationAppleIDProviderなどのクラス、LINEではLINEログインSDKというものが用意されています。 developer.apple.com developers.line.biz 元のフローを思い出してみると、外部IdPのトークンを受け取るのは認証サーバーでした。しかし、上記を使用した場合、IDトークンを受け取るのはアプリになります。それぞれについて、少し深掘りしてみましょう。 Apple まず、リクエスト ASAuthorizationAppleIDRequest でAppleIDを含む情報をIdPに送信します。 ASAuthorizationAppleIDRequest は ASAuthorizationOpenIDRequest を継承しており、オプションとして state と nonce を含めることができます。 成功すると、レスポンスとして ASAuthorization を受け取ります。これには ASAuthorizationAppleIDCredential が含まれており、 さらにその中身を見ることで IDトークン や state が手に入ります。 LINE LINEでは、 LoginManager を使います。 LoginManager.shared.login でリクエストを送信しますが、Appleの時と違い nonce と state はSDK内で自動的に生成、検証されます。ただし、 nonce は独自に指定することも可能です。 成功すると、 LoginResult を受け取ります。この中にアクセストークンやIDトークンなどが含まれています。 比較すると、以下の2つが共通していることがわかりました。 リクエストには独自の nonce を設定することができる。 成功すると、アプリがIDトークンなどを受け取ることができる。 IDトークンと nonce サーバー側の設計に進む前に、IDトークンと nonce について復習しておきます。ご存知の方は無視して次のパートに進んでいただいて問題ありません。 まず、IDトークンは次のような形をしています。 eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ. ewogImlzcyI6ICJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsCiAic3ViIjogIjI0ODI4OTc2MTAwMSIsCiAiYXVkIjogInM2QmhkUmtxdDMiLAogIm5vbmNlIjogIm4tMFM2X1d6QTJNaiIsCiAiZXhwIjogMTMxMTI4MTk3MCwKICJpYXQiOiAxMzExMjgwOTcwLAogIm5hbWUiOiAiSmFuZSBEb2UiLAogImdpdmVuX25hbWUiOiAiSmFuZSIsCiAiZmFtaWx5X25hbWUiOiAiRG9lIiwKICJnZW5kZXIiOiAiZmVtYWxlIiwKICJiaXJ0aGRhdGUiOiAiMDAwMC0xMC0zMSIsCiAiZW1haWwiOiAiamFuZWRvZUBleGFtcGxlLmNvbSIsCiAicGljdHVyZSI6ICJodHRwOi8vZXhhbXBsZS5jb20vamFuZWRvZS9tZS5qcGciCn0. NTibBYW_ZoNHGm4ZrWCqYA9oJaxr1AVrJCze6FEcac4t_EOQiJFbD2nVEPkUXPuMshKjjTn7ESLIFUnfHq8UKTGibIC8uqrBgQAcUQFMeWeg-PkLvDTHk43Dn4_aNrxhmWwMNQfkjqx3wd2Fvta9j8yG2Qn790Gwb5psGcmBhqMJUUnFrGpyxQDhFIzzodmPokM7tnUxBNj-JuES_4CE-BvZICH4jKLp0TMu-WQsVst0ss-vY2RPdU1MzL59mq_eKk8Rv9XhxIr3WteA2ZlrgVyT0cwH3hlCnRUsLfHtIEb8k1Y_WaqKUu3DaKPxqRi6u0rN7RO2uZYPzC454xe-mg https://openid.net/specs/openid-connect-core-1_0.html#id_tokenExample 2つのドットがあり3つのパートに区切られていることがわかります。これらは前から順にヘッダー、ペイロード、署名と呼ばれていて、このような形式のトークンを署名付き JWT (JSON Web Token) と言います。それぞれについて詳しく見ていきましょう。 まずヘッダーを Base64url デコードすると、以下のようなJSONが得られます。 { " kid ":" 1e9gdk7 "," alg ":" RS256 " } alg はJWTの署名に使用されたアルゴリズムを表します。 kid は署名検証用の公開鍵の識別子で、公開鍵暗号方式の署名の場合に含まれます。 type: "JWT" というフィールドが含まれている場合もあります。 次に、ペイロードを検証、デコードしてみると、だいたい以下のような JSON が得られます。 { " iss ": " https://server.example.com ", " sub ": " 248289761001 ", " aud ": " s6BhdRkqt3 ", " nonce ": " n-0S6_WzA2Mj ", " exp ": 1311281970 , " iat ": 1311280970 , " name ": " Jane Doe ", " given_name ": " Jane ", " family_name ": " Doe ", " gender ": " female ", " birthdate ": " 0000-10-31 ", " email ": " janedoe@example.com ", " picture ": " http://example.com/janedoe/me.jpg " } 下の方はプロフィール的な情報なので無視して、上側のフィールドの説明を書き込むと以下のようになります。 { " iss ": " トークンを発行したサーバーの識別子 ", " sub ": " ユーザーの識別子 ", " aud ": " トークンを受け取るアプリの識別子(クライアントIDなど) ", " nonce ": " nonce(下で説明します) ", " exp ": " トークンの有効期限(UNIXタイムスタンプ形式) ", " iat ": " トークンの発行日時(UNIXタイムスタンプ形式) " } nonce フィールドがありますね! nonce があることで、どのような利点があるかを以下で説明します。 登場人物は以下です。 IdP: IDトークンを発行する Relying Party: 認証のサービスを使う(アプリや、そのサーバー) ユーザー: ログインしようとしている 攻撃者: IDトークンを盗んで、不正にログインしようとしている まず、 nonce がない場合です。 nonceがない場合 IDトークンが正しく、有効期限内であれば、誰でも何回でもログインできてしまうことがわかります。このような攻撃を、リプレイ攻撃と呼びます。 次に nonce がある場合です。 nonceがある場合 ユーザーのログイン後、保存されていた nonce はすでに削除されているので、攻撃者が盗んだIDトークンは使えなくなっています。 nonce が "Number used once" の略である、ということが納得できるかと思います。 最後に署名です。署名は、ヘッダーとペイロードから、IdPだけが持っている秘密鍵を使って計算、付与されます。IDトークンを受け取ったクライアントは、 alg に書いてある方法に従って署名を検証します。ここで検証に失敗すれば、何者かによってヘッダーまたはペイロードが書き換えられているということになります。 サーバー側の実装 それでは、サーバー側の実装について考えていきましょう。結果的に、エンドポイントは2つになりました。 IDトークンの検証は nonce を含めサーバー側で行います。 LINEログインSDK のように nonce をアプリ側で検証をすることも可能ですが、結局それではIDトークンを何度も使えてしまい、サーバーから見ると nonce を含めていないのと同じ状態になってしまいます。そもそも、クライアントから送られてきたものをサーバーがそのまま信じることはよろしくありません。 そうすると、必然的に nonce の生成もサーバー側が行うことになります。 LINE と Apple の比較で、リクエストには独自の nonce を設定することができる、ということを確認したので、サーバー側から nonce を渡すエンドポイントを作れば良いですね。また、 nonce を生成してキャッシュするのにキーが必要なので、その認証認可セッションの ID もランダムに生成して返却します。これが1つ目のエンドポイントになります(わかりやすさのため、一部フィールドを省略しています)。 リクエスト { app_id: " アプリの識別子 ", service_id: " IdPの識別子(line or apple) ", type : " signup or signin " } レスポンス { nonce : " nonce ", session_id: " その認証認可セッションの識別子 " , } クライアント側はこの送られてきた nonce を使い、IdPからIDトークンを取得し、セッションIDとともにサーバーに送ります。これが2つ目のエンドポイントです。 リクエスト { app_id: " アプリの識別子 ", session_id: " その認証認可セッションの識別子 ", id_token: " IDトークン ", authorization_code: " 認可コード(アクセストークンなどの取得に使う、Appleのみ) " } サーバー側はセッション ID からキャッシュしていた nonce を取り出し、IDトークンの検証時に一致を確認します。検証に成功後、サーバーのDBにユーザーを作成、または更新し、アプリのIDトークン、アクセストークン、リフレッシュトークンを発行して返却します。アプリのトークンの発行部分については Amazon Cognito のカスタム認証を使っているのですが、ここでは触れないことにします。 レスポンス { access_token: " アプリのアクセストークン ", id_token: " アプリのIDトークン ", refresh_token: " アプリのリフレッシュトークン ", expires_in: " トークンの有効期限 " } nonce を使うことで、安全にIDトークンをやり取りしてネイティブ認証を実現できました!最終的な流れは以下の通りです。 ネイティブ認証のフロー LINE の公式ドキュメントでも、同様の方法が推奨されています。図も付いていてわかりやすいので、合わせてご覧ください。 developers.line.biz Appleの細かいポイント Bundle ID と Service ID Bundle ID はアプリ固有の識別子、 Service ID はWebアプリなどがサインインなどのWebサービスを使う際に使用する識別子です。 元々の Web アプリベースの認証方法では Service ID が使われていましたが、ネイティブ認証では Bundle ID が使われます。 この使い分けが必要になるのは、トークンの Exchange の処理、つまり、クライアントから送られた authorization_code を使ってアクセストークンやリフレッシュトークンを取得する処理です。 取得のためには 専用のエンドポイント を叩きますが、リクエストのパラメータの一つに client_secret というのがあります。詳しい作成方法については以下を参照ください。 developer.apple.com client_secret は署名した JWT で、 sub フィールドを持ちます。ここに入るものが、 Web アプリベースの認証なら Service ID 、ネイティブ認証なら Bundle ID となります(上記リンク先には App ID と書いてありますが、Bundle ID でも機能します)。 LINEの細かいポイント API SDKは自動で nonce や state を生成、検証してくれたりとなかなかリッチでしたが、APIも充実しています。特に、IDトークン検証用のエンドポイントが用意されています。 developers.line.biz ローカルでIDトークンを検証するのは少しだけ大変なので、これは有難いです。今回はAppleとIDトークンの検証処理を共通化させたかったので採用しませんでしたが、LINEのみ実装する場合には良い選択肢かと思います。 IDトークンの検証アルゴリズム LINEでは、ネイティブアプリと Web アプリで署名アルゴリズムが異なります。 公式ドキュメント で以下のように記されています。 ネイティブアプリやLINE SDK、LIFFアプリに対してはES256(ECDSA using P-256 and SHA-256)が、ウェブログインに対してはHS256(HMAC using SHA-256)が返されます。 ES256 は公開鍵暗号方式、HS256 は共通鍵暗号方式で、ES256の方が鍵管理のリスクが低いです。私も実装後に知ったのですが、一般に MPA (Multiple Page Application) の Web アプリなどはAPIサーバーとIdPが同一管理下にあることが多く、クライアントで署名検証をすることがないため共通鍵暗号方式でも十分、ネイティブアプリや SPA (Single Page Application) の Web アプリは IdP と API サーバーが分離していることが多く、場合によってはクライアント側で検証をすることもあるため公開鍵暗号方式が推奨されている、という背景があるようです。 最後に 既に動いているサービスで安全な認証認可を実現するために、いろいろな記事、動画を参考にさせていただきました。受け売りですが、認証認可や課金などの実装は、いろいろな方の集合知の上に成り立つ類のものだと考えています。この記事がまた、これから認証認可を実装する方の一助となれば幸いです。 それでは少し早いですが、皆様、今年も1年お世話になりました。 来年もどうぞよろしくお願いいたします。 参考資料 Authentication Services | Apple Developer Documentation アプリとサーバーの間で安全なログインプロセスを構築する | LINE Developers LINEログイン v2.1 APIリファレンス | LINE Developers OAuth 2.0 Threat Model and Security Considerations Final: OpenID Connect Core 1.0 incorporating errata set 1 OAuth 2.0 Threat Model and Security Considerations RS256 vs HS256 What's the difference? | Auth0
アバター
この記事は every Tech Blog Advent Calendar 2025 の 28 日目の記事です。 はじめに こんにちは!開発1部で食事管理アプリ ヘルシカ の開発をしている新谷です。これまでサーバーサイドを担当していましたが、直近ではiOS開発にも携わっています。 ヘルシカiOSでは、これまでWebViewベースの認証を採用していましたが、AppleとLINEのネイティブ認証を導入しました。ネイティブ認証では、Appleなら顔認証やパスコード、LINEならLINEアプリでのワンタップ認証が可能になり、ユーザー体験が大きく向上します。 本記事では、iOS側の実装について解説します。認証の仕組みやサーバー側の設計については、明日公開予定の「サーバー編」をご覧ください。 ネイティブ認証の全体像 ネイティブ認証のフローは以下のようになります。 ポイントは、認証サーバーが生成したnonceをSDKに渡すことです。これにより、サーバー側でID Tokenの検証時にリプレイ攻撃を防ぐことができます。nonceの役割や検証の詳細については、明日の「サーバー編」で解説します。 Sign in with Appleの実装 Sign in with AppleにはAuthenticationServicesフレームワークを使用します。 developer.apple.com ASAuthorizationAppleIDProviderの使い方 import AuthenticationServices func signInWithApple (nonce : String ) { let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName, .email] request.nonce = nonce // サーバーから取得したnonceを設定 let controller = ASAuthorizationController(authorizationRequests : [ request ] ) controller.delegate = self controller.presentationContextProvider = self controller.performRequests() } Delegateでの結果受け取り extension AppleNativeAuthProvider : ASAuthorizationControllerDelegate { func authorizationController ( controller : ASAuthorizationController , didCompleteWithAuthorization authorization : ASAuthorization ) { guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential , let identityTokenData = credential.identityToken, let idToken = String(data : identityTokenData , encoding : .utf8), let authorizationCodeData = credential.authorizationCode, let authorizationCode = String(data : authorizationCodeData , encoding : .utf8) else { // エラーハンドリング return } // idToken と authorizationCode をサーバーに送信 } func authorizationController ( controller : ASAuthorizationController , didCompleteWithError error : Error ) { // ユーザーキャンセルやその他のエラー処理 } } 取得できるもの Sign in with Appleからは以下の情報を取得できます。 項目 説明 ID Token JWTフォーマット。nonceが含まれる Authorization Code サーバーでのトークン取得に使用 User Identifier ユーザーの一意な識別子 Full Name 初回認証時のみ取得可能 Email 初回認証時のみ取得可能 LINE SDKの実装 LINE LoginにはLINE SDK for iOS Swiftを使用します。 developers.line.biz LINE SDKのセットアップ Swift Package Managerで以下のURLを追加します。 https://github.com/line/line-sdk-ios-swift.git Info.plistにも設定が必要です。 <key> LineSDKConfig </key> <dict> <key> ChannelID </key> <string> YOUR_LINE_CHANNEL_ID </string> </dict> <key> CFBundleURLTypes </key> <array> <dict> <key> CFBundleURLSchemes </key> <array> <string> line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER) </string> </array> </dict> </array> <key> LSApplicationQueriesSchemes </key> <array> <string> lineauth2 </string> </array> LoginManagerの使い方 import LineSDK func signInWithLine (nonce : String , from viewController : UIViewController ) { LoginManager.shared.login( permissions : [ .profile, .openID ] , in : viewController , parameters : . init (IDTokenNonce : nonce ) // サーバーから取得したnonceを設定 ) { result in switch result { case .success( let loginResult ) : guard let idToken = loginResult.accessToken.IDToken else { // エラーハンドリング return } // idToken をサーバーに送信 case .failure( let error ) : // ユーザーキャンセルやその他のエラー処理 } } } Appleとの違い LINE SDKとSign in with Appleの主な違いは、Authorization Codeの有無です。Appleではサーバーでのリフレッシュトークン取得にAuthorization Codeが必要ですが、LINEではリフレッシュトークンがSDK内部で管理されます。 共通点として、どちらも独自のnonceを設定でき、ID Tokenを取得できます。 最初の設計と問題点 クリーンアーキテクチャでの設計 ヘルシカiOSではクリーンアーキテクチャを採用しています。アーキテクチャの詳細については ヘルシカiOSアプリのアーキテクチャについて をご覧ください。 当初、ネイティブ認証も既存のアーキテクチャに従って以下のように設計しました。 Feature層(ViewModel) ↓ UseCase層 ↓ Repository層 ↓ Infra層(SDK呼び出し) ↓ 外部SDK(LINE SDK / AuthenticationServices) 問題点:認証処理がViewに影響を与える 実装を進める中で、この設計には問題があることがわかりました。 Sign in with Appleは ASAuthorizationController で認証処理を実行すると認証UIが表示され、 ASAuthorizationControllerPresentationContextProviding で表示先のWindowを指定します。 // Sign in with Apple:認証UIを表示するためにWindowを指定 extension AppleAuthProvider : ASAuthorizationControllerPresentationContextProviding { func presentationAnchor ( for controller : ASAuthorizationController ) -> ASPresentationAnchor { return window } } LINE SDKも同様に、認証処理を呼び出すとLINEアプリまたはWebViewが起動し、Viewに影響を与えます。 // LINE SDK:認証処理を呼び出すとLINEアプリまたはWebViewが起動 LoginManager.shared.login( permissions : [ .profile ] , in : viewController , parameters : . init (IDTokenNonce : nonce ) ) つまり、これらの認証処理を呼び出すとViewレイヤーに影響を与えることになります。 Infra層は本来、外部APIやLocalStorageなど、UIに依存しない外部リソースへのアクセスを担当する層です。 認証処理がViewに影響を与えるものをInfra層に配置するのは、アーキテクチャとして適切でないと考えました。 解決策:NativeAuthパッケージの分離 この問題を解決するために、認証処理をクリーンアーキテクチャの外に独立したパッケージとして分離しました。 新しいアーキテクチャ Feature層(ViewModel) │ ├──────────────────────> NativeAuthパッケージ(独立) │ ├── LineNativeAuthProvider │ └── AppleNativeAuthProvider ↓ UseCase層 ↓ Repository層 ↓ Infra層 ポイント NativeAuthパッケージをクリーンアーキテクチャとは独立した位置に配置 ViewModelから直接NativeAuthProviderを呼び出す構成に変更 UseCase/Repository/Infra層はサーバーとの通信(nonce取得、ID Token検証)に専念 この設計には、UIに依存する処理をInfra層に置かずに済み、認証処理を独立パッケージとして管理できるというメリットがあります。一方で、ViewModelが認証処理を直接呼び出すため、Feature層の責務が増えるというデメリットもあります。 ただ、Infra層にUI依存のコードを置くことの違和感の方が大きかったため、今回はこの設計を選びました。 まとめ 最近サーバーサイドからiOS開発も担当するようになったので、モバイルアプリ特有のアーキテクチャには苦戦しました。 特にViewはサーバーでは意識しない概念だったので、今後も適切な場所に配置できるよう気をつけていきたいです。 明日は「サーバー編」として、nonceの役割やID Tokenの検証など、サーバー側の実装についての記事が公開されます。ぜひそちらもご覧ください。 参考資料 Implementing User Authentication with Sign in with Apple | Apple Developer Documentation ASAuthorizationAppleIDProvider | Apple Developer Documentation GitHub - line/line-sdk-ios-swift: Provides a modern way of implementing LINE APIs. LINEログインSDK | LINE Developers
アバター
この記事は every Tech Blog Advent Calendar 2025 の 27 日目の記事です。 はじめに こんにちは。リテールハブ開発部の清水です。 私たちは小売向けサービスをLaravelで開発しています。 このプロジェクトではGit hooksのpre-commit設定を使用してコミットのタイミングでLaravel Pint, Larastanを呼び出すことでコード品質を整えるための仕組みを使用しています。 この仕組みのベースは、プロジェクト初期に整備されたものを引き継いだもので、今回その内容を見直しながら整理しました。 ちょうど良い機会でしたので、本記事で私たちが使用している設定内容をご紹介いたします。 Git hooksとは? Git hooksとは、git commit や git push などの Git 操作をきっかけに、自動でスクリプトを実行できる仕組みです。 コミット前にチェック処理を挟むなど、人の操作ミスを防ぐための自動処理を組み込む用途で使われます。 https://git-scm.com/docs/githooks Laravel Pintとは? Laravel Pint は、Laravel公式が提供している PHPコードの自動フォーマッターです。 決められたコーディング規約(Laravel / PSR-12 など)に従って、PHPコードの書き方を自動的に統一します。 https://laravel.com/docs/12.x/pint Larastanとは? Larastan は、PHP の静的解析ツール PHPStan を Laravel 向けに拡張したツールです。 コードを実行せずに解析し、存在しないプロパティや型の不整合などの問題を事前に検出します。 https://github.com/larastan/larastan コミットを行った時の処理の流れ git commitを実行すると、Git hooksのpre-commitフックが起動 コミット対象のPHPファイルを取得 Laravel Pintによるフォーマットチェック Larastanによる静的解析 3 or 4のチェックでエラーが検出された場合、コミットを中断 全てのチェックをパスした場合のみ、コミットが完了 pre-commit設定内容 #!/bin/sh set -eu # --- 1) コミット対象(ステージ済み)のPHPファイルだけ拾う --- php_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.php$' || true) # PHPファイルがなければ何もしない if [ -z "$php_files" ]; then exit 0 fi echo "コミット対象のPHPファイルをチェックしています..." # --- 2) Pint:フォーマットチェック(修正はしない) --- echo "Pintでフォーマットをチェックしています..." if ! echo "$php_files" | xargs ./vendor/bin/pint --test; then echo "" echo "❌ フォーマットエラーがあります。" echo " 以下のコマンドで修正してください:" echo " make lint" exit 1 fi echo "✓ フォーマットチェック OK" # --- 3) Larastan:静的解析 --- if echo "$php_files" | grep -qE '^app/'; then echo "Larastanで静的解析を実行しています..." if ! ./vendor/bin/phpstan analyse --no-progress --memory-limit=1G; then echo "" echo "❌ 静的解析エラーがあります。" echo " エラーを修正してから再度コミットしてください。" exit 1 fi echo "✓ 静的解析 OK" fi echo "" echo "✓ 全てのチェックが完了しました。" exit 0 Makeコマンドの紹介 コミットのタイミングだけではなく、手動でLaravel Pint, Larastanを実行したい場面もあります。 以下のMakeコマンドで実行できるようにしています。 # コードスタイルチェック&修正 lint: @files=$$(git diff --cached --name-only --diff-filter=ACM | grep "\.php$$" | sed "s|^$$(basename $$(pwd))/||"); \ if [ -n "$$files" ]; then \ docker compose -f $(COMPOSE_FILE) exec -T $(CONTAINER_PHP) sh -c "./vendor/bin/pint $$files"; \ else \ echo "ステージされたPHPファイルがありません"; \ fi # コードスタイルチェック lint-check: @files=$$(git diff --cached --name-only --diff-filter=ACM | grep "\.php$$" | sed "s|^$$(basename $$(pwd))/||"); \ if [ -n "$$files" ]; then \ docker compose -f $(COMPOSE_FILE) exec -T $(CONTAINER_PHP) sh -c "./vendor/bin/pint --test $$files"; \ else \ echo "ステージされたPHPファイルがありません"; \ fi # 静的解析 larastan: docker compose -f $(COMPOSE_FILE) exec $(CONTAINER_PHP) ./vendor/bin/phpstan analyse --memory-limit=1G 実際にコミットする流れ Laravel Pint, Larastanで弾かれる内容のコードを作成 <?php namespace App\Http\Controllers; // importが名前順になっていない use Retailapp\Common\Models\User; use Illuminate\Http\JsonResponse; use App\Http\Controllers\Controller; class TestController extends Controller { public function index () : JsonResponse { $ user = User :: find ( 1 ) ; $ name = $ user -> undefined_property; // 存在しないプロパティを呼び出している return response () -> json ([ 'name' => $ name ]) ; } } コミットを試みると、フォーマットエラーで弾かれる % git commit -m '弾かれてほしいコミット' コミット対象のPHPファイルをチェックしています... Pintでフォーマットをチェックしています... ⨯ ──────────────────────────────────────────────────────────────────── Laravel FAIL ............................................. 1 file, 1 style issue ⨯ app/Http/Controllers/TestController.php no_unused_imports, ordered_import… ❌ フォーマットエラーがあります。 以下のコマンドで修正してください: make lint pintで自動的に修正 % make lint ✓ ──────────────────────────────────────────────────────────────────── Laravel FIXED ...................................... 1 file, 1 style issue fixed ✓ app/Http/Controllers/TestController.php no_unused_imports, ordered_import… もう一度コミットすると、今度はLarastanで弾かれる % git commit -m '弾かれてほしいコミット' コミット対象のPHPファイルをチェックしています... Pintでフォーマットをチェックしています... ──────────────────────────────────────────────────────────────────── Laravel PASS ............................................................ 1 file ✓ フォーマットチェック OK Larastanで静的解析を実行しています... ------ -------------------------------------------------------------------------- Line Http/Controllers/TestController.php ------ -------------------------------------------------------------------------- :14 Access to an undefined property Retailapp\Common\Models\User::$undefined_property. 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property ------ -------------------------------------------------------------------------- [ERROR] Found 1 error ❌ 静的解析エラーがあります。 エラーを修正してから再度コミットしてください。 Larastanに違反する部分を修正 <?php namespace App\Http\Controllers; use Illuminate\Http\JsonResponse; use Retailapp\Common\Models\User; class TestController extends Controller { public function index () : JsonResponse { $ user = User :: find ( 1 ) ; $ name = $ user -> name ; // 修正 return response () -> json ([ 'name' => $ name ]) ; } } コミット成功 % git commit -m '通ってほしいコミット' コミット対象のPHPファイルをチェックしています... Pintでフォーマットをチェックしています... . ──────────────────────────────────────────────────────────────────── Laravel PASS ............................................................ 1 file ✓ フォーマットチェック OK Larastanで静的解析を実行しています.. [OK] No errors ✓ 静的解析 OK ✓ 全てのチェックが完了しました。 おわりに 本記事では、私たちの Laravel プロジェクトで使用している Git hookのpre-commit 設定と、その中で Laravel Pint・Larastan をどのように組み込んでいるかをご紹介しました。 同様の仕組みを検討されている方の参考になれば幸いです。 最後までお読みいただきましてありがとうございました。
アバター
Go 1.26で変わるgo fix この記事は every Tech Blog Advent Calendar 2025 の 26 日目の記事です。 はじめに go fixとは Go 1.26での変更点 modernizeとは 使い方 実行例 inlineとは 使い方 実行例 IDEでの修正 新しいgo fixでできること まとめ はじめに 開発本部でデリッシュキッチンアプリ課金ユーザー向けの開発を担当している hond です! 先日2026年2月にリリース予定のgo1.26のRelease Candidate 1であるgo1.26rc1がリリースされました。もうrc1は確認できたでしょうか?確認がまだの方は こちら から確認できるのでぜひ! 今回はGo 1.26でgo fixが大幅に変更されるとのことだったのでそちらについて説明しようと思います。 go fixとは go fix はGo 1リリースにて破壊的変更を含む古いAPIを特定し、新しいものに修正するツールとして追加されました。 go fix によって大まかな修正はできるため、私たちはコアな部分の修正に集中できるようになっていました。 Go 1リリースの際は go fix での修正対象は多く Go 1のRelease Notes で Updating: Running go fix と調べるだけでも14件ヒットし、 ソースコード を確認すると37件ものルールがあることが確認できます。その後も go fix のルールは追加削除が繰り返されていきましたが、Goがメジャーバージョン内での後方互換性を担保しているので最近はなかなか変更が行われず使わないコマンドとなっていました。 Go 1.26での変更点 github.com Go 1.26では cmd/go: fix: apply fixes from modernizers, inline, and other analyzers にて go fix が大幅に変更される旨のProposalがAcceptされました。 この変更の背景は、元のissueである cmd/fix: remove all functionality とGopherCon 2025でのAlan Donovanの発表( Analysis and Transformation Tools for Go Codebase Modernization )から確認できます。具体的には以下の3点が挙げられています。 「go fixとは」でも触れた既存 go fix の機能である context や buildtag の修正が不要になったこと コードレビュー時間の短縮や開発者の教育のためにモダン化が必要だったこと AI/LLMが生成するコードの品質改善のためにモダン化が必要だったこと これらの背景を踏まえ、 modernize や //go:fix inline のようなGoのコードのモダン化を目的とするアナライザーを go vet 同様に go fix でも利用可能にすることがこのProposalの目的となっています。 go vet と go fix の使い方は似ていますが、あくまで go vet はアナライザーを用いて静的解析した診断結果を 報告 するツール、 go fix は検出した問題をアナライザーが提案する修正案に 修正 するツールという使い分けになるようです。 以降では、Go 1.26の go fix で利用可能になる modernize と inline について詳しく説明します。 modernizeとは pkg.go.dev modernize は golang.org/x/tools/go/analysis/passes/modernize で公開されているアナライザーのスイートです。Goのコードを最新の構文や標準ライブラリの機能を利用して、より簡潔でモダンな形式に修正することを目的としています。 現在24個のアナライザーが登録されており、その中から代表的なものを紹介します。Go 1.26で追加される errors.AsType のアナライザーも既に登録されています! Analyzer 説明 any interface{} を any に修正する errorsastype errors.As をGo 1.26で追加された errors.AsType に修正する 使い方 以下のコマンドでmodernizeを実行することができます。 $ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./... また、特定のアナライザーを有効・無効化することも可能です。 # anyアナライザーを有効化 $ modernize -any =true -fix ./... # anyアナライザーを無効化 $ modernize -any =false -fix ./... 基本的には修正を適用しても動作が変わらないよう設計されていますが、 bloop のように完全に動作を保証できないアナライザーもあり、それらはデフォルトでは無効化されています。 実行例 以下では実際に any アナライザーを使って interface{} を any に修正してみます。 修正前のコード: package main import "fmt" type OldInterface interface {} func main() { old := OldInterface( 1 ) fmt.Println(old) } コマンド実行: $ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./... 出力(差分): import "fmt" -type OldInterface interface{} +type OldInterface any func main() { old := OldInterface(1) このように、 interface{} が any に自動で修正されたことが確認できます。 inlineとは pkg.go.dev inline は golang.org/x/tools/go/analysis/passes/inline で公開されているパッケージで、 inline と gofixdirective の2つのアナライザーが含まれています。 inline アナライザーは //go:fix inline コメントディレクティブに基づいて修正を行います。主に非推奨になった関数や定数を新しいものに置き換えるために使用され、 ioutil package などで利用されています。 具体的な使い方は以下の通りです。 関数の場合 :修正したい関数をラップした関数を作成し、その関数にコメントディレクティブを加えることで修正が可能です。 //go:fix inline func Square(x int ) int { return Pow(x, 2 ) } 定数の場合 :修正したい変数を左辺に、修正後の変数を右辺に記述し、コメントディレクティブを加えることで修正が可能です。 //go:fix inline const Ptr = Pointer 使い方 以下のコマンドで inline を実行することができます。 $ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./... また、goplsを用いているIDE上では対象をホバーすることでも修正が可能です。 実行例 以下では、 OldFunction と NewFunction を定義し、 OldFunction を NewFunction に修正する際の例を示します。 修正前のコード: package main import "fmt" func main() { OldFunction() NewFunction() } //go:fix inline func OldFunction() { NewFunction() } func NewFunction() { fmt.Println( "NewFunction" ) } コマンド実行: $ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./... 出力(差分): import "fmt" func main() { - OldFunction() + NewFunction() NewFunction() } このように、 OldFunction が NewFunction に修正されるのが確認できます。 IDEでの修正 コマンドを使わずに、IDE上で対象をホバーすることでも修正が可能です。 IDEでホバーした時の画像 Quick Fixを選択すると、修正候補が表示されクリックすることで修正が可能です。 Quick Fixを押した時の表示 新しいgo fixでできること ここまで説明した modernize と inline は、Go 1.26からは go fix を実行するだけで適用できるようになります。今までは破壊的変更を修正するためのツールだった go fix が、Goのコードをモダン化していくためのツールへとシフトしました。 実際にgo1.26rc1を使って go fix を実行してみます。 $ go1.26rc1 fix ./... modernizeの結果: import "fmt" -type OldInterface interface{} +type OldInterface any func main() { old := OldInterface(1) inlineの結果: import "fmt" func main() { - OldFunction() + NewFunction() NewFunction() } このように、 go fix を実行するだけでコードをモダン化された状態に持っていくことができます。コード品質の担保がより容易になるため、pre-commitフックやCI、AI/LLMの出力後に適用するルールとして定義するのが良さそうです! まとめ 私自身 go fix を使う機会がなかったので、今回のアップデートで既存の go fix とモダン化の為のツールとして進化した go fix を会社のコードに適用するいい機会になりました。既存の go fix での修正点は一部の context のみでしたが、新しい go fix を実行したところ1249の修正点が確認されました。10年もののサービスという事もありこれらのコードを全て手動で特定し、修正していくとなると莫大な労力を要するので公式からこのようなツールが出ていることはとても有り難いなと思いました。 上記で修正されたのは modernize に定義されたアナライザーやそれぞれのpackageにて //go:fix inline が定義されているものですが、 //go:fix inline に関しては普段の開発でも非推奨化したがバージョン互換性担保の観点で削除できないコードを管理する際にとても有用なので積極的に利用して行きたいです。 AIはどうしても既存のコードベースに品質が左右されてしまうので新しくなった go fix を用いて、継続的な品質改善が重要だと感じました。また、公開されたコードベースを元にAIは学習していくので今後の自分を含めたGopherがAIを通してより良いコードを書くためにも積極的にモダン化を行おうと思います。 ここまで読んでいただきありがとうございます。Go 1.26で go fix がどう変わるか迷っている方の助けになれたら幸いです!
アバター
この記事は every Tech Blog Advent Calendar 2025 の25日目の記事です。 目次 はじめに 設計から軌道修正まで 1. 何を目指していたか 2. 運用して顕在化した問題 検索クエリの生成が安定しない 見出しに合うレシピが必ず存在するかどうかはわからない 条件に合わないレシピが含まれてしまう 3. 問題の原因 プロンプトの肥大化 不要な思考(ニーズ分析)を挟んでいた ベクトル検索に対して除外の前後処理を入れていなかった 根本にあった認識の甘さ 4. どう軌道修正したか ニーズ分析のステップを排除 見出しの自由生成・クエリ生成を排除 レシピをベクトル検索からOpenSearch利用に変更 LLM-as-a-Judgeの導入 5. 前提条件の見直し 振り返りから見えたワークフロー設計の勘所 ステップごとに切り分けて進める AIに任せる部分・ルールベースにすべき部分を見極める 運用を見据えたガードレール設計 今後の取り組みと展望 参考文献 はじめに こんにちは。2025年4月にソフトウェアエンジニアとして新卒入社した 黒髙 です。普段は デリッシュキッチン の開発に携わっています。 デリッシュキッチンにはレシピだけでなく、料理に関する情報をまとめた 記事 や レシピ特集 など、多様なWebコンテンツがあります。コンテンツディレクターの業務効率化の一環として、ユーザーニーズを満たすコンテンツ作成をAIでサポートする試みを、私は設計から実装までを一貫して担当しました。 当初はAIに創造的なコンテンツ生成を任せようと意気込んでいたのですが、実運用では想定外の課題が次々と顕在化し、最終的にはルールベースの制約(ガードレール)を多く追加することになりました。 以下画像は、実際に生成された アボカドサーモンの魅力を引き出すレシピ集 | デリッシュキッチン のコンテンツの一部です。 ページの一例 実際に自動生成して公開しているコンテンツは、 セレクション一覧 からもご覧いただけます。 また、記事生成に関わる処理の全体像は次の通りです。AIワークフローの詳細は後述します。 処理の流れとアーキテクチャ 本記事では、AIワークフロー設計で何を期待し、何が起き、どのように軌道修正したかを振り返ります。LLMや生成AI活用の文脈では基本的な話も含みますが、現場で使えるものにするためにどう整えていったのかの判断を中心にまとめます。 設計から軌道修正まで 本章では設計段階から運用に至るまで、AIワークフローに関する部分の振り返りを行います。 1. 何を目指していたか 理想は、AIがWebページの構成を考え、分析結果に応じて組み替えられることです。ただし今回は、早期にコンテンツを公開することを優先したPoC(初期検証フェーズ)として、ページ構成を「見出し+説明文+レシピリンク集」の繰り返しに限定しました。 - 見出し1 - 見出し1の説明文 - 見出し1のレシピリスト - レシピA - レシピB - レシピC - 見出し2 ... (以下繰り返し) 当初は、ニーズ分析→見出し・説明文の生成→関連レシピの検索までをAIが一貫して行うことを目指しました。具体的には、次の5ステップのLLMパイプラインを設計しました。 当初のワークフローと具体例 また、初期検証をできるだけ早く進めるため、 デリッシュAI で利用しているレシピ一覧を埋め込みベクトルに変換したテーブルを使いました。 2. 運用して顕在化した問題 数パターンのキーワードで試したところ、課題は残るものの、記事として形になることは確認できました。そこでバックエンド/フロントエンドを実装し、キーワード一覧をもとにバッチで自動生成を開始しました。 ところが「キーワードに沿ったレシピが選ばれているか」の観点の公開前レビューでは、公開できる記事が想定より少ない結果になりました。さまざまな入力パターンを事前に十分検証し、試行段階で見えていた課題にこの時点で対処しておくべきでした。 具体的な問題としては以下のようなものがあります。 検索クエリの生成が安定しない 今回のフローでは画像の通り、見出しに基づいたレシピ検索クエリをLLMに生成させています。例えば 簡単に作れるごぼうと牛肉レシピ を入力しても、 簡単レシピ や 簡単牛肉 のように意図が欠けたクエリになることが稀にありました。結果として後続のLLMにも意図したリクエストを送れず、適切な記事が生成できない問題がありました。 見出しに合うレシピが必ず存在するかどうかはわからない 検索キーワード ごぼう 牛肉 から ごぼうと牛肉のローストビーフ のような見出しが生成されても、デリッシュキッチンにそのようなレシピがあるとは限りません。具体的に絞り込む見出し提案に従った結果、レシピが登場しないといった問題が発生しました。 条件に合わないレシピが含まれてしまう 例えば 簡単に作れるごぼうと牛肉のレシピ に対して、 味付け簡単! 牛すじとごぼうの塩麹煮 のような適切なレシピが出る一方で、 簡単で本格的な味! 豚バラ肉とごぼうの和風煮 といった、響きは似ているものの材料が欠けているレシピも混ざることがありました。 3. 問題の原因 プロンプトの肥大化 デリッシュキッチンでは、信頼性の高いコンテンツ品質を維持するため、多くの制作ルールが存在しています。以下は一例ですが、実際には数十個程度の記載NG項目があります。 - 健康・栄養に関する過度な表現禁止: ダイエット, ヘルシー ... - 食欲・体調への直接効果を謳う表現禁止: 食欲, 体力, 元気 ... 料理系コンテンツでは連想語がNGになることも多く、AIにとって制約の強い要求です。さらに、これらをすべてのステップで指示していたため、禁止事項を追加しても一度で要求を満たし切れないことがありました。 不要な思考(ニーズ分析)を挟んでいた ニーズに合致した構成にするためにStep 1としてニーズ分析を入れましたが、禁止事項を守らせることや、ヒットするレシピ数を担保するために無難な見出しに寄せる要件とは相性が悪く、逆にノイズになっていました。 ベクトル検索に対して除外の前後処理を入れていなかった レシピ検索としてベクトル検索とコサイン類似度によるスコアリングを用いていましたが、関係のないレシピも検索結果に含まれてしまう問題が発生していました。ベクトル検索は意味的な類似性を捉えるのには優れていますが、それ単体で材料として実際に使われているかどうかを判定することはできません。 根本にあった認識の甘さ これらの問題に共通していたのは、以下の点に対する認識の甘さでした。 運用では検証段階より多種多様なキーワードを扱うこと LLMによる不安定な出力が次段階の生成内容にも影響をもたらすこと 当初は創造的なWebサイト構築を漠然と想像していましたが、実際に必要だったのはAIが生成した記事をできるだけ早く公開し、生成から公開までのサイクルを回しながら検証を進めることでした。 しかし、少数のキーワードではそれなりに記事が生成できたことで安心してしまい、安定運用できるかという観点が抜け落ちたまま次の開発に進んでしまっていました。 4. どう軌道修正したか 問題を踏まえて、以下のような軌道修正を行いました。 以前のステップ 変更点 1. キーワードのニーズ分析 削除 2. 小見出し生成 LLMによるテンプレート選択 3. 検索キーワード抽出 ルールベースで生成 4. コンテンツ生成 変更なし 5. レシピ検索 OpenSearch + LLM-as-a-Judgeの追加 ニーズ分析のステップを排除 前述の通り、ノイズとなっていたStep 1(キーワードのニーズ分析)を削除しました。 将来的には分析データが揃い次第、数値に基づくニーズ分析を復活させ、LLMによる構成検討も行いたいと考えています。 見出しの自由生成・クエリ生成を排除 対象を絞り込まない抽象的な見出しにしたいこと 禁止ワードを含めたくないこと を踏まえると、見出しのパターンはある程度限られてくることが分かりました。ヒットするレシピを安定して出すために、LLMに自由生成させるのではなく、見出し候補を用意して選択してもらう方針に転換しました。 SUBHEADING_OPTIONS = [ { "kind" : 1 , "template" : "簡単に作れるxxのレシピ" }, { "kind" : 2 , "template" : "xxとxxの絶妙な組み合わせ" }, { "kind" : 3 , "template" : "xxを使ったアレンジレシピ" }, { "kind" : 4 , "template" : "新しいxxの料理アイデア" }, # ... ] KIND_KEYWORD_MAPPING: dict [ int , str ] = { 1 : "{keyword} 簡単" , 2 : "{keyword} 組み合わせ" , 3 : "{keyword} アレンジ" , 4 : "{keyword} アイデア" , # ... } また、見出しに基づくクエリ生成もLLMに任せるのをやめ、(検索キーワード+見出しの特徴)をルールベースで組み立てる方針に戻しました。 HyDE(Hypothetical Document Embeddings) のような手法も検討しましたが、1見出しに最大9個のレシピを網羅的に取得したい要件には合わないと判断し、簡易的で汎用的なクエリで対応することにしました。 レシピをベクトル検索からOpenSearch利用に変更 構想段階の試用では、デリッシュAIの基盤を一部利用したベクトル検索が手軽で都合が良かったので採用していました。 しかし、ワークフローを見直し、扱うキーワードや見出し表現をある程度固定化した結果、検索に求める要件も変化しました。 以下の理由から、レシピ検索機能で既に用いているOpenSearchを利用する方針としています。 多くの対象キーワードにおいて、通常検索の方が期待したレシピが安定してヒットすること レシピ検索機能で用いられているシノニム辞書をそのまま活用できること 数パターンに固定化した見出し表現においては、ベクトル検索の強みである意味的類似度の必要性が相対的に薄れたこと なお、今後扱うキーワードの幅や要件が変わった場合には、再びベクトル検索を含めた見直しを行う余地があると考えています。 LLM-as-a-Judgeの導入 更なるガードレールとして、レシピが見出しの意図に合っているかを審査するステップを追加しました。この判定を導入することで、条件に合わないレシピをより厳格に除外できることが期待できます。 { " recipe_id ": " 206162500360602656 ", " accept ": true , " reason ": " タイトル・材料に「牛すじ(牛肉)」とごぼうが含まれており、サブ見出しの「ごぼう 牛肉 簡単」に合致します(味付けが簡単な点も明記)。調理手順は煮込み工程があるものの、小見出しの意図と矛盾していません。 " } , { " recipe_id ": " 163718318442676716 ", " accept ": false , " reason ": " ごぼうは使われていますが、主な肉材が豚バラ肉であり、小見出しが想定する『牛肉』と一致しません。 " } , まとめると、フローは次の通りです。 ワークフロー 初期段階では1記事あたり3〜10件以上の修正指摘が発生していたものが、これらの改善によって修正指摘が0件のケースが大半となり、あっても1〜2件程度に収まるようになりました。 5. 前提条件の見直し ワークフローの見直しを進める中で、そもそもどのキーワードを生成対象とすべきかという前提条件にも課題があることが分かりました。 当初は、食に関連するキーワードを一律に処理していましたが、キーワードによって求められる判断や前処理の難易度が大きく異なることが次第に明らかになりました。 例えば カニ レシピ では、カニカマを許容するかどうかの意思決定が必要で、LLMには難しいです。また、 酒のあて のように抽象度の高いキーワードでは、どの切り口で掘り下げるかという追加設計が求められます。 こうした性質の異なるキーワードを同一の前提でAIワークフローに流していたことが、後段のLLM-as-a-Judgeや人手レビューの負荷を高め、全体の不安定さにつながっていました。 ひとまずは、AIワークフローの前段としてキーワードの取捨選択を行い、判断コストが高そうなものや、追加設計が必要なものは対象外とする方針で生成していきます。 振り返りから見えたワークフロー設計の勘所 これまでの試行錯誤を振り返ると、技術的な工夫以上に ワークフロー設計そのものの考え方 が重要だったと感じています。 ステップごとに切り分けて進める 今回のようにLLMの出力結果が後続の処理に大きく依存するワークフローでは、各ステップが単体で正しく機能するかをまず確認する必要があります。想定動作・実装方針・失敗時のハンドリングまで詰めたうえで次に進むことが、品質担保と手戻り削減の観点で重要であると感じています。 AIに任せる部分・ルールベースにすべき部分を見極める 本取り組みを通じて、LLMに任せる部分/任せない部分を明確にすることが、ワークフロー全体の安定性に直結することが分かりました。 特に意識したのは、次の3点です。 決定的に定義できる処理は、LLMに任せない 出力の揺れが後段に大きく影響する箇所では、強いガードレールを設ける 曖昧さを許容できる工程のみ、LLMの生成能力を活かす このように役割分担を整理したことで、LLMにすべてを任せる構成から、壊れにくいパイプラインへと移行できました。 運用を見据えたガードレール設計 最終目標(ユーザーニーズを満たすコンテンツ制作の工数をできるだけ減らすこと)自体は当初から変わっていません。一方でビジネス側には、まず記事を公開し、ランキングの変化を見ながら改善したいという目的がありました。そのためには、改善サイクルを回せるだけの安定した記事生成が前提になりますが、創造性を高める工夫を優先してしまったことが手戻りの原因でした。 今後の取り組みと展望 今回生成している記事は、文章の整合性や、検索キーワードに紐づくレシピになっているかという点では、一定の信頼性を確保できています。一方で、訪問ユーザーのニーズを満たすコンテンツとしては、まだ伸びしろがあると感じています。 一例ですが、以下を検討しています。 Self-Reflection によるLLMの自動改善サイクルの導入 幅広い検索キーワードに対応するルーティングの追加 ベクトル検索の再利用とレシピのスコアリング レシピページ以外のアセット活用(記事、特集、カテゴリページ) トレンドや訪問ユーザーのログを活用した記事の再生成 これらの改善により、更に品質の高い記事生成を目指していきます。 参考文献 Precise Zero-Shot Dense Retrieval without Relevance Labels Self-Reflection in LLM Agents: Effects on Problem-Solving Performance
アバター
この記事は every Tech Blog Advent Calendar 2025 の 24日目の記事です。 はじめに Swift 5.9で導入された Observation フレームワークは、 @Observable マクロを用いた簡潔な記述が可能で、特にSwiftUIのView更新において高いパフォーマンスを発揮します。 一方で、既存の Combine フレームワーク( ObservableObject )からの移行を検討する際、課題となる点がありました。それは ViewModel や Service など、 UI以外の場所での値の監視 です。Combine では @Published プロパティのProjected Valueを用いて値の変化をストリームとして扱えますが、それと同等の標準的な手段がこれまでの Observation フレームワークには不足していました。 Swift 6.2では、この点が解消されています。 @Observable クラスのプロパティの変化を AsyncSequence として監視する機能( Observations )が追加され、Combine に依存することなく、標準APIのみで値の監視が可能になりました。 本記事では、この新しい監視手法について整理します。 ObservableObject による値の監視 Observation フレームワークが登場する以前、SwiftUI では ObservableObject と Combine を用いた状態管理が一般的でした。 @Published プロパティの projected value( $ )を利用することで、値の変化をストリームとして扱える点は大きな利点でした。 import Combine class CounterViewModel : ObservableObject { @Published var count : Int = 0 private var cancellables = Set < AnyCancellable > () } let viewModel = CounterViewModel() // $をつけることで Publisher として扱える viewModel. $count .sink { value in print( "count changed:" , value) } .store( in : & viewModel.cancellables) この仕組みにより、View の更新だけでなく、ViewModel や Service など UI 以外のレイヤーでも、値の変化を一貫した方法で監視できていました。この点は、ObservableObject が持つ強みの一つです。 Observation フレームワークと withObservationTracking Swift 5.9 で Observation フレームワークが導入されると、View 更新のパフォーマンスと記述の簡潔さは大きく改善されました。一方で、View 以外で値の変化を検知する方法として利用されているのが withObservationTracking です。 import Observation @Observable class Counter { var value : Int = 0 } let counter = Counter() func observe () { withObservationTracking { // 1. ここで値を読む(アクセスする)ことで監視対象にする print( "Current value: \( counter.value ) " ) } onChange : { // 2. 変更前(willSetタイミング)に呼ばれる print( "Value will change..." ) Task { // 3. 再帰的に監視を続ける observe() } } } withObservationTracking は、クロージャ内で読み取られたプロパティへの依存関係を自動的に追跡し、それらが変更された際に onChange を呼び出します。これは SwiftUI の View 更新を支える中核的な仕組みです。 withObservationTracking の課題 しかし、この方法をロジック層で使うには、いくつかの問題がありました。 値そのものを直接受け取れない onChange で通知されるのは「変更される」という事実のみで、変更後の値は再度読み取る必要があります。 再登録が必要 変更を継続的に監視するためには、onChange の中で再び withObservationTracking を呼び出す再帰的な実装が必要です。 非同期処理との相性 withObservationTracking 自体は同期的であり、Callbackベースの記述になるため、async/await のフローと組み合わせる際に直感的な記述が難しくなります。 Swift 6.2: AsyncSequence による監視の導入 Swift 6.2 では、こうした課題を解消する形で、 @Observable クラスのプロパティの変化を AsyncSequence として監視できる Observations 型が追加されました。 import Observation @Observable class Counter { var value : Int = 0 } let counter = Counter() // 監視対象を定義 let counterChanges = Observations { counter.value } Task { for await value in counterChanges { print( "value changed:" , value) } } withObservationTracking が持っていた課題は解消され、簡潔な記述で安全に値の変化を監視できるようになりました。 Combine フレームワークに依存せず、自然な形でSwiftの並行処理モデルに直接統合されています。 まとめ Swift 6.2 の Observations 導入により、ObservableObject が持っていた「Combine による値の監視」という優位性は解消されました。 ただし、この機能を利用するためには iOS 26以降の環境が必要となるため、サポートOSの要件によっては、すぐにプロダクションコードで全面的に採用することは難しいかもしれません。 とはいえ、「View 以外での監視」という最大の課題に対する標準的な解法が示された意義は大きく、安心して Observation への移行を進められる環境が整ったと思います。
アバター
Goエンジニアになって半年経ったので振り返る この記事は every Tech Blog Advent Calendar 2025 の 23 日目の記事です。 はじめに こんにちは!デリッシュキッチンで主にバックエンドの開発を担当している秋山です。 私は今年の6月にエブリーへバックエンドエンジニアとして中途入社し、そこから実務でGo言語を使い始めました。 それまでは約3年間主にRuby on Railsを触っていました。 この記事では、Goに転向して半年経った今感じていることや学びを振り返りたいと思います。 Goを使い始めて感じたこと 他の言語から来ると最初は戸惑う部分もありますが、使っていくとGoの良さが見えてきます。 ここでは、他言語から移ってきた人の視点で感じたことを書きます。 Goバージョン間の後方互換性が嬉しい 2012年にGoの1系が出てから毎年2回メジャーアップデートされますが、Goでは後方互換性の維持を考慮されています。 そのため、比較的容易にGoのバージョンアップを行うことができます。 業務の中でGoのバージョンアップを行わなければならないことがありました。 Goに触れる前は「メジャーアップデートに破壊的変更はつきもの」だと勝手に認識していたので、Goのバージョンアップの容易さに驚かされました。 後方互換性が保たれているおかげで「アップデートしたら動かなくなるかも」という不安が少なく、安心して最新バージョンを追えるのが良いポイントだと思います。 エラーハンドリングに違和感があった Goで書かれたアプリケーションコードを初めて見た時の話です。 try/catch(rubyの場合はbegin/rescueですが)のエラーハンドリングに慣れていたこともあり、下記のようにnilチェックを行うエラーハンドリングに当時違和感がありました。 x, err := call() if err != nil { return err } ちなみに、rubyでは通常このようにエラーハンドリングが行われます。 begin x = call() rescue => e # call()で例外が発生した時にここの処理が実行される 今ではGoの書き方に慣れてきまして、エラーを値として扱うことで明示的に処理フローを追いやすくて良いなと思います。 GoのFAQが便利 他にもRuby/RailsからGoに移った時に クラスってないの? 継承ってないの? などのような疑問が出ましたが、下記のFAQに回答がありました。 go.dev このFAQを読んだだけでもGoの思想に対する理解を1歩進められそうです。 FAQ以外にもgithub上には議論が白熱しているissueやdiscussionもあるので、Goの機能追加の背景なども知れてより理解が深められそうです。 エラーハンドリングについても議論が白熱したみたいです。 github.com go.dev go.dev AI活用で転向のハードルは下がっている? AIにコードを書いてもらう機会が増えたり言語理解にAIを使用することができるようになり、新しい言語を学ぶハードルは確実に下がっていると思います。 ただ、Goを始める前「AIにコードを書いてもらうから転向もすんなりいけるかな」のように思っていた節がありましたが、実際にはそんなことはありませんでした。 日々の業務ではCursorを使わせてもらっており、Goの知識が少なかった私にとってはCursorは非常に強力なツールです。 しかし、AIが書いたアプリケーションコードを人間が全く確認せずにそのままリリースすることは現時点でリスクが高いので、AIが出力したコードをまずは自分でレビューします。 この時結局Goの知識がないとAIコードのレビューに時間がかかってしまいます。 また、チーム内でのレビューもGoの理解なしには難しいところがあるので、結局はGoの理解が大事だなと思いました。 Goエンジニアの成長を促進するエブリーの環境 Goエンジニアへの転向において、会社の環境も大きな助けになりました。 外部イベントへの積極的な参加 エブリーでは外部イベントの積極的な参加や登壇を推奨しており、実際に登壇を行っているメンバーもいます。 私自身も今年の9月に行われた Go Conference 2025 に参加しました! ちなみに、エブリーは今年もGo ConferenceにプラチナGoルドスポンサーとして協賛しました。 Go Conferenceの様子は下記ブログをご覧ください。 tech.every.tv 会社としてこのようなイベントに積極的に協賛しているのは私自身モチベーションにつながります。 また、つよつよエンジニアの方と話す機会もあり、僕もつよつよに成長したいと感じました。 定期的なGo勉強会 エブリーでは現在 2週に1回の頻度でGoの勉強会 が開催されています。 この勉強会では、持ち回りで担当者がテーマを持ってきて使い方を学ぶだけでなく、なぜそうするのか・どうしたら良さそうかなど参加者それぞれが疑問に思ったことをどんどん深掘りしていくスタイルになっています。 この勉強会に参加することで、日頃の業務で使っているだけでは得られないGoの知識をの取得や理解を深めることができています。 また既存のコードに対しても「本当はこの書き方の方が正しいよね」という気づきも多く、実務のコード品質向上にも繋がっています。 今後も日々邁進 AIが活躍する現在も言語理解や日頃の情報キャッチアップは大事だと思っています。 そのため、GoももちろんですがGoに限らずこれからも技術向上に邁進していきます! 参考 https://github.com/golang/go/issues/32437 https://go.dev/blog/error-syntax https://go.dev/issue/71460 https://tech.every.tv/entry/2025/09/28/195717
アバター