TECH PLAY

株式会社スタメン

株式会社スタメン の技術ブログ

231

はじめに:3ヶ月目の今、あえて途中経過を書く理由 スタメンのQAエンジニア、にーくらです。スタメン初のQAエンジニアとして入社して3ヶ月。まだ成果が出てきているフェーズではありませんが、ここで立ち上げ期の思考や試行錯誤を後で振り返るためにも残しておきたいと思います。ですので、本記事は成功事例ではなく、現在試行錯誤しながら行なっていることの話になります。 現在スタメンでは東京オフィスで積極採用中で、個人の努力だけでは回らない局面が見え始めていました。プロダクトや開発、意思決定のスピードが上がる一方で、属人化や判断基準のばらつきが課題となりつつありました。そこで必要とされたのが「QA」という役割です。 QAの役割と品質への考え方 ソフトウェア開発において、QA(品質保証)は単に検証活動を行うだけの役割ではありません。しばしばQAは「テストをする人たち」と誤解されがちですが、本質的な役割はもっと広く、深いものだと信じています。 品質とは、決して「テスト工程」だけに宿るものではありません。品質は次のような日々の積み重ねによって形作られます。 日々の判断 プロセスの設計と運用 暗黙の前提や共通理解 言い換えれば、品質は日々の活動や意思決定の中に自然と組み込まれていくべきものと考えています。だからこそ、品質を高めるためには 「仕組み」「プロセス」「文化」 の3つが極めて重要だと考えています。仕組みは作業を支え、プロセスは効率と一貫性を生み、文化はチーム全体の行動と価値観を方向付けます。これらが整って初めて、個々のテストやレビュー以上の品質が組織として実現できるのです。 プロセスを組織全体に浸透させるには、個人の努力だけでは限界があります。自身が品質保証を実際に取り組む中で、 組織としての意志と後押しの重要性 を感じていました。 プロセスを定着させるには、組織として明確な方針を持ち、積極的に後押しする姿勢が不可欠です。トップやマネジメントの支援がなければ、現場の努力だけでは継続が難しいのです。 今回転職活動をする中で、組織としての支援がどれだけ得られるのかを考えていました。その中でスタメンはQAを単なる個人の作業ではなく、組織的な役割として認識していると感じました。スタメンは人と組織で勝つ会社と明確に謳っているので、その組織の姿勢に共感し、「ここなら一緒にやれる」と思えたのです。 入社して見えた現実 入社して感じたのは、個々人が本気でプロダクトに向き合っていることでした。一方で、品質を横断的に見る仕組みや検証活動の整理・体系化はほぼ実行していないと感じました。しかし、これを否定的に捉えるのではなく、むしろ伸び代が大きい状態だと捉えています。これらの課題に取り組むことで、プロダクトの品質向上と効率化を図り、さらに良い成果を生み出せるものと確信しています。 組織に新しい考え方やフレームワークを導入する際、どこかで成功しているからと、そのまま持ち込むだけでは浸透しないと過去の経験から感じています。組織にその意味を説き、やり方を調整したテーラリングが不可欠です。まずは現実に何が起きているかを観察し、開発の流れを理解することを優先しました。組織で何が起きているのか、その際に必要なフレームワークやどこから浸透させて行くか作戦を練っていく必要があります。 入社後いくつか品質を上げるための課題と提案を進めて行きましたが、それを実行するためには関係者が多く、その人たちと合意していかないと進んでいかないもどかしさを感じていました。そのような悩みをCPOと話していく中で、プロダクト品質・プロセスに関する定例ミーティングが立ち上がることになりました。 当初は、分析に必要なデータも十分に揃っておらず、これをやった方がいいと提案から始まり、ミーティングはQAの私が一人で運営していく形。これでは避けたかったただの「他社の成功事例の持ち込み」になっていないか?と考えていました。 「品質文化」という考え方 品質を上げることで、どんないいことがあるか、どういう状態に持って行きたいか改めて伝えたいと思いました。イメージとしては全員参加で品質を上げていく組織、それはQAとしてよく語られる「品質文化」の浸透という話に置き換えられます。しかし、いざ説明しようとするとその内容を的確に説明することができません。ISOの品質マネジメントに置き換えることもできそうだと思いましたが、あえて「品質文化」という言葉を使っているので、別の定義をしなければいけないと考えました。 そこで出会ったのが、西康晴氏の 「嬉しい、強い、すごい組織」 という考え方です。 What is quality culture? Is it something tasty? | PDF 品質文化とは、ソフトウェアの価値、組織能力、エンジニアリング力を上げる取り組みをし、習慣化させていくこと。組織で勝つ文化のスタメンに合いそうだとこの言葉を借りて、スタメンなりの品質文化を言語化し始めています。 次のステップは、開発プロセスの中に品質意識を溶け込ませることです。新しいルールを増やすのではなく、どこでどんな判断が行われているのかを問い直し、必要な仕組みを整えていきます。決して煩雑にすることが目的ではなく、判断を簡単に、属人化せず、その知識を蓄積して新たにステップアップできる方法ができないかと試行錯誤中です。 おわりに 単なる検証者で終わらず、品質という言葉を元に組織全員を束ねて行きたいと思っています。そして私は組織にそれを考える刺激を与えたいと思っています。道半ばではありますが、少しずつ変化が生まれています。これからも「組織で勝つ」ための品質づくりを続けていきたいと考えています。 note.com herp.careers
アバター
こんにちは!スタメンで プロダクトデザイナーをしている森田かすみ( @KasumiMorita )です。 先日、12月25日に、エンジニア・プロダクトマネージャー(PdM)・デザイナー合同の社内LT会を開催しました! 今回の記事では、当日の様子や発表内容、そして私たちスタメンのプロダクト部門が大切にしている「学び合う文化」についてご紹介します。 なぜ今、プロダクトLT会なのか 目まぐるしく変化するIT業界において、企業が競争力を維持し続けるためには、常に新しい知識を取り入れ、お互いに学び合う姿勢が不可欠です。 スタメンではそうした文化をより強固なものにするため、2025年7月から有志の現場メンバーの起案でプロダクト部門全体でLT会を始めました。今回はその第2回目となります。 tech.stmn.co.jp 開催の狙いは、 全員で高め合う学びの場づくり です。 仲間が増えると価値観やカルチャーに少しずつばらつきが出てくるのも自然なことではありますが、スタメンのプロダクト組織でも同じような課題感を持っていました。 今こそ、自分たちの組織を育てていく意識をみんなが持ち、 「こういうことをやっていきたいよね」 「こういうことを大切にしたいよね」 という思いを一人一人が大事にし、言葉や行動にしていく重要性を感じています。 特定の職種だけで完結するのではなく、部門全体の知識共有と交流を深め、プロダクト開発における連携を強化することを目的としています。メンバーがそれぞれの知見や想いを発表し、質疑応答を通じて相互理解を深める。 そうすることで、個人のスキルアップはもちろん、組織全体の成長に繋げていきたいと考えています。 そんな想いを元に、私含め、おたけ( @takeruu__ )、きいろ( @yellow_flagger )、ちぇる( @ryuseikarito )の4名で運営しています。 当日のラインナップ 通常業務や他のメインプロジェクトがある中、多くのメンバーが登壇してくれました! キーノートから技術的な知見、組織論、プロダクトの活用まで、バラエティに富んだ素晴らしい内容ばかりでした。 プログラム 【キーノート】リーダーシップ論 CPO 長田 寛司 AIネイティブ時代のプロダクト開発:全員がチームリーダーになる時代 エンジニアリングマネージャー あさしん( @asashin227 ) カイゼンは行動だ。迷ったら、少しだけ良くする プロダクト開発部 フロントエンドエンジニア 伊賀本 衛 「降りてくる」のを待たない。チーム目標のつくり方 プロダクトデザイナー 森田 かすみ ( @KasumiMorita ) テックブログを書くことで得られるもの プラットフォーム部 バックエンドエンジニア 勝間田 亮 自分がユーザーとして使い倒せるプロダクトだけど、TUNAG使ってますか? プロダクトマネージャー 中野 孝夫 Watchy Windows クライアントアーキテクチャ変更記 Watchy事業部 エンジニア おさじゅん TUNAGの開発に携われるのは、実はすごく恵まれていると思う話 エンジニアリングマネージャー まっきー 白熱した受賞結果発表! 今回は、発表内容に対して 「ベストチーム賞」「ベストCTO賞」「ベストCPO賞」 を設けました。 ベストチーム賞は全てのメンバーからの投票が多かったもの、ベストCTO賞・ベストCPO賞はそれぞれのCxO陣から選出されたセッションに贈られます。 それぞれの視点で選出された、栄えある受賞タイトルをご紹介します! 🏆 ベストCTO賞 「Watchyクライアント配布形態変更の概要」 (Watchy事業部 エンジニア おさじゅん) ▼LTの内容紹介 Watchy事業のエンジニアであるおさじゅんさんから、ストアアプリ特有の「低レイヤー制御の制限」や「企業導入の障壁」に対し、自由度の高いデスクトップアプリ化で挑んだ事例が発表されました。インストーラー実装や電子署名、Windowsサービス併用による権限管理といった技術課題を解決し、過去最高受注に繋げた経緯が共有していただきました。 技術的な挑戦と具体的な実装へのアプローチがCTOから高く評価されました。 🏆 ベストCPO賞 「自分がユーザーとして使い倒せるプロダクトだけど、TUNAG使ってますか?」 (プロダクトマネージャー 中野 孝夫) ▼LTの内容紹介 プロダクトマネージャーの中野さんから、自社プロダクト「TUNAG」の開発組織において、全社平均より利用率が低いという課題に対し、ドッグフーディング(自ら使い倒すこと)の重要性をLT内で説かれています。開発者自身が一番のファンとなり、利用を通じてユーザーの痛みを自分ごと化し、違和感を改善のチャンスに変えていこうと呼びかけられました。 自社プロダクトの活用状況をデータで一覧化し、参加しているメンバーたちへ活用を推進する姿勢が評価されました。 🏆 ベストチーム賞 今回はみんなからの投票の結果、同率で2つの発表が選ばれました! 「テックブログを書くことで得られるもの」 (プラットフォーム部バックエンドエンジニア 勝間田 亮) 「自分がユーザーとして使い倒せるプロダクトだけど、TUNAG使ってますか?」 (プロダクトマネージャー 中野 孝夫) 中野さんはCPO賞とのダブル受賞となりました!おめでとうございます🥳🎉 ▼LTの内容紹介 テックブログの運営チームの一人である勝間田さんによる、テックブログ執筆の効用を個人・組織の両面から説いたセッションです。「理解の深化」や「資産化」という個人メリットに加え、「高品質なドキュメント」や「技術・文化アピール」という組織メリットを提示。執筆は一石二鳥以上の価値があるとし、互いに称賛し合う文化と共に、積極的なアウトプットを推奨されました。 🎁 賞品について ベストCTO賞とCPO賞の受賞者には、それぞれCTO・CPOとのスペシャルランチが贈られます。 そしてベストチーム賞には、 セッションの内容を思い出せるような、TUNAGスタンプ が贈呈されます! 自社プロダクトであるTUNAG上で使えるスタンプとして、今回の学びが形に残るのはスタメンならではの賞品を今回は用意しました。 今回の開催における工夫 反省点もありますが、今回はプロダクトLT会の形骸化を防ぎ、前回との違いを明確にするため、新たにテーマ設定を行いました。 また、内容の質を高めるべく、運営主導のオファー制ではなく「プロポーザル(公募)形式」を採用しました。 1.テーマ設定 今回のテーマは 「未来への貢献」 です。 このLT会はボトムアップで始まりましたが、今回はCTOの野口さんとの雑談をきっかけにテーマを決めました。「将来的にどのような貢献につながるのか示された内容だといいですよね」というアドバイスをいただいたことがヒントになっています。 「みんなで組織を作っていく」というこの会の姿勢にぴったりで、メンバー全員が想いやナレッジを共有するのに最適なテーマだと思い、設定しました。 2. プロポーザル形式の導入 デザイナーやバックエンドエンジニアで構成された運営チームが他職種のメンバー全員に対し、個別に内容を詰めて登壇オファーを出すことは困難であるため、半年の振り返りも兼ねてプロポーザルを提出してもらう形式を採用しました。 提出項目には、タイトルと内容に加え、 「セッションを通して伝えたい意思」 を提出してもらいました。単なるナレッジ共有ならブログでも十分ですが、そこに「意思」が乗ることで、LT会ならではのストーリー性のあるプレゼンになるのではないか、と考えました。そんな仮説と期待を込めて、この項目を設けました。 3.チームによる選抜 多様な職能・部署の発表を聞けるよう、各職能チームもしくは部署内でプロポーザルを選考し、誰が代表として登壇するかを自分たちで決めてもらうフローを採用しました。 結果 これらの新しいアプローチを取り入れた結果、前回は特定の技術やツールなどの「How(どうやるか)」が中心だったのに対し、今回は「Why(なぜやるか)」「どうあるべきか」といった 組織文化やマインドセットに関するトピック が増えました。 また、今回は自社プロダクト「TUNAG」を取り扱うLTもいくつか見られ、 プロダクトへの熱量や当事者意識 が強く感じられる会になったと感じています。 半年間の振り返りとこれから 2025年7月に開催した 第1回のプロダクトLT会 をきっかけに、エンジニア向けやプロダクト企画部向けの社内勉強会を毎月1回以上(多い時は2回)のペースで開催することが定着してきました。 「継続的な勉強会の開催」という枠組みからスタートをしている取り組みではありますが、結果として半年で17人の登壇者、4人の勉強会運営者が誕生しました。 継続的に開催する仕組みがこの半年でできてきたことは、組織として大きな一歩として捉えています。 今後は、この仕組みをベースにしつつ、 自発的な学び合い文化 を内側からさらに醸成していくことに努めていきたいと思っています。 誰かから言われて学ぶのではなく、メンバー一人ひとりが主体的にインプットし、ナレッジを共有し、他者と学び合う。そんな組織を目指して、これからも様々な取り組みを行っていきます! 一緒に働く仲間を募集しています! スタメンでは、このように職種の垣根を超えて学び合い、プロダクト開発に熱狂できる仲間を募集しています。 少しでも興味を持っていただいた方は、ぜひ以下の採用ページをご覧ください。カジュアル面談からでも大歓迎です! herp.careers
アバター
はじめに こんにちは。スタメンでエンジニアをしております、 mental-space1532 と申します!今回は、昨年10月に配属されてからエンジニア3ヶ月でOSSに貢献した経験についてお伝えできればと思います。 早速ですが、私は元々プロダクト職ではありませんでした。現在新卒2年目ですが、当初はビジネス職として入社し、1年半ほどインサイドセールスとして勤務しておりました。要はエンジニアとしてはスタートラインに立ったばかりの人間です(大学も文系です!)。似た境遇の方々や、OSSへの貢献を検討されている方の参考になれば幸いです。 1. 何をやったのか 内容としては、 半角 @ が入力された場合のみ表示されていたメンション候補を、全角 @ 入力でも表示されるように改善 しました。一般に、チャットアプリでは @ を入力するとメンションの候補が表示されます。しかし、日本語IME環境では全角 @ がデフォルトで入力されることが多く、日本語ユーザーはメンションの度にいちいち入力方式を切り替えることを強いられていました。これを切り替えることなしに実行できるようにするというのが今回のPRの趣旨です。 ちなみに、技術スタックとしてはReact/TypeScriptですので、これを踏まえた上で以降を読んでいただけると幸いです! 2. 動機 ビジネス職時代、こちらのOSSチャットを使っていた際に、毎回入力切り替えを行わなければならず、「めんどくさいなこれ」と思ったことがきっかけです。 ビジネス職の経験がある方なら共感いただけるかと思いますが、セールスやカスタマーサクセスの現場では「お客様への即応性」が何よりも重要です。結果としてメンバー間での情報共有スピードが求められ、伝達漏れを防ぐためにメンションを多用します。個人的な体感ですが、ビジネス職のメンション使用頻度はプロダクト職の5〜10倍ほど多いように感じられます。 特に顕著な例では、毎朝の進捗共有で6〜7名のメンバーにメンションを飛ばす際、毎回「半角切り替え→ @ 入力→日本語切り替え→名前入力」を繰り返す必要がありました。これが地味にストレスだったのです。 当時の私は不便さを「仕様だから」と受け入れていました。エンジニアにチャレンジすることになった10月初週に「どうせやるなら本家のOSSに還元して、世界中のユーザーの体験を改善しよう!」と思い立ち、上司に相談し、社内の業務を行いつつ、AIを活用したコーディングで進めるという条件でOKをもらうことができました。 3. やったこと 初めてOSSにコントリビューションする上で、以下のようなフローで進める想定でした。 課題を言語化する(企画) GitHub CopilotのCoding Agentで自律的に実装させる(実装) 正しく動作するかローカルで検証しつつ変更箇所のコードを解読する(QAその1) 社内でレビューをいただく(QAその2) OSSのリポジトリでPRを作成し、メンテナーの方にレビューを依頼する(メンテナーレビュー) いただいたレビューを受けて対応し、マージ(貢献完了) 自社プロダクトへの適用(統合) 企画(やりたいことの言語化)〜初期実装 当初、そもそもPRってどうやって作るんだ・・・?というレベルだったので、手っ取り早くGithub CopilotのCoding Agentで200文字程度のプロンプトを投げて自律的に実装してもらう方法を取りました。そのプロンプトがこちらになります。 現在の仕様において、メンション付きのメッセージを送るためには半角のアットマーク(以下@)を入力し、自分でメンションしたいユーザーの名前を入力するか、表示される候補の中から選択する形になっている。 この状態を、@だけではなく、@でもできるようにしたい。すなわち、メッセージの編集欄に@を入力した際にもメンション候補が表示されるような状態に変更をしたい。 今見るとかなりシンプルなプロンプトですね・・・🙃 どのような仕様にするか記述していないので、Copilot先生も困ってしまったのではないかと思います。できたものをローカルで検証したところ早速問題が発生しました。全角 @ でメンション候補は出て挿入できるものの、2個目以降のメンション挿入ができない(メンション候補をクリックまではできる)のです。AI(CopilotのAgent Mode)に聞きつつコードをいじっても解決せず、詰まってしまいました。後から考えれば、この時の自分は構文を理解しようとせずに、漫然とAIに「これどうすればいいのですか?」という曖昧な形でプロンプトを投げてしまっていました。流石にこれはまずい、ということになって同僚に助言を求めたところ、半角と全角の文字の幅の違いによって挿入ができなくなっていることが判明し、フロント側の文字列挿入ロジックを修正することで「実装したい機能がとりあえず動く」という状態に持っていくことができました。 社内レビュー この段階で一度社内レビュー実施していただきました。この時、自分は「ちゃんと動くようになったし、これでOSSにPRを作成できる」と無邪気に考えていました。が、当然そんなうまくいくわけもなく、まだまだ問題が山積みになっていることが明らかになりました。具体的には、私がWebアプリの基本的な構造(責務の分離)を理解できておらず、Copilotが行ったバックエンドAPIの改変(バックエンドAPI側のメンションロジックに全角 @ を追加する修正)を見逃してしまっていたのです。結論としては、そもそもフロント側で半角 @ と全角 @ を共に正規表現としていたので、この改変は必要ないものでした。この社内レビューによって、フロントエンドとバックエンドAPIの責務の分離、個々のファイルがどのような仕事を担当しているのかをざっくりと理解することができました。 というわけでバックエンドの修正を元に戻してフロントエンドの修正するべき箇所をある程度理解した上で再度コードを吟味することになりました。ある程度当たりをつけた上で、バックエンドAPIの件のような不必要な改変がないかを確認しました。 社内レビューを経たことでより具体的に各コードについてAIに聞くことができるようになり、結果的によりシンプルに実装する方法を編み出すことができました。これらの工程を踏まえて最初に社内レビューをいただいたときよりもさらにコードの質を高めることができました。ここで再度社内レビューをいただき、OKをもらうことができたので、晴れてOSS本家のリポジトリでのPRを作成にトライしました! OSS本家でのPR作成〜マージ PRの説明文の書き方なども何も分かっていなかったのですが、幸いにも規定のフォーマットが存在したので、AIに自分の拙い英文を校正してもらいながら何とかメンテナーの方からレビューをいただける状態にしました。そして、ついにコメントが返ってきました!曰く、「その共通コンポーネントの変更、本当に必要?」とのことで、共通UIはコマンド入力などにも使われるため、そこにメンション特有のロジックを入れるのは設計として美しくないし、影響範囲が広すぎるという指摘でした。 そこでメンテナーの方から 「UI側をいじるのではなく、データを供給する側が情報を正しく伝えれば良い」 というヒントをいただき、最終的に以下の修正に落ち着きました。 正規表現を修正し、 @ と @ の両方をキャプチャできるようにする 関数に実際にマッチした文字列( matchedPretext )を渡すよう引数を追加する このことにより、さらにシンプルに同じ機能を実装することができました。 また、海外のプロジェクトであるためPRを英語で書く苦労もありましたが、Geminiに添削してもらうことで乗り切ることができました。一見無理難題に思えることも、分解して各個撃破すれば意外といけるものです。まさに「困難は分割せよ」を実感した瞬間でした! 4. 元ビジネス職という点からプロダクトに貢献できること 弊社のようなSaaS企業ではドッグフーディング(自社製品を自ら使うこと)が推奨されますが、作り手側は「使い慣れている」がゆえに、ユーザーの多様な使い方を見落とすことがあります。 特に、職種によって機能の使用頻度やコンテキストは大きく異なります。元ビジネス職のエンジニアは、 プロダクト職とは異なる、現場ならではの切実な使い方を知っている お客様に近い立場からこぼれる「小さな不満」を拾いやすい という2点において、UX向上に大きく寄与できるアドバンテージがあると感じています。 5. まとめ ここまで書いてきましたが、伝えたいことはシンプルです! 一見理解不能なエラーも、試行錯誤を繰り返せば必ず糸口が見える。まずはやってみよう! 「元ビジネス職」という視点は、エンジニアリングにおいて強力な武器になる このエントリが、OSSへのコントリビューションを迷っている方の背中を押すきっかけになれば幸いです。 最後に 株式会社スタメンではエンジニアを絶賛募集しております!弊社には、AI活用・新人から様々なことにチャレンジできる文化が整っています。もし興味を持っていただけるなら、ぜひ一度カジュアルにお話しいたしましょう! herp.careers 最後まで読んでいただきまして、誠にありがとうございました!!!
アバター
はじめに はじめまして! 2026年1月から、株式会社スタメンにモバイルエンジニアとしてジョインしました、りあたそ (@riataso_kebin) です! 前職では約2年半、インフラ構築からバックエンド開発まで、サーバーサイド全般のシステム開発・保守を中心とした業務に携わってきました。 今回はサーバーサイドでのキャリアからなぜ、実務未経験のモバイル領域に転身したのか、 なぜ数ある企業の中からスタメンを選んだのかなど、少しでもスタメンのことが気になっている方向けに書かせていただきました! なぜ、サーバーサイドからモバイルへ? これまでの約2年半、サーバーサイドのエンジニアとして「システムの安定稼働」を支えることに心血を注いできました。インフラの構築やDB設計を練り上げる仕事には特有の面白さがありましたが、一方で 「ユーザーが直接触れる部分(フロントエンド)を自分の手で形にしてみたい」 という想いが次第に強くなっていきました。 その大きなきっかけとなったのが、 個人開発 です。 自分が日常で「欲しい」と感じた機能をアプリとして形にしてみた際、指先ひとつでUIが動き、体験が完結するモバイルアプリ特有の「手触り感」に、これまでにないワクワクを覚えました。 「裏側の仕組みを理解している強みを活かしつつ、このフロントエンドの楽しさを仕事として追求したい」。そのピュアな好奇心が、未経験領域であるモバイルへの転身を後押ししてくれました。 「名前を知っている会社」から「働きたい場所」へ 実は、スタメンという会社の存在自体は、学生時代の就職活動中から知っていました。 そんなスタメンを「自分事」として深く意識するようになったのは、学生時代の友人から誘われた、ある勉強会がきっかけでした。 それが、スタメンが運営している 「mobile.stmn」 という勉強会です。 実際に参加してみると、そこには現場の第一線で活躍するエンジニアの方々がいました。お話しさせていただく中で感じたのは、 技術に対する真摯さと、何より「楽しんで開発している」という圧倒的なエネルギー です。 会話のテンポが良く、非常に心地よい「ノリ」の良さがありながらも、プロダクトへの熱い想いがひしひしと伝わってくる。 その雰囲気に触れ、「この人たちと一緒に開発ができたら、きっと楽しいだろうな」と直感的に感じたことを今でも鮮明に覚えています。 「当事者意識」を「称え合う」文化が決め手に 自社プロダクトを持つ企業は数多くありますが、私が最終的にスタメンを選んだ決め手は、プロダクトに対する 「当事者意識の強さ」 と、それを 「称え合う文化」 でした。 個人開発を経験したからこそ、プロダクトを「自分のもの」として育てていく大変さと喜びを実感していました。入社後、メンバーと交流を深める「Welcomeランチ」や、1ヶ月かけて社員の皆さんと写真を撮る「ルーキーズミッション」といった制度を通じて皆さんとお話しする中で、改めて確信したのは、全員が同じ方向を向き、プロダクトの成長を自分事として楽しんでいる空気感です。 特に、成果を全員で喜び合う 「スタカネ」 の文化は、まさに私が求めていた「チームでプロダクトを育てる理想の姿」そのものでした。 ※スタカネとは: 新機能のリリースや目標達成、あるいはメンバーの初仕事の完了など、ポジティブな成果があった際にベルを鳴らし、全員で拍手をして称え合うスタメン独自の文化です。 先日、念願の「スタカネ」を初めて鳴らすことができました!🔔 スタカネ(成果を称賛する文化)の投稿内容 入社2週間のリアル:未経験の壁と「AI」という心強い相棒 ジョインしてからまだ2週間。正直なところ、現在はモバイル開発の圧倒的な情報量とスピード感に食らいつく毎日の連続です。 個人開発とは異なり、多くのエンジニアの手によって磨き上げられてきた実務のコードベース。そこから実装の意図やロジックを正確に読み解く難しさに直面し、自分の現在地を痛感する瞬間も少なくありません。 しかし、そんな「壁」をポジティブに乗り越えていけるのが、スタメンの開発文化の面白いところです。特に驚いたのは、新しい技術やツールを使いこなすことへの貪欲さです。 現在、私はAIを「相棒」としてフル活用しています。スタメンではDevinやCursorといったツールが当たり前のように開発環境に組み込まれており、AIと対話しながらコードを書くことが文化として根付いています。 さらに、先輩社員のアドバイスを通じて、 「Before(現状のコードやエラー)とAfter(実現したい理想の状態)を明確に言語化して連携する」 というプロンプト(指示出し)のコツを学びました。これだけでAIの回答精度が劇的に向上し、複雑なロジックの読み解きや、詰まっていた課題がスルスルと解決していく体験は、まさに目から鱗でした。 「大変なこと」を「面白い課題」に変えてくれる仲間と、それを支える最新のツールがある。入社してわずか2週間ですが、この環境ならどこまでも成長していけるという確信を得ています。 これからの抱負:モバイル領域からプロジェクト開発全体へ 今後の目標は、まずはモバイルエンジニアとして自立し、一日も早くチームの戦力になることです。しかし、単に「モバイルのコードが書ける」だけでは終わりたくありません。 私が持っている約2年半のサーバーサイドの知識---DB設計やインフラ、APIの裏側の仕組みは、 モバイル開発においても必ず強力な武器になると信じています。 「裏側(サーバーサイド)を知っているからこそ、より効率的で堅牢なフロントエンドを構築できる」。そんな、領域を跨いだ視点を持つエンジニアとして、スタメンのプロダクトを技術面から力強く牽引できる存在を目指していきます 最後に:今の環境から一歩踏み出そうとしている方へ 周囲のメンバーが自分のことのように喜び、拍手で迎えてくれるあの温かい雰囲気は、入社して一番感動した瞬間かもしれません。 インフラやバックエンドの知識という「土台」を大切にしながら、モバイルエンジニアとしての「表現力」を磨き、一日も早くプロダクトの成長を力強く牽引できる存在を目指していきます。 もし、この記事を読んで「 スタメンの雰囲気をもう少し詳しく知りたい」「実際にどんな人たちが開発しているのか気になる」 と少しでも感じた方がいれば、 ぜひスタメンが開催している 勉強会(mobile.stmnなど) をチェックしてみてください。 私自身、勉強会で現場のエンジニアと直接話し、その熱量や空気感に触れたことが入社の大きなきっかけとなりました。 選考という形ではなく、まずはカジュアルな場でスタメンの「リアルな楽しさ」を感じていただけたら嬉しいです✨ 「今までのキャリアを武器にしつつ、新しい領域で自分の熱量を形にしたい」 そんな思いを抱いている方にとって、スタメンはきっと面白い挑戦ができる場所だと思います。 これからもたくさんのことを達成して、皆さんと一緒にスタカネを鳴らせるように頑張ります❗️ herp.careers
アバター
はじめに みなさん、2025年はどうでしたか? 存分にエンジニアリングを楽しみましたか? こんにちは!株式会社スタメンの ちぇる です。私事で恐縮ですが、先日「 俺の忘年会2025 」というイベントに参加してきました。 今年から始まった「俺の勉強会」シリーズのイベントですが、 「名古屋のエンジニアを家から出す!」 をモットーに、非常に熱量の高いコミュニティとして盛り上がりを見せています。 参考: 俺の勉強会を振り返る2025(スライド) その熱量に触れ、本記事では改めて自分の1年を振り返ってみたいと思います。少し個人的な話になりますが、お付き合いいただけると幸いです。 今年、僕のエンジニアとしての仕事への向き合い方は180度変わったといっても過言ではありません。一言で言えば、「仕事 = 苦行」だった日々が、「仕事 = 全力で向き合える趣味」に変わったのです。 2025年を振り返って:仕事が「最高の趣味」になるまで 1. 激動の時代だからこそ、問われる「楽しむ力」 今、エンジニアを取り巻く環境は激変しています。GitHub Copilotがコードを書き、Geminiが爆速でレビューを返してくれる。面倒な作業が効率化され、コーディングの基準がグッと引き上げられた「過渡期」に私たちはいます。 AIが正解を教えてくれる時代に、エンジニアとして働く喜びはどこにあるのか?そのヒントをくれたのが、今年入社した「株式会社スタメン」での環境でした。 2. 自主性が生んだ「自分で仕事を楽しくする」サイクル 弊社スタメンの Vision は 「人と組織で勝つ会社」 です。 この言葉通り、ここでは驚くほどの裁量と自主性が重んじられています。 目標は自分で決める 半期の目標は、自分が感じている課題をもとに提案します。 手段も自分で選ぶ 実装をどう進めるか、AIをどう使いこなすか、いつ誰に相談するか、時間の使い方も自分次第です。 この「自由」という名の責任を与えられたことで、僕の意識は変わりました。 「与えられた仕事をこなす」のではなく、「自分の仕事を自分で楽しくする」 という発想になったのです。 例えば、入社してから「RuboCopの警告解消プロジェクト」を自ら立ち上げたり、社内勉強会の運営メンバーとして文化醸成に携わったり、時にはメインのバックエンド領域から飛び出して、不慣れなフロントエンド領域のタスクに挑戦することもありました。 こうして仕事のハンドルを自分で握ることで、 「仕事ってこんなにも面白いものなのか」 と驚きました。 3. 「机に向かう」だけがエンジニアの成長ではない 以前の僕は、「とにかく本を読み、一人で黙々とコードを書けるようになること」こそが正義だと思っていました。もちろん自学自習は大切です。しかし、スタメンの「交流を大切にする文化」に触れ、その考えも大きく広がりました。 今年は勇気を出して、外の世界へ踏み出してみました。 岐阜:「 ながらRuby会議01 」への参加( 参加レポート ) 東京:「 Kaigi on Rails 2025 」への参加( 参加レポート ) 名古屋:先日開催された「 俺の忘年会2025 」への参加 そこで目にしたのは、心から技術を楽しみ、情報交換をし、世の中を良くしようとする仲間たちの姿でした。 これまで自分は、「ビジネス成果を出すための手段」としてのみエンジニアリングを捉えていた時期もありました。しかし、変化が激しく、新しい技術が次々と生まれ、インターネットを通じて世界中にプロダクトを届けられる。そんな恵まれた環境にいるのに、ただお金を稼ぐためだけに働くのは、あまりにももったいないと気づいたのです。 そう思うと、エンジニアが家から出る価値って、本当に大きいなと感じました。 4. 2026年、さらなる「高み」を目指して 自分の周りにいる先輩方は、自作ツールの開発やOSSへのコントリビュート、イベント登壇など、常にワクワクしながら挑戦し続けています。 僕もその背中を追い、2026年はさらに視座を高めていきたいです。今年はイベントへの参加を通じて多くの刺激をいただきましたが、来年は自らプロポーザルを提出し、コミュニティへ「還元する側」に回ることに挑戦したいです。 2025年は視野が広がり、エンジニアリングの本質的な面白さに気づけた1年でした。2026年もこの高揚感を忘れず、さらに成長していきたいと思います。 最後に この記事が、「開発にもっと向き合いたい」「環境を変えてみたい」と感じるきっかけになれば嬉しいです。スタメンでは、「人と組織」を大切にしながら開発できる仲間を募集しています。コードを書くことにとどまらない、学びと刺激のある日々を一緒に送りませんか? herp.careers
アバター
はじめに こんにちは、スタメンにてエンジニアインターンをしております中村です。 「 TUNAGチャット 」では、データベースとしてGoogle Cloud(旧GCP)のAlloyDB for PostgreSQLを利用しています。以前から平日昼間のCPU使用率は55~65%の水準でしたが、ある時期からデプロイのタイミングでDBのCPU負荷が100%に達し、一時的に障害となる事態が問題となりました。 一週間で2度、CPU使用率が100%までスパイクした様子 本記事では、以下のアプローチで行ったパフォーマンスチューニングの詳細を書きました。 DB負荷の原因を探る :Google Cloudの機能を利用した原因調査 クエリの改善 : 統計情報を阻害していたSQLアンチパターンの解消 実行回数の削減 : アプリケーションロジックの見直し 今回のDB負荷の原因は「オプティマイザの行数推定ミス」というRDBに一般的なもので、クエリ改善により15~20倍に高速化できました。 1. DB負荷の原因を探る 改善のために、「なにが」「いつどこで」「なぜ」起きているのかを特定する必要がありました。以下のステップで調査を進めました。 スロークエリの特定 「なにが」負荷原因であるかを確認するため、AlloyDBの標準機能であるQuery Insightsを使用しました。 Query Insightsは高負荷なクエリを可視化し、実行計画までWeb上で確認できる分析ツールです *1 。 順位表を確認すると、Top 5のクエリだけでDB負荷の大半を占めていることが判明しました(下図)。 Query Insightsの順位表 また通常時・スパイク時それぞれで負荷割合の時系列グラフを見ると、以下のことがわかりました。 これらのクエリは、特定の時間だけでなく昼夜問わず恒常的に実行されている スパイクが発生したリリースで「新しい高負荷クエリ」が追加されたわけではなく、既存のクエリ構成のまま負荷が急増していた 左:通常時 右:スパイク時 呼び出し元の特定 次に、Top5のクエリを発行するAPIが「いつどこで」叩かれているかを調査しました。 アプリケーションの実装から「どこ」の当たりをつけつつ、Google CloudのLog Analyticsを使用して「いつ」叩かれているか確認します。 Log Analyticsでは、Google Cloudのプロジェクト単位で保存されているログに対してSQLを実行できます。TUNAGチャットではL7のHTTPロードバランサー(LB)を使用しているので、LBのログから特定のエンドポイントのアクセス傾向を分析できます。 以下のSQLを実行し、APIごとのリクエスト数を時系列で集計しました。 SELECT TIMESTAMP_TRUNC( timestamp , HOUR) as log_hour, count (*) as request_count FROM `{PROJECT_ID}.{LOCATION}._Default._Default` WHERE resource . type = " http_load_balancer " AND log_name LIKE " %/logs/requests " AND http_request.request_url LIKE " %{任意のAPIエンドポイントの部分文字列}% " GROUP BY log_hour ORDER BY log_hour DESC Log AnalyticsでのSQL実行結果 調べたAPIからは、深夜帯にも一定のリクエストがあり、クエリ負荷が深夜帯にもあることと一致しています。API発行元は、スパイクに関連していそうな場所として「リロード時の初期情報取得」・「WebSocket再接続時の情報取得」がありました。 スパイクを引き起こした原因の予想 ここまでの調査から「なぜ」起こったかを予想すると、 DB負荷の高いクエリがプロダクトに存在し、普段は分散して実行されることで耐えていた。 デプロイによるサーバー再起動がトリガーとなり、既存のWebSocket接続が一斉に切断されてクライアントが一斉に再接続を試みた。 「リロード時」「再接続時」に含まれていた高負荷クエリが瞬間的に集中し、CPU使用率のスパイクを引き起こした。 つまり、「重いクエリを放置したまま、アクセスが一点集中する状況を作ってしまったこと」がスパイクの直接的な原因のようでした。 再発防止のためには、この「重いクエリ」自体を軽量化する必要があります。 2. クエリの改善 スパイクの原因はデプロイ時のコード変更ではなく、既存実装の「重いクエリ」にあることがわかりました。 なぜか選択されるNested loop Query Insightsで高負荷な5つのうち1つのクエリの実行計画を確認したところ、Nested Loopがボトルネックとなっていました。 Query Insightsの実行計画 Nested loopが最大レイテンシとなっている SeqScanが重いことも注目すべきですが、SeqScanで40000行返されているのにも関わらず、オプティマイザ(クエリプランナー)がNestedLoopを選択していることがわかります。 なぜNested loopが選択されたのかを確認するため、実際のDBに対して再度実行計画を取得しました。 QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------- Finalize GroupAggregate (cost= 1000 . 42 .. 6444 . 51 rows = 3 width= 56 ) Group Key: threads.threadteamid -> Gather (cost= 1000 . 42 .. 6444 . 45 rows = 3 width= 56 ) // 計2プロセスで並列処理(リーダー+ワーカー 1 人) Workers Planned: 1 -> Partial GroupAggregate (cost= 0 . 42 .. 5444 . 15 rows = 3 width= 56 ) Group Key: threads.threadteamid -> Nested Loop (cost= 0 . 42 .. 5444 . 08 rows = 5 width= 32 ) // ⬇️threadsに対して、1プロセスあたり 238 行と見積もられている -> Parallel Seq Scan on threads (cost= 0 . 00 .. 3496 . 03 rows = 238 width= 51 ) Filter: (((threadteamid)::text = ' XXXXXXXXXXXX ' ::text) AND ( COALESCE (threaddeleteat, ' 0 ' ::bigint) = 0 )) -> Index Scan using threadmemberships_pkey on threadmemberships (cost= 0 . 42 .. 8 . 19 rows = 1 width= 35 ) Index Cond: (((postid)::text = (threads.postid)::text) AND ((userid)::text = ' XXXXXXXXXXXX ' ::text)) Filter: following オプティマイザは統計情報をもとに行数推定を行い、その値をみて実行計画を最適化します *2 。 rows が各プロセスでの行数推定の値であり、値は『テーブルにWhere句を適用したときに残っている見積もり行数』を意味します。 Query Insightsの実行計画に、SeqScanで返された行数が40767とあるので、実態としては40000*2(プロセス数)= 80000行程度がthreadsテーブルから返されています *3 *4 。 しかし実行計画上の推定行数( rows )はたったの238行と、極端に過小評価されていることがわかりました。 WHERE句で関数を使うと、行数推定に統計情報が使えない 行数推定が狂った原因は、以下のようにWHERE句で関数を使用していたことでした。 WHERE (~~ AND COALESCE (Threads.ThreadDeleteAt, 0 ) = 0 ~~) 行数推定の概要については、こちらのスライド *5 が私のような初心者にもわかりやすくまとまっていました。 推定行数は以下の式で計算されます *6 推定行数=『濃度(テーブルの総行数)』×『選択度(WHERE句にマッチする割合)』 『濃度』『選択度』は、それぞれPostgreSQLが統計情報として持っています。 今回のテーブルの濃度を取得してみます *7 。 SELECT relpages, reltuples FROM pg_class WHERE relname = ' threads ' ; relpages | reltuples ----------+----------- 2701 | 90908 また『選択度』はオプティマイザーがWHERE句の条件文を見て、統計情報を使える場合は使い、使えない場合は以下のようなデフォルト値 *8 を使用します。 /* default selectivity estimate for equalities such as "A = b" */ #define DEFAULT_EQ_SEL 0.005 /* default selectivity estimate for inequalities such as "A < b" */ #define DEFAULT_INEQ_SEL 0.3333333333333333 /* default selectivity estimate for range inequalities "A > b AND A < c" */ #define DEFAULT_RANGE_INEQ_SEL 0.005 /* default selectivity estimate for multirange inequalities "A > b AND A < c" */ #define DEFAULT_MULTIRANGE_INEQ_SEL 0.005 /* default selectivity estimate for pattern-match operators such as LIKE */ #define DEFAULT_MATCH_SEL 0.005 /* default selectivity estimate for other matching operators */ #define DEFAULT_MATCHING_SEL 0.010 ... 値を比較する際、 COALESCE(Threads.ThreadDeleteAt, 0) = 0 のように関数を通すとオプティマイザが出力を予想できず、『選択度』に最頻値などの統計情報が使えなくなります。その結果、今回はイコール式が条件文のため、オプティマイザは『選択度』として最初の行にある0.005を使用します。 今回の並列処理(2プロセス)に当てはめてざっくりと行数推定してみると、『濃度』*『選択度』=90908*0.005=454.54、1プロセスあたり454.54÷2(プロセス数)=227.27と、オプティマイザが計算した238に近い値になりました。 つまり、データの実態を無視して、Where句を適用した後の行は「全体の0.5%しかない」と見積もられた結果、4万行のループ処理が走っていたようです。 実行計画が実態を反映するようになった 負荷Top5のクエリ全てで、WHERE句に COALESCE が使用されていたため、統計情報が正しく機能するシンプルな比較式に書き換えました。 WHERE (~~ AND (Threads.ThreadDeleteAt = 0 OR Threads.ThreadDeleteAt IS NULL )) ~~) この修正により、オプティマイザは行数を約8万件と正しく認識するようになり、Nested Loopから Hash Join を選択するようになりました。 QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------- GroupAggregate (cost= 4047 . 87 .. 8320 . 14 rows = 3 width= 56 ) Group Key: threads.threadteamid -> Hash Join (cost= 4047 . 87 .. 8312 . 34 rows = 1554 width= 32 ) Hash Cond: ((threads.postid)::text = (threadmemberships.postid)::text)     // ⬇️実態が反映されている -> Seq Scan on threads (cost= 0 . 00 .. 4052 . 55 rows = 80727 width= 51 ) Filter: (((threaddeleteat = 0 ) OR (threaddeleteat IS NULL )) AND ((threadteamid)::text = ' XXXXXXXXXXXX ' ::text)) -> Hash (cost= 4026 . 18 .. 4026 . 18 rows = 1735 width= 35 ) -> Bitmap Heap Scan on threadmemberships (cost= 34 . 63 .. 4026 . 18 rows = 1735 width= 35 ) Recheck Cond: ((userid)::text = ' XXXXXXXXXXXX ' ::text) Filter: following -> Bitmap Index Scan on idx_thread_memberships_user_id (cost= 0 . 00 .. 34 . 19 rows = 1836 width= 0 ) Index Cond: ((userid)::text = ' XXXXXXXXXXXX ' ::text) 左:クエリ変更前 右:クエリ変更後 3. 実行回数の削減 クエリ単体の軽量化に加え、アプリケーション層でも「そもそもそのクエリを投げる必要があるか」を見直しました。 調査の結果、5つの高負荷クエリのうち一部は、特定の画面状態によっては実行が不要なものであることが判明しました。特にデプロイ後に負荷が集中する「リロード時」や「WebSocket再接続時」において、ロジック上不要なクエリの発行をフラグ制御でスキップするように修正しました。 エンドポイントのフラグ置換(青→緑)によりクエリ発行数を削減 改善結果 CPU使用率 赤棒は左から、応急措置として行なったスケールアップ(左の赤線)、元スペックへのスケールダウン(右の赤線)を示しています。対策適用を行なった以降は、インスタンススペックを元に戻した後も、 CPU使用率20〜30%程度 で安定して推移しています。 右の赤線以降、20-30%の使用率で落ち着いている クエリ実行速度 実行計画が適正化されたことで、レイテンシが劇的に改善しました。 平均実行時間は 約400msから20ms台 へと短縮され、 15~20倍高速化されました 。 番号 修正前 修正後 改善倍率 ① 436.71 ms 20.87 ms 約 20.9倍 ② 435.86 ms 20.75 ms 約 21.0倍 ③ 440.62 ms 29.06 ms 約 15.2倍 ④ 369.46 ms 25.20 ms 約 14.7倍 ⑤ 369.16 ms 24.99 ms 約 14.8倍 まとめ Where句で安易に関数を使うと、オプティマイザが行数推定を誤り、データ量に見合わない非効率な実行計画が選択される原因になります。 またスロークエリの原因を探る際は、単に「インデックスがあるか」だけでなく、オプティマイザの実行計画が正しいかどうかに注意する必要があることが、今回の調査で身にしみてわかりました。 最後に 今回、Google Cloudを触るのもDBチューニングも初めてでしたが、DB負荷改善という明確な目標に取り組みながら学ぶことで、自分にとって良い経験になりました。 まだまだGC,DBともに知らないことが多くあるので、業務に活かせるよう引き続き学んでいきたいと思います。 本記事は、会社で契約しているGeminiに校正をしてもらいました。 スタメンには、新人のうちから色々なことを経験させてもらえる文化があります。またエンジニアだけでなく全部署でAI活用を推進しています。少しでも興味を持たれましたら、以下フォームからぜひご応募ください! herp.careers *1 : Query Insights を使用してクエリのパフォーマンスを向上させる *2 : PostgreSQL 17.6文書 パート VII. 内部情報 第68章 プランナは統計情報をどのように使用するか *3 : パラレルクエリにおけるプロセス数について *4 : 上の補足 *5 : PostgreSQL:行数推定を読み解く/row-estimation *6 : PostgreSQLにおける行数推定の計算方法 *7 : オプティマイザが濃度として参照する値 *8 : GitHubで確認できる、PostgreSQLの選択度デフォルト値
アバター
はじめに こんにちは!株式会社スタメンの ちぇる です! 長らく動画変換を支えてきた Amazon Elastic Transcoder(以下 Elastic Transcoder)の EOL(サポート終了)が発表されました。AWS 公式でも AWS Elemental MediaConvert(以下 MediaConvert)への移行 が強く推奨されています。 弊社が運営する「TUNAG」でも、これまで Elastic Transcoder と MediaConvert が混在していましたが、この度 MediaConvert への全面移行を完了しました。本記事では、この移行プロジェクトを通じて学んだ「変換システムの思想の違い」と「設計の健全化」についてご紹介します。 動画変換システムの仕組み:思想の違い 両サービスを比較すると、動画変換に対する根本的な設計思想が異なることに気づきます。 Elastic Transcoder:Pipeline 中心設計 Elastic Transcoder の設計思想は、一言で言えば 「固定された変換ライン(Pipeline)に Job を流し込む」 方式です。 Pipeline: 入出力バケットや IAM ロール、SNS 通知などを定義する。 Job / Preset: 解像度やコーデックを定義し、Pipeline にキューイングする。 あらかじめ定義された「変換ライン」の処理能力に応じて順次実行されるため、管理はシンプルですが、入力動画の特性に合わせた柔軟な調整には限界がありました。 MediaConvert:Job 中心設計 対する MediaConvert の思想は 「動画 1 本ごとに詳細な注文票(Job)を作り、Queue でさばく」 方式です。 Job: 入出力設定、動画・音声の細かなパラメータ、加工設定など全ての情報を持たせる。 Queue: 並列実行数や優先度を制御する。 MediaConvert には Pipeline という概念がなく、動画ごとに Job を定義します。そのため、「入力動画の特性に合わせて解像度・ビットレートを動的に最適化する」 といった、より高度でプロフェッショナルな制御が可能になります。 「Pipeline(変換ライン)中心」から、より柔軟な「Job(注文票)中心」への転換が、このリプレイスの本質といえます。 移行で直面した「厳格さ」という注意点 移行を進める中で、特に注意が必要だったのが 「 音声のみ(映像トラックなし)のデータ 」 の扱いです。 「よしなに」の Elastic Transcoder、「厳格」な MediaConvert Elastic Transcoder: 映像+音声の設定になっているプリセットに「音声のみ」のファイルを投げても、足りない要素(映像)を自動でスキップして「音声のみ MP4」をよしなに出力してくれます。 MediaConvert: 商用レベルの品質を担保するため、バリデーションが非常に厳格です。映像設定があるのにソースに映像がない場合、「設定と入力が一致しない」としてエラーになります。(実装側で、入力に応じて Video Selector を動的に除外するといった制御が必要となります。) そのため、Elastic Transcoder では「よしなに」処理されていたファイルが、MediaConvert ではエラーとなり、「以前は投稿できていたファイルがアップロード(変換)できなくなる」という問題が発生する可能性があります。 厳格さが生む、運用の健全化 一見すると MediaConvert は手間がかかるように見えますが、これはサービス運営において大きなメリットになります。 例えば、これまでは動画アップロード導線に誤って音声データが混入した場合でも、「映像のない真っ暗な動画」として変換されてしまい、タイムラインに黒いフレームが表示されるといった状況が起こり得ました。今回の移行に伴い、「仕様の曖昧さ」を排除することで、コンテンツの種別に応じた最適な見せ方を強制できるようになったのです。 具体的には、音声データについては「音声専用のアップロード導線を用意する」か、「サムネイルを付与したうえで動画としてアップロードする」かの、いずれかの方法を取ることになります。 こういった仕様は一般的で、例えば YouTube でも音声データをそのまま動画としてアップロードすることはできず、公開には映像トラック(静止画を含む)が必須となっています。 まとめ TUNAG には複数の動画投稿導線が存在しており、新旧のシステムが混在していましたが、今回のリプレイスを通じて MediaConvert への一本化が完了しました。 サービスを運用する中で、EOL によるリプレイスを迫られるのは避けられない宿命です。しかしそれは同時に、 「システムの曖昧さを整理し、設計を健全化する絶好の機会」 でもあります。 今回の移行を経て、TUNAG の動画基盤はより堅牢で、今後の進化に耐えうるものになりました。本記事が、同じく動画変換基盤の運用やリプレイスに取り組んでいる方々の参考となれば幸いです。 最後に 株式会社スタメンでは、エンジニアを絶賛大募集中です! 技術の力でサービスをより良くしていきたい方、ぜひ一度お話ししましょう。詳しくは以下をご覧ください! herp.careers
アバター
目次 目次 はじめに 移行背景 Web Push通知の仕組み バックエンド側の実装 フロントエンド側の実装 トークンのライフサイクル管理 移行工程 最後に はじめに こんにちは。スタメンでTUNAGのバックエンド開発を行なっている きいろ です。 TUNAGは組織活動を支援するサービスで、Webアプリとモバイルアプリの両方で提供しており、Webアプリではユーザーアクションをリアルタイムに届けるためのWeb Push通知機能を備えています。 TUNAGの代表的な機能として「制度」があり、そこから社内報など情報共有のための各種コンテンツ投稿が行えます。 それら投稿のリアルタイム通知を行うなど、Web Push通知機能はプロダクトのユーザー体験に直結する重要な仕組みとなっています。 今回、そのWeb Push通知機能について、Firebase Cloud Messaging(以下、FCM)をベースに移行を行いました。 この記事では、移行当時の調査内容や、バックエンドおよびフロントエンドの両面で直面した課題、そこから得られた知見を紹介します。 同様にFCMを用いたWeb Push通知の導入や移行を検討されている方にとって、何かしら参考になれば嬉しいです。 移行背景 TUNAGでは長らくPushCrew(現 VWO Engage)を利用し、Web Push通知機能を提供していました。 PushCrewはWeb Push通知機能を手軽にサービスに導入できる一方、長期運用をする中で通知用トークン管理や料金面などの課題が上がっていました。 また、通知設定をPushCrewから提供されるサブドメイン上で行うため、通知の再設定フローがやや複雑になりやすいなど、運用面での細かい課題も表面化していました。 移行先としてFCMを選んだ理由は、すでにモバイルアプリの通知基盤として活用しており、開発環境ごとにFirebaseプロジェクトが存在していたこと、認証まわりの仕組みも整っていたことが大きいです。 新規構築する部分が比較的少なく、既存リソースを活かしながらWeb側の通知基盤も統合できる点が、移行先への決め手となりました。 Web Push通知の仕組み PushCrewからFCMへの移行にあたり、最も大変だったものがFCMの導入設計です。 当時の自分はWeb Push通知に関する知識が充分とは言えず、既存機能の連携構造も把握しきれていなかったため、Web Push通知送信に関する既存実装、PushCrew APIの仕様と利用形式を主軸に、地道にFCMの導入方針を定めていきました。 そもそもとしてのFCMベースでのWeb Push通知の仕組みについてですが、ブラウザ含めて以下4つの主要コンポーネントによって成立します。 コンポーネント 役割 Browser Web Push通知の受信 Push Service 通知の中継サービスその1。通知先エンドポイント管理。ブラウザに直接Web Push通知を届ける FCM 通知の中継サービスその2。アプリケーション側のリクエスト起点でWeb Push通知を送信する Application Web Push通知の送信 実際にWeb Push通知を行う場合は、(1)トークン作成→(2)Push通知送信の2ステップで行います。トークンとはWeb Push通知配信サービスを利用する際に、サービス事業者側でクライアント管理するために個別に発行するIDのことを表現しています。ID名称は通知配信サービス毎に異なりますが、この記事ではまとめて「トークン」と表記します。 Web Push通知を行う際の、具体的なコンポーネント間の連携フローは以下の通りです。 青線:トークン作成 赤線:Push通知送信 Web Push通知の処理フロー Push通知先のエンドポイントの発行管理はPush Service(Push Subscription)で行い、FCMはdevice tokenを通じてPush Subscriptionを管理します。 実際の通知送信時は、アプリケーション側はFCM APIを通じてdevice tokenをFCM側に連携し、FCMはdevice tokenをもとに対応するPush Subscriptionを内部で参照、その後Push Serviceを経由してWeb Push通知の送受信が行われます。 ちなみにですが、FCMを通知配信基盤として利用する場合、Firebase SDKがこれらを抽象化してくれるため、Web Push通知の内部構造やPush Serviceの存在を強く意識する必要はありません。 実際の実装手順としては以下のようなシンプルなものになります。 Service Workerの登録 Firebase SDKから提供される getToken() を用いてdevice tokenを取得 device tokenの保存 send API 呼び出し時に保存したdevice tokenをpayloadに含めて通知実行 一方で、実装を進める中で「Push通知時の送信先エンドポイントはどのように管理されているのか」といった点がブラックボックス的に映り、実装方針の構築に時間を要した部分もありました。 そのため通常の実装では深く意識しなくてもよいものの、背景として知っておくと挙動の理解がしやすくなるWeb Push通知の仕組みについて、整理の意味も兼ねて簡単にまとめました。 バックエンド側の実装 バックエンド側の実装設計を行うにあたり、軸にしたものはPushCrew APIとFCM APIの仕様差分でした。移行時点でTUNAGで行うWeb Push通知の利用形式として、以下3つのパターンがありました。 単一トークン通知 複数トークン同時通知 時刻指定通知 PushCrewはこれらの通知形式をAPIとして個別に提供してくれていたため、バックエンドではトークンを用意して用途に応じたAPIを呼び出すだけで通知制御が完結していました。 しかし、FCMが提供するものは単一トークン通知用API(先のsend API)のみです。そのため、複数トークン同時通知・時刻指定通知の制御はアプリケーション側で構築する必要がありました。 また、トークンはユーザーに紐づくものですが、厳密にはユーザーのクライアント環境に応じて発行されます。同一ユーザーであっても、複数のデバイスからログインした場合はそれぞれ別IDとしてトークン発行が行われます。 そのため、アプリケーション側では ユーザー:トークン = 1:Nの対応関係を保った管理 が必要となります。 このデータ構造を考慮した上で、複数トークン同時通知および時刻指定通知の制御は、Sidekiqを用いて以下のような処理フローを設計しました。 イベント発火時に通知対象ユーザーを決定 ユーザー単位で通知ジョブを発行 各ユーザーに紐づくトークンを取得 トークン単位でFCM API呼び出しジョブを発行 %%{init: {'theme': 'dark'} }%% sequenceDiagram participant caller as caller participant worker1 as UserWorker participant worker2 as TokenWorker caller->>worker1: perform(user_id,msg) activate worker1 worker1->>DB: user取得 activate DB DB->>worker1: user deactivate DB worker1->>DB: token取得 activate DB DB->>worker1: token deactivate DB loop token_id worker1->>worker2: perform(token_id,msg) deactivate worker1 Note over worker2: Web Push通知実行 end 複数トークンへの同時通知については通知時に並列でジョブを走らせることで対応し、時刻指定通知においてはジョブそのものの実行時間指定で制御しています。 Web Push通知は複数の機能から利用される前提の機能であるため、通知内容と対象ユーザーを指定するだけで利用できるインターフェースに統一し、内部処理はSidekiqで吸収する方針にしました。 通知処理中の失敗に対するリトライ制御についてもアプリケーション側で個別に持たず、Sidekiqの標準機能に集約することで、通知処理全体の見通しが良くなるようにしました。 手探りの実装でしたが、他機能との連携や拡張性を見据えた構成にできたかなと思っています。 フロントエンド側の実装 バックエンド側のコアロジックが固まった後は、フロントエンド側の移行を行います。フロントエンド側で移行が必要な箇所は以下2つです。 トークン発行処理 通知表示処理(フォアグラウンド通知、バックグラウンド通知) まず、トークン発行処理ですが、ここでもPushCrewとFCMの仕様の違いが大きく影響しました。 FCMでのトークン発行処理はFirebase SDKが提供する getToken() 関数を用いて行う想定で、その前段としてService Workerの登録が必要となります。 Service Workerは、ブラウザに登録されるJavaScriptスクリプトで、Web Push通知機能に関してはプッシュ通知イベントが発生した際(Web Push通知を受信した際)にブラウザがワーカーとして実行し通知表示の処理を行います。 PushCrewでは、Web Push通知に必要となるService Workerが公式CDNから提供されていたため、トークン発行時にはそのエンドポイントの呼び出し、取得したスクリプトをService Workerとして登録をするだけでWeb Push通知表示が可能でした。一方で、FCMを用いたWeb Push通知では、Service Worker向けのスクリプト配信自体をサービス事業者側で担う必要があり、登録用のエンドポイントを含めた配信設計から検討する必要がありました。 このWeb Push通知処理に向けたService Worker取得用のエンドポイントを実装する上で、Web Push通知を受け取るWebサービスのドメイン直下で取得する、ということが重要になります。(例: https://service-domain/service-worker.js など) これはFCM公式でも推奨されているプラクティスになります。 というのも、Service Workerがイベントハンドリングにおいて制御可能な範囲は、スクリプトを取得したURLのパスに依存しており、その配下にあるページやリクエストのみが対象となるからです。 例えば、Web Push通知を受けとり、そこに仕込まれたURLリンクに直接遷移する際、遷移先で特定要素にフォーカスを当てるなどの挙動が可能ですが、そのフォーカスを当てられるページは、Service Workerが取得したURLパスに依存します。 ドメイン直下で取得しない場合、先の例だと遷移先ページによってはフォーカス処理がうまく動作しない、などの通知挙動の違いが外部起因によって生まれてしまうことになります。 そのため、Web Push通知に関するService Workerの取得エンドポイントはドメイン直下で取得することが推奨されています。 TUNAGでもこのプラクティスに沿ってエンドポイントを実装しようとしたのですが、既存のアプリケーション構成との兼ね合いから、ドメイン直下でのスクリプト配布ができない状況でした。 この制約により、Service Workerの配置パスやスコープ設計を検討する必要が生じ、一部のページが制御対象外となるリスクも含めて実装判断を行うことになりました。 最終的に、先のService Workerスコープ仕様と既存の通知仕様を踏まえ、直近は問題にはならないと判断し、ドメイン直下ではなくサブパスを挟んでのService Worker登録を行う運用になりました。 通知表示処理の移行ですが、こちらは公式プラクティス通りの実装を行ったため割愛します。 トークンのライフサイクル管理 Web Push通知機能は通知先クライアントの識別用にトークンを利用しますが、これはクライアントの持ち物であり、Service Workerのライフサイクルと連動します。 これまで利用していたPushCrewでは、通知用Service Workerが専用ドメインで管理されており、トークンの有効期間についても長期利用を前提とした仕様でした。 そのため、クライアント側でトークンを定期的に再取得する必要性は高くありませんでした。 一方で、FCMではトークンは長期利用を想定したものではなく、トークンそのものが不意に利用出来なくなることを前提としたリソースとして扱っています。 そのため、今回のFCM移行において、適当なタイミングでの再取得処理を実装する必要が出てきました。 これらを踏まえ、最終的に実装した一連のトークン管理フローは以下の通りです。 %%{init: {'theme': 'dark'} }%% stateDiagram-v2 direction LR state トークン管理 { 取得 --> 保存 : 差分あり 取得 --> 破棄 : 差分なし 保存 --> 取得済み 破棄 --> 取得済み } [*] --> 取得可否を判定 取得可否を判定 --> 取得 : true 取得可否を判定 --> 取得不可 : false 取得済み --> 取得可否を判定 : 特定導線を踏む 取得不可 --> [*] TUNAGでは通知の安定性を維持するため、ユーザーアクションに紐づけてトークンの再取得を行い、トークンの鮮度を保つことにしました。 また、バックエンド側で実際に通知を行う際は通知APIのレスポンスを元に、利用期限に達したトークンについてはその時点で破棄する処理も組み込み、不要なトークンがアプリケーション上で残り続けないようにしています。 移行工程 実際の基盤移行については、TUNAGの既存ユーザーへの影響が避けられないため、以下4つのフェーズを策定して進めました。 フェーズ 通知状況 FCM移行前 PushCrew APIを用いて通知 FCM移行中 ユーザーのトークン状況に応じてPushCrew APIとFCM APIを使い分け FCM移行後 FCM APIを用いて通知 PushCrewトークン削除 FCM APIを用いて通知 FCM移行中のフェーズにおいてはTUNAGユーザーを以下の3パターンで分類し、移行の前後で互換性を持たせることでトークン保持状況の移行期間を設けました。 ユーザーパターン 通知形式 PushCrewトークンのみ保持するユーザー PushCrew APIを用いて通知 両トークンを保持するユーザー FCM APIを用いて通知 FCMトークンのみ保持するユーザー FCM APIを用いて通知 以上を踏まえ、移行に関する全体工程は以下の通りとなりました。 通知基盤の移行フロー PushCrew API切り離し時期の策定において、どのタイミングで行ったとしても一部の移行前ユーザーへの影響が避けられない懸念がありましたが、そこは十分な移行期間を設けることで影響の最小化を行いました。 移行期間の区切りについては、FCMトークン総数の増加を時系列でモニタリングしつつ、その傾向が安定化する時期までとしました。 その時点で移行が済んでいないユーザーについてはWeb版を積極的に利用していない層と扱いリスク許容をした上で、最終的なPushCrew APIの切り離しに至りました。 最後に 今回の記事では、TUNAGにおけるFCMへのWeb Push通知基盤移行で直面したポイントを紹介しました。 自分自身、Web Push通知の実装に本格的に関わるのは初めてで、知識のない状態から手探りで進めた部分も多かったのですが、社内のエンジニアの方々に細かく相談しつつ、最終的には無事移行することができました。 設計や実装の節目ごとに意見をもらえたことで、現在は大きな問題もなく運用できています。 今後も安定した通知体験を提供できるよう引き続き改善していく予定です。 FCMはメジャーなサービスではありますが、Web Push通知の仕組みなど前提知識が求められるケースも多く、必要最小限のAPIのみが提供されているため、移行元サービスとのギャップに戸惑う場面もあるかと思います。 この記事が同じようにFCMの導入を検討されている方々の手助けになれば嬉しいです。 スタメンでは、Rubyエンジニアに限らず全技術領域で、共にTUNAGを成長させていきたいエンジニアを募集しています。 ご興味いただけましたら、ぜひまずはカジュアル面談からご応募いただけると嬉しいです。皆さんとお会いできることを楽しみにしています。 herp.careers
アバター
はじめに こんにちは、プロダクト開発部の 勝間田 です。 非同期処理は、即時の応答が不要な処理をバックグラウンドで並行処理することでユーザー体験を向上させるものであり、私たちのサービス TUNAG(ツナグ) では主にSidekiqを利用しております。 即時の応答が不要な処理とはいえ、そこで大きな 遅延(Latency) が発生してしまえば、ユーザー体験を損ねることにつながってしまいます。 Sidekiqの設定でキューの重みづけを行い、ユーザーへのインパクトが大きいジョブが優先的に処理されるよう工夫はしていましたが、優先度の低いキューとはいえ、その遅延が大きくなることがありUXに少なからず影響が出始めていました。 これまでスケーリングについては ECSタスクのCPU使用率ベースのオートスケーリング に任せていました。 それとは別で定期実行によりSidekiqのキューが一定数以上滞留してしまった場合はSlackにアラートを通知し、エンジニアがそれに気づいて手動でECSのタスク数を増やす、という運用を行なっていました。 こうした属人的な対応・検知から遅延解消までのタイムラグ・休日対応が難しい点などが課題となっており、これらを解消すべく今回の仕組みを導入するに至りました。 また この記事では下記のことについては触れませんので、あらかじめご了承ください 。 CloudWatchカスタムメトリクス送信の具体的な実装 各AWSリソースの具体的な設定手順 余談ですが、最近サブサービスで Solid Queue を導入しました。導入の背景や手順について別のブログで紹介しておりますので、ご興味のある方はぜひご覧ください。 tech.stmn.co.jp 対応内容 これらの課題を解決するため、監視する指標をCPU使用率から SidekiqのキューLatency(遅延時間) そのものに変更することにしました。 やりたいこと:キューのLatencyが一定の閾値を超えたら、エンジニアの手を介さず自動でECSタスクをスケールアウトさせる 具体的な対応内容は以下の通りです。 Napkinを使ってみました 1. LambdaでアプリのAPIよりSidekiqのLatencyを取得 Sidekiqには、キューやワーカーの状態を取得するためのAPIがあります。 github.com 以下のようなコードで簡単にキューの情報を取得することができます。 stats = Sidekiq :: Stats .new stats.queues.keys.map do |key| queue = Sidekiq :: Queue .new(key) { name : queue.name, size : queue.size, latency : queue.latency } end このSidekiq APIを利用してLatencyを取得する方法として、「LambdaからSidekiqのデータストア(Redis)に直接接続する」のではなく、 「アプリケーション(Rails)側に、Sidekiq APIを呼び出して結果をJSONで返す専用のエンドポイントを実装する」 ようにしています。 このようにすることで以下のメリットがあると思います。 Lambda側でRedisとの接続が不要になるので、あらゆる設定をする必要がない。 将来SidekiqのバージョンアップなどでAPIの仕様が変わったとしても修正はアプリケーションのみとなり、Lambda側のコードは一切変更する必要がなく返却されるLatencyに集中すればよくなる。 2. CloudWatchカスタムメトリクスにQueueのLatencyを送信 Latency情報をオートスケーリングのトリガーとして使えるよう、CloudWatchに送信します。 TUNAG(ツナグ) では、処理する機能が大きく異なる非同期処理についてそれぞれ異なるECS Serviceで動かしているため、個別にオートスケーリングさせる必要がありました。 そのためメトリクス名は同じ Latency としつつ、CloudWatchの ディメンション を利用してECS Serviceごとに設定することで、CloudWatchアラームがLatencyを個別に監視し、対応が必要なECS Serviceのみをスケールアウトさせることができます。 3. EventBridgeをLambda実行のトリガーに設定 用意したLambdaを定期実行するために、トリガーとしてEventBridgeのスケジュールを設定しました。 スケールアウトが後手後手にならないようある程度短めにLambdaを実行するようルールを設定しています。 4. CloudWatch アラームによる監視と発火 CloudWatchに Latency メトリクスが送信されるようになったので、次はこのメトリクスを監視するCloudWatchアラーム を作成します。 アラームも個別に作成することができ、サービスごとに独立したスケーリングができるようにしています。 下の画像のように、ECS Serviceで分けたディメンション単位でアラームを設定しています。 CloudWatchメトリクスの参照タブ抜粋 5. ECSオートスケーリングポリシーの実行 作成したCloudWatchアラームが ALARM 状態になることをトリガー として、ECSタスクを増やすスケーリングポリシーを設定します。これにより、Latencyが高くなったらECSタスクが設定した数だけ自動でスケールアウトするようになります。 ここまででやりたいことの「キューのLatencyが一定の閾値を超えたら、エンジニアの手を介さず自動でECSタスクをスケールアウトさせる」が実現できます。 ただスケーリングポリシーを設定する上で、コストとパフォーマンスのバランス調整に悩みました。「いつ、どのくらいタスクを増減させるか」という判断には、2つの課題があります。 スケールアウトから解消までのタイムラグによる「過剰スケールアウト」のリスク ECSタスクを増やしてから、そのタスクが起動し、キューのLatencyが実際に解消されるまでにはタイムラグが発生します。 もし、アラームが発火し続ける間タスクを増やし続ける設定にすると、本来なら(起動中のタスクで)捌ききれる量だったにも関わらず、過剰にタスクを増やしてしまい、無駄なコストが発生する恐れがあります。 スケールインによる「ピーク時パフォーマンス低下」のリスク 非同期処理の特性上、平常時はLatencyが0に近い状態が続きます。もしLatencyの値だけを見てタスクを減らすと、ピーク時以外の時間帯でタスクが必要最低限まで減りすぎてしまいます。 その結果、次のジョブのピークが来た際に、スタート時点のタスク数が少なすぎるため、そこからスケールアウトを始めても処理が間に合わなくなる恐れがあります。 上記のような課題があったため、現状は下記のような対策によりバランスをとっています。 スケーリングポリシーに 適度なクールダウン期間を設定 ECSタスクをスケールアウトさせ、新しいタスクが起動してジョブを処理し始めてもすぐにLatencyが解消されるわけではありません。このタイムラグを考慮しないと、Latencyがまだ高い状態を見てアラームが発火し続けることで必要以上にタスクを増やしてしまい、過剰なスケーリングが発生する可能性があります。 そのためスケーリングポリシー側に適切なクールダウン期間を設けることで、「一度スケールアウトしたら、次のスケーリング判断まで一定時間待機する」ように設定し、過剰なスケールアウトを防ぐようにしました。ただこの期間についても増やすタスクの数と同様でサービスによって適切な値が異なってくると思います。最初は短めに設定しつつ、Latencyの解消具合を見ながら微調整していくと良さそうに思いました。 キューのLatencyによる「スケールイン」をあえて設定しない 非同期処理の特性上、アラームがOK(Latencyが低い)状態の時間帯のほうが多いため、OK状態をトリガーにタスク数を減らしてしまうと、次のジョブのピーク(エンキューが急増する時間帯)が来た際に、タスク数が少ない状態からスケールアウトをすることになります。 Latencyによりスケールアウトした時間帯は、継続してジョブがエンキューされる可能性が高い時間帯です。クールダウン期間もあるため、少ないタスク数からのリカバリーでは間に合わなくなる恐れがあります。 そのため、Latencyベースでは「増やす」ことだけを担当させ、タスクを「減らす」処理については、従来から設定してあるCPU使用率ベースのスケーリング(CPU使用率が低ければ減らす)に引き続き任せる構成としました。 現状はこのような設定で運用しております。 これらの設定についてもサービスの特性やピーク帯の有無等で変わりそうです。より良い設定があればぜひ知りたいです。 結果 Latencyベースのオートスケーリングを導入した結果、これまでキューの滞留件数が増えるたびに飛んでいたSlackアラートが、ほとんど飛ばなくなりました。 手動対応についても導入後は一度も行っておりません。 これによりエンジニアの手動対応コストを削除し、処理遅延によるUXの悪化を未然に防ぐことができました。 まとめ 今回ご紹介したSidekiqのLatencyをトリガーにしたオートスケーリングは、AWSの標準的な機能(Lambda, EventBridge, CloudWatch, ECS Auto Scaling)を組み合わせることで実現できるため、導入自体のハードルは比較的低いと感じました。 今後はより細かなチューニングもしていければと考えています。現状では閾値を超えた際に決まったタスク数を増やしていますが、Latencyの値やキューの種類によって増やす数を変えたり、監視するキューをより厳選したりなどコストパフォーマンスとのバランスを考えながら改善していきたいです。 最後に スタメンでは、Rubyエンジニアに限らず全技術領域で、プロダクトを成長させていくエンジニア、デザイナー、プロダクトマネージャーの方を募集しています。 ご興味いただけましたらぜひご応募いただけますと嬉しいです。 皆さんとお話できることを楽しみにしています。 herp.careers
アバター
🏁 はじめに 株式会社スタメンにてプラットフォーム部で SRE / DevEx などに取り組んでいるもりしたです。今回は Ruby on Rails アプリケーションに Solid Queue を導入したお話を書こうと思います。 こんな人に読んでもらえるとうれしく思います。 Solid Queue に興味がある Ruby on Rails アプリケーションで手軽に定期実行ジョブを作りたい すでに稼働している定期実行ジョブの Solid Queue へのリプレイスを検討している 💡 Solid Queueとは? DB完結型ジョブキューの基礎 Solid Queue は Active Job 向けに設計された、DBベースのキューイングバックエンドです。 Sidekiq や Resque のように Redis などの外部サービスが不要 Recurring Tasks 機能を利用することで Cronのように、設定されたスケジュールに従ってジョブを自動的に定期実行できる 利用するDBは既存アプリケーションと共存することも Solid Queue 独自で分離することもできる 📌 導入の背景 ジョブを定期実行したかったためです。 スタメンの主力事業 TUNAG(ツナグ) は TUNAGのコア部分(以降、TUNAG本体)と機能単位で切り出した複数サービスが連携して動作していますが、機能単位で切り出したタスク機能サービスで定期的に実行したい処理がありました。 しかしながら、タスク機能サービスにはジョブを定期実行する基盤がなかったため、基盤構築から始めることとなりました。 ⚙️ 既存の定期実行システム:AWS/ECS連携のアーキテクチャ まずは既存の定期実行がどのように動いているかを確認しました。 TUNAG本体では elastic_whenever gem を利用して ECS Scheduled Task にて rake タスクを実行しています。 rake タスクを追加して定期実行するまでの流れは下記となります。 プロダクトコードにて管理するスケジュールファイル config/schedule.rb で下記のように定義する every ' */5 * * * * ' do # 5 分おき rake ' scheduled:foo:bar_baz ' end デプロイ時に定義内容から Amazon EventBridge ルールを作成する Amazon EventBridge ルールのイベントスケジュールでECSタスクが起動して rake タスクが実行される ECS Scheduled Task を利用した定期処理については 2021年のテックブログでも紹介しています tech.stmn.co.jp このアーキテクチャの場合、ジョブを動かすためにAWS側の設定を行う必要がありました。 どのように実現しているのか全体像の理解がやや難しいと感じています。 ✅ Solid Queueを選んだ理由:Redis/AWSからの脱却 既存の定期実行の仕組みを把握したうえで、新しくタスク機能サービスに導入する定期実行の基盤として Solid Queue を選びました。 複雑なインフラ環境の設定はなるべく行いたくない なるべく標準以外の新しいライブラリは増やしたくなかった Solid Queue を小さいところから導入を試してみたかった ECS Scheduled Task を利用した仕組みは実績がありますが、AWS側の設定を行うなどの準備が多く、今回はなるべくその手間を減らしたかった。 スタメンでは利用ライブラリのアップデートを毎週行っており、その対象が増えるため、elastic_whenever のような gem をなるべく増やしたくなかった。一方で Solid Queue は、Ruby on Rails の一部であるため、gem が増えても許容できると考えました。 以上が理由の2つですが、結局は、 Solid Queue を試してみたい が一番の理由です。 タスク機能サービスは影響範囲が絞られた小さいサービスなので導入を試してみるには最適でした。 🖥️ Mission Control — Jobs:稼働状況の確認 後述の導入手順で設定が完了し、ジョブが動くようになったら Mission Control — Jobs で実行状況が確認できます。 Sidekiq のダッシュボードと比べると非常にシンプルな印象です。 本番で動いている Mission Control — Jobs の画面をピックアップしてご紹介します。 Recurring tasks タブ:スケジュールの一元管理 ここが Solid Queue による定期実行の最大のメリットを示す画面です。 従来、 config/schedule.rb ファイルや Amazon EventBridge ルールに分散していたスケジュール定義が、リポジトリ内の config/recurring.yml ファイルに集約され、その実行状況がこのダッシュボードで一元管理できます。 Recurring(=繰り返し)のタスクとして登録したジョブを確認できます 1つ目のジョブであれば、月〜金の 10:00 に繰り返し実行します Run now を押下するとジョブを即時実行できます Finished jobs タブ 完了したジョブが一覧で確認できます Finished jobs 詳細 実行にかかった時間などが確認できます 🛠️ 導入手順 今回導入のために行った手順です。 Solid Queue と Mission Control — Jobs を導入します。 やりたいことによっては設定が異なる可能性があるので、あくまで参考として読んでください。 前提条件 Active Record マイグレーションでデータベーススキーマのマイグレーションを行っている Solid Queue のテーブルは既存のDBに投入する。独自のDBは用意しない Solid Queue 主には Solid Queue の README および README の翻訳記事を参考にしました github.com techracho.bpsinc.jp Gemfile に gem "solid_queue" を追加する bundle install で依存する gem をインストールし、Gemfile.lock を更新する bin/rails solid_queue:install を実行する # bin/rails solid_queue:install create config/queue.yml create config/recurring.yml create db/queue_schema.rb create bin/jobs gsub config/environments/production.rb Solid Queue 用のマイグレーションファイルを作成する 前手順で Solid Queue で利用するテーブルのスキーマ定義が db/queue_schema.rb ファイルとして作られるが、このファイルができただけでは Active Record マイグレーションでDBにテーブルは作られない マイグレーションを実行するためにファイルを作成する # bundle exec rails generate migration CreateSolidQueueTables invoke active_record create db/migrate/20250819042726_create_solid_queue_tables.rb 作成した Solid Queue 用のマイグレーションファイルに db/queue_schema.rb ファイルの内容を反映する db/queue_schema.rb のテーブル名やカラム名は文字列で指定されているので文字列を Symbol に置き換え これはマイグレーションファイルは基本、 Symbol で定義しているための対応 正規表現にて文字列を置き換えた マイグレーションを実行 Solid Queue 用テーブルと外部キーがDBに反映され、合わせて db/schema.rb が更新される # bin/rails db:migrate == 20250819042726 CreateSolidQueueTables: migrating =========================== -- create_table(:solid_queue_blocked_executions, {force: :cascade}) -> 0.0317s ... 省略 ... == 20250819042726 CreateSolidQueueTables: migrated (0.3559s) ================== -- execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'") Model files unchanged. 動作確認用ジョブを bin/rails generate job で作成する はじめてジョブを生成した場合、 app/jobs/application_job.rb が作られる # bin/rails generate job FooBar::BazQuxJob invoke rspec create spec/jobs/foo_bar/baz_ qux_job_spec.rb create app/jobs/foo_bar/baz_qux_job.rb create app/jobs/application_job.rb 動作確認用ジョブを修正する queue_as で指定する queue を任意で変更する 実行したことが確認できるようにログ出力だけ実装する class FooBar :: BazQuxJob < ApplicationJob queue_as :background # generate したときは default となっている def perform (*args) Rails .logger.info( " FooBar::BazQuxJob called with args: #{ args.inspect }" ) end end 自動生成された ApplicationJob を確認する self.queue_adapter = :solid_queue であること これにより、 ApplicationJob を継承したクラスが Solid Queue にて実行される class ApplicationJob < ActiveJob :: Base self .queue_adapter = :solid_queue # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end config/recurring.yml にて動作確認用ジョブをスケジューリングする queue は 動作確認用ジョブ の queue_as で指定した queue と一致させる 動作確認しやすいように短い周期の1分ごとに実行する default : &default foo_bar_baz_qux : class : FooBar::BazQuxJob queue : background schedule : every 1 minutes development : <<: *default production : <<: *default config/application.rb を修正 # require "active_job/railtie" のコメントアウトを外して、ActiveJob を有効化 死活監視のために supervisor のPIDファイルを作成するようにする config.solid_queue.supervisor_pidfile = Rails .application.root.join( " tmp/pids/solid_queue_supervisor.pid " ) config/queue.yml を必要に応じて修正する 並列でジョブを実行する場合、threads、processes を調整する 以上で 動作確認用ジョブが定期実行できる状態となったかと思います。 次は動かしてみます。 動かしてみる bin/jobs で起動します。 Dockerで起動する際、以下のコマンドにて、pid ファイルを削除してから起動するようにしました。 bash -c " rm -f tmp/pids/solid_queue_supervisor.pid && bin/jobs " 実行中のアプリケーションログ アプリケーション起動〜動作確認用ジョブが2回実行されるまでのログです。 下記の3つのログが2回出力されていることから、ジョブがスケジューリングの周期でエンキューされて実行していることが分かります Enqueued FooBar::BazQuxJob Performing FooBar::BazQuxJob Performed FooBar::BazQuxJob # tail -1000f log/development.log | grep -E "SolidQueue-1.2.2|ActiveJob" SolidQueue-1.2.2 Register Supervisor (31.3ms) pid: 1, hostname: "b9fb9f17bddb", process_id: 27, name: "supervisor-b90acad5c891087512fb" SolidQueue-1.2.2 Started Supervisor (136.9ms) pid: 1, hostname: "b9fb9f17bddb", process_id: 27, name: "supervisor-b90acad5c891087512fb" SolidQueue-1.2.2 Prune dead processes (9.2ms) size: 0 SolidQueue-1.2.2 Register Dispatcher (47.7ms) pid: 17, hostname: "b9fb9f17bddb", process_id: 28, name: "dispatcher-fd60cfd986a70188ddad" SolidQueue-1.2.2 Started Dispatcher (51.5ms) pid: 17, hostname: "b9fb9f17bddb", process_id: 28, name: "dispatcher-fd60cfd986a70188ddad", polling_interval: 1, batch_size: 500, concurrency_maintenance_interval: 600 SolidQueue-1.2.2 Register Worker (50.4ms) pid: 21, hostname: "b9fb9f17bddb", process_id: 29, name: "worker-7b1d65c64db3d68e456a" SolidQueue-1.2.2 Register Scheduler (49.5ms) pid: 25, hostname: "b9fb9f17bddb", process_id: 30, name: "scheduler-0aa92e643ddec8a6b637" SolidQueue-1.2.2 Started Worker (54.6ms) pid: 21, hostname: "b9fb9f17bddb", process_id: 29, name: "worker-7b1d65c64db3d68e456a", polling_interval: 0.1, queues: "*", thread_pool_size: 1 SolidQueue-1.2.2 Started Scheduler (74.4ms) pid: 25, hostname: "b9fb9f17bddb", process_id: 30, name: "scheduler-0aa92e643ddec8a6b637", recurring_schedule: ["foo_bar_baz_qux"] SolidQueue-1.2.2 Unblock jobs (17.6ms) limit: 500, size: 0 [ActiveJob] TRANSACTION (0.6ms) BEGIN /*application='****'*/ [ActiveJob] SolidQueue::Job Create (1.9ms) INSERT INTO `solid_queue_jobs` (`active_job_id`, `arguments`, `class_name`, `concurrency_key`, `created_at`, `finished_at`, `priority`, `queue_name`, `scheduled_at`, `updated_at`) VALUES ('f061f654-51e9-425d-aa84-2100f4e559dd', '{\"job_class\":\"FooBar::BazQuxJob\",\"job_id\":\"f061f654-51e9-425d-aa84-2100f4e559dd\",\"provider_job_id\":null,\"queue_name\":\"background\",\"priority\":null,\"arguments\":[],\"executions\":0,\"exception_executions\":{},\"locale\":\"en\",\"timezone\":\"Tokyo\",\"enqueued_at\":\"2025-10-26T01:20:00.038662472Z\",\"scheduled_at\":\"2025-10-26T01:20:00.038481305Z\"}', 'FooBar::BazQuxJob', NULL, '2025-10-26 10:20:00.070323', NULL, 0, 'background', '2025-10-26 10:20:00.038481', '2025-10-26 10:20:00.070323') /*application='****'*/ [ActiveJob] TRANSACTION (0.1ms) SAVEPOINT active_record_1 /*application='****'*/ [ActiveJob] SolidQueue::Job Load (0.2ms) SELECT `solid_queue_jobs`.* FROM `solid_queue_jobs` WHERE `solid_queue_jobs`.`id` = 15 LIMIT 1 /*application='****'*/ [ActiveJob] SolidQueue::ReadyExecution Create (0.2ms) INSERT INTO `solid_queue_ready_executions` (`created_at`, `job_id`, `priority`, `queue_name`) VALUES ('2025-10-26 10:20:00.106445', 15, 0, 'background') /*application='****'*/ [ActiveJob] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 /*application='****'*/ [ActiveJob] Enqueued FooBar::BazQuxJob (Job ID: f061f654-51e9-425d-aa84-2100f4e559dd) to SolidQueue(background) with arguments: SolidQueue-1.2.2 Enqueued recurring task (117.1ms) task: "foo_bar_baz_qux", active_job_id: "f061f654-51e9-425d-aa84-2100f4e559dd", at: "2025-10-26T01:20:00Z" [ActiveJob] [FooBar::BazQuxJob] [f061f654-51e9-425d-aa84-2100f4e559dd] Performing FooBar::BazQuxJob (Job ID: f061f654-51e9-425d-aa84-2100f4e559dd) from (background) enqueued at 2025-10-26T01:20:00.038662472Z with arguments: [ActiveJob] [FooBar::BazQuxJob] [f061f654-51e9-425d-aa84-2100f4e559dd] FooBar::BazQuxJob called with args: [] [ActiveJob] [FooBar::BazQuxJob] [f061f654-51e9-425d-aa84-2100f4e559dd] Performed FooBar::BazQuxJob (Job ID: f061f654-51e9-425d-aa84-2100f4e559dd) from SolidQueue(background) in 2.35ms [ActiveJob] TRANSACTION (0.2ms) BEGIN /*application='****'*/ [ActiveJob] SolidQueue::Job Create (1.3ms) INSERT INTO `solid_queue_jobs` (`active_job_id`, `arguments`, `class_name`, `concurrency_key`, `created_at`, `finished_at`, `priority`, `queue_name`, `scheduled_at`, `updated_at`) VALUES ('bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5', '{\"job_class\":\"FooBar::BazQuxJob\",\"job_id\":\"bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5\",\"provider_job_id\":null,\"queue_name\":\"background\",\"priority\":null,\"arguments\":[],\"executions\":0,\"exception_executions\":{},\"locale\":\"en\",\"timezone\":\"Tokyo\",\"enqueued_at\":\"2025-10-26T01:21:00.007415430Z\",\"scheduled_at\":\"2025-10-26T01:21:00.007366555Z\"}', 'FooBar::BazQuxJob', NULL, '2025-10-26 10:21:00.009421', NULL, 0, 'background', '2025-10-26 10:21:00.007366', '2025-10-26 10:21:00.009421') /*application='****'*/ [ActiveJob] TRANSACTION (0.2ms) SAVEPOINT active_record_1 /*application='****'*/ [ActiveJob] SolidQueue::Job Load (0.6ms) SELECT `solid_queue_jobs`.* FROM `solid_queue_jobs` WHERE `solid_queue_jobs`.`id` = 16 LIMIT 1 /*application='****'*/ [ActiveJob] SolidQueue::ReadyExecution Create (0.2ms) INSERT INTO `solid_queue_ready_executions` (`created_at`, `job_id`, `priority`, `queue_name`) VALUES ('2025-10-26 10:21:00.024004', 16, 0, 'background') /*application='****'*/ [ActiveJob] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 /*application='****'*/ [ActiveJob] Enqueued FooBar::BazQuxJob (Job ID: bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5) to SolidQueue(background) with arguments: SolidQueue-1.2.2 Enqueued recurring task (34.3ms) task: "foo_bar_baz_qux", active_job_id: "bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5", at: "2025-10-26T01:21:00Z" [ActiveJob] [FooBar::BazQuxJob] [bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5] Performing FooBar::BazQuxJob (Job ID: bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5) from (background) enqueued at 2025-10-26T01:21:00.007415430Z with arguments: [ActiveJob] [FooBar::BazQuxJob] [bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5] FooBar::BazQuxJob called with args: [] [ActiveJob] [FooBar::BazQuxJob] [bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5] Performed FooBar::BazQuxJob (Job ID: bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5) from SolidQueue(background) in 1.4ms 🧠 プロセスを確認:4つの役割と協調動作 Solid Queue は、DBベースでありながら効率的なジョブ処理を行うため、主に以下の4つのプロセスで構成されています。 これらのプロセスが、共通のデータベースを利用して協調動作することで、外部キュー(Redisなど)なしでのジョブ実行を実現しています。 supervisor : 全体の親プロセスとして、 dispatcher 、 worker 、 scheduler の各子プロセスを起動・監視します。 dispatcher : キュー内のジョブを監視し、 worker がすぐに実行できる状態(Ready)に配置する役割を担います。 worker : dispatcher によって配置された Job を実際に取得し、実行します。 scheduler : config/recurring.yml に基づき、定期実行の時刻が来たジョブを判断し、dispatcher を介してキューにエンキューします。 # ps -aux | grep solid-queue root 1 0.3 0.9 1026296 106708 ? Ssl 10:19 0:02 solid-queue-supervisor(1.2.2): supervising 17, 21, 25 root 17 0.2 1.0 974356 114088 ? Sl 10:19 0:02 solid-queue-dispatcher(1.2.2): dispatching every 1 seconds root 21 1.5 1.0 1043072 118048 ? Sl 10:19 0:12 solid-queue-worker(1.2.2): waiting for jobs in * root 25 0.0 0.9 965768 110944 ? Sl 10:19 0:00 solid-queue-scheduler(1.2.2): scheduling foo_bar_baz_qux root 58 0.0 0.0 3660 1688 pts/0 S+ 10:33 0:00 grep solid-queue Mission Control — Jobs Solid Queue は Mission Control — Jobs をダッシュボードとして利用することを推奨しています。また、Solid Queue 自体としては Dashboard UI を持っていないため、README に従って導入を進めます。 こちらも Mission Control — Jobs の README および README の翻訳記事を参考にしました。 github.com techracho.bpsinc.jp Gemfile に下記の2行を追加する propshaft は Mission Control — Jobs を導入するサービスが API-only Applications に相当するため、追加した gem " mission_control-jobs " gem " propshaft " bundle install で依存する gem をインストールし、Gemfile.lock を更新する config/application.rb に下記の2行を追加する Basic認証がデフォルトだが、別の認証方式を利用するのでOFFにする config.mission_control.jobs.adapters = [ :solid_queue ] config.mission_control.jobs.http_basic_auth_enabled = false config/environments/production.rb などの環境ごとの設定ファイルに下記の行を追加 Mission Control — Jobs のURLへのアクセスはこの Controller を経由させる 開発時は認証をスキップしたいため、 config/environments/development.rb には行を追加しなかった config.mission_control.jobs.base_controller_class = " Foo::JobsDashboardController " Foo::JobsDashboardController を実装する このクラスで認証を行い、認証に失敗した場合、401 Unauthorized のレスポンスを返却する class Foo :: JobsDashboardController < ApplicationController before_action :authenticate! private def authenticate! # Implement your authentication logic here. rescue AuthenticatorError => e head :unauthorized end end config/routes.rb を修正して Mission Control Job のエンジンにアクセスできるようにマウントする Rails .application.routes.draw do # ... mount MissionControl :: Jobs :: Engine , at : " /foo/jobs " 以上で Mission Control — Jobs が表示できるようになったかと思います。 ブラウザから config/routes.rb で指定したパスにアクセスしてみてください。 ✨ まとめ:Solid Queue導入で得られたメリット Solid Queue の導入は比較的容易に行うことができました。 ECS Scheduled Task の設定などが不要で、対象サービスのリポジトリ内のコード修正でほぼ完結するのは後から入ったメンバーの学習コスト低減に繋がり、大きなメリットだと考えます。 この記事をきっかけに Solid Queue に触れてみたという方がいたらうれしいです。 🤝 さいごに 株式会社スタメンでは、プロダクト開発に関わる全ての領域で、プロダクト職種の採用を積極的に行っています。 ご興味をお持ちいただけたなら、ぜひご応募いただけますとうれしいです。 皆さんとお話できることを楽しみにしています。 herp.careers herp.careers herp.careers
アバター
はじめに こんにちは。スタメンで Watchy というIT資産管理・操作ログ管理ツールのプロダクトエンジニアをしている yun8boo です。 スタメンでは、業務以外の場でもエンジニアの成長機会づくりを重視しており、カンファレンス参加の補助制度があります。今回はその制度を活用し、2025年10月7日–8日に開催された React Conf 2025 in Las Vegas の現地参加の機会をいただきました。 この記事では、参加の経緯からダイジェスト、印象的だったセッション、現地で得た気づきを順にまとめています。 参加の経緯 代表の大西から急遽参加の機会をいただき、即座に承諾しました。オンラインでも視聴できる時代ですが、会場の雰囲気を肌で感じ、グローバルなコミュニケーションを直接体験したいと考えていました。 初めての海外カンファレンス、しかも一人での参加ということもあり、出発前は期待と不安が入り混じっていました。 ダイジェスト 公式から「React Conf 2025 Recap」も公開されています。カンファレンス全体の熱気や雰囲気がよく伝わる内容ですので、こちらもぜひご覧になることをお勧めします。 react.dev React Conf 2025 Day 1 React Keynote: Lauren Tan氏らが登壇し、Reactエコシステムの現状と未来について語りました。 React Compiler 1.0 の正式リリースや React Foundationの設立 など、カンファレンスの目玉となる重要な発表が行われました。 View Transitions and Activity: 新しいコンポーネントである <ViewTransition> と <Activity> について深掘りされました。これらがいかにして、よりスムーズなUIアニメーションと状態管理を実現するかが解説されました。 Profiling with React Performance tracks: DevToolsに新しく搭載された「React Performance tracks」を使い、アプリケーションのパフォーマンスを詳細に分析・改善する方法が紹介されました。 Async React: 非同期処理はReactの重要なテーマです。このセッションでは、データ取得や状態更新における非同期のベストプラクティスと、今後の展望が語られました。 React and AI: ReactがAI(人工知能)とどのように連携していくか、AIが開発プロセスをどう変えるかについて、Christopher Chedeau氏らが議論しました。 Exploring React Performance: Reactのコア開発者であるJoe Savona氏が、アプリケーションのパフォーマンスを最大限に引き出すための実践的なテクニックや考え方を解説しました。 Lightning Talks & Q&A: Reactを使ったモダンなEメール開発など、多岐にわたる短いセッションが行われました。最後にはReactチームが直接コミュニティからの質問に答えるQ&Aセッションで締めくくられました。 Day 2 React Native Keynote: Hermes V1の発表や新アーキテクチャへの完全移行など、React Nativeのパフォーマンスを飛躍的に向上させる発表が行われ、コミュニティを沸かせました。 React Native, Amplified: Amazonが開発した新しいOSであるVega OSへのReact Nativeの統合と、それによって可能になった新しいクロスプラットフォーム開発体験に焦点を当てたものでした。 React Strict DOM: Webとネイティブアプリ間でのコード共有をより安全かつ容易にするための新しいアプローチ「React Strict DOM」について、Nicolas Gallagher氏が解説しました。 Reimagining Lists in React Native : React Nativeにおけるリストのパフォーマンス課題、特にブランキング現象の解消と、新しいアーキテクチャを活用したリストのための新しい基本要素の導入に焦点を当てたものでした。 React Everywhere: Bringing React Into Native Apps: 既存のネイティブアプリに、Reactを段階的に導入していく「ブラウンフィールド」開発の実践的な手法が共有されました。 フレームワーク & ツール セッション: Parcel, Vercel, Expo, React Router, RedwoodSDK, TanStack Startといった主要なフレームワークやツールの開発者が登壇しました。 What's The Framework of the React Future?: Reactの未来を担うフレームワークはどうあるべきか、業界のエキスパートたちが活発な議論を交わし、2日間のカンファレンスを締めくくりました。 なお、カンファレンス全体の詳細については、りんたろーさんのブログ記事に大変分かりやすくまとめられています。より深く理解したい方は、こちらも合わせてお読みいただくことをお勧めします。 blog.re-taro.dev 特に印象的だったセッション React Keynote: React Compiler v1 の発表: v1の発表で会場が大いに盛り上がりました。これは単なる自動最適化ツールではなく、より良いコードを書くための支援ツールでもあるとのことです。 パフォーマンスを向上させるためには useMemo や useCallback が有効ですが、どこに使うべきかの判断は簡単ではありませんでした。結果として、適切な箇所で使えていなかったり、逆に不要な場所でコードを複雑にしてしまったりすることもあったと思います。今回のv1の発表によって、プロダクションへの導入がより一層しやすくなったと感じています。 View Transitions: View Transitions APIの簡素化により、Webアプリケーションでもシームレスな画面遷移を簡単に実装できるようになったと解説されました。他のセッションでのライブコーディングも会場を盛り上げていました。 Async React: このセッションでは、 ui = f(state) というReactの従来のメンタルモデルは、現代のアプリケーションには適していないと語られました。すべてのユーザー操作を同期的に処理するのは不可能で、体験を損なう可能性があるためです。 代わりに await ui = await f(await state) という新しいメンタルモデルが提案されました。これは、ユーザーイベント、状態更新、UI更新のすべてが非同期であることを明示しています。 このセッションでもライブコーディングが行われました。ハプニングがあり時間内に話しきることができませんでしたが、会場は大いに盛り上がっていました。(2日目の最後に時間をもらい、残っていた箇所の説明が行われました) React Native Keynote: Hermes v1 (Static Hermes): JavaScriptコードを事前にネイティブコードへコンパイルすることで、アプリのパフォーマンスが劇的に向上し、アプリサイズも小さくなるとのことでした。 DevToolsの進化: React Native DevToolsに待望のパフォーマンスパネルとネットワークパネルが追加されることが発表されました。これにより、Web開発者にはおなじみの強力なデバッグ体験が、ネイティブ開発にももたらされます。 私が学生だった8年ほど前、React Nativeでアプリ開発を経験したのですが、当時はデバッグにとても苦労した記憶があります。それ以来、しばらくReact Nativeから離れており最新の情報を追えていませんでした。しかし、今回のKeynoteで発表された進化を目の当たりにして、ぜひもう一度触ってみたいと強く感じました。 現地で得た気づき コミュニケーションの壁と突破口 セッションの合間や食事の時間での会話はリアルタイム翻訳が難しく、最終的にスマートフォンの画面を見せ合いながらの会話となり、少しテンポが悪くなってしまいました。特に会場が盛り上がっている場面や複数人での会話では、リアルタイム翻訳の精度と速度が追いつかないのが現実でした。 そのため、技術的に深い話までは踏み込めませんでしたが、「日本から来た」こと自体が話題になり、会話のきっかけとして機能しました。SNSで数名とつながり、後日フォローアップできる土台を確保できたのは大きな収穫でした。(そして、日本のアニメは偉大でした!) 交流会 現地参加ならではの体験 ライブコーディングや目玉発表が成功した瞬間に会場が一体となって沸き立つ体験は、オンラインでは得難いものであり、学びへのモチベーションを一段と引き上げてくれました。どの話題で会場が沸くか、どこが強調されているのかを肌で感じられるのは大きなメリットです。また、スポンサーブースには普段使っているライブラリの開発者たちがおり、ユースケースなどを直接聞ける貴重な機会でした。 海外カンファレンス参加へのハードルが下がった 出国前は英語に自信がなく不安でしたが、実際に行ってみると「意外となんとかなる」と感じました。この経験を通じて自信がつき、海外カンファレンスへの心理的なハードルが大きく下がりました。 セッションのスライドは分かりやすくシンプルな英語で書かれており、登壇者もプレゼンテーションに慣れている方が多いため、スライドに沿って進むセッションは内容を理解しやすかったです。 終わりに 今回の一番の収穫は、「モチベーションの向上」でした。会場の一体感と熱量に触れ、日々の学びに向き合う姿勢がぐっと前向きになりました。英語は完璧でなくても会話は成立することを実感した一方で、「次はもう一段レベルを上げた状態で臨みたい」という気持ちが強くなり、英語での発信や対話への意欲が大きく高まりました。 最後に、今回の参加は社内のカンファレンス参加補助制度があってこそ実現できたものです。この貴重な機会に感謝しつつ、高まったモチベーションを今後の実装という形でチームに還元していきます。 Watchyではエンジニアを絶賛募集中です 🙌 herp.careers
アバター
導入 お久しぶりです!株式会社スタメンの ちぇる です。前回の 「ながらRuby会議01」 に続き、今回は「Kaigi on Rails 2025」に参加してきました! Kaigi on Rails とは、「初学者から上級者までが楽しめるWeb系の技術カンファレンス」です!年に一度開催され、国内外から多くの参加者やスピーカーが集まり、Railsに関するさまざまなテーマでの講演や交流が行われます。 kaigionrails.org 弊社スタメンの 福利厚生 には「まるっとカンファレンス補助」という制度が存在し、今回はこちらを利用して、名古屋から東京へ1泊2日でカンファレンスに参加させていただきました🙌 東京駅に到着!ここから徒歩圏内の会場で行われました🙆‍♂️ 弊社スタメン社員の参加者です👨 印象に残ったセッション 2日間にわたって開催された Kaigi on Rails は、新たな発見のある講演ばかりでした。その中でも、本記事では特に印象に残ったセッションに関してご紹介させていただきます🖊️ 入門 FormObject speakerdeck.com Ruby on Railsで実装していると、Rails Wayから外れることってありませんか?本セッションでは、その代表例である FormObject の使い所に関するお話を聞くことができました。僕自身も、以前に業務で FormObject を使っていたこともあり、非常に勉強になる点が多かったです。 そもそも、 FormObject の使用基準は何でしょうか?パッと考えてみると、以下のような観点が挙げられます。 バリデーションに if がついたら? メールフォームを実装するとき? accepts_nested_attributes_for の代わり? 本セッションでは、普段なんとなく使っていた FormObject の使い所を明確に言語化・整理されており、とても納得感がありました。 FormObject の特徴 ① DBに紐づかないオブジェクト ② モデルと同じインターフェースでコントローラから呼ばれる ③ 独自のライフサイクル処理をもてる ④ ビューの状態を保持できる (例) class UserNewForm # ① include ActiveModel :: Model # ② attr_accessor :email , :name validates :email , :name , presence : true # ③ before_action :email_to_lower_case # ③ def save # ... end private def email_to_lower_case # ... end end class UsersController < ApplicationController def create @form = UserNewForm .new(create_params) # ② if @form .save redirect_to new_users_path, notice : ' ユーザを作成しました ' else render :new # ④ end end end FormObject の使い所 「なぜ、 FormObject が必要になるのか?」という問いに対する結論は、以下であると述べられていました。 Rails Wayの制約があると、ユーザの目的が実現できないから Rails Wayのフォーム処理は、次のような前提のもとに成り立っています。 一度に操作するモデルの数が一つ モデルのライフサイクル処理のパターンが一つ つまり、 これら前提が壊れる時 が FormObject の使い所なのです!例えば、以下のようなケースが挙げられます。 一度に操作するモデルの数が一つではない モデルを操作しない(メールフォーム等) 複数のモデルを操作する(会社情報と個人情報の同時更新等) モデルのライフサイクル処理をアクションごとに分ける(ユーザの作成と編集でバリデーションを分ける等) 特に、現代のユーザは複雑な操作をワンタップで実現したいという要望も強く、Rails Wayから外れることがしばしばあります。しかし、こういった「レールの伸ばし方」は先人たちが用意してくれているため、巨人の肩に乗る姿勢を持つことが大切であるとお話しされていました。 今後は、 FormObject を「なんとなく使う」のではなく、「使うべき所で使う」ように注意していきたいと思いました! 2分台で1500examples完走!爆速CIを支える環境構築術 speakerdeck.com CIが高速であるということは、 ビジネスに直結 します。なぜなら、不具合の原因特定と修正のサイクルを早く回すことができ、安定したリリースを早期にすることができるからです。 本セッションでは、開発において欠かせないCIをどのように速くしたか、その体験談をお聞きすることができました。 RSpecの実行を早くするには 全体のCI処理が遅くなってしまう原因として、「直列実行でCPUを使いきれていない」ことが挙げられます。そのため、まずは並列処理にすることが、CIの実行時間を短縮する選択肢の一つと考えられます。 そこで、TwoGateさんでは parallel_tests を導入するに至ります。この gem は、マルチプロセスによる並列実行をサポートしています。 また、spec単位での実行時間にはバラツキがあります。つまり、均等に並列実行しないと求めている効果が得られません。そこで、spec単位の実行時間の履歴を記録して最適な分割をしてくれる knapsack_pro-ruby の gem も導入されていました。 これらの対応後、 8コアCPU で 8並列 で Rspec を実行したそうです。結果、本来であればCI実行時間の 1/8 程度の短縮を期待できそうですが、実際は 29分38秒 → 23分20秒 の短縮にとどまり、思うような結果が得られませんでした。なぜなら、8並列にしたことで、DB書き込みによるディスクI/Oに負荷がかかってしまったためです...。 次の改善として、ストレージに tmpfs を利用しました。 tmpfs はメモリ上に構築されるファイルシステムで、再起動時にデータが消失する点が特徴です。そして、この変更によってディスクI/Oの負荷が軽減され、CIの実行時間は 29分38秒 から 5分 へと短縮され、なんと 約6倍 のスピードアップを実現することができたそうです! さらになんと、サーバーをクラウドマシンから物理マシンにしてスペックを上げたことで、 1分59秒 までCI実行時間の削減を実現されていました。物理マシンの導入は、多くの企業で容易にできる選択ではないと思いますが、コスト観点からもTwoGateさんはメリットを享受できたそうです。すごいですね。 弊社でも、CI実行時間には一定の課題感を持っており、本セッションの内容を応用できないか、是非とも参考にさせていただきたいと感じました! 「技術負債にならない・間違えない」権限管理の設計と実装 speakerdeck.com 権限管理は、システムにおいて非常に重要な機能を果たします。これらが間違って設定されると、「給与を第三者に公開してしまった」「取引先を公開してしまった」といった事態になりかねません。それにより、サービスへの信頼が大きく下がり、事業への損失が大きくなります。つまり、 権限管理のミスは許されない のです。 そんな権限管理において、本セッションでは適切な実装方法をレクチャーしていただきました。まずは、よくある間違った実装を見てみます。 class ProjectsController < ApplicationController def create unless current_user.admin? # 役割に依存 redirect_to root_path, alert : ' 権限が必要です。 ' return end end end admin? の判定はアンチパターン です。なぜなら、役割は変わりゆくからです。また、 admin? と記載した場合、admin がどのような権限を持っているか、その先のコードを見にいかなければなりません。 役割に依存した実装は、権限が暗黙的になるので、技術的負債になりがちです。さらに、そもそもの役割の権限が変化した際、多くの判定箇所を見に行って修正する必要があります。 レビュワーは、常に役割の権限を知らないといけない。 判定箇所が散らばり、どのような権限が定義されているのかわからない。 そこで、 役割ではなく権限に依存する 必要があります。 class ProjectsController < ApplicationController def create unless current_user.can_create_project? # 権限に依存 redirect_to root_path, alert : ' 権限が必要です。 ' return end end end class User < ApplicationRecord # ... enum :role , %i(admin manager normal external) def can_create_project? admin? || manager? end end これだけでも、だいぶ見やすくなります。 さらに、権限の要素を 「対象」 ・ 「操作」 ・ 「役割」 ・ 「条件」 に分割して考えることで、権限管理をより綿密に行うことができるとお話しされていました。 【権限操作の呼び出し】 # レコードに対して権限があるかの判定 project = Project .find( 1 ) readable_project = Policy .authorize(current_user, project, :read ) # 権限があるレコードの絞り込み projects = Project .all readable_projects = Policy .authorize_scope(current_user, projects, :read ) # 権限一覧の取得(主にクライアント側で利用) permissions = Policy .permissions(current_user) #=> JSON # { # "projects": { # "read": true, # "create": false, # "update": false, # "delete": false # } # } 【権限操作のインターフェース】 module Policy def self . authorize (user, record, action) context = Context .new( user :) context.authorize(record, action) end def self . authorize_scope (user, scope, action) context = Context .new( user :) context.authorize_scope(scope, action) end def self . permissions (user) context = Context .new( user :) context.permissions end end 【上記の内部実装】 module Policy class Context def authorize (record, action) policy = policy_class(user, record.class).new( user :, record :, mode : :record ) policy.record end def authorize_scope (scope, action) policy = policy_class(user, scope.klass).new( user :, record :, mode : :scope ) policy.public_send(action.to_sym) end def permissions # ... end private def policy_class (user, record_class) role = user.role.camelize # メタプロを活用し、具体的な権限クラスを動的に生成 " Policy:: #{ record_class } ::Roles:: #{ role }" .safe_constantize end end end 【権限の基底クラス】 module Policy class Base attr_reader :user , :record , :scope , :mode def initialize ( user :, record : nil , scope : nil , mode : nil ) @user = user @record = record @scope = scope @mode = mode end end end 【具体的な権限クラス】 module Policy module Project # 対象 module Role class Manager < Base # 役割 def update # 操作 case mode when :record assignee? || author? # 条件 when :scope # ... when :list # ... end end end end end end このような設計にすることで、権限の有無が一目で分かるようになりました。 その結果、プレックスさんでは CS や PdM がソースコードの一次情報を見て理解できるように なり、開発側への問い合わせが減少。エンジニアは他の開発業務に専念できるようになりました。 さらに、Railsサーバー側からフロントに対して権限のマッピング情報を提供するようになり、フロント側の実装も自然と 役割ではなく権限に依存する ようになったそうです。 複雑な権限管理を正しく設計したことで、多くの嬉しい副次的効果が得られたんですね! まとめ Kaigi on Rails、本当に面白かったです。Railsエンジニアとして、他の活躍されているエンジニアの方々の知見を直接共有していただける機会は、非常に貴重だと感じました。 以下、カンファレンスで特に印象に残った言葉です。 ソフトウェアの実装における最適解は、常に 場合によります Rails Wayがあるとはいえ、環境や目標は企業ごとに異なります。そのため、最適解も状況によって変わります。今回のようなカンファレンスに参加し、自社の取り組みだけでなく、他社の事例も取り入れることで、視野を広げ、より柔軟に考えるきっかけになったと感じました。 今後も実装で迷うことは多々あると思いますが、どの選択が最善か、常に考え続けていきたいと思います! 最後に 株式会社スタメンでは、一緒に働く仲間を募集しています!詳しくはこちらをご覧ください🙌 herp.careers
アバター
プロダクト開発部でTUNAGの開発をしている hisa です。最近公開された「ひゃくえむ。」がとても良く、久々劇場で涙を流しました。 今回は少し前になりますが、デザイナーと協業でリリースした社内ツール開発について紹介できればと思います。 はじまりは、何気ない雑談から 「バナー、毎回同じような構成で作ってるんだけど、テンプレートにできたら楽かもね」 「Zoomの背景も、各自Googleスライドで作ってるから、ばらつき出ちゃってて…」 そんな、オフィスでの雑談からこのプロジェクトは始まりました。 デザイナーとエンジニア、それぞれの 気になっていたこと を持ち寄って、「じゃあ、作ってみようか」と動き出しました。 もともとの課題 今回取り組んだ背景には、2つの あるある課題 がありました。 ① バナー作成の手間と属人化 イベントや発表のたびに、バナーを毎回新しく作る必要がある 一定の構成パターンはあるものの、毎回手作業で時間がかかる デザイナーに依頼が集中しがちで、ちょっとした更新にも時間がかかる ② Zoom背景のバラバラ感 社内ではメンバーが各自で背景を作成(Googleスライドなど) 統一感がなく、資料やスクショで見たときに違和感が出る 「ちゃんとデザインされた感」が伝わりづらくなることも デザイナー × エンジニアでつくったもの 作ったのは、 社内向けの画像ジェネレーター です。 イベント用バナーやZoom背景などを、誰でも簡単に、統一感のあるデザインで生成できるツールです。 テンプレートに沿って、イベント名・日付・名前などを入力するだけ バナー画像やZoom背景が即座に生成される デザイナーが設計したテンプレートをベースに、エンジニアが画像生成やUI部分を構築 非エンジニアでも使えるUI、かつ実用レベルでの品質を意識 技術的な挑戦とトライ 今回の取り組みでは、いくつか新しい挑戦も行いました。 画像生成ライブラリの選定と検証 弊社主力サービス TUNAG では使っていないライブラリを選定し、初めて導入してみました。 特に文字の縦位置揃えや、Canvas描画の比率など、調整には細かな工夫が必要でした。 技術的に試したかったUI設計 普段使っている技術とは異なるUI構成で、柔軟なデザイン反映にチャレンジ。 デザイナーが意図したトンマナを壊さず、かつ汎用化できる設計に落とし込んでいます。 非エンジニアの開発参加(AIエージェント活用) Cursorなどのツールを用いて、デザイナー自身がスタイル調整や軽微な修正を担当。 「ちょっと試してみる」が現実的にできる環境があることで、参加のハードルが下がりました。 やってみてよかったこと リリース後、社内からはこんな声がありました。 「統一感が出て、資料に載せやすい」 「サクッと作れるのがありがたい」 「非デザイナーでもレイアウトが整うのは助かる」 デザイナーに依頼しなくても誰でもちょっとした画像が作れる社内ツールを開発しました! ①イベント告知バナー②名前入りZoom背景③記事のサムネイル を出力できます。 エンジニアと一緒に、自己研鑽としてCursorなどを使いながら作りました。良い学びになったし、社内のみんなにも喜んでもらえて pic.twitter.com/FbxvZ0Qf9Q — スタメン デザイナーたち✉️ (@stmn_designer) 2025年7月2日 小さな機能でも、 ちょっと便利 / ちょっと嬉しい を確実に増やすことができました。 プロダクト本体とは別文脈で技術を試し、協業の型をアップデートできたのも良い循環です。 これからやっていきたいこと 開発のハードルをさらに下げ、 誰でもツールを作れる組織 へ デザイン・エンジニアの境界をゆるやかにし、 共創を当たり前 に 雑談ベースの課題感から始まる 小さなプロジェクトを、もっと増やしていきたい おわりに 今回の社内ツールは、「ちゃんと課題を解決したい」ではなく、「これ、あったらちょっといいよね」から始まりました。 その ちょっといい を一緒に形にできたのは、職種を超えたコラボレーションのおかげです。 小さな一歩の積み重ねが、働き方や関係性そのものを変えていく気がしています。 これからも、いろんな あったらいいな を拾って、形にしていきます! このブログの サムネイル画像も、今回開発したジェネレーターで作成 しました。 弊社ではプロダクト開発に関わる全ての領域で、プロダクトを共に良くしていけるメンバーを募集しています。 私たちと一緒に最高のチームとプロダクトを作りましょう! herp.careers
アバター
こんにちは!プロダクト開発部の おしん ( @38Punkd ) です。 先日9月19日(金)から21日(日)にかけて、有明セントラルタワーホール&カンファレンスで開催された iOSDC Japan 2025 に参加しました! iosdc.jp 2016年初開催されたiOSDCは今年で遂に10回目を迎え、今年もiOSDCらしい、賑やかで学びの多い最高の3日間でした。 今年も弊社はシルバースポンサーとしてiOSDC Japan 2025に協賛しました🙌 今年はいつもとちょっと違う形のペンライト 弊社社員のとんとんぼさん(@Ktombow1110)。楽しんでました 白熱するSwiftコードバトル お題を満たすプログラムをSwiftで短く書けた方が勝ち、という1 vs 1のガチバトル。 予選を勝ち抜いた6名の選手が決勝に進出し、カンファレンス当日に優勝を目指して戦う様子を観戦しました。 コードをできるだけ短くするために普段の業務では目にしない手法が使われていて、『Swift でこんな書き方もあるのか!』と刺激を受けました。 決勝のお題 特に印象的だったセッション カスタムUIを作る覚悟 スピーカー:まつじ( @mtj_j )さん iOS 27から対応が必須とアナウンスされているLiquid Glassに向き合うため、カスタマイズされた既存のモバイルアプリのナビバーやタブバーに対して、今一度向き合う必要がありました。 まつじさんのセッションを聴いて『なぜカスタムUIは大変なのか』が自分の中で漠然とした経験則に基づく直感が、明確に言語化されました。 デザイナーとエンジニアが、Figma等の静的な画面デザインを元に協業をする場合、アニメーションやインタラクション、パフォーマンス、アクセシビリティ、ローカライゼーションといった、静的なデザインでは表現しにくい部分を決めにいく必要があり、Appleの標準コンポーネントではこれらが既に実装されています。 『標準搭載された機能を捨てて一から作り直し、かつそれをメンテナンスする覚悟はあるか』 カスタムUIを作る・作らないの議論を進める上では、標準コンポーネントに備わっている機能をきちんと理解しておくことが重要だと改めて思いました。 fortee.jp アセンブリで学ぶCPUアーキテクチャ スピーカー:akkey( @AkkeyLab )さん 過去にアセンブリ言語について学ぼうと思い本を買ったのですが、難しすぎて挫折した経験があるので、「今回こそは」と思い akkey さんのセッションを聴きました。 これまで自作メソッド以外でクラッシュが起きると思考停止していましたが、セッションでスタックトレースを辿る方法を学んだことで、難解な暗号に見えていたアセンブリ言語が、デバッグの強力な武器に変わりうると感じました。 fortee.jp 感想 私自身、iOSDCへの参加は今年で3回目を迎えました。回を重ねる毎に横の繋がりが増えている事を実感しています。 私は名古屋に住んでいるので、社外の方とは基本的にはX(Twitter)やDiscord等のオンラインのやり取りがメインです。 そういった方々とオフラインでは年に数回しか会えないので、会えるとやっぱり嬉しいですね。 iOS開発続けてて良かったなあと思います。 また、数名の方から、「名古屋で何度かモバイルの勉強会をやられてますよね」と声をかけていただけ、「Nagoya.swift、開催して良かったなあ」「mobile.stmn、継続してて良かったなあ」としみじみ思いました。 Nogoya.swift は名古屋で開催したiOSエンジニア向けのイベントですが、一緒に運営してくださったかっくんさん( @fromkk )が、Nagoya.swiftを含む (region).swift の各地域の開催者へのインタビュー記事を書いてくださり、その記事が今年のiOSDCのパンフレットに載っています。 読んでいただけると嬉しいです。 iOSDC Japan 2025のパンフレット記事を書いた方法 #iosdc https://t.co/w6q3d1Zx8y pic.twitter.com/tnSQ2ARTEG — かっくん (@fromkk) 2025年9月9日 スポンサーとして参加する以上、スポンサーとしての責務を全うする事は勿論ですが、私にとってiOSDC は、日々の業務へのエネルギーチャージをする場だと感じています。 また皆と笑顔で近況を話せるよう、普段の開発を頑張ろうと思いました。 圧巻のクロージング 最後に 弊社ではプロダクト開発に関わる全ての領域で、プロダクトを共に良くしていけるメンバーを募集しています。 私たちと一緒に最高のチームとプロダクトを作りましょう! herp.careers
アバター
はじめまして!株式会社スタメンでバックエンドエンジニアをしている ちぇる と申します。 この度、2025/9/6(土)に開催された「ながらRuby会議01」に参加しました。 会場は岐阜県の「うかいミュージアム」で、自然に囲まれた素敵な場所でした✨ 近くには金華山(きんかざん)があり、山頂に築かれた岐阜城が見えます🏯 いざ、会場へ! まず驚いたのは、 今回開催された「ながらRuby会議01」でしたが、なんと応募はほぼ満員...! 東海地方にも、これだけ多くのRubyistが集まり、熱量を持って交流できる場があることに感激しました。 ちなみに、nagara.rbは、今回の「ながらRuby会議01」で記念すべき第100回目の開催だそうです。Rubyコミュニティを作ってくれた先人たちに感謝です。 アフターパーティーでは、皆さんと鵜飼に参加しました✨ 以下、実施されたプログラム内容となります! ふむふむ。ジュニアエンジニアの自分にとって、目の前に Ruby のアタラシイ世界が広がっているぞ...🤔 regional.rubykaigi.org この記事では、特に気になったセッションについてご紹介させていただきます。 refinementsのメソッド定義を4000倍速くした話 スピーカー:alpaca さん( @alpaca_tc ) speakerdeck.com 社内で Ruby をアップデートしたところ、どうやらアプリケーションの起動やファイル変更時における反映が遅くなってしまったようです。 そのため、 vernier という gem を使って実際に速度を計測します。 すると、Ruby3.2 と比べて Ruby3.3以降では、 Module#refine を呼び出すタイミングで、なんと実行時間に4000倍以上もの差分が見られたそうです👀( 計測 ) Module#refine とは、簡単にいうと「モンキーパッチを安全に使うための仕組み」です。 例えば、以下のように書くと、String 全体に shout メソッドが定義されてしまいます。 class String def shout self .upcase + " ! " end end " hello " .shout # => "HELLO!" しかし、refine を使うことで、モジュール内での定義にスコープを限定することが可能となります。 module StringRefinement refine String do def shout self .upcase + " ! " end end end using StringRefinement # こちらを実行した場合のみ有効に! " hello " .shout # => "HELLO!" では、この refine がなぜ遅くなってしまったのでしょうか...? ここから原因調査に入ります。 調査を続けていくうちに、rubyリポジトリ内で rb_clear_all_refinement_method_cache(); の処理が追加されたことが判明します。 これが怪しいのでは...?👀 そこで、実際に Revert パッチを当てて確認すると、なんと速度が改善されました🎊 ( 再現 ) つまり、Ruby3.3以降では、refineに関連する callcache の無効化処理が追加され、それによってパフォーマンスリグレッションが起こってしまったのですね。 callcacheとは「同じメソッドを再度呼び出すときに、以前の呼び出し結果を再利用する仕組み」のことです。 PR にあるように、refineに関するキャッシュによるバグが発生してしまったために、このキャッシュを使用しない変更が反映されたんですね。 Rubyでは、メソッドを実行する際、継承チェーンに沿って順にメソッド定義を探します。 しかし、今回の alpacaさんのケースでは、 ObjectSpace になんと、70億のオブジェクトが存在したそうです...😳 そのため、refine 関連のメソッド探索がキャッシュされない場合、相当のオーバーヘッドが発生することは想像できますね...。 そこで alpacaさんは、refine callcache 専用の格納先を用意する方針で実装することにしたそうです。 しかしご存知の通り、Ruby は C言語で書かれています。そのため、上記の方針をC言語で書いて実現する必要があります。 Rubyistにとって、なかなか大変な作業だと考えられます...。 alpacaさんは案の定、C言語特有のGC(ガベージコレクション)によるメモリ管理や、セグメンテーションフォールト(不正なメモリアクセスを行ったときに OS が強制終了させるエラー)等に苦戦します。 しかし、それらにもめげず、ChatGPT や rubyhackchallenge を駆使して、ついにマージまで実現したそうです!🎉( 改善 ) すごいですよね。 該当PR: Optimize callcache invalidation for refinements ここから、alpacaさんは以下の教訓を話されてました。 パフォーマンス改善の流れは同じ(計測 → 再現 → 改善) AIやコミュニティの助けを得て貢献できる また、alpacaさんは以前に RubyKaigi にも参加されており、GC やセグフォについても学んでいました。こうした知識が、実際の現場で活かせることを実感されたそうです。 TUNAGのモノリスとアーキテクチャ また、スポンサーセッションとして、弊社の近藤( @sei_kondo97 )も登壇しました! 弊社は、現在 TUNAG というエンゲージメントプラットフォームを提供させていただいております。TUNAGはモノリス構造のシステムですが、年々サービス内容が肥大化しており、今後はサブサービスやサブアプリケーションに分割し、各ドメインごとに分けて管理していく方針となります。 最近では、「TUNAG本体」から「TUNAGチャット」が分離されました。また、「TUNAG本体」を認可サービスとして利用することで、「TUNAGチャット」へのSSOログインも可能となりました。 最後に 僕は今回、初めて地域Ruby会議に参加しました。そこで感じたのは、 自分が使っている道具の構造を知ることの重要性 です。 現実として、アプリケーション側のエンジニアであれば、道具の使い方さえ分かっていれば、内部構造を知らなくても開発は可能です。高水準言語の強みはまさにここにあります。たとえば、多くの開発者は機械語やアセンブリの仕組みを知らなくてもコードを書けますし、私自身も知りません。 コミッターの方々がRubyの内部をブラックボックス化してくれているからこそ、私たちは便利に開発できているわけです。しかしその一方で、構造を理解していないことで見落としたり、最適な使い方ができない場合もあります。 Rubyの内部やメソッドの実装を知っていれば、パフォーマンスチューニングやデバッグでより効率的で安全な判断が可能になります。つまり、「道具の使い方」と「構造の理解」は表裏一体で、両方あって初めて深い応用力が得られるのではないかと感じました。 今回の「ながらRuby会議01」における学びは、普段はブラックボックスとして使っている部分にも興味を持ち、仕組みを知ることで、開発者としての視野や判断力が広がるということです。これからは、自分のコードだけでなく、その下で動く仕組みや背景にも目を向けていきたいと思います。 株式会社スタメンでは、一緒に働く仲間を募集しています!詳しくはこちらをご覧ください🙌 herp.careers
アバター
👋 あいさつ こんにちは!株式会社スタメンのフロントエンドエンジニア、伊賀本です。 これまでフロントエンドを中心にキャリアを積んできましたが、スタメンに入社してからは バックエンド領域にも挑戦 を始めています。 今回は、2025年6月に入社して3ヶ月の節目ということで振り返りをしてみようと思います。気軽に読んでいただけると嬉しいです! 🎯 入社理由 転職を考えていたとき、私は「新しい領域に挑戦できる環境」と「仲間と成果を称賛し合える文化」を探していました。 スタメンを知ったきっかけは、元同僚が勤めていたことでした。その同僚から聞く会社の雰囲気やカルチャーが、まさに私が求めていたものと一致していました。 スタメンに惹かれたのは、行動指針「StarWay」にある Work Bravely (大胆に攻め、挑戦や失敗を讃える)という言葉です。 さらに、面接で感じたフラットな雰囲気と「挑戦を称賛する文化」が、自分の理想と重なりました。 行動指針「StarWay」にある Work Bravely(大胆に攻め、挑戦や失敗を讃える) 🛠️ 入社後のリアル体験 技術面の挑戦 入社直後から、既存機能の改善を任されるなどフロントエンドの強みを活かす一方で、サーバーサイドの開発にも挑戦しています。スタメンでは、OSSのgood first issueのようなオンボーディングタスクが用意されており、新しい技術領域に挑戦する最初のステップとして最適な環境が整っています。慣れないことに苦戦もしましたが、レビューや議論を通して学びを得られる環境がありました。 good first issueのようなオンボーディングタスク オンボーディング(ウェルカムミッション) 組織が大きくなるほど、顔と名前は知っているけれど話したことがない人が増えがちです。スタメンではその解消のため、入社から1ヶ月は所属部署や他部署へあいさつに伺い、簡単な自己紹介と写真撮影を行うオンボーディングがあります。この取り組みによりファーストコンタクトのハードルが下がり、のちに業務で関わる際も自然に声を掛け合える関係が築けました。 ウェルカムミッションで実際撮影した写真 働き方とコミュニケーション 前職はフルリモート中心の働き方でしたが、現在は週3出社・週2リモートのハイブリッドです。出社日には雑談やその場での意思決定が増え、レビューや相談も早く回るようになりました。結果としてコミュニケーション量が増え、開発はより円滑になっています。 その成果を「スタカネ(成果を称賛する文化)」で認めてもらえたとき、 挑戦を称賛してくれる会社だと実感 しました。 スタカネ(成果を称賛する文化)の投稿内容 🤝 チームの雰囲気 メインプロダクトである「TUNAG」では日常的にサンクスカードが飛び交い、小さな努力や挑戦も称賛されます。 年齢や役職に関係なく意見を交わせるため、安心して「やったことがないこと」に手を挙げられるのも魅力です。 若手でも裁量を任される環境だからこそ、自然と「一歩踏み出してみよう」と思える雰囲気があります。 実際にいただいたサンクスカード 🚀 今後の挑戦 今後はフロントエンドとバックエンドの両方を経験し、 フルスタックに活躍できるエンジニア を目指していきたいです。 さらに、得た知見をチームに還元し、スタメンのプロダクトをよりスケーラブルに進化させる挑戦に貢献していきたいと考えています。 📣 未来の仲間へのメッセージ スタメンは「挑戦を称賛する」文化が根づいている会社です。 バックエンドに挑戦したいフロントエンドエンジニア フルスタックを目指して成長したい人 仲間と成果を分かち合いながら、大胆にチャレンジしたい人 そんな方にとって、スタメンは最高の環境になるはずです。 herp.careers 私と一緒に、挑戦を楽しみながら「スターになる成長」を経験しませんか?
アバター
株式会社スタメンでAndroidアプリ開発を担当している鈴木と申します。 この記事では、2025/09/10(水) 〜 2025/09/12(金)に行われたDroidKaigi2025(以下DroidKaigi)に参加して、特に印象に残ったセッションや、カンファレンス全体を通して感じた技術トレンドなどを共有します。 株式会社スタメンはサポーターズスポンサーとしてDroidKaigi2025に協賛しました。 カンファレンスの全体像と雰囲気 今回のDroidKaigiは、代表のmhidakaさんによると過去最高の参加者数とのことでかなりの盛り上がりを見せていました。 Day0のワークショップでは、Compose MultiplatformとKotlin MultiPlatformを使用したアプリケーション開発の方法を学びました。 去年のalpha版だったFleetからAndroid Studioを使用するようになったためか、結構安定感のある実行環境になっているように感じました。 メインコンテンツであるセッションについては、公正取引委員会の方によるスマホ新法に関するものや、Googleの方からのPlayStoreに関するセッションなど例年とは少し毛色の違った登壇もあり、より強い新鮮さを味わうことができました。 アフターパーティーで行われたマグロの解体ショー 特に印象的に残ったセッション 基礎から学ぶ大画面対応 〜「Large screen differentiated」認定アプリの開発知見〜 スピーカー: tomoya0x00さん Android16から画面向きを固定化することや、アスペクト比の指定が無視されるようになったため、自身の開発にも影響のあるトピックでした。 まとめると以下の点が語られていました。 Android16からの仕様変更について 3段階のサポートレベル 実際の対応例 対応が必要ということは把握していたのですが、サポートレベルの段階が存在していることまでは調べられていなかったのでとてもためになるセッションでした。 自身のプロジェクトでは第1段階(Large screen differentiated)ではありますが、対応の方向性は合っていたことが確認できたのでホッとした面もありました。 第3段階のLarge screen readyの状態にするためにはNavigation3への置き換えが不可欠なため、大変そうだなと思いつつ指標があることを知ることができたので少しずつでもUXを向上していきたい気持ちが高まりました。 2025.droidkaigi.jp Performance for Conversion! 分散トレーシングでボトルネックを特定せよ スピーカー: andousanさん このセッションでは、分散トレーシングを用いてアプリケーションのパフォーマンスを測定する方法ついて、以下の様な点が語られました。 パフォーマンス管理の重要性 既存のツールによる限界 分散トレーシングの有効性 モバイルアプリケーションへの導入方法 私が普段開発しているプロジェクトでもWebViewをネイティブ実装に変更する利点として、パフォーマンスが向上するデータを提示するような場面があったのですが、普段からウォッチできる環境を構築できていればそのようやり取りもよりスムーズに行えていたことと思います。 まずは自身が関わっているプロジェクトから導入を進める動きをしていきたいと思います。 2025.droidkaigi.jp 最後に 今年のDroidkaigiは例年通りの雰囲気もありつつ、コーヒーの注文システムが新たに構築されていたり等新しい要素もありつつとても楽しいお祭りでした。 スタメンではエンジニアを絶賛募集中です🙌 👇👇👇気になる方はこちらからアクセスをお願いします👇👇👇 herp.careers
アバター
はじめに 株式会社スタメンでモバイルアプリの開発をしているカーキ(X: @khaki_ngy )です。 これまでJetpack Composeを採用した画面で、大量のアイテムをページング表示したい場合は、多くのケースで AndroidX Paging ライブラリ(以下、Paging3)を採用してきました。 直近で担当したタイムライン機能のリプレイスに際しても、投稿一覧を表示するために当初 Paging3 を採用しましたが、開発を進める中で、Paging3 が持つ特性と、我々の要件との間にギャップがあり、Paging3 の利用を見送る必要がある状況になりました。 今回のブログでは、Paging3 の基本的な使い方を振り返りつつ、我々が直面した課題と、それをどのように乗り越えたかについて紹介をします。 Paging3×Composeの基本的な使い方 Paging3 とは、大規模なデータセットを効率的に、少量ずつページ単位で読み込み、表示するためのGoogle公式のライブラリです。ユーザーのスクロール操作に応じて次のページを自動で読み込むことで、メモリ使用量を抑え、スムーズなユーザー体験を提供します。 ここでは、Paging3 を利用する上で必須となる登場人物と、その基本的な接続方法に絞って、シンプルなコード例を紹介します。 1. データソースを定義する ( PagingSource ) まず、データをどのように取得するかを定義する PagingSource を実装します。この例では、API通信などを模した上で、20個の文字列アイテムを擬似的に生成しています。 // PagingSourceの実装。キーはページ番号(Int)、値は取得するデータ(String) class MyPagingSource : PagingSource< Int , String >() { // ページングされたデータを読み込むための中心的な関数 override suspend fun load(params: LoadParams< Int >): LoadResult< Int , String > { val page = params.key ?: 1 // 読み込むページ番号(初回は1) // 実際にはここでAPIリクエストなどを行う // この例では、ページ番号に基づいて擬似的なデータを生成 val data = List ( 20 ) { index -> "アイテム ${ index + (page - 1 ) * 20 } " } // 読み込み結果をLoadResult.Pageとして返す return LoadResult.Page( data = data, prevKey = if (page == 1 ) null else page - 1 , // 前のページ nextKey = if (page < 5 ) page + 1 else null // 次のページ(5ページで終わり) ) } // リストが更新されたときに、新しいPagingSourceをどこから読み込み始めるかを定義する // この例では簡単のため実装を省略 override fun getRefreshKey(state: PagingState< Int , String >): Int ? { return null } } 2. ViewModelで Pager をセットアップする 次に、 ViewModel で PagingSource を利用して Pager を構築し、UIに公開するための Flow<PagingData<String>> を作成します。 class MyViewModel : ViewModel() { val items: Flow<PagingData< String >> = Pager( // PagingConfigで、ページサイズなどの設定を行う config = PagingConfig(pageSize = 20 ), // 新しいPagingSourceのインスタンスを生成するファクトリを渡す pagingSourceFactory = { MyPagingSource() } ).flow .cachedIn(viewModelScope) // Flowをキャッシュし、画面回転などの構成変更後もデータを保持 } 3. Composableでデータを表示する 最後に、Composable関数で ViewModel から Flow を受け取り、 LazyColumn で表示します。 paging-compose ライブラリが提供する専用の items 拡張関数を利用すると、より簡潔に記述できます。 @Composable fun MyPagingScreen(viewModel: MyViewModel) { // ViewModelのFlowを、Composeで扱えるLazyPagingItemsに変換 val lazyPagingItems = viewModel.items.collectAsLazyPagingItems() LazyColumn { // Paging3ライブラリ専用のitems拡張関数 // これを使うと、アイテムの取得やキーの管理などを自動で行ってくれる items( items = lazyPagingItems, key = { it } // 各アイテムの安定したキーを指定 (今回はString自体をキーに) ) { item -> // itemはnull許容型なので、nullでないことを確認 if (item != null ) { Text( text = item, modifier = Modifier .fillMaxWidth() .padding( 16 .dp) ) } } } } lazyPagingItems.loadState を監視することで、リストの末尾にローディングインジケーターやエラーメッセージを表示することも可能ですが、ここでは基本的なアイテムの表示に絞って紹介しました。 以上がPaging3をJetpack Composeで利用する際の、最もシンプルで基本的な実装の流れです。 Paging3 の「ツラミ」との遭遇 タイムラインリプレイスでのユースケースと課題 今回リプレイスしたタイムライン機能では、ユーザーが投稿に対して「リアクション」や「コメント」といったリアクションを行えます。これらの操作はAPIを通じてサーバーのデータを更新しますが、ユーザー体験を考慮し、操作後は画面全体を再読み込みするのではなく、対象の投稿アイテムの「リアクション」や「コメント数」といった情報をローカルで即時反映させたい、という要件がありました。 タイムライン上に表示される投稿の例 しかし、ここで大きな課題に直面しました。 Paging3で表示しているリスト内の特定のアイテムの状態を、ローカルで部分的に更新することは原則サポートされていませんでした。 なぜPaging3の要素は(簡単には)更新できないのか? この課題の根本原因は、Paging3の設計思想にあります。Paging3が提供する PagingData は、 その時点でのデータの不変なスナップショット として扱われます。これは、宣言的にUIを構築するJetpack Composeの考え方とも一致しています。 身近なもので例えるなら、 PagingData は「印刷済みの写真アルバム」のようなものです。アルバムに印刷された写真の一部分だけを後から修正するのは困難であり、もし修正したい場合は、元のネガフィルム(データソース)から、修正を加えた新しい写真を印刷し直し、アルバムのページごと差し替えるのが正しい手順です。 Paging3も同様に、表示されているリスト(写真)を直接操作することは推奨されていません。データの整合性を保つため、 変更は常に単一の信頼できる情報源(Single Source of Truth)、すなわち大元のデータソース(ネガフィルム)に対して行われるべき だとされています。データソースが更新されると、Paging3はそれを検知し、新しい PagingData のスナップショットを生成してUIにストリームします。この単一方向のデータの流れにより、複雑なページング処理や非同期なデータ読み込みの中でも、データの整合性が保たれるのです。 したがって、リスト内のアイテムをローカルで更新したい場合、 PagingData が参照している大元のデータソース(データベースなど)を更新し、 PagingSource の無効化(invalidation)をトリガーして、Paging3に新しいスナップショットを再生成させる のが正規のフローとなります。 最終的な回避方法 PagingDataAdapter.refresh() を呼び出してリスト全体を再取得する方法も検討しましたが、ユーザーのスクロール位置がリセットされるなど、UXの観点から今回の要件には合致しませんでした。 そこで、 更新が必要な要素のページングに関しては、Paging3を利用しない という方針を決定しました。 その代替として、 LazyColumn のスクロール状態を LazyListState で監視し、 ユーザーがリストの末尾近くまで表示したことを検知して、次のページを読み込むコールバックをトリガーする という方法を実装しました。 【ViewModel】 class TimelineViewModel( private val repository: TimelineRepository) : ViewModel() { private val _items = MutableStateFlow< List <TimelineItem>>(emptyList()) val items: StateFlow< List <TimelineItem>> = _items private val _isLoading = MutableStateFlow( false ) val isLoading: StateFlow< Boolean > = _isLoading private var currentPage = 1 private var canLoadMore = true // これ以上読み込むページがないことを示すフラグ init { loadMoreItems() // 最初のページを読み込む } fun loadMoreItems() { // ローディング中、またはこれ以上読み込むページがない場合は処理を中断 if (isLoading.value || ! canLoadMore) return viewModelScope.launch { _isLoading.value = true try { // Repositoryから次のページのデータを取得 val newItems = repository.fetchTimeline(page = currentPage) if (newItems.isNotEmpty()) { // 既存のリストに新しいアイテムを追加 _items.value + = newItems currentPage ++ } else { // 新しいアイテムがなければ、それ以上読み込まないようにフラグを更新 canLoadMore = false } } catch (e: Exception ) { // 実際にはエラー状態をUIに通知するなどの処理が必要 } finally { _isLoading.value = false } } } } 【Composable】 @Composable fun TimelineScreen(viewModel: TimelineViewModel) { val items by viewModel.items.collectAsState() val isLoading by viewModel.isLoading.collectAsState() val listState = rememberLazyListState() LazyColumn(state = listState) { items(items) { item -> TimelineItem(item = item) } // ローディング中であれば、リストの末尾にインジケーターを表示 if (isLoading) { item { Box(modifier = Modifier.fillMaxWidth().padding( 16 .dp)) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } } } } // LazyColumnのスクロール状態を監視 val isScrolledToEnd by remember { derivedStateOf { val layoutInfo = listState.layoutInfo val visibleItemsInfo = layoutInfo.visibleItemsInfo if (layoutInfo.totalItemsCount == 0 ) { false } else { // 最後に表示されているアイテムが、リスト全体の最後のアイテムであるか val lastVisibleItem = visibleItemsInfo.lastOrNull() lastVisibleItem?.index == layoutInfo.totalItemsCount - 1 } } } // リストの末尾までスクロールされたら、次のページを読み込む LaunchedEffect(isScrolledToEnd) { if (isScrolledToEnd) { viewModel.loadMoreItems() } } } この実装により、Paging3を使わずにページング機能を実現しつつ、 ViewModel が保持する StateFlow のリストを直接更新することで、ローカルでのデータ更新にも柔軟に対応できるようになりました。 取り得た別の選択肢 もしPaging3を選択した上で今回のケースを対応するとしたら、ローカルデータベースを管理するライブラリである Room を用いて、ローカルデータベースをデータソースとして扱ってページングする方法もありました。 Paging3 では、API から取得したデータを Room と同期をしながらページングする実装 RemoteMediator が用意されています。この記事では詳しくは述べませんが、これを利用すれば、ローカルで持っている Room に対して更新をかけることで、表示状態を更新することができます。 こちらの公式ドキュメントがよくまとまっています。 ネットワークとデータベースからページングする 今回のトレードオフ 先述の通り、 Room のようなローカルデータベースを信頼できる情報源(Single Source of Truth)として利用し、Paging3にその変更を監視させるのが、今回の要件を満たす上での最も望ましいアーキテクチャであったと認識しています。タイムラインの投稿は、ローカルにキャッシュすることのメリットも享受することができます。 しかし、今回は リリースまでの期間が短く設定されていたこと 、そして オフライン対応なども見据えたローカルデータベースのスキーマ設計には、慎重に時間をかけたかったこと から、この方法は見送りました。当時の開発の状況としては、致し方ない判断だったと思っています。 まとめ:Paging3 の注意点 今回のPaging3の一連の取り組みから、 Paging3で取得したデータは、不変なスナップショットとして扱われるため、リスト内の一要素をローカルで簡単に更新することはできない。 ということを学びました。チームとして Paging3 に今まで頼ってきていた経緯はありましたが、これを機に Paging3との付き合い方を考え直すきっかけになりました。 スタメンでは、Androidエンジニアを含めて、モバイルアプリ開発者を募集しています! herp.careers
アバター
Watchy が実践するAI活用 はじめに 👋 こんにちは。スタメンでソフトウェアエンジニアをしている yunboo です。 AI 技術の急速な発展により、プロダクト開発の現場も大きく変わろうとしています。しかし、単純に AI ツールを導入するだけでは、その真価を発揮することは難しいものです。 この記事では、私たち Watchy が実践している、AI 活用によるワークフロー改善について紹介します。プロダクト開発における根本的な課題を解決し、AI を「使用する」だけでなく「活用する」ためのアプローチをお伝えします。 プロダクト開発における根本的な課題 🤔 「なぜ作るのか」が不明確な開発要求 プロダクト開発の現場では、以下のような曖昧な要求に遭遇することがよくあります。 「これ作ったら売れる」 「お客様が欲しいと言ったから」 これらの要求は一見もっともらしく聞こえますが、詳細を聞いてみると実は作らなくても解決できることが多々あります。 「なぜ作るのか」(Why) が不明確 なままでは、真に価値のあるプロダクトを作ることはできません。 背景情報の属人化という問題 この問題の根本には、 背景情報の属人化 があります。 セールスは商談以外の業務も多く、重要な顧客の声が記録・共有されないまま失われる。 チーム全体で「なぜ作るのか」を腹落ちして開発できない状況が生まれる。 このような課題を解決するために、私たちは AI を活用したワークフロー改善に取り組んでいます。 AI を活用したワークフロー改善:フェーズ 1 🎯 目標:「なぜ作るのか」を可視化し、チーム全体で共有 フェーズ 1 では、営業・顧客対応から PRD(Product Requirements Document)作成までのプロセスを改善しました。 Step 1: "Why" を可視化する 👀 tl;dv を活用した商談の記録と分析 すべての商談を自動で録画・文字起こしし、いつでも振り返れる状態にしています。特に重要なのは、Meeting Template 機能を使って以下のようなキーワードを設定し、顧客の声を自動で抽出することです。 Current workflow(現在のワークフロー) Motivation(動機・課題感) Problems(具体的な問題) Feature requests(機能要望) これにより、「なぜその機能が必要なのか」という背景を確実にキャッチできるようになりました。 Step 2: チームに背景を共有する 🤝 tl;dv + Zapier + Notion による情報の自動蓄積 抽出した顧客の声を、チームの資産として確実に蓄積するために tl;dv で生成されたサマリーや文字起こしを Zapier 経由で Notion データベースに自動格納 重要な情報が属人化せず、フロー情報ではなくストック情報として蓄積される仕組みを構築 Step 3: 日常業務を効率化する ⚡ Notion AI によるカスタム自動入力 商談後の付帯業務を大幅に削減するために、Notion データベースへのアイテム追加をトリガーとして 商談後の御礼メール本文 を自動作成 Salesforce 用の議事録 を自動作成 これにより、営業担当者は商談そのものに集中できるようになりました。 Step 4: 今後の展開 🔮 現在計画している改善項目 Notion AI による要望の集約 : 商談 DB から要望をまとめ、要望 DB へ自動起票 PRD 作成プロセスのサポート : 要望 DB から PRD のファーストドラフトを自動作成 プロダクトビジョン、競合情報、市場分析、今期の注力ポイントなどをコンテキストとして活用 AI を活用したワークフロー改善:フェーズ 2 🚀 PRD 作成からリリースまでの自動化 AI の進化に伴い、開発プロセス全体の最適化も進めています。現在実験的に動かしているワークフローは以下の通りです。 Notion に起案アイテムを追加 優先順位を確定し、ステータスを 着手可能 に変更 ステータス変更をトリガーに GitHub Issue を自動作成 Notion + Slack + Devin Notion MCP + Cursor + GitHub MCP Zapier を使ったワークフロー構築などを検討中 Issue のレビュー Claude Code Action などで プルリクエスト を作成 プルリクエスト のレビュー・リリース このように、企画から実装まで一貫した自動化を目指しています。 その他の AI 活用事例💡 問い合わせ対応の効率化 課題 問い合わせ対応の履歴がメールやチャットに散在し、ナレッジが属人化 類似の問い合わせに毎回対応するコストが発生 解決策 全ての問い合わせ内容を Notion データベース に集約 Notion AI を使って、過去の類似問い合わせがないか検索できる仕組みを構築 情報のサイロ化を防ぐ 課題 「あのドキュメントどこだっけ?」 「〇〇機能のやり取りどこのチャンネルでしてたっけ、確認漏れないかな」 探す時間、人に聞く時間で作業が中断される 解決策 Notion AI で検索能力を強化(標準検索では見つけにくい情報も発見可能) Notion AI コネクター で Slack、GitHub、Google Drive を横断検索 スライド作成の AI 活用 課題 スライド作成ツールの操作やデザイン調整に時間を取られ、本来伝えたい内容に集中しづらい レビューや修正が煩雑になりがち 解決策 Cursor + Marp を使いスライドを作成 Markdown でスライドをテキストベースで AI(Cursor)に相談しながらブラッシュアップ Marp で Markdown から直接スライド化、見た目の調整も CSS で一括管理 テキストファイルなので Git でバージョン管理や共同編集も容易 まとめ:AI を「活用する」ために重要なこと 🎯 「解空間」を意識したアプローチ AI の能力を最大限に引き出すには、 インプットとなる情報の整理 と ワークフローの見直し が不可欠です。特に 「解空間」を意識 して業務やドキュメントを整備することが重要です。 解空間が曖昧だと AI も曖昧な答えしか返せない 解空間を明確に定義し、必要な情報を整理しておくことで、AI はより的確な提案やアウトプットができる ただし、創造力を豊かにするために意図的に限定しないケースもある Notion を組織の「脳みそ」に Watchy では、Notion を中心とした情報集約・AI 活用を進めています。 「まず Notion を見よう」「まず Notion AI に聞いてみよう」が当たり前の文化 を醸成することで、情報の活用度が大幅に向上しました。 今後の展望 AI 活用のベストプラクティスを社内で体系化し、誰でも活用できるようにしたい プロダクトの要望や問い合わせを自動で分類・優先順位付けする仕組みを作りたい tl;dv をもっと活用したい AI ファーストで業務や仕組みを設計し直すことが、組織の生産性や競争力向上につながるのでは おわりに 🌈 AI は単なるツールではなく、組織のワークフロー全体を変革する可能性を秘めています。重要なのは、AI に何を求めるのか、どんな情報を与えるのかを明確にすること。つまり「解空間」を意識した設計です。 私たち Watchy の取り組みが、皆さんのプロダクト開発にも少しでも参考になれば幸いです。AI 活用についてご質問やご意見があれば、ぜひお聞かせください。 Watchyではエンジニアを絶賛募集中です 🙌 herp.careers watchy.biz
アバター
はじめに こんにちは。スタメンでバックエンドエンジニアをしているきいろです。 今回、2025年6月28日(土)に京都の先斗町歌舞練場にて開催された関西Ruby会議08にTakeスポンサーとして出展してきましたので、その振り返りレポートを書きたいと思います。 関西Ruby会議ならではなのかスポンサーグループがMatz、Take、Umeなのは面白いですね。(最初"テイク"スポンサーと読んでいた) カンファレンス会場の先斗町歌舞練場。 軒先に並ぶ各スポンサーブースののぼり旗。壮観。 筆書きタッチのフォントで個人的には好みです。 セッション開場 開場近くの鴨川。この日は最高気温が34度まで上がった快晴でした。アツカッタ... 企業ブース この日展示していたブースの様子です。 スタメンのメインプロダクトであるTUNAGの機能 TUNAGベネフィット にちなんだ抽選を行いました。 来場者の方にその場で抽選にチャレンジしてもらい、当たった方にAnkerの充電器をプレゼントしました。また、ブースに訪問していただいた方全員にオリジナルステッカーも配布しました。 \関西Ruby会議08 #kanrk08 / スタメンブース、準備完了しました! ステッカーを無料で配布します!! 弊社プロダクトのTUNAG(ツナグ)の抽選機能を体験できます! 当たるとAnkerの充電器をゲットできます。 お気軽にお越しください! #TUNAG #kanrk08 pic.twitter.com/HQWRAINvL6 — stmn, inc. Developers (@stmn_eng) 2025年6月28日 気になったセッション Rubyを使った10年の個人開発でやってきたこと speakerdeck.com 自分が印象に残ったポイントは以下です。 自由にクラス設計がためせて楽しい 個人開発なので自分が欲しいものを作る(無理しない) 所属プロダクトチームが導入するコード規約や既存設計に縛られることなく、自由に自分のやりたい設計ができるのは個人開発の醍醐味かなとは思います。 クラス設計について、その時良いパターンだと思った設計を導入しても後から見ると...という点には自分の過去の実装でも痛いほど感じ入る部分があり、首がもげそうになりました。(自分のは設計というより実装がイケてないくらいの軽微なものですが) それでもその時に信じた設計を都度導入して振り返っていくことが大切なのだと個人的には感じました。 同じくオフライン参戦した まっきー の感想 Witchcraft for Memory speakerdeck.com memory_profiler gemではピーク時のメモリ使用量を計測することが難しく、 majo というメモリプロファイラを実装したという発表でした。 ガベージコレクションを生き延びた、寿命の長いオブジェクトを計測する機能があるとのこと。 GC周りの挙動の話を聞いて、native extensionを含むruby gemの実装の難易度を知れたことが印象に残りました。 また、C言語周りの話やコピーオンライト/プロセスfork/ソケット/パイプなど、学生時代にLinuxカーネルの授業を受けていた時によく聞いていた単語が出てきて懐かしい気持ちになりました。 おわりに 久々の京都の堪能しつつ、関西のRubyistたちと交流を深められた良いカンファレンスでした。 次の開催場所は滋賀県だそうです。次の地域交流も楽しみです。 スタメンではRubyエンジニアを絶賛募集中です🙌 herp.careers
アバター