TECH PLAY

株式会社エブリー

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

385

はじめに DevEnableグループの羽馬( @NaokiHaba ) です。 この度、エブリーは2024年6月22日(土)に開催される『Kotlin Fest 2024』に、ひよこスポンサーとして協賛することになりました! www.kotlinfest.dev エブリーでは、Ver.1.0からKotlinを使用してDELISH KITCHENを構築してきました! 今回の協賛を通して、さらなるKotlinコミュニティの発展に貢献できればと考えております。 今年も、 「Kotlin を愛でる」 をテーマに、Kotlinに関する情報交換や交流を通じて、新たな出会いや気づきを得ることができるでしょう。 ぜひ、タイムテーブルをご覧いただき、気になるセッションに参加してみてください。 fortee.jp 私たちのブースでは、Kotlinの活用事例等をご紹介する予定です。エブリーのエンジニアが直接皆様からのご質問にお答えしますので、ぜひお立ち寄りください。 エブリーにおけるKotlinの活用 エブリーでは、DELISH KITCHEN アプリ Ver.1.0 から Kotlin を採用してきました。 Kotlin の採用は、当時としては比較的早い判断でしたが、結果として開発効率を大きく向上させることができました。 特に、Null安全性や拡張関数、スコープ関数など、Kotlinの優れた言語機能により、 簡潔で安全なコードを高い生産性で書くことができるようになりました。 Ver.1.0からの⻑い開発の中で、Kotlinは常に⼒強い味方であり続けてくれました。 アプリの規模が⼤きくなるにつれ、その恩恵はさらに際⽴ったものになってきています。 Kotlinは、エブリーにとってなくてはならない存在であり、これからも積極的に活⽤していきたいと考えています。 エブリーのテックブログでも、Kotlinに関する記事を随時公開していますので、ぜひご覧ください。 tech.every.tv 皆様とお会いできることを楽しみにしています! Kotlin Fest 2024 では、当社がどのようにKotlinを活用しているか、ブースでお話しできる機会を楽しみにしています。 ぜひ、お気軽にお立ち寄りください! エブリーでは、ともに働く仲間を募集しています。 エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
はじめに DELISH KITCHENでデータサイエンティストをやっている山西です。 今回は レシピ動画のサムネイル画像の自動抽出の取り組み について紹介いたします。 OpenCVを用いた画像処理 画像とテキスト情報のペアを扱う大規模モデル 等を用いつつそれを試みた事例になります。 ※記事後半で具体実装を扱っている部分では、周辺知識がある前提で説明を進めていることをご了承ください。 every Tech Blog Advent Calendar 2024(夏) 9日目の記事になります。 出来たもののイメージ どんなものが出来たかを先に紹介します。 一言で表すと、 レシピ動画の中から「調理手順を表すのに良い感じのサムネイル画像」をAI的振る舞いで自動で抽出してくれるシステム になります。 これを ワンパンカルボナーラ というレシピに適用した例を以下に載せています。 図1: AIシステムによるレシピサムネイル抽出例 このように、5つの”手順”別にサムネイル画像の候補が抽出され、最終的に1枚が選定されます。 取り組みの背景 なぜこれを作ろうと思ったかを説明していきます。 レシピ手順説明文と共にサムネイル画像を追加したい DELISH KITCHENでは約5万本のレシピ(2024/6月現在)を提供しており、全てのレシピに 調理工程を撮影、解説した動画 が付いています。 そして、 調理手順ごとに区切られた説明文 をレシピページ上で読むことが出来ます。 しかし、従来のスマホブラウザ版のDELISH KITCHENでは、文字情報だけでここの手順を読み進める作りになっていました。 そんな中、「各手順に動画から抽出したサムネイル画像を加えることで、工程がよりイメージしやすくなる」という仮説のもとで、サムネイルを自動付与する施策が企画されました。 結果、DELISH KITCHENの全レシピにサムネイル画像が機械的に施されることとなりました。 AWS Elemental MediaConvert を用いたこの取り組みは以下の記事に詳しいです。 tech.every.tv 図2: スマホブラウザ版のDELISH KITCHENの手順欄 困りごと 自動処理により全レシピにサムネイルを付与出来たのは良いものの、これらはあくまで機械的なルールで付与されているため、「必ずしも調理手順の説明文に合った画像とは限らない」問題が発生し、その品質には課題が残ることとなりました。 図3: イマイチなサムネイルの例 そのため現在(2024/6月時点)、「イマイチなサムネイル」を人力で毎日少しずつ入稿して差し替える手間がかかっています。 しかし、約5万本もあるレシピを対象にこれらを行うのも骨が折れる作業です。 PoCの実施 こうした取り組みを横目で見ている中、「画像処理や大規模モデル等の技術スタックを使えば、”AI”として良い感じのサムネイルを抽出可能なのでは」という閃きが生まれました。 このアイデアを エブリー社内エンジニアで定期開催している挑戦WEEK の企画として提案したところ好評だったので、1週間PoCとして取り組んでみました。 システム構成 ここから成果物の具体の説明になります。 このAIシステムは、 ①画像処理パート 、 ②AI処理パート の2段階で構成されます。 ①事前に良さそうなサムネイル候補画像を数枚ピックアップ しておき、 ②その中から最も良いものをAIに選ばせる という思想になります。 図4: レシピサムネイル抽出(再掲) ① OpenCV画像処理パート 各手順の「サムネイル画像候補」を画像処理にて抽出する(最大10件ほど) 全動画フレームに対してOpenCVによる画像処理を行い、それを実現する ② AI処理パート ①で抽出された「サムネイル画像候補」の中から、その手順に相応しい1枚を選び出す 手順説明文のテキスト情報とサムネイル候補の画像情報を共に解釈し、「サムネイルとしての相応しさ」を判定できるようなAIモデル(画像とテキスト情報を共に処理できるマルチモーダルモデル)を採用する これから、実装詳細について説明します。 実装詳細 ①画像処理パート: サムネイル候補画像の抽出 「良いサムネイル候補」を満たす要件の仮説をまず立て、それを実装に起こしつつ検証していきました。 これを、処理の流れと共に追っていきます。 仮説1. 動画の前後のフレームで「動き」が大きいほど、候補として重要な場面である サムネイルとして見栄えのするシーンは大抵、「ダイナミックな動きがあったり、画角の中に多くの要素があったりする」ものではないかと考えました。 具体的には以下のような例が挙げられます。 フライパンの上でかき混ぜているようなシーン フライパンの上に肉や野菜等の具材がたくさん盛り付けられているシーン そして、「動画の前後のフレームで画像データ間の”違い”が大きいシーンを見つけ出す」ことで、上記アイデアを OpenCVを用いた動画像処理 に落とし込めるのではないかと仮説立てました。 そこで、今回は AKAZEアルゴリズム によって各フレーム画像の特徴点を抽出し、動画内の前後のフレームの 特徴点の総当たりマッチング によって「距離」を算出する という実装に落とし込みました。 平たく言えば、「特徴点という”違いの判断材料"を各フレーム画像ごとに作り、前後のフレーム間でそれらの “違い度合い”を数値(距離)として表現する」アプローチです。 詳細な説明は本記事の対象外とします。 代わりといっては何ですが実装の雰囲気や参考記事を以下に載せます。 コード例 # 動画ファイルを読み込む cap = cv2.VideoCapture(target_recipe_video_path) # フレームレートと総フレーム数を取得 fps = cap.get(cv2.CAP_PROP_FPS) total_frames = int (cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 開始フレームと終了フレームを計算 start_frame = int ((step_start_msec / 1000 ) * fps) end_frame = int ((step_end_msec / 1000 ) * fps) # 動画を読み込み、指定された範囲のフレームを書き込む cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) print ( 'start_frame: ' , start_frame) print ( 'end_frame: ' , end_frame) previous_frame = None previous_target_des = None previous_mean_distance = None for frame_num in range (start_frame, end_frame): ret, frame = cap.read() # Crop the frame # frame = crop_frame(frame) # 明暗変化による動体誤判定を防ぐために、グレースケール化 frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # BFMatcherオブジェクトの生成 bf = cv2.BFMatcher(cv2.NORM_HAMMING) # AKAZEを適用、特徴点を検出 detector = cv2.AKAZE_create() (_, target_des) = detector.detectAndCompute(frame, None ) if previous_target_des is not None : try : # BFMatcherで総当たりマッチングを行う matches = bf.match(target_des, previous_target_des) #特徴量の距離を出し、平均を取る distance = [m.distance for m in matches] mean_distance = sum (distance) / len (distance) print ( 'frame:' , frame_num, 'ret' , mean_distance) frame_and_mean_distances[step_num][frame_num] = mean_distance except : # エラーが出た場合は、直前の距離指標で補完する print ( 'frame:' , frame_num, 'error occured.' , 'distance: ' ,[m.distance for m in matches]) frame_and_mean_distances[step_num][frame_num] = previous_mean_distance previous_mean_distance = mean_distance previous_frame = frame previous_target_des = target_des 参考記事 aicam.jp こうして、「ある時点のフレームが、1つ前のフレームに対してどの程度”違い”があるか」が距離値として算出されることになります。 その結果を時系列でグラフ化したものが以下の図です。 図5: 前後フレームの距離値の時系列グラフ 動画の進行の中で、どのあたりでシーンの移り変わりがあったか を時系列的なデータとして表現出来ました。 ここの値が大きいシーンは、「ダイナミックな調理の動きや、多く食材があるシーンかもしれないフレーム」つまり、良いサムネイルの候補になりそうだと見立てることが出来ます。 仮説2: それぞれの候補画像が、調理手順内の多様なシーンを切り取ったものになっている 「前のフレームとの"違い"」とは別に考慮すべき要因として「シーンの多様性」があります。 各調理手順の中から良い感じの候補画像を抽出するには、「手順内全体を俯瞰してみたときに、なるべくさまざまな調理シーンが切り取られている」のが良いという考えです。 ※ 例えば、同じ調理手順内といっても、まな板の上で別々の野菜を順に切ったり、調味料を加えたり混ぜたりするようなシーンが連なっていることが多々あります。こういう"多様性"をなるべく網羅したいという話です。 そこで、信号処理の視点を応用してみました。 下図6のように、移動平均で慣らしてピークとなった部分をサムネイルとして抽出すれば、「動画全体の移り変わり」の視点も加味しつつ「前後の”違い”が大きいシーン」を選べるのではないかと考え、実装に落とし込みました。 scipyに find_peak という便利なライブラリがあったので、図6の雰囲気で使ってみました。 図6: ピーク地点検出の例 ここで特定されたピーク地点付近に相当する画像が、各手順のサムネイル候補になります。 (上図の場合は、手順1で6枚, 手順2で7枚, ...といった具合に抽出されていきます。) 仮説3: 候補画像は、なるべく鮮明で、ブレていないものである フレームをサムネイル候補として選ぶ以上、ブレているシーンは見栄えが悪いのでなるべく避けたいです。 そこで、画像のエッジ検出に用いられる ラプラシアンフィルタ を用いて、なるべく鮮明な輪郭を持つフレームを優先的に採用するルールを処理に加えました。 図7: ブレた画像の例 コード例 # 「ピーク検出されたフレームの前後のフレーム」の番号をまとめてframe_candidate_num_listに格納 # これらに該当するフレームに順繰りにフィルタを適用し、結果得られる値が最大のものを「ブレてない」画像として採用 for frame_candidate_num in frame_candidate_num_list: # フレームを取得 frame_candidate = tmp_frame_dict[frame_candidate_num] # グレースケール化 frame_candidate = cv2.cvtColor(frame_candidate, cv2.COLOR_BGR2GRAY) # ラプラシアンフィルタを適用 v = cv2.Laplacian(frame_candidate, cv2.CV_64F, ksize= 7 ).var() # 分散値をリストに追加 laplacian_var_dict[frame_candidate_num] = v # vが最大となるフレームを抽出 max_frame_num = max (laplacian_var_dict, key=laplacian_var_dict.get) picked_frames[max_frame_num] = tmp_frame_dict[max_frame_num] 参考記事 piccalog.net サムネイル候補の抽出結果 こうして組み上げた仕組みをいくつかのレシピに適用した例を紹介します。 図8: サムネイル候補の例 ※実際の動画やページが↓で閲覧出来ます↓ サクッとほくほく♪ かぼちゃの天ぷらのレシピ動画・作り方 | DELISH KITCHEN こんがり焼くだけ! 豆腐とキムチのチーズ焼きのレシピ動画・作り方 | DELISH KITCHEN なんとなくの所感ですが、「多様なシーン、かつ、意味のありそうな候補を抽出できている」気がします。 ※ ぶれていたり不鮮明だったりする画像が完全に無いわけでは無いですが、ラプラシアンフィルタを仕込まない場合に比べるとその発生頻度や質は改善されている所感でした。 ②AI処理パート: 「サムネイル画像候補」の中から一番良い1枚を選び出す ここから、手順の説明文に最も見合う画像をサムネイル候補の中から1枚選定するパートです。 今回の用途だと、 入力された画像とテキスト情報の関係をマルチモーダルに処理、判断できる大規模モデル が相性が良いのではと考えました。 そこで、 日本語特化版CLOOBモデル の利用に至りました。 これは、画像×テキスト情報を判断可能な大規模モデル CLIP の改良版として、rinna社によって提供されているモデルとなります。 CLOOBの事前学習済みモデル は、既に画像と日本語テキスト同士の兼ね合いを判断する能力を内部表現として獲得していると予想されます。 この能力を、レシピデータに用いてみてどうなるかを試してみました。 提供元 huggingface.co 画像特徴量とテキスト特徴量のコサイン類似度の計算 今回、 サムネイル候補と手順説明文の当てはまり度合い は コサイン類似度 として表現することとなります。 CLOOBモデルの画像Encoder(ViT-Bベース) 、 テキストEncoder(BERTベース) それぞれから得た特徴ベクトル間のコサイン類似度を計算することでこれを実現します。 以下図9がその図解です。 「複数のサムネイル候補画像の中から、"にんにくは粗みじん切りにする。"というテキストに対して、最もコサイン類似度の高いものを選ぶイメージです。 図9: サムネイル候補画像と手順説明文間のコサイン類似度計算の例 コード例 model, preprocess = ja_clip.load( "rinna/japanese-cloob-vit-b-16" , device=device) tokenizer = ja_clip.load_tokenizer() # 中略 # 画像とテキストそれぞれの特徴ベクトルを各種Encoderから抽出し、コサイン類似度を計算する # content: サムネイル候補画像のバイナリ, description: 手順説明文の文字列 def calc_cosine_similarity (content, description): with torch.no_grad(): # サムネイル候補画像の読み込み nparr = np.frombuffer(content, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) img = Image.fromarray(img) img = preprocess(img).unsqueeze( 0 ).to(device) # cloobモデルにサムネイル候補を渡し、画像特徴量を得る image_features = model.get_image_features(img) # cloobモデルに手順説明文を渡し、テキスト特徴量を得る description_encodings = ja_clip.tokenize( texts=description, max_seq_len= 150 , device=device, tokenizer=tokenizer, ) description_features = model.get_text_features(**description_encodings) # 画像特徴量とテキスト特徴量間のコサイン類似度を計算する probs = torch.cosine_similarity(image_features, description_features) return probs.tolist()[ 0 ] # scalar 参考記事 cedro3.com サムネイル選定結果 この仕組みを用いて、具体的にどんなサムネイルが採用されたのか、これまたレシピ例で見てみます。 図8で紹介したサムネイル候補の中から、最終的に選定された画像を赤枠で囲っています↓ 図10: サムネイル選定結果 図11: 「かぼちゃ天ぷら」のサムネイル選定結果 図12: 「豆腐とキムチのチーズ焼き」のサムネイル選定結果 これまた定性的な評価になりますが、実態にそぐうサムネイルが選定されている印象を受けます。 (全てでは無いですが)、「混ぜる」「揚げる」「載せる」などの動きを表すシーンを汲み取ってくれているような気がします。 やってみた所感 今回は思いつきドリブンで、やりたいことてんこ盛りで色々試しましたが、想像以上に"それっぽい"ものが出来て手応えを得ました。以下、所感をまとめています。 OpenCV等々を組み合わせた比較的シンプルな(Not機械学習の)アルゴリズムだけでも、「多様なシーンを切り取る機構」が作れて手応えを得ました。 レシピの情報を何も与えていない事前学習済みモデルを用いただけでも、想像以上に「レシピ手順説明文の文脈」をCLOOBが読み取ってくれたことに感銘しました。大規模モデルの可能性を改めて実感することになりました。 一方、実運用を見越すとなるとコスト面での課題はあるなと感じました。 今構築している環境で平均約1分のレシピ動画を捌くとなると、計10分弱(①画像処理パートで4分、②CLIPパートで6分ほど)費やすこととなります。 これを如何にして、数万本もあるレシピ処理に計算/コストの観点で最適化し、スケールさせていくかが課題となります。 終わりに 今までレシピ動画メディアでありながら、あまりデータサイエンスの文脈で動画像データを活用できていなかったので、こういう取り組みが出来て新鮮でした。 まだまだデータに眠る価値はあるなと思いました。 社内でも割と好評だったので、今は本取り組みを実用化出来ないか整理しています。PoCから実運用への昇華を目指したいところです。 この記事が何かの参考になれば幸いです。
アバター
はじめに こんにちは、株式会社 エブリー DevEnableグループです。 本日、6年ぶりのオフライン開催となった Go Conference 2024 にプラチナGoルドスポンサーとして参加してきました! Go Conference運営の皆様および参加された皆様、お疲れ様でした! 今回はオフラインのみの開催となったので、参加されていない皆さんにもGo Conference 2024の盛り上がりをいち早くお伝えしたく、早速参加レポートをさせていただきます。 エブリー初のスポンサーブースを出しました! 今回、エブリーとしては初めてスポンサーブースを出させていただきました。足を運んでいただいた皆様、本当にありがとうございました! 今回は、弊社が提供するDELISH KITCHENのサービスをイメージしてブースの雰囲気を作っていきました。 多くの方から「DELISH KITCHENを使っています!」とのお声をかけていただいたり、DELISH KITCHENで使う技術について意見交換ができたりと開発者としてもとても貴重な機会となりました。会場では、『DELISH KITCHENのAPIサーバーとGoの歩み』などこれまでの取り組みを赤裸々に綴ったパネルも用意しました。 ノベルティ 今回は以下のようなノベルティを用意させていただきました。 クッキー ドリップバックコーヒー 会社・サービスのステッカー DELISH KITCHENグッズ DELISH KITCHENグッズに関してはXフォローでの抽選プレゼントキャンペーンを行い、多くの方に参加していただきました。 (弊社エンジニアXアカウントは こちら です) DELISH KITCHENグッズに関してはたくさんの商品があるのですが、その中でも人気のある商品を中心に5つ準備させていただきました。 レンジ調理鍋 まな板 計量スプーン 鍋つかみ しゃもじ アンケート 今回のGo Conferenceのテーマは『一期一会』です。参加者の方々がコミュニケーションを取れるようなきっかけを作りたく、アンケートボードを用意しました。 お題はGoでもあまり決まったデファクトスタンダードがないORMに関して、いくつかの選択肢を用意して『GoのORM、何を使ってる?』としました。回答いただいた多くの皆様、ありがとうございました! 最終結果はこちら...! 1位👑: go-gorm/gorm 2位 : jmoiron/sqlx 3位 : sqlc-dev/sqlc やはりgormは多くの方が採用している結果となりましたが、それ以降に関してはどれも僅差の結果となっており、改めてGoでのORMの選択肢の広さを実感しました。 また、これをきっかけにブースを訪れていただいた方々とのコミュニケーションもたくさん取れて、各社での知見を聞けるいい機会ともなりました。 各社スポンサーブースの様子 スポンサーブースでは、各社趣向を凝らしたブースが展開されました。 ガチャやクイズ、アンケートボードなど様々な企画が用意されていて、会場全体が賑わっていました。 特に、「最近買ってよかったもの」をアンケートしていたブースでは、多くの回答が集まっており、エンジニアに馴染み深い「HHKB」から、「家の購入」といった意外な回答まで様々な回答がありました。 また、スポンサーブースでは、各社のエンジニアと直接話すことができる機会もあり、普段なかなか話すことができないような話もできてとても楽しかったです。 セッションの紹介 今回発表されたセッションの中から気になったものをいくつかまとめさせていただきました。 イテレータによってGoはどう変わるのか 発表者: tenntennさん ( https://twitter.com/tenntenn ) https://audience.ahaslides.com/cl965inb88/review?lookback-tab=slides こちらのセッションでは、Go1.22で一部がリリースされ、Go1.23でリリース予定のイテレータについて紹介されていました。 Goにおけるイテレータは任意の構造体に対して関数を通してシーケンシャルにアクセスする仕組みのことという定義の部分から、具体的にどのように使われるのかまで説明してくださっていました。 セッションの前はイテレータが導入されることによる具体的なメリットがあまりわかっていなかったのですが、イテレータが導入されることでデータ構造へのアクセスや一連の処理の結果をまとめるといった点で便利になるというお話を聞いたことでイテレータのメリットについて実感が湧きました。 特にデータ構造へのアクセスの仕方でmapにkeysが導入されるという話は、mapのkeysがないことは普段から不便に感じていたので期待が持てると思いました。 これは完全に余談ですが、tenntennさんの会社でGoのスキルを測定してくれるサービスがβリリースされたらしく個人的には興味を惹かれました。 https://yourwork.knowledgework.com Dive into gomock 発表者: utgwkkさん( https://twitter.com/utgwkk ) speakerdeck.com こちらのセッションでは、Goのユニットテストのモックに使われるgomockについて実際の実装を通して紹介されていました。 gomockの中で使用されているmatcherやgomock.Controllerがどのような役割なのか普段はなかなか意識しない部分もあり目から鱗でした。 印象に残ったのは発表の中でテクい実装と紹介されていた WantFormatter() の実装です。 matcherはinterfaceとして構造体に渡してfmt.Stringerはそのまま渡すなど普段実装をしているとあまり思い付かないこともライブラリの実装を通して知ることができるのは面白いと思いました。 下記のコードはuber-go/mockからの引用です。 https://github.com/uber-go/mock/blob/v0.4.0/gomock/matchers.go#L37 func WantFormatter(s fmt.Stringer, m Matcher) Matcher { type matcher interface { Matches(x any) bool } return struct { matcher fmt.Stringer }{ matcher: m, Stringer: s, } } gomockは弊社でも普段から使われていてどのように使うのかは知っているつもりでしたが、ライブラリの裏側について知ることができて勉強になりました。 セッションの最後になぜライブラリの実装を読むべきかというお話もしてくださったのですが、腑に落ちる部分も多くライブラリの実装を読んでいかなければと思いました。 バイナリを眺めてわかる gob enconding の仕様と性質、適切な使い方 発表者: convtoさん( https://twitter.com/convto ) speakerdeck.com こちらのセッションでは、gobのencoding結果であるバイナリを確認していくことで、gobの仕様や性質が解説されていました。 gobとはGoが標準パッケージで実装している独自のエンコーディングのことです。 メリットとしては、Goのプログラム上から特別な宣言なしに利用できたり、エンコーディング後の情報転送効率が高いことです。 また、gobは自己言及的であるため、メッセージ自身にどのような構造をしているのか送信できます。 そのため、メッセージ一つで構造が解釈可能で、事前に準備するものは不要といった点は非常に強力だと感じました。 バイナリを実際に確認して仕様を理解するというアプローチは面白く、またスライドも分かりやすく丁寧に解説されており非常に勉強になりました。 Mapのパフォーマンス向上のために検討されているSwissTableを理解する 発表者: replu5さん ( https://twitter.com/replu5 ) speakerdeck.com こちらのセッションでは、現状のMapの実装とは異なるSwissTableという仕組みを導入することでパフォーマンスを向上させる仕組みについて解説されていました。 現状Mapは作成するとbucketという箱が用意され、その中にあるtophashというものと用いて比較をしていく仕組みになっています。 一方、ここで挙げられているSwissTableとは、8 または 16要素分の追加情報をまとめてmetadataとして扱いマッチングを行うことで高速化を図ります。 現状ランタイムのMapの実装にSwissTableを使用したものが議論されているようで、要素数が少ないパターン以外はパフォーマンスが向上している点は興味深いと感じました。 既存のMapの実装を学ぶことができ、またそれを向上させるためのアイデアを学ぶことができた貴重な機会でした。 弊社エンジニアのセッション エブリーからは、DELISH KITCHENヘルスケア開発部、兼TIMELINE開発部の内原がスポンサーセッションのスピーカーとして登壇しました。 セッションでは、「DELISH KITCHENにおけるマスタデータキャッシュ戦略とその歴史的変遷」というタイトルで話をしました。 speakerdeck.com DELISH KITCHENは2016年のサービス最初期からバックエンドにGoを使用し続けているプロダクトですが、様々な要素を考慮してデータキャッシュへの向き合い方を考えてきました。 そんなキャッシュ戦略の歴史と展望について、Goでどのように実装されているのかを踏まえて、実際に直面してきた課題とともに解説しました。 最後に 最後になりますが、Go Conference の運営の皆さん、カンファレンスの運営をしていただき本当にありがとうございました! また、参加者の皆さん、カンファレンスへの参加お疲れ様でした! 今年は、6年ぶりのオフライン開催で暑いなか多くの方が参加されて、改めてGoのコミュニティの盛り上がりを感じることができました! アフターイベント「Go Bash」のお知らせ Go Conference 2024 にスポンサー and 参加した エブリー / アンドパッド / LayerX / STORES の Gopher たちが Go Conference 2024 に刺激を受け、トークや感想戦を繰り広げ、 Beer ではなく Go で盛り上がるイベントを開催します! andpad.connpass.com Go Conference 2024 に参加された方も、参加されなかった方も、ぜひご参加ください! エブリーでは、ともに働く仲間を募集しています。 エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 7日目の記事です。 はじめに エブリーでソフトウェアエンジニアをしている本丸です。 Go Conference 2024 もいよいよ明日開催ですね。 Goに関する話ということでDELISH KITCHENのユニットテストで使用されているライブラリを紹介したいと思います。 弊社ブログの過去の記事にテストの可読性についてのものがあるので興味があればぜひ読んでみてください! Go testにおける可読性を保つ方法を考える DELISH KITCHENのユニットテストで使用しているライブラリ DELISH KITCHENではユニットテストを行うときに主に以下の4つのライブラリを使用しています。 gomock testify/assert go-cmp httptest gomock 名前の通り、ユニットテストの際にモックを提供してくれます。 https://github.com/uber-go/mock gomockでは以下のようなinterfaceからmockを生成することができ、 type UserRepo interface { Insert(age int ) User BulkInsert(ages [] int ) []*User Change(u User) *User } テストコードの中で下記のように使います。 ctrl := gomock.NewController(t) repo := NewMockUserRepo(ctrl) repo.EXPECT().Add( 20 ).Return(User{}) これだけでも便利なのですが、社内のコードに個人的に便利な機能だと思うものがあったので、いくつか紹介します。 Do() ドキュメント からの引用ですが、下記のことを行ってくれます。 Doは、呼び出しがマッチしたときに実行するアクションを宣言します。後方互換性を保つため、関数の戻り値は無視されます。 社内で具体的にどのように使っているかというと repo.EXPECT().Change(gomock.Eq(u)). Do( func (user) { u.ID = 1 }). Return(&u) 構造体を受け取って、その構造体のフィールドを変更して返す関数のモックを含む時に使用しています。 InAnyOrder() こちらも ドキュメント からの引用ですが、下記のことを行ってくれます。 InAnyOrderは、順序を無視して同じ要素のコレクションに対して真を返すMatcherです。 社内で具体的にどのように使っているかというと idMap := map [ int ] struct {}{ 19 : struct {}{}, 20 : struct {}{}, } ids := make ([] int , 0 , len (idMap)) for id := range idMap { ids = append (ids, id) } repo.BulkInsert() のような処理があり、BulkInsert()のmockを作りたい時に repo.EXPECT().BulkInsert(gomock.InAnyOrder([] int64 { 20 , 19 })). Return() arrayの順番が保証されないためこちらを使用しています。 testify/assert ある値がこうなるはずだというアサーションのチェックを行ってくれます。 https://github.com/stretchr/testify testify/assertを使用してある関数のレスポンスが期待したものと一致するか確認したい場合は以下のようになります。 want := true got := doAnything() assert.Equal(t, want, got) 様々なアサーションが用意されているのですが、 Equal 以外では下記に示したものがDELISH KITCHENだとよく使用されていました。 Contains() Error() Len() go-cmp オブジェクトを比較してくれるライブラリで、ユニットテストでもオブジェクトの比較のために使用しています。 https://github.com/google/go-cmp if diff := cmp.Diff(want, got); len (diff) != 0 { t.Errorf( "got diff = %v" , diff) } go-cmpはオプションを使用することで様々なケースに対応することが可能です。 その中から社内で使われているものを一部紹介します。 IgnoreUnexported IgnoreUnexportedをオプションとして指定すると、構造体の中のprivateなフィールドなどunexportedなものを無視して比較してくれます。 type SearchRequest struct { Client http.Client url string } 例えば、上記のような構造体があった場合はClientだけ比較されて、urlは無視されるといった挙動になります。 IgnoreFields IgnoreFieldをオプションとして指定すると、構造体の中の指定したフィールドを無視して比較してくれます。 type User struct { ID int CreatedAt time.Time } opts := []cmp.Option{ cmpopts.IgnoreFields(User{}, "CreatedAt" ), } 上記のように指定すると、CreatedAtが無視されてIDだけ比較されるという挙動になります。 SortSlices SortSlicesをオプションとして指定すると、指定したarrayのフィールドをソートした後に比較してくれます。 func GetUserIDs() [] int { // 要素の順番がランダムなuserIDのarrayを返す処理 } got := GetUserIDs() want := [] int { 1 , 2 , 3 } opt := cmpopts.SortSlices( func (i, j int ) bool { return i < j }) if diff := cmp.Diff(got, want, opt); diff != "" { t.Errorf( "GetUserIDs() = %v, want %v" , got, tt.want) } 例えば、GetUserIDs()という関数のテストをしたい時に、実際のコードではランダムな順序で問題ない場合でもテストでは順序も含めて比較を行うため失敗してしまうということが起こり得ます。このような時に SortSlices をオプションとして指定すると任意の順番にソートした後に比較を行うため、配列の順番でテストが失敗するということは起こらなくなります。 httptest 標準ライブラリなので趣旨と少しズレるかもしれませんが、テスト用のモックサーバーとして利用しています。 func NewRequestHTTPRequestMock() (*httptest.Server, func () string ) { var body string return httptest.NewServer(http.HandlerFunc( func (w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) body = string (b) })), func () string { return body } } func Test_Request(t *testing.T) { requestMock, getRequestContent := NewRequestHTTPRequestMock() _, _ = s.Search(requestMock.URL) got := getRequestContent() if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf( "got diff (-want +got): \n %s" , diff) } } DELISH KITCHENでは http.Client を使用している箇所があり、利用箇所のテストを行うためにhttptestを利用しています。 上記のコードでは、NewRequestHTTPRequestMock()でモックサーバーを作成して、そこに対してリクエストを行うことでリクエストの中身が正しいのかのテストを行なっています。 まとめ 改めてまとめてみるとDELISH KITCHENではGoのテストではメジャーなライブラリが使われているといった印象でした。それと同時に、普段使用しているライブラリでも改めてドキュメントを読み直してみると、自分は使いこなせていない部分も多いと気付かされました。 Go Conference 2024 まで、あと1日! https://gocon.jp/2024/ 株式会社エブリー は、Platinum Gold スポンサーとして Go Conference 2024 に参加します。 ぜひ、ブースやセッションでお会いしましょう! https://gocon.jp/2024/sponsors/2/
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 6 日目の記事です。 目次 はじめに イントロダクション そもそもメールヘッダーとは net/mail パッケージ メールの解析 ヘッダーの取得 Body の取得 net/mail パッケージのメール解析で辛いところ MIME マルチパートメッセージの解析が不完全 MIME マルチパートメッセージとは デコード機能が不十分 メールプロトコルに沿わせた構成にするのが大変 メールプロトコルとは net/mail で解析したメールを送信可能なメールにするために jhillyerd/enmime パッケージ メールヘッダーの設定 net/mail と jhillyerd/enmime の比較 net/mail メリット デメリット jhillyerd/enmime メリット デメリット まとめ 最後に はじめに こんにちは!最近推しの配信が多くなってきて嬉しい @きょー です!DELISH KITCHEN 開発部のバックエンド中心で業務をしています。 業務でメール内容を解析、処理する機会があり、そこで経験した学びについて話していこうと思います。 イントロダクション 業務中に メールを解析 、 メールヘッダーのカスタマイズ 、 メールの送信 をするという場面に出くわしましたが、Go の標準パッケージである net/mail では解決が難しいことがわかり、苦労した経験があります。この記事では net/mail の基本的な使い方や遭遇した辛いところを紹介し、その辛さを解決してくれるパッケージ jhillyerd/enmime についてお話しようと思います。 そもそもメールヘッダーとは メールヘッダとは、メールの詳細情報が書かれている部分のことです。具体的には、メールが配送された経路や時間、経由したサーバーなどが記録されています。 以下は、一般的なメールヘッダーの例とその説明です。 From: 送信者のメールアドレスが記載されています。 To: 主な受信者のメールアドレスが記載されています。 Subject: メールの件名が記載されています。 Received: メールが経由したサーバーとその日時が記載されています。これはメールの配送経路を追跡するのに使われます。 Content-Type: メールの本文の形式(例:text/plain, text/html)が記載されています。 MIME-Version: メールが MIME(Multipurpose Internet Mail Extensions)規格を使用している場合、そのバージョンが記載されています。 メールヘッダーは、メールのトラブルシューティング、スパムの検出、セキュリティ分析などに使用されます。たとえば、 Received ヘッダーを調べることで、メールがどのサーバーを経由してきたかを追跡し、スパムやフィッシングメールの出所を特定することができます。 net/mail パッケージ pkg.go.dev net/mail パッケージは、メールメッセージを解析するための機能を提供します。このパッケージを使用すると、メールのヘッダー情報やアドレスの解析、メッセージの本文の取得などが行えます。 net/mail パッケージの基本的な使用方法について紹介していきます。 メールの解析 net/mail パッケージを使用してメールを解析するには、まず mail.ReadMessage 関数を使用してメールデータを読み込みます。 // メールのサンプルデータ rawEmail := `From: sender@example.com To: recipient@example.com Subject: This is a test email Content-Type: text/plain; charset="utf-8" This is the body of the email.` // ←がBody部分 // io.Readerの作成 reader := strings.NewReader(rawEmail) // ReadMessageを使用してメールを解析 msg, _ := mail.ReadMessage(reader) ヘッダーの取得 メールのヘッダーは Header 型で表され、これは下記のような map[string][]string の型定義です。 type Message struct { Header Header Body io.Reader } type Header map [ string ][] string ヘッダーの値は Header.Get(key) メソッドを使用して取得できます。このメソッドは指定されたキーに対応する最初の値を返します。 // ヘッダーの取得 header := msg.Header // Fromヘッダーの取得 from := header.Get( "From" ) fmt.Println( "From:" , from) // From: sender@example.com Body の取得 以下のように Message 構造体の中にある Body からメールの本文を取得できます。 // 本文の取得 bytes, _ := io.ReadAll(msg.Body) fmt.Printf( "Body: %s" , string (bytes)) // Body: This is the body of the email. net/mail パッケージのメール解析で辛いところ net/mail パッケージは、基本的なメールメッセージの解析機能を提供しますが、いくつかの辛みポイントがあります。以下にその主な点を挙げます。 MIME マルチパートメッセージの解析が不完全 net/mail パッケージは MIME マルチパートメッセージ の解析を直接サポートしていません。 Message 構造体の Body フィールドには、メールの本文が含まれますが、 MIME マルチパートメッセージ の場合、下記のコードのような boundary 文字列( --000000000000abcdefg12345 )や各パートのヘッダーなどがそのまま含まれてしまいます。これにより、メールの本文だけを簡単に取得することができない、という問題が生じます。 --000000000000abcdefg12345 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: base64 44GT44KT44Gr44Gh44Gv --000000000000abcdefg12345 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: base64 PGRpdiBkaXI9ImF1dG8iPuOBk+OCk+OBq+OBoeOBrzwvZGl2Pg== --000000000000abcdefg12345-- MIME マルチパートメッセージとは MIME マルチパートメッセージ は テキスト や html 、 画像 などそれぞれ異なるパートに分け、それらを組み合わせ構成されたものです。この仕組みは複数のファイルを電子メールに添付するときなどに使用されます。 上記のメールの本文では、 text/plain や text/html の部分が組み合わされ一つのメッセージとなっています。画像や動画を送る場合は image/png 、 video/mp4 などのパートがメッセージに追加されます。 developer.mozilla.org デコード機能が不十分 net/mail パッケージにはほぼデコードの機能がありません。( ParseAddress 関数を除く) そのため、日本語で書かれたメールの 件名 や 本文 を エンコード 方式( base64 や quoted-printable など)に合わせ適切に デコード しなければ文字化けしてしまいます。 また、 net/mail パッケージでは Header ではなく Body の中に MIMEマルチパートメッセージ の エンコード 方式が書かれています。そのため デコード するために形式を取得したくとも簡単には取得できない、という問題があります。 メールプロトコルに沿わせた構成にするのが大変 メールプロトコルとは メールを送信する上で意識しなければいけないのが メールプロトコル です。メールプロトコルとは、電子メールの送受信に関する規則や手順を定めたもので、電子メール通信をする上でメールデータが正しくやり取りされるために必要です。 RFC2822 でメールプロトコルが規定されています。下記に内容の一部を紹介していきます。 ASCII コードで構成されること 一行は 78 文字以下が推奨 Header フィールドは、フィールド名の後にコロン(":")、フィールド本体が続き、CRLF で終了 Body の前は空行にする これらの規則や手順を守らないと、メール送信できなかったり送信できても文字化けしてしまうなどの問題に繋がります。 net/mail で解析したメールを送信可能なメールにするために net/mail パッケージでは Message 構造体の中に Header と Body フィールドがあります。メール送信するためにはこれらを組み合わせ []byte 型にしなければいけなく、具体的には以下のような処理が必要になります。 複数の Header のフィールド名と値をセットで取り出し、1 行に 1 セット設定する 一行が 78 文字以上にならないように適宜改行コードを入れる Header と Body を組み合わせて[]byte に変換 これを自分で対応しようとすると骨の折れる作業になります。実際に行った記事としても以下のような記事がよくまとまっています qiita.com 上記の記事のコードを手元で管理したくないという思いから、MIME のエンコードやデコードを気にせず、電子メールの生成や解析をしてくれるパッケージを探し始めました。 そこで見つけたのが以下で紹介するパッケージです。 jhillyerd/enmime パッケージ jhillyerd/enmime パッケージは MIME エンコードおよびデコードライブラリで、MIME エンコードされた電子メールの生成と解析に重点を置いています。 net/mail パッケージでは Message 構造体のフィールドの Header と Body がそれぞれ分かれていたため、 解析 → Header 修正 → MIME 対応 → Header をエンコード → Body と組み合わせる → メール送信可能な構造に修正 → メール送信 といった流れでした。 jhillyerd/enmime パッケージでは Header も Body も全て一緒に MIME に対応した解析と生成をするため 解析 → Header 修正 → MIME 対応したエンコード → メール送信 のように処理が簡易化されます。 実際に例を見てみましょう。 pkg.go.dev メールヘッダーの設定 // objはio.Reader型 // メールの内容を解析 envelope, err := enmime.ReadEnvelope(obj) // Fromヘッダーの上書き err = envelope.SetHeader( "From" , [] string {fmt.Sprintf( "%s <%s>" , senderName, senderEmail)}) // Toヘッダーの上書き err = envelope.SetHeader( "Subject" , [] string { "new subject" }) buf := & bytes.Buffer {} // MIMEに対応したエンコード err = envelope.Root.Encode(buf) _ := sendEmail(buf.Bytes()) 以上を踏まえ、簡単に net/mail と jhillyerd/enmime のメリット、デメリットについて以下にまとめてみました。 net/mail と jhillyerd/enmime の比較 net/mail メリット 標準パッケージのため、追加の依存関係を導入しなくて済む 公式が管理しているため、安定してメンテナンスされる API がシンプルで、処理が追いやすい デメリット MIME マルチパートメッセージやテキストエンコーディングの解析など、複雑なメール処理に必要な高度な機能が不足している jhillyerd/enmime メリット MIME マルチパートメッセージの解析、添付ファイルの処理、エンコーディングの変換など、複雑なメール処理に対応している デメリット 管理しているコミュニティが小さく、メンテナンスが継続されないリスクがある まとめ メールヘッダーを取得・設定するだけであれば net/mail パッケージだけで十分だと思いました。 MIME マルチパートメッセージ の解析・エンコーディングをする必要がある場合は、複雑な処理を管理しなくて済むので jhillyerd/enmime の利用を検討してみても良いかもしれません。 最後に Go Conference 2024 まで、あと 2 日! gocon.jp 株式会社エブリー は、Platinum Gold スポンサーとして Go Conference 2024 に参加します。 ぜひ、ブースやセッションでお会いしましょう! gocon.jp
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 5 日目の記事です。 はじめに こんにちは、TIMELINE 開発部 Service Development をしている ほんだ です! 初の Go Conference オフライン参戦なので浮かれてる今日この頃です。 今回はスマホ向けネットスーパーアプリの API を Python から Go へ移行する際のデータベース操作の観点での課題と実際にどのような解決策を取ったのか実装をメインに紹介します。 ネットスーパーアプリのリプレイスを行うことにした背景やシステム全体の課題、解決策に関しては前回のブログに記述しているので是非ご一読ください。 tech.every.tv 技術スタック 以下は今回の記事に関係のあるリプレイス前後の技術スタックになります。 言語 DB ORM リプレイス前 Python MySQL PyMySQL リプレイス後 Go MySQL sqlboiler + sqlx 課題 リプレイスを行うにあたりデータベース操作の観点で以下の 4 点の課題がありました。 テストの不在 既存の実装にテストがないため、リプレイス後のコードが正しく機能するかを検証する手段が限られています。これにより、修正後のコードが期待通りの動作をするかの判断が困難です。 長大な SQL の扱い 200 行を超える長大な SQL クエリを sqlboiler で書き換えることは非常に困難です。これは、sqlboiler が主に CRUD 操作に最適化されており、複雑なクエリの扱いには向いていないためです。 名前付きプレースホルダーの問題 元のクエリでは以下の例のように名前付きプレースホルダー( %(format)s )が多用されていますが、sqlboiler はこの機能をサポートしていません。これにより、プレースホルダー(MySQL では ? )で実装されたクエリでは、クエリが長くなるほど可読性と保守性が損なわれます。 WHERE item.item_name like %(search_word)s OR item.item_area like %(search_word)s OR item.item_spec like %(search_word)s OR event_item.event_item_name like %(search_word)s OR event_item.event_item_area like %(search_word)s OR event_item.event_item_spec like %(search_word)s 型の厳格化 既存の Python 実装ではレスポンスが dict 型で返されるため、柔軟なデータ構造を扱うことができます。しかし、sqlboiler でデータベース操作を行うとレスポンスは tag を元に構造体にバインドされるため厳格な型定義が必要となり、これがリプレイスの際の追加の課題となります。 実装 先に挙げた課題点に対処するため、以下の実装方針を採用しました。 長大なクエリの移行 : 長大なクエリは、可能な限りそのまま Go に移行します。これにより、既存のクエリロジックを保持し、移行に伴うリスクを最小限に抑えることができます。 名前付きプレースホルダーの使用 : sqlx を使用して、名前付きプレースホルダーを実装します。これにより、クエリの可読性と保守性を向上させることができます。 汎用的な実行関数の作成 : 生の SQL クエリを実行し、結果を Go の構造体にバインドする汎用的な関数を作成します。このアプローチにより、異なるタイプのクエリに対しても柔軟に対応することが可能になります。 クエリの移行について 「長大なクエリは可能な限りそのまま Go に移行する」という方針に基づき、sqlboiler で移行可能なクエリと生クエリを明確に区別するために、次のようなディレクトリ構成を採用しました。 repository/ ├── models/ │ ├── item.go │ ├── favorite.go │ ├── menu.go │ └── user.go ├── rawquery/ │ ├── util.go │ ├── item_builder.go │ └── menu_builder.go ├── item.go ├── favorite.go ├── menu.go └── user.go repository ディレクトリ直下には、sqlboiler を用いて移行されたクエリの実装があります。一方で、repository/rawquery ディレクトリには、生クエリを直接扱う実装を配置しています。これらの生クエリは、sqlboiler の Raw 関数をラップしたユーティリティ関数を介して、repository 直下のファイルから呼び出されます。repository/models ディレクトリには、クエリ実行時に結果をバインドするための構造体が定義されています。 この構成により、クエリの種類ごとに責務を分離し、コードの整理と保守性の向上を図っています。 名前付きプレースホルダーを sqlx で実装 次に、名前付きプレースホルダーの実装について説明します。既存の Python 実装では pymysql を使用し、 %(format)s 形式で名前付きプレースホルダーを実装していました。しかし、sqlboiler にはこの機能がないため、sqlx を採用しました。 名前付きプレースホルダーを使用することで、長大なクエリにおける多数の引数や重複する引数の取り扱いが容易になります。ここでは、名前付きプレースホルダーを含む生クエリ、引数の実装、およびそれらをバインドする関数の実装について順を追って説明します。 以下は、repository/rawquery にある名前付きプレースホルダーに渡される引数をフィールドに持つ構造体、初期化関数、名前付きプレースホルダーを含む生クエリを返すメソッド、および引数を返すメソッドの実装例です。 // repository/rawquery/item_builder.go package rawquery type ItemBuilder struct { price int janCode string tax int } func NewItemBuilder(name string , price int , janCode string , tax int ) *ItemBuilder { return &ItemBuilder{ price: price, janCode: janCode, tax: tax, } } func (b *ItemBuilder) BuildQueryWithArgs() (ReBindedQueryArgs, error ) { return buildQueryWithArgsDefault(b.rawQuery(), b.args()) } func (b *ItemBuilder) rawQuery() string { q := ` SELECT name,item_code,price,jan_code,tax_rate FROM item WHERE price > :price AND jan_code = :jan_code` if b.tax != nil { q += " AND tax_rate = :tax_rate" } return q } func (b *ItemBuilder) args() map [ string ] interface {} { args := map [ string ] interface {}{ "price" : b.price, "jan_code" : b.janCode, } if b.tax != nil { args[ "tax_rate" ] = *b.tax } return args } ItemBuilder 構造体は、クエリに必要な引数を保持します。 BuildQueryWithArgs メソッドを呼び出すと、sqlx を使用して名前付きプレースホルダーが含まれる生クエリのプレースホルダーを適切な形式に置き換え、引数の順序に準拠した interface{} 型のスライスを返します。 以下は、 BuildQueryWithArgs メソッドの実行結果の例です。 // repository/rawquery/util.go type ReBindedQueryArgs struct { Query string Args [] interface {} } func buildQueryWithArgsDefault(rawQuery string , args map [ string ] interface {}) (ReBindedQueryArgs, error ) { namedQuery, namedArgs, err := sqlx.Named(rawQuery, args) if err != nil { return ReBindedQueryArgs{}, err } return ReBindedQueryArgs{Query: sqlx.Rebind(sqlx.QUESTION, namedQuery), Args: namedArgs}, nil } sqlx.Named(rawQuery, args) は、生クエリ(rawQuery)と引数(args)を受け取り、クエリ内の名前付きプレースホルダーを引数の値で置き換えます。置き換えられたクエリ(namedQuery)と引数(namedArgs)を返します。 sqlx.Rebind(sqlx.QUESTION, namedQuery) を使用して、名前付きプレースホルダーを ? に再バインドします。そして、再バインドされたクエリと引数を含む ReBindedQueryArgs を返します。 以下は buildQueryWithArgsDefault を実行した結果になります。 sql := ` SELECT name,item_code,price,jan_code,tax_rate FROM item WHERE price > :price AND jan_code = :jan_code` args := map [ string ] interface {}{ "jan_code" : 12345 , "price" : 200 , } queryArgs, _ := buildQueryWithArgsDefault(sql, args) fmt.Println(queryArgs) # 実行結果 { SELECT name,item_code,price,jan_code,tax_rate FROM item WHERE price > ? AND jan_code = ? [ 200 12345 ] } 名前付きプレースホルダー :price , :jan_code が ? に、引数が名前付きプレースホルダに対応した順序の slice になっていることがわかります。 sqlboiler を用いたクエリの実行関数 次に生クエリを実行し Go の構造体に bind する汎用的な関数について説明します。 以下が具体的な実装になります。 // repository/rawquery/util.go func Execute[T any](ctx context.Context, exec boil.ContextExecutor, queryArgs ReBindedQueryArgs) (*T, error ) { var result T if err := queries.Raw(queryArgs.Query, queryArgs.Args...).Bind(ctx, exec, &result); err != nil { return nil , err } return &result, nil } 型引数 T には response に期待する構造体を指定します。 引数に指定された ReBindedQueryArgs の Query と Args を用いて queries.Raw でクエリを生成、 Bind で result にクエリの結果をバインドます。 実行方法 最後に repository 直下のファイルの実装について説明します。 以下のように実装することで生クエリを意識することなくデータベース操作を行えるようにすること、生クエリを廃止し sqlboiler での実装に統一した時の影響が最小限になるようにしています。 // repository/item.go type ItemRepository struct {} func NewItemRepository() *ItemRepository { return &ItemRepository{} } func (r *ItemRepository) ListItem(ctx context.Context, exec boil.ContextExecutor, name string , price int , janCode string , tax int ) (*models.Items, error ) { queryArgs, err := rawquery.NewItemBuilder(name, price, janCode, tax).BuildQueryWithArgs() if err != nil { return nil , fmt.Errorf( "failed to build item query args: %w" , err) } res, err := rawquery.Execute[models.Items](ctx, exec, queryArgs) if err != nil { return nil , fmt.Errorf( "failed to get items: %w" , err) } return res, nil } まとめ この記事では、リプレイスプロジェクトにおけるデータベース操作の課題と、それに対する実装方針について詳しく紹介しました。理想的には、リプレイス前に既存コードにテストを追加し、最低限のリファクタリングを行うことが望ましいです。しかし、今回は迅速な移行と、Go への書き換え後にリファクタリングを進めるという方針のもと、生クエリをそのまま移行することにしました。 sqlboiler と sqlx という二つの異なる ORM を併用することには無理があるように思われるかもしれませんが、結果として責務が適切に分割され、より良いコードへと近づいたと感じています。 Go Conference 2024 まで、あと 3 日! gocon.jp 株式会社エブリー は、Platinum Gold スポンサーとして Go Conference 2024 に参加します。 ぜひ、ブースやセッションでお会いしましょう! gocon.jp
アバター
はじめに この記事は every Tech Blog Advent Calendar 2024(夏) 4 日目の記事です。 こんにちは!トモニテで開発を行っている吉田です。 今回はGo 言語の特徴的な機能である並行処理について書いていきます。並行処理を支えるゴルーチン (goroutine) とチャネル (channel) の仕組みと使い方を、サンプルコードとともに紹介します。 並行処理を実現するにあたり まずはゴルーチンとチャネルについて理解を進めます。 ゴルーチンとは ゴルーチンとは 他のコードに対して並行に実行している関数のことです。 前提として全ての Go のプログラムには最低 1 つのゴルーチンがあります。それがメインゴルーチンです。 下記のように関数の前に go キーワードを追加することでゴルーチンを起動することができます。 func PrintStr(str string ){ fmt.Println(str) } go PrintStr( "start goroutine!" ) // 即時関数で実装することも可能 go func () { fmt.Println( "start goroutine!" ) } チャネルとは チャネルは、同時実行中のゴルーチンをつなぐパイプです。あるゴルーチンからチャネルに値を送り、その値を別のゴルーチンで受け取ることができます。 チャネルはデータを順序よく受け渡すためのデータ構造(queue)になっており、バッファを持つことができます。 また Go のチャネルはブロックをします。キャパシティがいっぱいのチャネルに書き込もうとするゴルーチンはチャネルに空きが出るまで待機し、空のチャネルから読み込もうとするチャネルは少なくとも要素が 1 つ入るまで待機します。 下記のように make 関数を使ってチャネルを初期化します。 ch := make ( chan interface {}, 100 ) // 第2引数でバッファを指定 バッファのあるチャネルがブロックするのは、バッファが一杯になったときだけでバッファに空きが出たら値を受け取ります。 バッファ付きチャネルが空で、それに対する読み込みチャネルにも空きがある場合にはバッファはバイパスされ送信元から受信先へと直接値を渡すことができます。 その他の特徴 チャネル利用時は値を chan 型の変数に渡しプログラムのどこかの場所でそのチャネルから読み込む チャネル同士はお互いが何をしているのかは知らずチャネルが存在しているメモリの同じ場所を参照している ex.) package main import "fmt" func main() { send := make ( chan string ) // 双方向チャネルの初期化 // データの送信 go func () { send <- "hello!" // ゴルーチンでデータを送信 }() receive := <-send // メインゴルーチンでデータを受信 fmt.Println(receive) // "hello!" を出力 } 上記のように書くことでメインゴルーチンの処理とは別に並行で異なる処理を行うことができます。 ゴルーチンとチャネルを使うことで、複数のタスクを同時に実行することができますがどのような場面でその良さが出るのでしょうか。 ここでは運用しているサービスでユーザー全員にメッセージを送信する必要があるという場面を例にゴルーチンを使用した場合とそうでない場合の差を見てみます。 ※それぞれ Go のバージョンは 1.22.3 で実施しています ゴルーチンを使わない場合 package main import ( "fmt" "sync/atomic" "time" ) type ( MessageInfo struct { User string Message string } ) var messageCount int64 // GetUsers 対象ユーザーの抽出 func GetUsers() [] string { var names [] string for i := range 10000 { names = append (names, fmt.Sprintf( "Mr. %d" , i)) } return names } // Setting ユーザーごとにメッセージ作成 func Setting() ([]MessageInfo, error ) { users := GetUsers() target := make ([]MessageInfo, 0 ) // ユーザーごとにメッセージを作成 for _, user := range users { params := MessageInfo{ User: user, Message: fmt.Sprintf( "Dear. %s. We are excited to announce that our supermarket, XX, has recently opened a new branch in YY!" , user), } target = append (target, params) } return target, nil } // SendMessage メッセージを送信する func SendMessage(param MessageInfo) { time.Sleep( 10 * time.Millisecond) // 送信処理に時間がかかると仮定 // 送ったメッセージ数をカウント // 複数のゴルーチンが同時にmessageCountを更新することによる競合を防ぐためatomicパッケージを使用 atomic.AddInt64(&messageCount, 1 ) } // Send 全ユーザーに対してメッセージ送信 func Send(targets []MessageInfo) error { for _, target := range targets { SendMessage(target) } return nil } func main() { start := time.Now() targets, err := Setting() if err != nil { fmt.Println(err) return } Send(targets) fmt.Printf( "No Goroutine method took %s \n " , time.Since(start)) fmt.Printf( "Messages sent: %d \n " , atomic.LoadInt64(&messageCount)) } かかった時間 $ go run main.go No Goroutine method took 1m49.024703667s Messages sent: 10000 ゴルーチンを使う場合 package main import ( "fmt" "sync" "sync/atomic" "time" ) type MessageInfo struct { User string Message string } var messageCount int64 // GetUsers 対象ユーザーの抽出 func GetUsers() [] string { var names [] string for i := range 10000 { names = append (names, fmt.Sprintf( "Mr. %d" , i)) } return names } // Setting ユーザーごとにメッセージ作成 func Setting() (<- chan MessageInfo, error ) { users := GetUsers() targets := make ( chan MessageInfo, 100 ) // チャネルにバッファを設定 go func () { defer close (targets) for _, user := range users { targets <- MessageInfo{ User: user, Message: fmt.Sprintf( "Dear. %s. We are excited to announce that our supermarket, XX, has recently opened a new branch in YY!" , user), } } }() return targets, nil } // SendMessage メッセージを送信する func SendMessage(user, message string ) { time.Sleep( 10 * time.Millisecond) // 送信処理に時間がかかると仮定 atomic.AddInt64(&messageCount, 1 ) // 送ったメッセージ数をカウント } // Send 全ユーザーに対してメッセージ送信 func Send(targets <- chan MessageInfo) error { var wg sync.WaitGroup for taraget := range targets { // 各メッセージ送信は独立したgoroutineで処理 wg.Add( 1 ) go func (taraget MessageInfo) { defer wg.Done() SendMessage(taraget.User, taraget.Message) }(taraget) } wg.Wait() return nil } func main() { start := time.Now() targets, err := Setting() if err != nil { fmt.Println(err) return } Send(targets) fmt.Printf( "Goroutine method took %s \n " , time.Since(start)) fmt.Printf( "Messages sent: %d \n " , atomic.LoadInt64(&messageCount)) } かかった時間 $ go run main.go Goroutine method took 31 .348791ms Messages sent: 10000 並行処理を使わない場合は使う場合に比べ3倍ほどの時間がかかっており、使う場合と使わない場合の差を実感することができました。 続いては並行処理に用いた実装について説明します。 まずは対象者に向けてメッセージを作成する Setting メソッド内にある defer close(targets) についてです。 // Setting ユーザーごとにメッセージ作成 func Setting() (<- chan MessageInfo, error ) { users := GetUsers() targets := make ( chan MessageInfo, 100 ) go func () { defer close (targets) for _, user := range users { targets <- MessageInfo{ User: user, Message: fmt.Sprintf( "Dear. %s. We are excited to announce that our supermarket, XX, has recently opened a new branch in YY!" , user), } } }() return targets, nil } 冒頭説明したように go キーワードでゴルーチンが作成できます。 その直後、 defer close(targets) があります。 これはチャネルが閉じてこれ以上値が送信されることがないことを伝えるために用いられます。今回の場合だと targets チャネルにこれ以上値が送信されないということを伝えています。 // Send 全ユーザーに対してメッセージ送信 func Send(targets <- chan MessageInfo) error { var wg sync.WaitGroup for target := range targets { // 各メッセージ送信は独立したgoroutineで処理 wg.Add( 1 ) go func (target MessageInfo) { defer wg.Done() SendMessage(target.User, target.Message) }(target) } wg.Wait() return nil } なぜチャネルに値が送信されないかを伝える必要があるのかについてですが、これは targets チャネルを利用している Send メソッド内の for taraget := range targets が targets チャネルが閉じられるまで別のチャネルから値を受信し続ける(ループが永遠に終わらない)ためです。 試しに defer close をコメントアウトして実行すると fatal error: all goroutines are asleep - deadlock! というエラーが発生しました。これはゴルーチンが値を待ち続けて処理をブロックしてしまうためデッドロックが発生していたということです。 続いては上記 Send メソッド内の sync.WaitGroup についてです。sync パッケージは同期的な処理によく用いられますが WaitGroup はゴルーチンを終了を待つために使っています。 そもそもどうしてゴルーチンの終了を待つ必要があるのでしょうか?答えはメインスレッドはゴルーチンの終了を待ってくれないからです。 WaitGroup をコメントアウトして試してみます。 // 変更がないところは省略します。 // Send 全ユーザーに対してメッセージ送信 func Send(targets <- chan MessageInfo) error { // var wg sync.WaitGroup for taraget := range targets { // 各メッセージ送信は独立したgoroutineで処理 // wg.Add(1) go func (taraget MessageInfo) { // defer wg.Done() SendMessage(taraget.User, taraget.Message) }(taraget) } // wg.Wait() return nil } func main() { start := time.Now() targets, err := Setting() if err != nil { fmt.Println(err) return } Send(targets) fmt.Printf( "Goroutine method took %s \n " , time.Since(start)) fmt.Printf( "Messages sent: %d \n " , atomic.LoadInt64(&messageCount)) } $ go run main.go Goroutine method took 19 .215833ms Messages sent: 3356 送りたい数は 10000 ですが 3356 しか実行されておらず sync.WaitGroup の必要性を確認することができました。 コード内 wg が何をしているのか簡単に説明すると以下の通りです。 wg.Add(1) ... 待機したいゴルーチンの数(カウンタ)を設定。カウンタが 0 になると、後述 Wait でブロックされているすべてのゴルーチンが解放される。監視対象のゴルーチンの直前に書くのが慣習 wg.Done() ... カウンタを 1 減らす。defer キーワードを用いてゴルーチンのクロージャーが終了する前に WaitGroup に終了することを確実に伝えるために使用 wg.Wait() ... WaitGroup カウンターがゼロになるまでメインゴルーチンをブロックする 最後に 以上が Go における並行処理についてです。 ゴルーチンとチャネルを使うことで、複数のタスクを同時に実行することが可能になり、プログラムの効率を大幅に向上させることができます。 今回の記事を通じて、Go の並行処理についての理解が深まっていれば幸いです! ここまでお読みいただきありがとうございました! Go Conference 2024 まで、あと【4】日! gocon.jp 株式会社エブリー は、Platinum Gold スポンサーとして Go Conference 2024 に参加します。 ぜひ、ブースやセッションでお会いしましょう! gocon.jp 参考 www.oreilly.co.jp pkg.go.dev gobyexample.com www.spinute.org
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 3 日目の記事です。 はじめに こんにちは、トモニテでバックエンド周りの開発を行っている rymiyamoto です。 最近は学園アイドルのプロデューサー業に追われています。 今回は、Go 言語で CLI ツールを開発する際によく使われるライブラリである cobra と go1.21 から標準パッケージで使えるようになった slog を使って、CLI ツールを開発する方法について紹介します。 選定理由 現状の課題 Go 言語だとスクリプト処理を実装する際、簡単なものであれば main.go にそのまま処理を書いていくことが多いですが、コマンドライン引数を取るような処理を書く場合、コードが複雑になりがちです。 実際トモニテ内のスクリプト処理も当時の実装メンバーに依存しており以下のような課題がありました。 コマンドのフォーマット サブコマンド指定だったり引数だったり設計者依存 hoge --param=1 or hoge -param 1 それぞれで無駄な共通引数定義 dry-run ログの出力 標準の logger だと使いにくい logrus apex/log 、 zap と割と自由にしがち 同じような pkg が多いとメンテナンスも辛い これらの課題から、コマンドライン引数を取る処理を簡単に実装できるパッケージとして cobra を、ログは go1.21 から標準パッケージで使えるようになった構造化ログが扱える slog を採用しました。 cobra について cobra は Go 言語で CLI ツールを開発する際に歴史があり、Kubernetes、Hugo、GitHub CLI などの多くの Go プロジェクトで使用されています。 コマンドライン引数を取る処理を簡単に実装できるだけでなく、サブコマンドを定義することで複数のコマンドを持つ CLI ツールを簡単に作成することが可能です。 また、CLI ツールで cobra-cli が提供されており、コマンドからスクリプトファイルの作成ができます。 github.com github.com slog について go1.21 から導入された構造化ログを扱うことができる go の標準パッケージです。 構造化ログは JSON や key=value 形式でログを出力することができ、ログの解析や可視化が容易になります。 また、標準パッケージであるため、外部パッケージを追加することなく go の標準ライブラリでログを出力することができます。 pkg.go.dev イメージ logger := slog.New(slog.NewJSONHandler(os.Stdout, nil )) logger.Info( "hello" , "count" , 3 ) { " time " : " 2024-06-03T15:28:26.000000000-05:00 " , " level " : " INFO " , " msg " : " hello " , " count " :3 } 環境作成 以下のようなディレクトリ構成で CLI ツールを作成していきます。 $ tree . ├── Dockerfile ├── Makefile ├── cobra.yml └── compose.yml 事前準備 Dockerfile cobra-cli を使いたいので、Go のイメージに cobra-cli をインストールします。 ARG GO_VERSION=1.22.3 FROM golang:${GO_VERSION} AS dev RUN go install github.com/spf13/cobra-cli@v1.3.0 compose.yml name: go-cli-management services: scripts: container_name: scripts build: context: . dockerfile: ./Dockerfile target: dev working_dir: /scripts volumes: - .:/scripts tty: true Makefile cobra-cli を使ったコマンドやコマンドの実行をやりやすくするために作成しています。 container = scripts .PHONY: dev dev: docker compose up -d .PHONY: init init: dev docker compose exec $(container) go mod init $(name) docker compose exec $(container) cobra-cli init .PHONY: add add: dev @$(eval script_file := ${name}.go) @$(if $(name),, $(error name is not defined)) @$(eval script_file_exists := $(shell ls . | grep ${script_file})) @$(if $(script_file_exists), $(error $(name) is already exists)) docker compose exec $(container) cobra-cli add $(name) --config ./cobra.yml .PHONY: run run: dev docker compose exec $(container) go run ./main.go $(line) cobra.yml cobra-cli でコマンドを追加する際の設定ファイルです。 このファイルを編集することでコマンドの中身を拡張できます。 github.com name: author_name useViper: true 初期設定 以下のコマンドから gomod の初期化と cobra-cli の初期化を行います。 $ make init name =go-cli 実行後は以下のようなディレクトリ構成になります。 $ tree . ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd │ └── root.go # cobra で生成されたファイルで、このファイルをベースにしてコマンドを追加していきます ├── cobra.yml ├── compose.yml ├── go.mod ├── go.sum └── main.go # CLI ツールのエントリーポイント 拡張 現状 cmd/root.go に処理をベタ書きしていけばそのままコマンドとして実行できますが、それだと拡張性が失われてしまうのでサブコマンドやデフォルトフラグを追加して取り回しを良くしていきます。 デフォルトフラグの追加 cmd/root.go に初期値を追加します。 今回は並列処理の管理と dry-run モードを追加します。 const ( // concurrencyDefault デフォルトの並列数 concurrencyDefault = 10 // waitTimeDefault デフォルトの処理チャンク単位の待機時間 waitTimeDefault = 1 ) // ... func init() { rootCmd.PersistentFlags().Bool( "dry-run" , false , "Dry run mode" ) rootCmd.PersistentFlags().Uint( "concurrency" , concurrencyDefault, "並列更新数(1以上)" ) rootCmd.PersistentFlags().Uint( "wait-time" , waitTimeDefault, "処理チャンク単位の待機時間(秒)" ) } サブコマンドの追加 cobra-cli を使ってサブコマンドを追加します。 $ make add name =hello 実行すると cmd 配下に hello.go が作成されます。(以下参照) /* Copyright © 2024 rymiyamoto */ package cmd import ( "fmt" "github.com/spf13/cobra" ) // helloCmd represents the hello command var helloCmd = &cobra.Command{ Use: "hello" , Short: "A brief description of your command" , Long: `A longer description that spans multiple lines and likely contains examples and usage of using your command. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.` , Run: func (cmd *cobra.Command, args [] string ) { fmt.Println( "hello called" ) }, } func init() { rootCmd.AddCommand(helloCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // helloCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: // helloCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } この状態で make run line=hello を実行すると hello called が出力されます。 $ make run line =hello hello called フラグの追加 フラグの追加は作成された cmd/hello.go に cmd/root.go のときと同様に行います。 // ... func init() { rootCmd.AddCommand(helloCmd) helloCmd.Flags().StringP( "target-at" , "t" , time.Now().In(time.FixedZone( "Asia/Tokyo" , 9 * 60 * 60 )).Format(time.DateOnly), "対象日(e.g 2023-10-05)" ) } 処理の整形 Run メソッドではコマンドの実行時の処理を記述しますが、エラーを返すことができないため、エラーハンドリングが限定的です。これに対し、 RunE メソッドを使用すると、エラーを呼び出し元に返すことができ、より柔軟なエラー処理が可能になります。 pkg.go.dev また、slog を使用してデフォルト引数やフラグの値をログに埋め込むことで、実行時の状況を明確に記録できます。slog の JSON ハンドラを標準出力に設定することで、レイヤードアーキテクチャにおいても、中間層を介さずに直接ログを出力することが可能です。これにより、ログの伝播に関するコードの複雑さが軽減されます。 // ... RunE: func (cmd *cobra.Command, args [] string ) error { // デフォルトフラグ dryRun, _ := rootCmd.Flags().GetBool( "dry-run" ) concurrency, _ := rootCmd.Flags().GetUint( "concurrency" ) waitTime, _ := rootCmd.Flags().GetUint( "wait-time" ) // サブコマンド固有フラグ targetAt, _ := cmd.Flags().GetString( "target-at" ) base := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{})) logger := base.With( "dry-run" , dryRun, "concurrency" , concurrency, "wait-time" , waitTime, "target-at" , targetAt) slog.SetDefault(logger) slog.Info( "hello world!" ) return nil }, 実行すると、以下のような構造化ログが出力されます。構造化ログは、ログデータをキーと値のペアで表現することで、自動化された解析や人間による読解を容易にします。これにより、ログの監視や分析が効率的に行えるようになります。 # サブコマンド実行 $ make run line = " hello " { " time " : " 2024-05-29T11:20:21.059692464Z " , " level " : " INFO " , " msg " : " hello world! " , " dry-run " :false, " concurrency " :10, " wait-time " :1, " target-at " : " 2024-05-29 " } # デフォルトフラグの書き換え $ make run line = " hello --dry-run " { " time " : " 2024-05-29T11:20:46.268378503Z " , " level " : " INFO " , " msg " : " hello world! " , " dry-run " :true, " concurrency " :10, " wait-time " :1, " target-at " : " 2024-05-29 " } # サブコマンド固有フラグの書き換え $ make run line = " hello --target-at=2024-06-02 " { " time " : " 2024-05-29T11:21:23.834180257Z " , " level " : " INFO " , " msg " : " hello world! " , " dry-run " :false, " concurrency " :10, " wait-time " :1, " target-at " : " 2024-06-02 " } あとはサブコマンドの中身を実装や追加をしていけば、CLI ツールの開発が進められます。 まとめ 今回は Go 言語で CLI ツールを開発する際によく使われるライブラリである cobra と go1.21 から標準パッケージで使えるようになった slog を使って、CLI ツールを開発する方法について紹介しました。 cobra はコマンドライン引数を取る処理を簡単に実装できるだけでなく、サブコマンドを定義することで複数のコマンドを持つ CLI ツールを簡単に作成することが可能です。 また slog を使うことで構造化ログを出力することができ、ログの解析や可視化が容易になります。 RunE の繰り返しは面倒な作業ですが、これを改善する方法を模索していく予定です。 今後は、このベースを使って実際の処理を実装していくことで、より実用的な CLI ツールを開発していきたいと思います。 Go Conference 2024 まで、あと 5 日! gocon.jp 株式会社エブリー は、Platinum Gold スポンサーとして Go Conference 2024 に参加します。 ぜひ、ブースやセッションでお会いしましょう! gocon.jp
アバター
目次 はじめに CPUが理解できる言葉 プログラミング言語が機械語として理解されるまで アセンブリ言語 プログラミング言語の解釈 コンパイラ リンカ インタープリタ まとめ 参考 はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 every Tech Blog Advent Calendar 2024(夏) の2日目の記事執筆担当者として参加させていただいております! tech.every.tv 今回の記事では、普段書いているプログラムがCPUによってどのように理解されているのかについて、気になって勉強したのでまとめてみたいと思います。 (厳密には異なる表現があるかもしれませんが、概念的な理解を目指すものなので、ご容赦ください。) CPUが理解できる言葉 CPUが理解できる言葉は、機械語と呼ばれるものです。 我々エンジニアが普段から書いているプログラミング言語は、CPUから見れば 「意味のわからない、ただの文字列でしかない」と言えるでしょう。 機械語とは、例えば、以下のように表現することができます。(CPUの種類によって表現が異なっていたり、そもそもの解読が辛かったりするので正確な表現・値ではありません) 01 00 10 各数値は16進数で表されており、これで「アドレス 00 番地に値 10 を書き込む」というように、処理( 01 )と必要な対象を組み合わせて1つの命令を表現します。 では、プログラミング言語はどのように機械語としてCPUに解釈されるのでしょうか。 プログラミング言語が機械語として理解されるまで アセンブリ言語 現在一般的に使われているプログラミング言語の話をする前に、アセンブリ言語について触れたいと思います。 アセンブリ言語とは、人間が理解しやすいように機械語と1対1で対応させた言語です。 アセンブリ言語がアセンブラと呼ばれるプログラムによって機械語に変換されることで、CPUが言語を理解できるようになります。 アセンブリ言語に関しては、実際にシェル上で objdump コマンド 1 を利用することで確認することができます(これはオブジェクトファイルの情報を表示するコマンドですが、 -d フラグをつけることで機械語を逆アセンブルすることができます)。 例として、筆者のマシン上で echo 命令を逆アセンブルしてみます。 $ objdump -d /bin/ echo すると、以下のような出力が得られます。 (なお、出力結果の全体は非常に長いので、先頭の数行を抜粋しています) /bin/ echo ( architecture x86_64 ) : ( __TEXT,__text ) section 100000bbc: 55 pushq %rbp 100000bbd: 48 89 e5 movq %rsp, %rbp 100000bc0: 41 57 pushq %r15 100000bc2: 41 56 pushq %r14 100000bc4: 41 55 pushq %r13 100000bc6: 41 54 pushq %r12 100000bc8: 53 pushq %rbx 100000bc9: 48 83 ec 28 subq $4 0, %rsp ... 左から、ファイル(今回は /bin/echo )上でのオフセット、実際の機械語(16進数の値2~4つの組)、機械語に対応する命令を表すニーモニック(mnemonic)の順に並んでいます。 プログラミング言語の解釈 一般的に、我々が日々書いているソースコードは、アセンブリ言語への変換を目指して解釈が進められ、最終的にCPUが実行可能な機械語へと変換されます。 この解釈の過程を担っているのが、コンパイラやインタプリタと呼ばれるものになります。 コンパイル型言語やインタプリタ型言語というのは、この解釈の過程がどのように行われるかによって分類されます。 コンパイラ コンパイラ は以下のように定義できます 2 (説明のため一部表現を変えています)。 言語 のプログラムを言語 のプログラムに変換するプログラム 一般的なコンパイル型言語では、ソースコードをアセンブリ言語まで変換する役割をコンパイラが担っていることが多いでしょう(アセンブラはコンパイラに含まれている場合もあります)。 この定義から考えると、アセンブラもコンパイラの一種と言えると思いますが、 アセンブリ言語から機械語への変換は、変換元がアセンブリ言語であることを強調するためにアセンブラと呼ばれることがあります。 コンパイル型言語の1つとして、弊社でも利用されているGo言語が挙げられます。 Go言語では以下のステップを経てソースコードがコンパイルされます 3 。 Parsing Lexical analysis (tokenize) Syntax analysis (parse) AST construction Type checking IR construction Middle end (最適化) Walk (順序評価、構文の低級化) Generic SSA(Static Single Assignment 4 ) Generating machine code Goのコンパイラにはアセンブラも含まれているため、最終的には機械語に変換されていることがわかります。 Goの標準のコンパイラである gc はGoで実装されています。 これは、セルフホスティングと呼ばれる手法で、自身の言語で自身のコンパイラを書くというものです。 gcは元々C言語で書かれていましたが、この手法を用いることでGoで書かれたコンパイラが実現されています 5 。 自身の言語で実装されたコンパイラを実行するために、コンパイラのコード自身がコンパイルされている必要があります。 そのため、異なる言語で実装されたコンパイラ を用いて自身の言語で実装されたコンパイラ をコンパイルし、 最後に で 自身をコンパイルすることで、自身の言語で書かれたコンパイラが完成します( イメージ )。 また、コンパイラの実現方法の他に、コンパイルの手法にもJIT(Just-In-Time)コンパイラ 6 やAOT(Ahead-Of-Time)コンパイラ 7 などの種類があります。 TypeScriptからJavaScriptへの変換(トランスパイル)もコンパイルの一種です。 リンカ 一般的に、プログラムは複数のソースコードから構成されます。そのため、 コンパイラによって生成されたそれぞれのオブジェクトファイルは、そのままでは実行可能な1つのプログラムとはなりません。 これらのオブジェクトファイルをリンカと呼ばれるプログラムによって結合することで、実行可能な1つのプログラムが生成されます。 リンカは、関数のエントリーポイント情報を補完するなどして、複数のオブジェクトファイルを結合した1つの実行可能ファイルなどを生成します。 通常、ソースコードを機械語にコンパイルしてリンクするまでの過程を指して「ビルド」と呼びます。 なお、生成された実行可能ファイルは、ローダーによってストレージ(外部記憶装置)からメインメモリ(RAM)などに読み込まれます。 また、リンクには静的リンクと動的リンクの2種類があります。 静的リンクは、プログラムの実行に必要なライブラリなどを単一の実行ファイル内部にリンクする方法、 動的リンクは、呼び出される側のライブラリが実行時にリンクされる方法です。 リンク方法が静的か動的かどうかで実行ファイルのサイズや開発サイクルのスピードなどに違いが出てきます。 インタープリタ インタープリタ は以下のように定義できます 8 (説明のため一部表現を変えています)。 言語 を用いて実現した、言語 のプログラムが動作するプログラム インタープリタは、実行時にソースコードを逐次解釈して実行するものです。 インタープリタの解釈手法には、ソースコードをそのまま逐次解釈するものもあれば、 一度バイトコードなどに変換(コンパイル)してから逐次解釈するものもあります。 例えば、Pythonの標準的なインタープリタであるCPython 9 は、 ソースコードをバイトコード(中間表現)に変換してから逐次解釈され、 PVM(Python Virtual Machine)によって処理されます 10 。 ちなみに、Pythonのバイトコードは dis モジュール 11 を利用することで確認することもできます。 例えば、以下のようなコードを実行する場合を考えます(Google Colaboratoryでの実行を前提にしています)。 import dis def print_hello (): print ( "Hello!" ) dis.dis(print_hello) このコードを実行すると、以下のような出力が得られます。 4 0 LOAD_GLOBAL 0 ( print ) 2 LOAD_CONST 1 ( ' Hello! ' ) 4 CALL_FUNCTION 1 6 POP_TOP 8 LOAD_CONST 0 ( None ) 10 RETURN_VALUE 左から、ソースファイル内での行番号、命令のバイトコード(とその該当バイトインデックス)、 命令が取得する引数の参照インデックスと引数の順に並んでいます。 まとめ 今回の記事では、普段意識することのなかったプログラムの処理系について勉強したことをアウトプットしてみました。 これを知ったからといって普段のコーディングが劇的に変わるということはないと思いますが、 こういった基礎的な知識がシビアなシーンでは役に立つことも多いと思います。 この記事が、「なんかそれっぽいこと書いてるだけで勝手にPCが結果を出してくれる」を脱却したい人の一助になれば幸いです。 最後まで読んでいただき、ありがとうございました。 参考 大堀淳の計算機科学チャネル | コンパイラ ー原理と構造ー 筑波大学 | プログラミング言語処理 講義資料 | 言語処理系とは Rui Ueyama, 低レイヤを知りたい人のためのCコンパイラ作成入門, 2020/03/16 東京情報大学 | オペレーティング・システム | 第8回 プログラムの実行制御(その3) プログラムの実行 本当に初心者の人に捧げるコンピューター入門 | 1.4.3 まずは機械語 wikipedia | 機械語 Go コンパイラのコードを読んでみよう About the go command Introduction to the Go compiler GO | Frequently Asked Question logmi Tech | コンパイラが作ったバイナリをつなぎ合わせるプログラム「lld」の作者が語る、リンカの仕組み IT用語辞典 e-words | リンカ IT用語辞典 e-words | 静的リンク IT用語辞典 e-words | 動的リンク IT用語辞典 e-words | ローダー speakerdeck | Goコンパイラをゼロから作ってセルフホスト達成するまで / How I wrote a self hosted Go compiler from scratch what is a self-hosting compiler? wikipedia | インタープリタ CPython Python Glossary synopsys ブログ | Pythonバイトコードの知識 objdump ↩ コンパイラ ー原理と構造ー 第2回:計算機の模倣、プログラミング言語の構造と原理、プログラミング言語開発の枠組み(25:32くらい) ↩ Introduction to the Go compiler ↩ Static Single Assignment(静的単一代入) ↩ what compiler technology is used to build the compilers? ↩ JIT コンパイラー ↩ AOT コンパイラー ↩ コンパイラ ー原理と構造ー 第2回:計算機の模倣、プログラミング言語の構造と原理、プログラミング言語開発の枠組み(21:25くらい) ↩ cpython ↩ bytecode ↩ disモジュール ↩
アバター
はじめに この記事は、 every Tech Blog Advent Calendar 2024(夏) の1日目の記事です。 DELISH KITCHEN開発部の羽馬(@NaokiHaba)です。 この記事では、DELISH KITCHEN チラシ で使用している Vuex の Pinia への移行について紹介します。 chirashi.delishkitchen.tv 本記事では、これらの知識があることを前提に説明を進めます。 Vue.jsの基本的な知識 Nuxt.jsの基本的な知識 Vuexの基本的な知識 Piniaとは Pinia(ピーニャ)は、Vue.js用の新しい状態管理ライブラリです。Vuexの次のイテレーションとして開発が始まり、Vuex 5に組み込むことを想定していたアイデアを多く取り入れています。 pinia.vuejs.org Piniaは、Vuexと比較して以下のような特徴や利点があります。 シンプルなAPIを提供し、学習コストが低い TypeScriptとの連携が強化され、型の恩恵を受けやすい モジュール方式を採用せず、ストアを個別に定義できるため、コードの可読性や保守性が向上する Vue Devtoolsとの統合が進んでおり、開発体験が良い Piniaは、Vue.js v2とv3の両方に対応しており、Nuxt.jsにも対応しています。Nuxt v3からは、VuexからPiniaが公式に推奨されるようになりました。 なぜPiniaに移行するのか DELISH KITCHEN チラシ では、以下の理由からPiniaへの移行を決定しました。 Nuxt3への移行を見据えて、早めにPiniaを導入しておきたかった Vuex は現在メンテナンスモードであり、今後のアップデートが見込めないため Nuxt3以降もPiniaの公式サポートが続くと予想されるため Piniaへの移行によって、Nuxt3への移行をスムーズに進めることができると考えました。 移行の手順 1. Piniaの導入 まずは、Pinia を導入します。 pinia.vuejs.org $ yarn add pinia @pinia/nuxt # or with npm $ npm install pinia @pinia/nuxt 次に、nuxt.config.js に Pinia の設定を追加します。 移行時点では、Vuex と Pinia を併用することとなるため、disableVuex を false に設定します (disableVuex はデフォルトで true になっているため、Vuex が無効化されます) // nuxt.config.js export default defineNuxtConfig ({ buildModules : [ // set `disableVuex` to false if you need to use Vuex alongside Pinia [ '@pinia/nuxt' , { disableVuex : false } ] , ] , }) 以上で、Pinia の導入は完了です。 2. VuexストアのPiniaストアへの移行 次に、既存のVuexストアをPiniaストアに移行します。 Piniaでは、ストアをdefineStore関数を使って定義します。defineStore関数には、ストアの名前を表すidと、ストアの定義を表すoptionsの2つの引数を渡します。 pinia.vuejs.org 以下は、Vuexストアの例です。 // store/todo.js export default { state : { todos : [] , } , mutations : { setTodos ( state , todos ) { state . todos = todos } , } , actions : { async fetchTodo ({ commit } , id ) { try { const response = await this. $axios . get ( `https://jsonplaceholder.typicode.com/todos/ ${ id } ` ) commit ( 'setTodos' , [ response . data ]) } catch ( error ) { console . error ( error ) } } , } , getters : { allTodos : state => state . todos , } , } このVuexストアを、Piniaストアに移行すると以下のようになります。 // stores/todo.js import { defineStore } from 'pinia' export const useTodoStore = defineStore ( 'todos' , { state : () => ({ todos : [] , }) , actions : { async fetchTodo ( id ) { try { const response = await this. $nuxtAxios . get ( `https://jsonplaceholder.typicode.com/todos/ ${ id } ` ) this. todos = [ response . data ] } catch ( error ) { console . error ( error ) } } , } , getters : { allTodos : ( state ) => state . todos , } , }) Piniaストアでは、mutationsが削除され、actionsとgettersのみが残っています。これは、Piniaではmutationsの概念がなくなり、actionsで直接ステートを更新するためです。 また、actions内でのthisの扱いが変わっています。Piniaでは、thisがストアのインスタンスを指すため、this.todosのように直接ステートを更新できます。 ここで、 this.$axios が this.$nuxtAxios に変更されていることに注目してください。 Piniaでは、ストアの中で this がストアのインスタンスを指します。したがって、Vuexストアで使っていた this.$axios をそのまま使うことはできません。 代わりに、Nuxtのコンテキストからプラグインを介して $axios を取得し、 this.$nuxtAxios として使用しています。 このプラグインは、以下のように定義します。 // plugins/pinia-inject-axios.js export default defineNuxtPlugin (( nuxtApp ) => { nuxtApp . $pinia . use (() => ({ $nuxtAxios : markRaw ( nuxtApp . $axios ) , })) ; }) ; そして、nuxt.config.js でこのプラグインを登録します。 Nuxt3では、 $fetch を使うことが推奨されており、 @nuxtjs/axios は利用できないため、このプラグインは不要になります。 // nuxt.config.js export default defineNuxtConfig ({ plugins : [ '~/plugins/pinia-inject-axios.js' , ] , }) nuxtServerInit の扱い Vuexでは、 nuxtServerInit はサーバーサイドレンダリング(SSR)時に、サーバー側での初期化処理を行うための特別なアクションでした。Nuxt.jsでは、SSR時に store ディレクトリ内の各ストアの nuxtServerInit アクションが自動で呼び出される仕組みがあります。 一方、Piniaでは nuxtServerInit が自動で呼び出される仕組みがありません。代わりに、 plugins や middleware を利用して、 nuxtServerInit の処理を移行する必要があります。 例えば、 plugins/nuxt-server-init.js というファイルを作成し、以下のようなコードを記述します。 export default defineNuxtPlugin(nuxtApp => { if (process.server) { // サーバー側での初期化処理をここに記述 } }) 3. コンポーネント内でのストアの利用方法の変更 最後に、コンポーネント内でのストアの利用方法を変更します。 VuexではmapState、mapGetters、mapActionsなどのヘルパー関数を使ってストアにアクセスしていました。 Piniaでも同様のヘルパー関数が用意されていますが、mapGettersの代わりにmapStateを使うことが推奨されています。 pinia.vuejs.org <template> <div> <div v-for="todo in todos" :key="todo.id"> {{ todo.title }} </div> <button @click="fetchTodo(1)">Fetch Todo</button> </div> </template> <script> import { mapState, mapActions } from 'pinia' import { useTodosStore } from '~/stores/todosStore' export default { fetch({ app, error, $pinia }) { const todosStore = useTodosStore($pinia) todosStore.fetchTodo(1) }, computed: { ...mapState(useTodosStore, [ 'todos' ]), }, methods: { ...mapActions(useTodosStore, [ 'fetchTodo' ]), }, } </script> Composition APIを使う場合は、useStore関数を使ってストアのインスタンスを取得し、直接ストアの状態やアクションにアクセスできます。 <script setup> import { useTodosStore } from '~/stores/todosStore' const todosStore = useTodosStore() await todosStore.fetchTodo(1) </script> <template> <div> <div v-for="todo in todosStore.todos" :key="todo.id"> {{ todo.title }} </div> <button @click="todosStore.fetchTodo(1)">Fetch Todo</button> </div> </template> まとめ この記事では、DELISH KITCHEN チラシ におけるVuexからPiniaへの移行について紹介しました。 Piniaは、Vuexと比べてシンプルなAPIを提供し、TypeScriptとの連携が強化されているため、Nuxt3での開発をスムーズに進めることができます。 Nuxt3での開発を行う際には、ぜひPiniaの導入を検討してみてください。
アバター
はじめに DELISH KITCHEN開発部 兼 Dev Enableチームの羽馬(@NaokiHaba)です。 初夏の陽気が心地よい今日この頃、every Tech Blog ではもうすでに夏へのカウントダウンが始まっています。 そして今年は、その夏を少し先取りする形で、6月にアドベントカレンダーを開催します! every Tech Blog Advent Calendar とは every Tech Blog Advent Calendar は、2023年12月に始まった弊社のエンジニアによる技術ブログ企画です。 Advent Calendarにちなんで、12月は1日から25日まで日替わりで記事を公開してきました。 Webフロントエンドからバックエンド、インフラ、機械学習、データ分析など幅広い分野の記事が集まり、多くの方にご覧いただけました。 tech.every.tv every Tech Blog Advent Calendar 2024 (夏) の見どころ 今回のアドベントカレンダーでは、前回の知見を踏まえつつ、さらに多様で深い技術記事を 6月1日から28日までの28日間 毎日お届けします! 暑い夏を熱いテクノロジーで乗り切るべく、エンジニアたちが知恵を絞って記事を執筆中です。 ご期待ください! 注目の企画として、今回はエブリーがスポンサーを務める2つのカンファレンスを盛り上げるべく、カウントダウン企画を実施します! 6/3〜6/7 Go Conference 2024カウントダウンブログ Go Conference 2024が開催される6月8日まで、Go言語に関する記事を毎日公開します。 イベント詳細はこちらから! gocon.jp 6/19〜6/21 KotlinFest 2024 カウントダウンブログ KotlinFest 2024が開催される6月22日まで、Kotlinに関する記事を毎日公開します。 イベント詳細はこちらから! www.kotlinfest.dev 公開日 テーマ URL 2024/06/01 Vuex から Pinia への移行を行いました https://tech.every.tv/entry/2024/06/01/170000 2024/06/02 プログラムが CPU に理解されるまでのプロセスをまとめてみた https://tech.every.tv/entry/2024/06/02/103000 2024/06/03 go 言語で cobra と slog を使った CLI ツール開発 https://tech.every.tv/entry/2024/06/03/103933 2024/06/04 Go 言語の並行処理: ゴルーチンとチャネルの活用法について https://tech.every.tv/entry/2024/06/04/100307 2024/06/05 ネットスーパーリプレイス〜長大なクエリと向きあう編〜 https://tech.every.tv/entry/2024/06/05/150124 2024/06/06 Go 言語で行うメール解析 https://tech.every.tv/entry/2024/06/06/192547 2024/06/07 DELISH KITCHEN のユニットテストで使用しているライブラリ https://tech.every.tv/entry/2024/06/07/104820 2024/06/08 Go Conference 2024 に プラチナ Go ルドスポンサー として参加しました! https://tech.every.tv/entry/2024/06/08/200152 2024/06/09 レシピ動画からサムネイル画像を自動抽出する AI システムを作りました https://tech.every.tv/entry/2024/06/09 2024/06/10 社内ナレッジ活用のための RAG 基盤の PoC を行いました https://tech.every.tv/entry/2024/06/10/110918 2024/06/11 API Gateway から Amazon Data Firehose へ Lambda を使わずにデータを流す https://tech.every.tv/entry/20240611 2024/06/12 Xcode 15 の画像/色のシンボル自動生成機能を SPM マルチモジュール環境で使う https://tech.every.tv/entry/2024/06/12/111801 2024/06/13 Databricks Model Serving と AWS API Gateway で作る ML API https://tech.every.tv/entry/2024/06/13/170411 2024/06/14 mamadays.tv から tomonite.com へドメインを変更しました https://tech.every.tv/entry/2024/06/14/144222 2024/06/15 新規プロダクトのリポジトリ構成にモノレポを採用してみた https://tech.every.tv/entry/2024/06/15/0001 2024/06/16 N1 分析してみる https://tech.every.tv/entry/2024/06/16/000000 2024/06/17 ML のスモールスタート時に Databricks の Feature Store を導入するべきか否か https://tech.every.tv/entry/2024/06/17/140157 2024/06/18 Flutter エンジニアが年 150 万円のサーバー費用を削減する話 https://tech.every.tv/entry/2024/06/18/175820 2024/06/19 LiveData を Kotlin Coroutines Flow に移行した話 https://tech.every.tv/entry/2024/06/19/114100 2024/06/20 Android プロジェクトの KSP 化を検討するにあたって https://tech.every.tv/entry/2024/06/20/095753 2024/06/21 8 年前に Kotlin を採用してたくさん恩恵を受けた話 https://tech.every.tv/entry/2024/06/21/100756 2024/06/22 Kotlin Fest 2024 に ひよこスポンサー として参加してきました! https://tech.every.tv/entry/2024/06/22/185445 2024/06/23 AWS Summit Japan 2024 に参加しました https://tech.every.tv/entry/2024/06/23/144931 2024/06/24 RDS で EBS BurstBalance が枯渇した事例の紹介 https://tech.every.tv/entry/2024/06/24/195434 2024/06/25 Go 言語で multipart/form-data を使用して画像を受け取り外部に送信する https://tech.every.tv/entry/2024/06/25/110115 2024/06/26 Amazon QuickSight を使用してインタラクティブな可視化をしてみる https://tech.every.tv/entry/2024/06/26/114130 2024/06/27 リアーキテクチャを支えるテスト駆動開発:効果的なリファクタリングの方法 https://tech.every.tv/entry/2024/06/27/121736 2024/06/28 Golangでアプリ課金(iab/iap)を実装するときは awa/go-iap が便利って話 https://tech.every.tv/entry/2024/06/28/115518 投稿された記事は、 株式会社エブリー 開発部の公式X でポストするとともに、こちらの技術ブログにも順次掲載していきますので、ぜひブックマークやコメント・シェアをお願いします! それでは、6月1日からお楽しみに! 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
TSKaigi 2024 に参加してきました! はじめに Dev Enableチームの羽馬( NaokiHaba ) と 庄司( ktanonymous )です。 2024年5月11日(水)に開催されたTSKaigi 2024に参加してきましたので、イベントの様子や印象に残ったセッションをいくつかご紹介します。 各セッションのアーカイブも公開予定とのことですので、ぜひ公式サイト・YouTubeチャンネルなどをチェックしてみてください。 tskaigi.org www.youtube.com イベントの様子 TSKaigi 2024は、今年から開催された新しいイベントです。TypeScriptを中心にしたカンファレンスで、TypeScriptの最新情報や活用事例などが紹介されました。 会場には、国内外から多くのエンジニアが集まり、盛況のうちに開催されました。会場内では、様々なブースが設けられ、最新のツールやサービスの紹介が行われていました。 特に印象に残ったブースは、アセンド株式会社 様のブースで開催された 「TypeScriptコンパイルチャレンジ」です。 このコンパイルチャレンジは、以下のような内容でした。 参加者は、青色の「型カード」を引く 机に並べられた赤色の「値カード」から一つをめくる 引いた型カードと値カードを使って、TypeScriptのコンパイルにチャレンジ コンパイルに成功すると、景品が贈呈される(高難易度コンパイルに成功した方には、HHKBなどの豪華景品も) 弊社メンバーも、あと一歩のところで当選を逃してしまいましたが、楽しい体験ができました。 参加レポート Keynote: What's New in TypeScript 発表者: Daniel Rosenwasser さん( https://twitter.com/drosenwasser ) レポート: 庄司 Microsoft / TypeScript Principal Product Manager の Daniel Rosenwasser さんによる Keynote では、TypeScript の最新情報が紹介されました。 TypeScript 5.4 および TypeScript 5.5 Beta の新機能について、 ライブコーディングを交えながら、各機能の使い方や利点が丁寧に解説されていました。 主な新機能は以下の通りです: TypeScript 5.4 The NoInfer Utility Type Preserved Narrowing in Closures Following Last Assignments TypeScript 5.5 Beta Type Imports in JSDoc Regular Expression Syntax Checking Inferred Type Predicates Isolated Declarations 特に NoInfer は型推論を制御する上で強力な機能だと感じました。 型の絞り込み (Narrowing) の改善や JSDoc での型インポートのサポートなど、日々の開発で嬉しい機能が多数含まれていました。 Regular Expression の構文チェックは地味ながら実用的な機能追加だと思います。 TypeScript は着実に進化を続けており、次のバージョンが今から楽しみです。 TypeScript の抽象構文木を用いた、数百を超える API の大規模リファクタリング戦略 発表者: やなえもん さん( https://twitter.com/yanaemon169 ) レポート: 庄司 speakerdeck.com こちらのセッションでは、数百のAPIを抱える Express コードを、AST(抽象構文木, Abstract Syntax Tree) を利用して Nest.js コードに大規模移行するという取り組みが紹介されました。 コードのリプレイスと言えば、正規表現を利用したスクリプトによる変換やIDEによる一括置換などが一般的ですが、こちらは AST を利用しているという点が新鮮でした。 AST を利用することで、微妙な表記揺れなどを気にせず、コードの構造に則したリプレイスが可能になるという話には説得力がありました。 TypeScriptのコンパイラでもASTが利用されているように、ASTとTypeScriptの相性の良さを感じました。また、近年の生成AI技術の発展によって、ASTの取り扱いもより容易になるのではないかと期待が持てます。 一方で、レビュアーの負担が大きいという課題にも触れられていましたが、全体としてとてもチャレンジングで興味深い取り組みだと感じました。 AST については、HireRoo さん( https://twitter.com/hirerooinc ) が発表された TypeScript ASTを利用したコードジェネレーターの実装入門 でも詳しく解説されていましたので、興味のある方はそちらもチェックしてみてください。 TypeScriptから始めるVR生活 発表者: TamaG さん( https://twitter.com/TAMAGOKAKE_G_ ) レポート:羽馬 speakerdeck.com Resonite 上でビジュアルプログラミング言語「ProtoFlux」を使った開発の様子が紹介されました。 ProtoFluxは「ノード」と「ノード」をつなぐことでプログラミングができる言語ですが、バージョン管理や関数化ができないという問題点がありました。 これらの問題を解決すべく生まれたのが「MirageX」です。MirageXは、TypeScriptとReactを使ってResoniteの開発ができるフレームワークです。 コードベースの開発になるため、バージョン管理やAIの力を借りることができるようになりました。また、ライブラリを使うこともできるため、本格的なシューティングゲームなども作成可能です。 VRプラットフォームでのTypeScriptを使った開発事例が紹介され、その可能性と課題について理解を深めることができました。VRという新しい領域でのTypeScriptの活用法について学べる貴重な機会でした。 興味を持った方は、ぜひ MirageXのGitHubリポジトリ をチェックしてみてください! サービス開発におけるVue3とTypeScriptの親和性について 発表者: からころ / karacoro さん( https://twitter.com/karan_corons ) レポート:羽馬 speakerdeck.com Vue3ではComposition APIの登場により、コンポーネントのロジックを外部ファイルに切り出しやすくなり、型付けも改善されました。 また、コンポーネントランタイムの型付け強化により、Props、Emit、Provide/Injectなどでも型の恩恵を受けられるようになっています。 さらに、Volar.jsとvuejs/language-toolsの貢献により、テンプレートへの型の反映などエディタ連携の問題も解決されました。 講演の丁寧な解説と豊富なコード例は、Vue3とTypeScriptを活用したサービス開発のベストプラクティスを学ぶ上で非常に参考になる内容だと感じました。 まとめ TSKaigi 2024は、TypeScriptを中心にしたカンファレンスとして、多くのエンジニアにとって有益な情報が得られるイベントでした。 TypeScriptの最新情報や活用事例を学ぶことができ、新しい技術やアイデアに触れることができました。 今後も、TypeScriptコミュニティの発展と、エンジニアのスキルアップに貢献するイベントとして、TSKaigiが続けられていくことを期待しています。 また、今回の参加レポートが、TypeScriptを学びたい方や、TypeScriptを活用したい方の参考になれば幸いです。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
はじめに DelishKitchen や ヘルシカ でインフラをやったりバックエンドをやったりしているyoshikenです。 今回は、Treasure Dataにログを送信しようとfluentdとfluent-bitを使っていたときにハマった話を書きます。 fluentdからfluent-bitへ もともと弊社では歴史的背景でfluentdを使っていました。が、 大々的なlogの加工が必要なものはTreasure Dataなど別サービスで行う リソースの消費がやや気になる FireLensはじめ、Fargateでfluentbitのほうが相性が良い などの理由より、新規サービスはすべてfluent-bitを使用することになりました。 移行から数ヶ月~数年経っていますが、flunetdに比べ軽量であるため期待した通りのパフォーマンスを発揮してくれています。 fluent-bitでTreasure Dataにログを送信できない現象 ヘルシカではアクセスログをTreasure Dataに送信する要件がありましたので、fluent-bitでTreasure Dataにログを送信する設定を行いました。 confを 公式ドキュメント通り に記述しましたが、ログが送信されず、logを漁ってみると以下のようなエラーが出ていました。 [202x/xx/xx xx:xx:xx] [ warn] [output:td:td.0] HTTP status 404 {"status_code":404,"message":"Resource not found","severity":"error","error":"Resource not found","text":"Resource not found"} 同じような設定でflunetdを動かしてみると問題なく送信/挿入できたので、fluentbit固有の問題と考えdebugしていきます。 fluent-bitとfluentdではTreasure Dataプラグインの挙動が違う件 結論からいうと、fluent-bitのTreasure Dataプラグインはfluentdの同名のプラグインと挙動が微妙に異なります。 fluentdではテーブルが存在しない場合、正確に記すと「upload時に 404 not found httpステータスコードが帰ってきた場合」はテーブルを作成する処理を行います https://github.com/treasure-data/fluent-plugin-td/blob/master/lib/fluent/plugin/out_tdlog.rb#L209-L224 begin begin @client.import(database, table, UPLOAD_EXT, io, size, unique_str) rescue TreasureData::NotFoundError unless @auto_create_table raise end ensure_database_and_table(database, table) io.pos = 0 retry end 対してfluent-bitでは、テーブルが存在しない場合でも特に追加処理などせずにそのままエラーを返却する形になっています https://github.com/fluent/fluent-bit/blob/master/plugins/out_td/td.c#L188-L207 /* Validate HTTP status */ if (ret == 0) { /* We expect a HTTP 200 OK */ if (c->resp.status != 200) { if (c->resp.payload_size > 0) { flb_plg_warn(ctx->ins, "HTTP status %i\n%s", c->resp.status, c->resp.payload); } else { flb_plg_warn(ctx->ins, "HTTP status %i", c->resp.status); } goto retry; } else { flb_plg_info(ctx->ins, "HTTP status 200 OK"); } } else { flb_plg_error(ctx->ins, "http_do=%i", ret); goto retry; } 理由ついてはissueなどを漁ってみましたが、特に言及はなかったです。 一応歴史的にはfluentdも昔はflunet-bit同様にエラーをそのままエラーで返していたましたが、途中でリトライ処理が追加された形になります。 Prevent retrying unretriable errors by cyberdelia · Pull Request #35 · treasure-data/fluent-plugin-td まとめ まとめると以下の表になります。 fluentd fluent-bit テーブルが存在する 送信可能 送信可能 テーブルが存在しない 自動生成 404 not found 弊チームではデータチームと話し合い、"エラーが出続けるのは健全ではない"・"fluentdと同じ仕様と勘違いし、Treasure Data側のテーブル作成を忘れてしまう"などの懸念が生じ、即座のリアルタイム性が必要なログではないため、"一度S3にoutput。その後、Treasure Dataのbatch importで挿入する。"という形で対応することとなりました。 同じようなプラグインでも挙動が異なるというレアケースを引いてしまったため、後世に同じような人がハマらないように記事に残しておきます。
アバター
はじめに Dev Enableチームの羽馬( @NaokiHaba )です。 この度、エブリーは2024年6月8日(土)に開催される『Go Conference 2024』に、プラチナGoルドスポンサーとして協賛することになりました! gocon.jp エブリーでは、Go言語を積極的に採用し、様々なプロジェクトでその力を発揮しています。今回の協賛を通して、さらなるGo言語コミュニティの発展に貢献できればと考えております。 今年のGo Conference 2024のテーマは「一期一会」です。Go言語に関する情報交換や交流を通じて、新たな出会いや気づきを得ることができるでしょう。 ぜひ、タイムテーブルをご覧いただき、気になるセッションに参加してみてください。 https://sessionize.com/api/v2/7zlcfd7c/view/GridSmart 弊社も、17時50分からのスポンサーセッションでGo言語を活用したプロダクトやサービスの開発事例をご紹介いたします。ぜひご期待ください! また、私たちのブースでは、Go言語の最新技術情報や活用事例をご紹介する予定です。エブリーのエンジニアが直接皆様からのご質問にお答えしますので、ぜひお立ち寄りください。 エブリーにおけるGo言語の活用 ここでは、これまでのエブリーのテックブログで公開してきたGo言語関連の記事をいくつかご紹介します。 Go testにおける可読性を保つ方法を考える tech.every.tv テストコードの複雑化や保守性の低下といった問題に直面した際に、テストコードの可読性を維持するための方法について紹介しています。 ネットスーパーアプリ GraphQL から REST へ移行始めました tech.every.tv ネットスーパーアプリでGraphQLからRESTへ移行した経緯と、その過程で得られた知見について紹介しています。 PythonからGoへのリプレイスを行っている事例として参考になる内容となっています。 sqlboilerとoapi-codegenの活用事例 tech.every.tv APIサーバー開発にsqlboilerとoapi-codegenを導入した事例を紹介しています。 これらのツールを活用することで開発の生産性を向上させた経験について説明しています。 WebSocket APIを用いたリアルタイム通知の実装 tech.every.tv Next.jsとGoを組み合わせ、AWS API GatewayのWebSocket APIを用いてAPIサーバーからフロントエンドにリアルタイム通知を送る方法について解説しています。 WebSocketを用いたリアルタイム性の高いアプリケーション開発の参考になる内容です。 その他にも、Go言語を活用したプロダクトやサービスの開発に関する情報を今後も随時公開していきますので、ぜひご確認ください。 tech.every.tv 皆様とお会いできることを楽しみにしています! Go Conference 2024では、当社がどのようにGo言語を活用しているのか、具体的な事例を交えながらご紹介できることを楽しみにしています。また、皆様と直接お話しできる機会を大切にしたいと思っておりますので、ご質問やご意見があればお気軽にお寄せください。 みなさまとお会いできることを心より楽しみにしております。6月8日、Go Conference 2024でお会いしましょう! 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv
アバター
はじめに こんにちは。DELISH KITCHEN 開発部 SERS グループ兼、CTO 室 DevEnable グループ所属の池です。 SERS グループでは主に小売向けプロダクトの開発を行なっており、DevEnable グループでは社内開発組織活性化に向けた活動を行なっています。 DevEnable グループについては以下の記事で紹介しているので、よければご参照ください。 tech.every.tv 本記事では今年度から初開催となるエンジニア新卒研修の取り組みについてご紹介します! エンジニア新卒研修を開催するに至った背景 エブリーでは、新卒社員全員を対象に内定者研修からはじまり、定期的な研修を行い、事業理解や業務におけるスキル獲得など早期の成長をサポートしています。 昨年までの研修ではエンジニアリングに特化した研修は行っておらず、スキル指導は配属後の OJT に依る部分が多いようなオンボーディング体制となっていました。これにより、配属後の実務において以下のような課題が生じていました。 マインドセット エブリーのエンジニアとして働く上で期待されるマインド・スタンスがわからない 領域外の自分ごと化 全体像を把握できずに専門領域外のことを自分ごと化できない 実務における前提知識の学習 インフラを体系的に学ぶ機会がない テストやアーキテクチャに関して馴染みがない そこで、今年度からエンジニア領域におけるオンボーディング体制を強化し、エンジニアとしての早期成長をサポートすべく、エンジニアを対象とした新卒研修を開催することにしました。 エンジニア新卒研修の目的と方針 目的 エンジニア新卒研修の主な目的は次の通りです。 『オーナシップを持ってプロダクト課題の解決に動けるエンジニアへの土台を作る』 上述した課題感を解消するとともに、この目的を達成するための施策を検討しました。 方針 目的に基づき、研修中と研修後に分けて次のような方針を決めました。 研修中 エブリーのエンジニアとして求められるマインドを理解する 専門領域を超えてエブリー全体で使われている技術スタックを理解する 研修後 内部のオリジナルコンテンツによる研修での支援が難しい領域については、配属後のスキル支援環境を提供する エンジニア新卒研修のカリキュラム DevEnable グループと開発部の役員・部長・マネージャーを中心にカリキュラムを策定しました。 今年からの取り組みであるため、コンテンツも役員・部長・マネージャーを中心にゼロから全て作成したものとなっています。 全体で実施期間 5 日です。 マインド研修 CTO からのメッセージ(1 時間) インシデントへの向き合い方(30 分) 技術スタックの把握(講義 + ハンズオン) バックエンド/インフラ(1.5 日) モバイル(1 日) Web(1 日) データ(1 日) ランチ会 (研修後) AWS JumpStart 2024 for NewGrads ここからは各講義について概要を説明します。 CTO からのメッセージ この講義では、CTO が今までのキャリアを通じて大切にしているマインドセットを中心講義しました。 CTO の成功談や失敗談、どういう行動が評価されてきたかなど、エブリーの CTO ならではの経験談がふんだんに盛り込まれた内容となっており、配属に向けて大きな刺激となる講義でした。 CTO講義の様子 インシデントへの向き合い方 以前から、インシデント対応は新卒社員にとって精神的なハードルが高く、入っても何をやっていいかわからず、主体的に取り組みにくいという声が多く上がっていました。 また、新卒社員に限らず会社全体としても同様の課題感を持っていたということもあり、インシデント対応におけるマインド理解を研修の題材として選定しました。 この講義は、そのような課題を解消すべく、エブリー開発部におけるインシデントに対する向き合い方・マインドを教える講義です。 内容は次の通りで、インシデントにおける行動指針を学べる内容となっています。 インシデントが起きたらまずどうすればよいか 関係ありそうな人間を巻き込む インシデントが起こってそう 野次馬でも参加しましょう 別の部署でインシデントが発生している 関係なくても参加しましょう 小さくてもインシデントはインシデント インシデントかどうかは上長が判定するのでとりあえず報告しましょう(割れ窓理論 インシデントは終わってからも大事 ポストモーテム バックエンド/インフラ バックエンド/インフラ講義では、次のような目標を設定しました。 エブリーで共通的に用いられる技術や知識について、一通り触れて理解する 自分たちが開発するシステムが具体的にどのような環境で動いているかを理解する なぜ今の構成になっているかを理解する パフォーマンス観点で取り組み方について理解する ハンズオンを通して上記目標の内容を理解できる形式となっています。 ハンズオンはいくつかのパートに分かれています。 Go を利用した簡易的な API サーバをもとにシステム開発を体験 API を操作・改修 テストコード実装 パフォーマンス改善 デプロイ 手動でプログラムを AWS 上のサーバに配置し、インターネット上に公開された状態を構築する手動デプロイ ECS を用いた半手動デプロイ terraform や CI/CD を用いた自動デプロイ モバイル モバイル講義は座学とハンズオンを通して学べる形式となっており、次のような内容を行いました。 環境構築およびパッケージ構成、マルチターゲットの説明 画面遷移 UIKit, SwiftUI ViewController, ViewModel, View を作成し、画面遷移できるようにする API 接続 Network と Model、非同期処理 API に接続して情報を取得し、Model 変換を行う View 作成 Figma を参考に View を作成。ViewModel と接続し、Model を View に反映 分析 アプリログ収集、データフロー、ログ設計、Crashlytics モバイル講義の様子 Web Web 講義では、次のような目標を設定しました。 『フレームワークによらない web の基礎知識を理解し、今後の web 開発のベースにする』 (座学)web 開発でベースとなる知識を身につける (座学)web 開発で意識するポイントを理解する (ハンズオン)実際の web 開発のイメージをつける 以下のような講義内容となっています。 エブリーでの web 開発 エブリーの web 開発で利用される技術スタック Web 開発の歴史 Web 開発で知っておきたい基礎知識 Web 開発で意識されるポイント ハンズオン:仮想の簡易的な DELISH KITCHEN アプリを用いて、デザインをもとに画面を作成 Web講義の様子 データ データ講義では、データ領域の各分野毎に講義を行うようなカリキュラムとなっています。 データエンジニア データエンジニアとは エブリーで扱うデータ 一般のデータベースとの違い データサイエンティスト [業務理解]エブリーのデータサイエンティストが何をやっているか知る [協業の視点]エンジニアリングとデータサイエンスの違いを知る データストラテジスト データストラテジストとは データストラテジストの具体的な業務内容 ハンズオン Databricks SQL ランチ会 研修期間中は、以下の目的のもと、毎日各領域ごとのエンジニア社員とのランチ会を行いました。 人的ネットワークの構築 実務イメージを深める 各領域の社員とのランチ会を行うことで、全ての領域において気軽に話せるようなネットワークを作ることができました。 受講者の声 今後のエンジニア新卒研修の改善に向けて、受講後のアンケートを通じて、受講者からのフィードバックを収集しました。 全体の満足度に関する質問項目では、受講者全員から最も高い評価を得ることができました。 続いてポジティブな声の一例を紹介します。 マインドセット 新卒でもインシデントを発見した場合は報告する 自分が苦手だと思う部分があったので、配属してから実際その技術を使用するまでに、しっかりと自習しておく 領域外の自分ごと化 クライアントチームやデータチームが何をしているか理解できたことはかなり良かった。今後一緒に仕事するときに相手のことを考えながら業務に取り組めるので、よりスムーズに業務が進められると思う。 実務に必要な前提知識の学習 tfstate の知識などが早速タスクで役に立った 利用している技術・ツールについての全体像が掴めた 研修の目的としていた課題感の解消に関連するようなコメントがあり、大枠の目的は達成できたと思っています。 しかし、一方で次のようなネガティブな声もあり、改善点も見つかりました。 最低限必要な知識などを事前に共有することで、もう一段階踏み込んだ講義内容になると感じた。 研修で使用する各種ツールの使い方についての研修か資料があると取り組みやすいと思いました。 今回の研修は、受講者の専門領域外を含めて全ての領域を学ぶような研修だったこともあり、初めて扱う技術やツールが多く出てきます。 また、運営側で受講者の前提知識の基準を高く設定していた部分もあったため、講義に必要な前提知識を持っていないと理解が困難な内容が一部ありました。 次回開催時にはアンケート結果を踏まえてより新卒社員にとって学びの多い研修になるように改善していく予定です。 おわりに 本記事では初開催となったエンジニア新卒研修について紹介しました。 初めての取り組みで改善すべき点も多くありましたが、全体的には開催して意義のある取り組みだったと思います。 プロダクト開発全体に関する解像度を高める機会になったことや、新卒自身が技術スキルにおける課題を把握できたこと、ランチ会で各領域のエンジニアとの接点を構築できたことなど、配属に向けたエンジニアとしての早期成長の土台作りに繋がったと感じています。 エンジニア新卒研修を含め、今後もスキルアップのためのり組みや体制を整えていく予定です。 他の取り組みを開催した際には同様にレポートをお届けできればと思うので、ご期待ください。
アバター
はじめに ML Kit とは ML Kit Document Scanner API とは 主なクラスについて GmsDocumentScannerOptions (オプション) スキャナーのモード 最大ページ数 フォトギャラリーからのインポートの可否 結果のフォーマット GmsDocumentScanning (スキャナーの開始) GmsDocumentScanningResult (結果処理) まとめ 余談 未実装のキャプチャーモード はじめに トモニテでAndroid開発を担当している岡田です。 先日、ML Kit Document Scanner API のベータ版がリリースされました。 公式ドキュメントやサンプルアプリを参考に、今回はAndroidでの実装方法・内容をご紹介したいと思います。 以下に参考にしたサイトのリンクを示します。是非覗いてみてください。 android-developers.googleblog.com developers.google.com github.com ML Kit とは Googleの機械学習の機能を、Android/iOSアプリとして提供するモバイルSDKです。 例えば顔検出やバーコードスキャンなどの機能を簡単に実装することができます。 ML Kit Document Scanner API とは 紙の資料をカメラでスキャンして、デジタル資料として読み込むことができる、ドキュメントスキャナーSDKのAPIです。 用意されたスキャナーは自動キャプチャ・切り抜き・自動回転検出機能だけでなく、フィルター機能など編集もできます。 つまり、すごいリッチなスキャナーを簡単に実装できるということです。 スキャナーの使用感については 公式のサンプルアプリ でご確認ください。 実装の簡単な流れは以下の通りです。 スキャナーのオプションを決める スキャナーを呼び出す スキャナーから結果を取得する 主なクラスについて 主に以下の3つのクラスで構成されています。 GmsDocumentScannerOptions GmsDocumentScanning GmsDocumentScanningResult それぞれ名前の通り、スキャナーのオプション、スキャン開始、スキャン結果に関するクラスになっています。 これらについて、実際に確認してみたいと思います。 GmsDocumentScannerOptions (オプション) GmsDocumentScannerOptions でスキャナーに関してのオプションを設定できます。 公式ドキュメントのコード では、以下のように紹介されています。 val options = GmsDocumentScannerOptions.Builder() .setGalleryImportAllowed(false) // フォトギャラリーからのインポートの可否 .setPageLimit(2) // 最大ページ枚 .setResultFormats(RESULT_FORMAT_JPEG, RESULT_FORMAT_PDF) // 結果のフォーマット .setScannerMode(SCANNER_MODE_FULL) // スキャナのモード .build() 設定できるオプションは以下の通りです。 スキャナーのモード 最大ページ枚 結果のフォーマット フォトギャラリーからのインポートの可否 それぞれ見ていきます。 スキャナーのモード setScannerMode() を用いて、スキャナーの設定ができます。 現段階では3種類のモードが要されています。 public static final int SCANNER_MODE_BASE = 3; public static final int SCANNER_MODE_BASE_WITH_FILTER = 2; public static final int SCANNER_MODE_FULL = 1; SCANNER_MODE_BASE 基本的な編集機能(ページの切り抜き、回転、並べ替えなど)が使用できます。 SCANNER_MODE_BASEのプレビュー画像 SCANNER_MODE_BASE_WITH_FILTER SCANNER_MODE_BASEモードに画像フィルタ(グレースケール、自動画像補正など)が追加されます。 SCANNER_MODE_BASE_WITH_FILTERのプレビュー画像 SCANNER_MODE_FULL(デフォルト) SCANNER_MODE_BASE_WITH_FILTERモードの機能に加えて画像クリーニング機能(汚れや指の消去など)が追加されます。 SCANNER_MODE_FULLのプレビュー画像 最大ページ数 setPageLimit() を用いて、最大のページ数を指定できます。 int型 で指定します。 以下は最大ページ数を2とした場合のスクリーンショットです。最大数に達すると、ページを追加する "+" アイコンが出ないことがわかると思います。 最大ページ数のプレビュー画像 フォトギャラリーからのインポートの可否 フォトギャラリーからのインポートの可否を設定できます。 setGalleryImportAllowed() を用い、Booleanで指定します。 結果のフォーマット setResultFormats() を用いて、出力結果のフォーマットを指定できます。 現段階では、JPEGかPDF、またはその両方を選択できるようです。 public static final int RESULT_FORMAT_JPEG = 101; public static final int RESULT_FORMAT_PDF = 102; GmsDocumentScanning (スキャナーの開始) スキャナーは GmsDocumentScanning を用いて、以下のように記述できます。 コードは公式のドキュメントとサンプルを参考にしました。 GmsDocumentScanning.getClient(options) // optionsは先ほど紹介したGmsDocumentScannerOptions .getStartScanIntent(activity) .addOnSuccessListener { intentSender -> scannerLauncher.launch(IntentSenderRequest.Builder(intentSender).build()) } .addOnFailureListener { // 失敗した際の処理 } メソッドチェーンでわかりにくいので、順を追って説明します。 はじめに、 GmsDocumentScanning の getClient() を呼び出します。 getClient() は GmsDocumentScannerOptions を引数にとり、 GmsDocumentScanner を返します。 public final class GmsDocumentScanning { @androidx.annotation.NonNull public static com.google.mlkit.vision.documentscanner.GmsDocumentScanner getClient(@androidx.annotation.NonNull com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions options) { /* compiled code */ } private GmsDocumentScanning() { /* compiled code */ } } 返ってくる GmsDocumentScanner はInterfaceです。 getStartScanIntent() というメソッドが用意されています。 こちらは Activity を引数にとり、 Task<IntentSender> を返します。 Task が返されるので、 addOnSuccessListener と addOnFailureListener が使えます。 成功時に IntentSender が返ってきます。 IntentSender はスキャナーを起動するために使用します。 public interface GmsDocumentScanner extends com.google.android.gms.common.api.OptionalModuleApi { @androidx.annotation.NonNull com.google.android.gms.tasks.Task<android.content.IntentSender> getStartScanIntent(@androidx.annotation.NonNull android.app.Activity activity); } scannerLauncher は後述する、 ActivityResultLauncher<IntentSenderRequest> 型の変数です。 こちらは終了したActivityの結果を受け取り、処理します。 GmsDocumentScanningResult (結果処理) GmsDocumentScanningResult を用いて、結果を処理できます。 公式ドキュメント にて、以下のように記述されています。 val scannerLauncher = registerForActivityResult(StartIntentSenderForResult()) { result -> { if (result.resultCode == RESULT_OK) { val result = GmsDocumentScanningResult.fromActivityResultIntent(result.data) // ここで結果を受け取る result.getPages()?.let { pages -> for (page in pages) { val imageUri = pages.get(0).getImageUri() // imageUriを用いた処理 } } result.getPdf()?.let { pdf -> val pdfUri = pdf.getUri() val pageCount = pdf.getPageCount() // pdfUriやpageCountを用いた処理 } } } } GmsDocumentScanningResult の fromActivityResultIntent() を用いて、結果を受け取ります。 Intent を引数に取り、 GmsDocumentScanningResult として返してくれます。 @androidx.annotation.Nullable public static com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult fromActivityResultIntent(@androidx.annotation.Nullable android.content.Intent data) { /* compiled code */ } 返される GmsDocumentScanningResult ですが、 Page と Pdf を持っています。 public abstract class GmsDocumentScanningResult implements android.os.Parcelable { ... public static abstract class Page implements android.os.Parcelable { @androidx.annotation.NonNull public abstract android.net.Uri getImageUri(); public Page() { /* compiled code */ } } public static abstract class Pdf implements android.os.Parcelable { public abstract int getPageCount(); @androidx.annotation.NonNull public abstract android.net.Uri getUri(); public Pdf() { /* compiled code */ } } } 現時点で page は画像の Uri 、 Pdf はページ数と Uri を取得できるようです。 それぞれ結果に合わせて、処理を記述できます。 説明については、以上になります。 まとめ ML Kit Document Scanner API を用いると、簡単に高品質なドキュメントスキャナーが実装できました。 実装の簡単な流れは以下の通りです。 スキャナーのオプションを決める スキャナーを呼び出す スキャナーから結果を取得する 余談 未実装のキャプチャーモード com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions のコードには CaptureMode なるものが存在しました。 public static final int CAPTURE_MODE_AUTO = 1; public static final int CAPTURE_MODE_MANUAL = 2; 現在はオートのみだが、今後はマニュアルで選択できるような機能が追加されるかもしれない……? アップデートが楽しみです。今後も追っていきたいと思います!
アバター
こんにちは 開発本部データ&AIチームでデータエンジニアを担当している塚田です。 今回は、 AWS Certified Data Engineer - Associate を受験しましたので、準備と感想とエブリー(業務)でどのように活かせそうかをまとめたいと思います。 なお、 AWS 認定プログラムアグリーメント に則り試験内容については触れませんのでご承知おきください。 本試験について AWS Certified Data Engineer - Associate は AWS(トレーニングと認定) で以下のように説明されています AWS Certified Data Engineer - Associate は、コアデータ関連の AWS サービスに関するスキルと知識や、データの取り込みと変換、プログラミングの概念を適用しながらのデータパイプラインのオーケストレート、データモデルの設計、データライフサイクルの管理、データ品質の確保といった能力を検証します。 データ分析というよりかはデータパイプライン全般に対してのスキルを評価するものだと感じました。 AWSで適切なサービスを利用してスケールのしやすさや保守運用・セキュリティなどの能力を評価すると 試験ガイド にも記載されているので、体験したものと差異はないように感じています。 なぜ、受験しようと思ったか 現在の業務範囲に通じるものがあること が一番大きな理由ですが、他に理由があるとすれば以下が上げられます。 AWS Certified Data Engineer - Associate は2024年4月現在、比較的新しい試験であること 今まで取得したすべてのAWS Certifiedの有効期限が切れてしまったこと 試験準備 公式問題集 を解く ある程度AWSサービスを利用したことがあったため事前準備なしで回答しました(何度でも受験可能です) 悩んだ問題や間違えたものは解説を読んで理解できたかを再度解くことで確認しました どのような粒度の問題が出るかなどイメージすることができとても有用でした 上記以外にも各資格試験で公式問題集があるので合わせて解いて周辺知識の補完に利用しました 試験ガイド を読む 聞いたことがない・実際利用するとしたときに手が動かせないと思ったサービスは サービス別資料 から該当のサービス資料を確認し、実際にAWSコンソール上で操作してみました あまり特筆する部分はありませんが、今までの経験と未経験の部分を勉強によって補足するイメージで準備を行いました。 受験・受験後 余裕を持って予約した時間・会場で受験しました 80分程度で問題を一周し再確認などして20分残して退出しました その場では結果は受け取れず、後日連絡が来る形でした 結果としては合格はしたものの、まだまだ経験が必要だなと感じるものでした 業務で活かせるか 何かインプットしたらアウトプットしていきたいと思うところですが、 今回は本試験を通じて得たものについてどのように活かせるかを考えたいと思います。 現在、エブリーのデータ&AIチームではDatabricksを活用したデータフローの作成やML・ABテストの運用を行なっています。 tech.every.tv AWSをフルに活用している構成ではありませんが、 セキュリティやコストについてはどのプラットフォームを利用したとしても意識するべきことだと感じていて 知識としてアップデートでき、そういった重要性について再認識することができました。 まとめ データエンジニアとしてはデータを安全に使いやすく必要なセキュリティを担保していくことはもちろんですが、 データ&AIチームではAI・MLをプロダクト導入していくことも推進しています。 tech.every.tv tech.every.tv 今回はAWSの資格試験を切り口に記事を書かせていただきましたが、 ここで得た知識以外にも新しい技術、考え方を柔軟に取り入れていきデータをより利活用できる環境を作っていきたいと考えています。
アバター
DELISH KITCHEN 開発部で小売向き合いでFlutterのアプリ開発をしている野口です。 本記事では、弊社で開発しているFlutterのアプリをFlutter Webでリリースできるかどうかの調査を行った時の知見についてお話しします。 FlutterアプリをWebで動かすとは Flutterはマルチプラットフォーム開発できるので、Android / iOS / Web / Windows / macOS / Linuxで同じソースコードで開発できます。なので、iOS、Android用に作成したアプリでもリリースできます。 一般的なWebサイトを作るときは、HTMLやCSS、JavaScriptを使用しますが、FlutterはiOS、Androidと同じ見た目になるように、HTML、CSS、Canvasなどを使用して描画してくれます。また、FlutterはDartという言語で書かれていますが、それをJavaScriptに変換してくれています。 ただ、パッケージを使用した場合、モバイル特有の機能(ネイティブコードでないと実現できないもの)などDartで書かれてない可能性があるため、パッケージの公式ドキュメントを見てWebに対応しているか確認する必要があります。ここにWebの記載があればWeb対応しているパッケージだと判断できます。 https://pub.dev/packages/flutter_riverpod riverpod FlutterアプリをWebで動かすにあたっての課題 まとめると以下のような課題がありました。 パッケージがWebに対応しているか Platform.isAndroid Platform.isIOSの分岐エラー 具体的な対応 パッケージがWebに対応しているか 対応していないもの そもそもWebに対応してないパッケージがあるので、その場合は代替を探すか、Javascriptで書くか、Webではその機能を諦めるかをしないといけません。 今回は以下のパッケージが使用できませんでした。 - firebase_crashlytics(そもそもWebはクラッシュしないのでいらない) - path_provider - flutter_html - adjust_sdk - flutter_appauth - dart_jsonwebtoken 対応していたが、途中で動かなくなったもの isar isarはv3ではエラーが出て動かなくなっていました。 エラー内容 Error: The integer literal 288085404374050446 can't be represented exactly in JavaScript. Try changing the literal to something that can be represented in JavaScript. In JavaScript 288085404374050432 is the nearest value that can be represented exactly. id: 288085404374050446, https://pub.dev/packages/isar issue も出ており、v4では動くようになっているようですが、公式ドキュメントに ISAR V4 IS NOT READY FOR PRODUCTION USE とあるので本番環境で使用するのは避けたほうが良さそうです。 https://pub.dev/documentation/isar/4.0.0-dev.14/ 対応としては、 isarはローカルデータベースを扱うためのパッケージなので代替になるパッケージに書き換えるが良いかと思います。 v4がstableになるのが待てるのであれば待った方がいいですが、、、v4のPrereleaseが出てから時間が経っており、いつstableになるかわからない状態なので、一旦考えない方針にしています。 バージョンを上げれば対応されるもの flutter_secure_storage エラー内容 Unsupported operation: Platform._operatingSystem 使用しているバージョンではWebが対応していないため、5.0.0に上げれば解決します。 Platform.isAndroid Platform.isIOSの分岐エラー エラー内容 Unsupported operation: Platform._operatingSystem Webで実行時にPlatform.isAndroid Platform.isIOSがあると起こるようです。 この記事のように、Webの分岐を入れるか、universal_platform( https://pub.dev/packages/universal_platform )を使用することで対応できるかと思います。 https://zenn.dev/ryo_ryukalice/articles/140a64f894afad Flutte Webを採用して開発運用を行う上でのビジネス上のリスク(考慮事項) ビジネス上のリスクは以下が挙げられるかなと思います。アプリの複雑度によってリスクの重みは変わるかもしれないですが、これらが許容できればいい選択肢かなと思います。 やりたいことを実現するためのWebに対応しているパッケージがない isarのようにWebに対応していたパッケージが、更新されなくなりWebが動かなくなる 2が原因でflutterのバージョンを上げづらくなる iOS、AndroidアプリをFlutter Webで動かす iOS、Androidアプリ Flutter Webアプリ Flutter Webを動かした結果、画像のようになりました。 見た目としてはiOS、Androidアプリがブラウザのサイズに合わせてそのまま大きくなっています。 このままでも見た目はそんなに悪くないかなと思いますが、商品情報が大きすぎるなどの場合はレスポンシブ対応か、モバイルのサイズに統一するなどすると良くなると思います。 動作が重くなる様子はなかったのでリリースはできるかなと思いました。 まとめ Flutter Webで開発をする際の主な考慮点は使用するパッケージがWebに対応しているかどうかということがわかりました。 ただ、Webに対応していても動かなくリスクがあるので、Webだけは使えない機能が出る可能性もありそうですね。 個人的にはシンプルなアプリであれば基本的には動きそうなので用途によっては良い選択なのではと思いました。
アバター
はじめに エブリーでソフトウェアエンジニアをしている本丸です。 先日、弊社からヘルスケアアプリ「 ヘルシカ 」がリリースされたのはご存知でしょうか?ヘルシカは弊社のサービスであるDELISH KITCHENのヘルスケア機能を切り出したサービスなのですが、ヘルシカの裏側で認証・課金の共通基盤が動いています。 今回はこの認証・課金の共通基盤(社内でDAPと呼んでいるため、以降はDAPと表記します)についてお話しできればと思います。なお、実装の詳細には触れず概要の説明に留める予定です。 システムの概要 DAPとは DAPとは、認証・課金の共通基盤で、IdP(IDプロバイダー: ユーザーIDを保存および検証するサービス)としての役割と、課金を管理する役割を持っています。 DAPという名称は、一般的に使われるものではなくいわゆる造語なのですが、社内やチーム内で認識を合わせるために命名されたという経緯があります。 DAPの目的は、複数サービスでのユーザーの管理を一元化することです。 下図はDAPとそれに関わるものを表した概要図です。 DAPではSNSを用いた認証をサポートしているので、LINEやAppleといった外部のプラットフォームを利用します。以降は、DAP内の認証サーバーのことをInternal IdP、認証に利用する外部のプラットフォームのことをExternal IdPとします。 矢印は、依存の方向を示していてPaymentはInternal IdPに依存しているという関係になっています。DAPはExternal IdPや外部の課金プラットフォームに依存しており、弊社のサービスがDAPを利用するという形になっています。 認証サーバとしての役割 認証サーバーとしての役割は、ユーザーがどのアカウントと紐づくのかの認証を行うというのが主な役割になります。サインアップの時は図のようなフローになるのですが、全て説明すると長くなってしまうので要点だけお話しします。 Internal IdPとExternal IdPの間の認証情報 Internal IdPはExternal IdPに認証を委譲しています。Internal IdPはExternal IdPからIDトークンを受け取るのですが、このIDトークンの中にIDトークンの発行者や一意の識別子などが含まれており、それをもとにどのユーザーなのかの判断を行います。 Internal IdPとApplication ServerとClientの間の認証情報 ClientからApplication ServerのAPIを呼び出す時にはAccess Tokenを認証済みかどうかの判定に利用します。このAccess TokenはInternal IdPで発行しています。Application ServerはClientからAccess Tokenを受け取った時に、Internal IdPを通して認証を行い、認証が成功した場合に後続の処理を行うことになります。 Web View 図の中で、web viewに言及している箇所があるのですが、これはClientがスマホのアプリの時の挙動を示したものです。ClientとExternal IdPの間で直接認証する場合は、External IdPが用意してくれているSDKを利用した方が便利ではあるのですが、Internal IdPを経由させる目的でweb viewから認証を行うようにしています。 課金サーバとしての役割 課金サーバとしての役割は主に2つです。 1つ目はユーザーが商品を購入した際にappleから受け取ったレシートを検証して、商品の有効性を確かめることです。 2つ目はレシートとユーザー状態の管理・更新です。DAPではappleからレシートの情報が更新された時に通知を受け取り、それをトリガーとしてユーザーの状態の更新を行なっています。 レシートとユーザー状態の管理については、弊社ブログの過去記事にもありますのでよければご覧ください。 https://tech.every.tv/entry/2022/04/07/170000 RFCに則った実装 少し、話は逸れるのですがDAPの中のInternal IdPに関してはRFCやOIDCのドキュメントに則った実装が基本方針になっています。 社内用にカスタマイズされたドキュメントではないので、一見とっつきにくくもあるのですが、Internal IdPに関しては下記の理由などでドキュメントに則った方が良いという判断になったようです。 - IdPなので社内特有のロジックが入り込みにくい - 社内ドキュメントよりドキュメントのメンテナンスが維持されやすい - セキュリティ的な要件も満たせる 動作確認の困難さ 要件として、従来のDAPの仕様に則っていないサービスと新規のDAPの仕様に則っているサービスでユーザーのアカウント・課金状態を紐づける必要がありました。 この連携パターンが、従来のサービスでSNS連携されているか、新規サービスでSNS連携されているかなどに加えて課金状態の確認まで必要だったため、かなり複雑に感じました。 動作確認の段階でどのようなパターンがあるか洗い出してテストをしたのですが、動作確認の段階で考慮漏れなどが見つかり修正に追われるといったこともありました。複雑なシステムを作るときは想定されるパターンをあらかじめ洗い出してから開発すべきだったかなというのが反省です。 まとめ 記事にしたこと以外でもリリースまでに色々と大変なこともあったのですが、なんとか致命的なバグはなく動いているようなので一安心といったところです。 複雑なシステムなので概要を話すだけの形にはなってしまいましたが、認証・課金基盤でどのようなことをやっているのかの導入になれば幸いです。 最後に宣伝になりますが、このDAPを裏で利用しているヘルシカというサービスがリリースしたのでよければ使ってみてください。 参考資料 https://datatracker.ietf.org/doc/html/rfc6749 https://datatracker.ietf.org/doc/html/rfc7519 https://datatracker.ietf.org/doc/html/rfc8252 https://openid.net/specs/openid-connect-core-1_0-final.html
アバター
目次 はじめに 1年目を振り返る 入社前について 実際に入社してから 2年目に突入して まとめ はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 4月1日をもって新卒入社してから1年が経ちました。 そこで、今回の記事では、これまでの振り返りと2年目を迎えた今感じていることについて書きたいと思います(文字ばかりですがご容赦ください)。 1年目を振り返る 入社前について 1年目を振り返る前に、入社前の経緯について少しだけ触れておこうと思います。 大学では工学部の電気情報物理工学科に所属し、情報分野だけでなく物理学や電磁気学なども学んでいました。 また、大学院ではマルチエージェントシミュレーション 1 に関する研究を行っていました。 講義や研究を通してAIやコンピュータの基礎を学んでいく中で、情報分野/エンジニアリングに興味を持つようになりました。 しかし、実際にものづくりをした経験は無く、エンジニアリングに関しては専門的な知識がほとんどありませんでした。 そのため、大学院時代にはプログラミングの基礎を学ぶために、Pythonを中心に学習を始めました。 また、 A Tour of Go も少しだけやりました。 そのほかにも、友人と一緒に簡単なwebアプリを作ったり、ハッカソン型のインターンに参加したりして、 少しでも開発経験を多く積めるよう努力をしました。 そして、就職活動をする中で、イベントの中でエブリーと出会い入社まで至りました。 実際に入社してから 入社してからは、1週間の新卒研修を受けた後、実際の業務に携わることになりました。 OJTですぐに実際の業務に携わり、入社直後に事業貢献できるスピード感はベンチャー企業ならではだと思います。 トモニテではバックエンドにはGo、フロントエンドにはJavaScript/TypeScript(フレームワークはReact/Next.js)を使って開発をしています。 初めのうちは、既に実装されているAPIの改修やコンポーネントの表示ロジックの改修など、バックエンド/フロントエンドどちらかのみのタスクを担当していました。 徐々にタスクの幅も広がり、新規APIを作成したり画面を作成したりすることも増えました。 それからは、APIと画面を実装して疎通させたりLPを1から作成したりもしました。 また、サービスのリブランディングという大きなプロジェクトも経験することができました。 リブランディングプロジェクトを通じてサービスの目指す方向性を改めて考えることができ、 サービスに込めた想いをチームの一員として実現していくことに対する責任感も強くなりました。 社内では、事業向き合いの業務がメインでありつつ、積極的な技術的挑戦の機会を提供するための施策も用意されています。 11月に社内で行われた挑戦weekでは、ChatGPTを利用した社内ChatAppのテンプレート機能の実装にも挑戦しました。 実務未経験から約1年を経て、今では、新機能の開発および開発のリードを担当させていただけるようにもなりました。 自分が開発をリードすることになるため、新機能の開発に必要となるAPIやDB設計なども担当し、 モバイルアプリ開発側との連携やプロダクトマネージャーとの仕様・工数の調整なども行っています。 今開発している新機能では複雑なロジック・仕様も含まれていて、実装は簡単ではありませんが、 サービスを大きく成長させる機能にするために、チーム全体で協調しつつ開発に取り組んでいます。 この新機能も近いうちにリリースされる予定ですので、リリースされたら是非使い倒していただけると幸いです。 責任範囲の広いタスクは大変ではありますが、難しいタスクに挑戦できることや施策をリードする側として動くことで プロダクトに対する責任感も高まり、技術的な視点だけでなくマネジメント・ビジネス的な視点を持つことの重要性も感じることができました。 CI/CDやインフラ周りの知識についてもまだまだ浅識なので、隙を見て地道なキャッチアップを続けています。 また、 冒頭 でも書いている通り、最近はDev Enableグループも兼任させていただいています。 Dev Enableグループでは、開発本部を横断し、組織の活性化・成長環境の提供・発信・広報の強化・採用など、さまざまな課題解決の推進を目指します。 詳しくは以下の記事をご覧ください。 1年目は実務経験の無い状態からのスタートでしたが、 自分のスキルアップ・マインドの醸成のために積極的にチャレンジし、新しい技術や知識を吸収することができました。 任せてもらえる領域も広がり、自身の成長を大きく感じられる1年でした。 2年目に突入して 4月1日には新卒社員の入社式も行われました。 後輩を迎え、改めて自分が2年目を迎えたことを実感しました。 4月に入り、新卒社員のオンボーディングプロジェクトにも携わっています。 また、今年度から新卒エンジニア向けの研修プログラムも予定されています。 これは、Dev Enableグループが主導で行っているプロジェクトの一つです。 サービス開発を通じての事業貢献だけではなく、組織の活性化やエンジニアの成長など会社全体への貢献もできるようになり、 エブリーの一員としての責任感もより一層強くなってきました。 今後も、自分のスキルアップはもちろん、トモニテ/エブリーの成長に貢献できるよう積極的にチャレンジしていきたいと思います。 まとめ 今回の記事では、4月を迎えた今だからこそ書ける内容だと思い、新卒1年目を迎えての振り返りと2年目を迎えて感じたことについて書いてみました。 就職活動中、ハッカソン型のインターンシップに参加することはありましたが、実務経験は一切ない状態での入社だったので経験豊富な同期や先輩方に負けないよう必死でした。 この記事を書くことで、自分のこれまでの挑戦を振り返ることやこれからについて改めて考えることができる良い機会になりました。 まだまだ未熟で、今でも日々のキャッチアップや新しい技術の習得に励んでいますが、1年目を振り返ると、非常に大きく成長できた1年だったと感じています。 2年目も、サポートしてくれる周囲の人への感謝を忘れずに、1年目以上に成長できるようにチャレンジしていきたいと思います。 最後まで読んでいただき、ありがとうございました。 マルチエージェント・シミュレーションとは? | MAS COMMUNITY ↩
アバター