Timee Product Team Blog

タイミー開発者ブログ

スクラムで品質を上げ続けるために完成の定義(Definition of Done)を作りました

読んで欲しいと思っている人

  • POやステークホルダーと品質について共通言語や目標が欲しい開発者
  • 開発者と品質について共通言語や目標が欲しいPO
  • スクラムで品質について困っている人

読むとわかること

  • 完成の定義(Definition of Done)とはどんなものか
  • スクラムと非機能的な品質の関係性
  • タイミーのWorkingRelationsSquadでどんな完成の定義を作り、活用していきたいと思っているか
続きを読む

Vertex AI PipelinesとCloud Run jobsを使って機械学習バッチ予測とA/Bテストをシンプルに実現した話

こんにちは、タイミーでデータサイエンティストとして働いている小栗です。

今回は、機械学習バッチ予測およびA/BテストをVertex AI PipelinesとCloud Run jobsを使ってシンプルに実現した話をご紹介します。

経緯

タイミーのサービスのユーザーは2種類に大別されます。お仕事内容を掲載して働く人を募集する「事業者」と、お仕事に申し込んで働く「働き手」です。

今回、事業者を対象に機械学習を用いた予測を行い、予測結果を元にWebアプリケーション上で特定の処理を行う機能を開発することになりました。

要件としては以下を実現する必要がありました。

  1. 定期的なバッチ処理でのMLモデルの学習・予測
  2. MLモデルのA/Bテスト

最終的に、Vertex AI PipelinesとCloud Run jobsを活用したシンプルな構成でバッチ予測とA/Bテストを実現することにしました。

本記事では主に構成とA/B割り当ての仕組みをご紹介します。

構成

まず、全体構成とその構成要素についてご紹介します。

構成図

Webアプリケーション側の構成・実装についてもご紹介したいところですが、今回は機械学習に関係する部分に絞ってお話しします。

前提として、データサイエンス(以下DS)グループはGoogle CloudをベースとしたML基盤を構築しています。MLパイプライン等はCloud Composerに載せて統一的に管理しており、今回も例に漏れずワークフロー管理ツールとして採用しています。

MLパイプラインはVertex AI Pipelinesで実装しています。MLモデルのA/Bテストを実現するため、MLモデルごとにパイプラインを構築し、並行で稼働させています。同時に、それぞれのMLモデルの予測値と付随情報をBigQueryの予測結果テーブルに蓄積する責務もMLパイプラインに持たせています。

もちろんそれだけでは予測結果がテーブルに蓄積されるだけでA/Bテストは実現できないので、各事業者に対する予測結果のA/B割り当てをCloud Run jobsの責務とし、MLパイプライン実行の後段タスクとして実行しています。同時に、割り当て結果をBigQueryテーブルに出力する処理も実施します。

当初はA/B割り当てを含めたすべての責務をVertex AI Pipelinesに集約する案も議論の中で出たのですが、将来的に類似の取り組みにて実装や思想を使い回せそう等の理由から、取り回しのしやすいCloud Run jobsを採用しました。

Cloud Run jobsの利用はDSグループ内でも初めてではありましたが、グループ内のMLOpsエンジニアに相談・依頼してCloud Run jobs用CI/CDの導入などML基盤のアップデートを並行して進めていただくことで、スムーズに開発を進められました。

今回、モジュール間のデータやり取りのIFとしてBigQueryを採用していますが、読み込み・書き込みの操作に関しては、以前ご紹介した社内ライブラリを活用することでサクッと実装できました。

また、各処理には実行日時などの情報が必要なため、Cloud Composerのオペレータからパラメータを渡してキックする形にしています。

例えば、Cloud Run jobsは2023年のアップデートからジョブ構成のオーバーライドが可能になっており、それに併せてCloud Composer側でもCloudRunExecuteJobOperatorを介したオーバーライドが可能になったため、そちらを利用して必要なパラメータを実行時に渡しています。

さて、A/B割り当ての結果が出力されたのち、Webアプリケーション側はデータ連携用テーブルを参照して、事業者に対してバッチ処理を行います。残念ながら機能や施策の具体についてはご紹介できないのですが、機械学習の予測結果を元に事業者ごとに特定の処理を行う仕組みになっています。

A/B割り当ての仕組み

次に、Cloud Run jobsの中身で実施しているA/B割り当てについて、より具体的にご紹介します。

どう設定を管理するか

A/B割り当てに必要なパラメータはyamlファイルで指定する形にしています。例えば、実験期間や各MLモデルへの割り当て割合などです。

- experiment_name: str # 実験名。割り当てに用いるキーも兼ねる e.g. 'experiment_1'
  start_date: str # A/Bテストの開始日 e.g. '2024-08-01'
  end_date: str # A/Bテストの終了日 e.g. '2024-08-31'
  groups:
    - model_name: str # MLモデルの名前 e.g. 'model_1'
      weight: float # このMLモデルに割り当てる割合 e.g. 0.5
    - model_name: ...
- experiment_name: ...
  ...

この方法を用いる問題点として、”PyYAML”というライブラリを使えばyamlを読み込むこと自体は可能なのですが、開発者が想定していない形式でyamlが記述されるとエラーや予期せぬ挙動に繋がります。

当初の開発者以外がyamlファイルを更新することを見越して、ファイルの中身をバリデーションすることが望ましいと考えました。そこで、型・データのバリデーションが可能なライブラリである”Pydantic”を活用することにしました。

上記の形式のyamlファイルを安全にパースするために、以下のようなPydanticモデルクラスを定義しています。

# コードの一部を抜粋・簡略化して記載しています

import datetime

from pydantic import BaseModel, Field, field_validator

class ABTestGroup(BaseModel):
    model_name: str
    weight: float = Field(..., ge=0.0, le=1.0)

class ABTestExperiment(BaseModel):
    experiment_name: str
    start_date: datetime.date
    end_date: datetime.date
    groups: list[ABTestGroup]

    @field_validator('groups')
    @classmethod
    def validate_total_weight(cls, v: list[ABTestGroup]) -> list[ABTestGroup]:
        """
        各groupのweightの合計が1.0であることを確認する。
        """
        total_weight = sum(group.weight for group in v)
        if not math.isclose(total_weight, 1.0, rel_tol=1e-9):
            raise ValueError('Total weight of groups must be 1.0')
        return v

class ABTestConfig(BaseModel):
    experiments: list[ABTestExperiment]

例えば、weight(各グループに割り当てる事業者の割合)に対しては、型アノテーションとFieldを使って型と数値の範囲のバリデーションを実施しています。

加えて、各グループの割合の合計が1.0を超えることを避けるため、フィールドごとのカスタムバリデーションを定義可能なfield_validatorを使用し、独自のロジックでバリデーションを実装しました。

このようなPydanticを使ったバリデーション処理をML基盤のCIを通して呼び出すことにより、不適切なyamlファイルを事前に検知できるようにしています。

どう割り当てるか

A/B割り当てに関しては、事業者を適切にバケット(=グループ)に割り当てるために、事業者のIDとキーを使用しています。

# コードの一部を抜粋・簡略化して記載しています

import hashlib

def _compute_allocation_bucket(company_id: int, key: str, bucket_size: int) -> int:
    """
    company_idとkeyに基づき、company_idに対してバケットを割り当てる。
    keyはexperiment_nameなどを想定。
    """
    hash_key = key + str(company_id)
    hash_str = hashlib.sha256(hash_key.encode("utf-8")).hexdigest()
    return int(hash_str, 16) % bucket_size + 1

まず、実験名をkeyとして事業者のIDと結合し、ハッシュ化の元となる文字列を生成します。次に文字列をハッシュ化し、16進数の文字列を取得します。ハッシュ値を整数に戻した後、バケットサイズで割った余りをバケット番号とします。

その後、各MLモデルに対してweightに基づいてバケット範囲を割り当てることでMLモデルと事業者を紐付けます(コードは省略します)。

ややこしい点はありつつも上記のロジックにより、同じ事業者とキーの組み合わせに対して一貫して同じバケットを割り当てることができます。

実行のタイミングによって割り当てが変化する等の問題が生じず、A/Bテストの管理が容易になります。また、ハッシュ関数を使うことで入力値をほぼ均等に分散させることができ、実質的にランダムにグループ分けすることができます。*1

おわりに

機械学習バッチ予測とA/Bテストをシンプルに実現した話をご紹介しました。

今回、データサイエンティストとバックエンドエンジニアの共同開発については黎明期といった状況での開発でしたが(そこがまた楽しいのですが)、主にPdM含めた3人で協力しながら手探りで設計・実装を進めていきました。

その他にもMLOpsエンジニア、データエンジニアなど多くのポジションの方々に協力いただいており、部門を跨いでスムーズに協業できる組織体制が整ってきたことを開発を通して感じました。

今回ご紹介した構成にはやや課題が残っていたりするのですが、部門を横断しつつ解決を図っていけるのではと考えています。

We’re Hiring!

タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!!

現在募集中のポジションはこちらです!*2

「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう!

*1:ハッシュ関数を活用したA/B割り当てに関してはGunosyさんのブログ記事が分かりやすいです。

*2:募集中のエンジニア系のポジションはこちらです!

Platform Engineering Kaigi 2024 に参加しました

OGP

2024/07/09 に Platform Engineering Kaigi 2024(PEK2024) が docomo R&D OPENLAB ODAIBA で開催されました。

www.cnia.io

タイミーは Platinum スポンサーとして協賛させていただき、プラットフォームエンジニアリンググループ グループマネージャーの恩田が「タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介」を発表しました。

タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。この制度を活用してタイミーから4名のエンジニアがオフライン参加しました。

productpr.timee.co.jp

各エンジニアが印象に残ったセッションの感想を参加レポートとしてお届けします。

What is Platform as a Product and Why Should You Care

What is Platform as a Product and Why Should You Care

speakerdeck.com

チームトポロジーの共著者であるマニュエルさんによる Platform as a Product という考え方がなぜ重要なのか、Platform as a Product を実現するためには特に何を念頭においてプラットフォームを構築すべきなのかを紹介するセッションでした。

価値あるプラットフォームとはストリームアラインドチームの認知負荷を下げるものであり、価値あるプロダクトとは顧客の何らかの仕事を簡単・楽にするものという話がありました。

私は元々 Platform as a Product という単語を知らない状態でこのセッションを聞いていたのですが、顧客をストリームアラインドチームと置き換えるとプラットフォームとプロダクトが同一視でき、価値あるプロダクトを生み出すためのアプローチをプラットフォームに応用というのはなるほど一理あると感じました。

プラットフォームをプロダクトと同一視すると、色々と伸び代が見えてきます。

  • プラットフォームは作って終わりではなく Go To Market まで考える
  • プラットフォームは足し算で機能を足していくのではなく引き算も考える必要がある

このあたりに関してはエンジニアリングというよりもプロダクトマネジメントの領域です。この発表を聞いてプラットフォームチームにもプロダクトマネージャーを配置する重要性を強く感じました。

もう一つ印象的だったのが、ストリームアラインドチームからプラットフォームチームを信頼してもらうことが重要という点です。チームトポロジーを踏まえた組織設計を考える上で、ストリームアラインドチーム・プラットフォームチームは認知していたのですが、あくまで構造として認知をしていました。ですが、チームを構成するのは人です。ストリームアラインドチームがプラットフォームチームを信頼していなければ、プラットフォームチームが作ったプラットフォームは信頼されないし、信頼されないと諸々デバフがかかった状態で物事を進める必要が出てくるため、プラットフォームチームが価値あるプラットフォームを生み出していたとしても浸透に時間がかかるようになります。この点は認識できていなかったのでハッとしたポイントでした。

(@euglena1215)

Platform Engineering at Mercari

Platform Engineering at Mercari

speakerdeck.com

Mercai deeeetさんの講演です。個人的にはSRE(Site Reliability Engineering)の導入やGoogle CloudでのGKE運用の方法など様々な情報発信をされていて、ありがたく参考にさせていただいています。このセッションでは、deeeetさんが入社して7年、立ち上げ当初から関わっている MercariにおけるPlatformEngineeringの歴史を振り返ってお話をされるという内容でした。

MercariのPlatform Engineeringのミッションは「メルカリグループの開発者がメルカリのお客様に対して新しい価値やより良い体験を早く安全に届けることができるようにサポートするインフラ、ツール、ワークフローを提供すること」とありました。今まで別のmeetupなどでもお話をされていますし、さらっと冒頭にあった言葉ですが、改めて目にするとシンプルで分かりやすく過不足なく定義されているなという印象を受けました。

さて、Platform Engineeringの具体的な実施事項(どういう組織・どういうツールセットか)などは是非セッション動画を見ていただく方が良いのでここでは記述を割愛いたしますが、”コラボレーションから始める”という言葉がすべてを物語っていました。

Platform Engineeringの歴史と、どのように作り上げていったかについての説明がなされました。それはモノリス・レガシーインフラからマイクロサービス・Kubernetes環境へのマイグレーションを開発者とコラボレーションしながら設計したりツールセットを整えたりしてきた、とのことでした。

マイグレーション当時はPlatform Engineeringチームメンバーが開発者の席に散っていって隣に座って一緒に会話をしながら設計やツールセットを一緒に作ったりしていたそうです。これは狙ってやったわけではなく、目的を達成するためにやっていたことが結果的に後から振り返って良い戦略だったなと思ったとのこと。泥臭いけれど、近くで会話するというのは本当に良いことです。現在はリモートワークが主流のところも(弊社も含め)多く、物理的に離れた場所にいる組織ではどのようにデザインするか工夫が必要そうだなと思いました。

以上までは立ち上げフェーズでの話でしたが、2020年ごろから現在まではPlatform Engineering組織のアップデートをしているとのこと。一つはメンバーが増えてきて(10名→15名程度→さらに増やす)、かつ見るべき領域が多くなり認知負荷が高まってきたため分割を考えたとのこと。この中でも印象的なのはProductチームとPlatformチーム間のInterfaceとなるPlatform DX(Developer Exprerience)チームを配置したとのこと。今まで1つの塊だったPlatformチームが細分化されるにあたって、Productチームが「これはこのチーム、あれはあのチーム…」と細かな事情を抑えてコミュニケーションをする認知負荷を軽減する目的のようでした。

まだいくつか気になった点はありますが、長くなりそうですのでこのあたりで留めておこうと思います。もし興味を持たれましたら是非資料やアーカイブをご覧ください!(@橋本和宏)

タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介

タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介

speakerdeck.com

弊社 恩田からの講演となります。直近1年での元々あったチーム課題をどのように解決してきたのか、また取り組んだ内容の反省点などが示されています。

ここでは詳細をあえて書かず、是非アーカイブ動画もしくは下記のスライドをご覧いただき、皆様の参考になると幸いと考えています。是非ご覧くださいませ! (@橋本和宏)

プラットフォームエンジニアリングの功罪

プラットフォームエンジニアリングの功罪

speakerdeck.com

DMMさんにおけるプラットフォームエンジニアリングについて、題名にもあるとおり”功罪”という側面で実例を交えてお話をされていました。

  • マルチクラウド”k8s”の功罪

    話を聞いている途中で私自身が”マルチクラウド”の功罪だと聞いてしまっていた節があったので、”k8s”と強調した題名にしました。あくまでkubernetes環境が異なるプラットフォーム上に存在して両方の足並みを揃えて運用するところにツラみがあったということになります。

    さまざまなビジネス要件があり、それらのシステムが個別に作られるにあたってAWSとGCP、異なる技術スタックなどがあり大変だったところが出発点だったとのこと。これらをまずはkubernetesと周辺エコシステム(ArgoCDなど)に揃えることは良かった(功)とのこと。

    対して、EKS(AWS)とGKE(GCP)という同じマネージドkubernetesであるものの、細かな違いに翻弄されたり、エコシステムの選定が両環境で動くことに引きづられてしまったりと運用の大変である(罪)であるとのことでした。マルチクラウド”k8s”は手を出すときには覚悟がいるものだなということを実例を持って示していただけて大変参考になりました。

  • セルフサービスの功罪

    多くの(15のサービス)の開発者からの依頼を4人のプラットフォームエンジニアがレビューすることは捌けなくはなかったが、規模の拡大に伴いボトルネックになりうることを懸念してセルフサービス化を推進したとのことです。この点は他の会社等の事例でも語られている通り正しい選択で良かった(功)であったとのこと。

    対して、レビューの人的コストを削減することは達成できたものの、その後表出した課題はレビューそのものを少なくしたことによるもの(罪)であった点はとても学びがありました。podの割当リソース最適化ができていないことや(request/limitが同じ値でかなり余裕をもった値で開発者が設定してしまっていると想像)、セキュリティリスク(ACLが適切でないingressが作成できてしまった)などは確かに通常はインフラが分かる人のレビューを持って担保しているものです。

    これらの課題に対しては仕組みによる担保(Policy as Codeなどによる機械的なチェック)をすることとして、セルフサービス化の恩恵獲得に倒しているという決断も共感できるものでした。

  • プラットフォームチームの功罪

    共通化されたプラットフォームシステムを作って移行することで、組織としてもプラットフォームチームというものが組成され、インフラ(プラットフォーム)エンジニアがボトルネックになることなく開発者が開発に集中できるようになったのは良かった(功)とのことです。

    罪の部分に対してどのようなものだろう?と聞いている途中で興味深かったのですが、プラットフォーム化やセルフサービス化を進めても、開発者側で対応可能な問い合わせが結構な割合で来てしまい、対応コストが依然としてかかる = プラットフォームチームがボトルネックになってしまっている部分がまだまだあるとのことでした。

やはり「実際にやってみたら色々と大変だった」という話はとても価値があります。使っている技術スタックが異なることがあっても行き着く先にあるのはヒトや組織にあり、ある程度通底するものがあるのだなと改めて痛感しました。

(@橋本 和宏 )

いつPlatform Engineeringを始めるべきか?〜レバテックのケーススタディ〜

いつPlatform Engineeringを始めるべきか?〜レバテックのケーススタディ〜

speakerdeck.com

レバテックさんの基盤システムグループという Platform Engineering の流れ以前に作られたチームが機能不全になっていることに気付き、棚卸しを行なって基盤システムグループを大幅に縮小するまでの流れを紹介しながら、今あなたの会社は Platform Engineering を始めるべきなのか?に対して示唆を与えるセッションでした。

基盤グループメンバーの声、ストリームアラインドチームの声から現状を深掘っていくスタイルはその頃の状況がとてもイメージがしやすく分かりやすかったです。個人的には聞いたセッションの中ではマニュエルさんの keynote の次に良かったセッションです。

タイミーにはプラットフォームチームが既に存在しているため、いつ Platform Engineering を始めるべきか?に悩むことはないですが、ユーザーの声を元に仮説を立てて仮説を検証し次の洞察につなげていく進め方・考え方はとても参考になりました。

発表内容からは少し逸れるのですが、基盤システムグループを大幅に縮小するという決断をグループリーダーが行えたことは素晴らしいと感じました。リーダーの影響力はチームメンバー数に比例すると思っていて、チームの縮小とはリーダーの影響力の縮小を意味すると思っています。ここに対してしっかりとアプローチを行なっていたのは素晴らしいなと思いました。

(@euglena1215)

Platform Engineering Kaigi 2024 トラックB

マルチクラスタの認知負荷に立ち向かう!Ubieのプラットフォームエンジニアリング

マルチクラスタの認知負荷に立ち向かう!Ubieのプラットフォームエンジニアリング | Platform Engineering Kaigi 2024

speakerdeck.com

Ubieのセッションでは、アプリケーションエンジニアやSREが直面する設定作業や問い合わせ対応の負担を軽減するための取り組みが紹介されました。

Ubieの講演会では、アプリケーションエンジニアとSREが直面する課題と、それに対するソリューションについての興味深い話がありました。特に、設定作業や問い合わせ対応の負担が大きい現状に対して、マルチクラスター移行を検討する必要性が述べられました。しかし、クラスター間の通信やデプロイメントの課題が浮上するため、簡単ではないとのことでした。

その解決策として、Ubieは「ubieform」と「ubieHub」を開発しました。

  • ubieform これは、GKEクラスターやBackStageなどを自動で作成し、k8sの設定からアクションの設定、GCPの設定までを出力してくれるツールです。このツールを導入することで、認知負荷を減らし、エンジニアの作業を大幅に効率化できるとされています。しかし、ある程度の完成度になるまで展開しない方針を取っており、質の確保に注力している点が印象的でした。
  • ubieHub 情報を取得するためのハブとなるもので、基本的にはBackStageをベースにしているようです。新しいツールやポータルの導入は、小規模に始めて早期にフィードバックを得ることが重要とされています。また、リンク切れなどの問題が発生しやすいため、定期的に情報を更新し、最新の状態を保つ必要があります。

新しいツールの導入時には、「ドッグフーディング」つまり、自社内で試用することが必須とされており、初期のバグ対応やチーム内でのコミュニケーションが重要であることが強調されました。

全体として、Ubieはツールの開発と導入において非常に戦略的かつ実践的なアプローチを取っており、その姿勢が印象深かったです。ツールの完成度と認知負荷のバランスを取りながら、効率的な業務環境の構築を目指す姿勢に感銘を受けました。

(@hiroshi tokudomi )

最後に

Platform Engineering という言葉は概念を表すものであり、細かな実装は各社各様さまざまなものがあります。大事なのは言葉の定義そのものではなく、何に対して価値提供するか・できているかを考え続けることだと考えます。

PEK2024は様々な環境・会社における具体的な課題やその解決のための実装を知ることで、自社における課題解決へとつながるきっかけが多くありました。

様々なセッションの聴講を通じて共通のキーワードとして以下の3点があることに気づきました。

  • 知ってもらう
  • 頼ってもらう
  • 評価してもらう

何を提供しているか知ってもらって使ってもらわなければ価値提供できているとは言えない。頼ってもらう存在にならなければ、そもそも価値提供につなげることができない。また、提供したものが評価されるものでなければ、価値提供できているとは言えない。ということになります。

これらのことは何か特別な定義でもなく、プロダクトを提供する立場の人であれば当たり前のことであると感じられると思います。この当たり前のことを当たり前のこととして”やっていく”ことがプラットフォームエンジニアリングを担うものとして意識すべきことだと強く意識した良い機会となりました。

(@橋本 和宏)

flake8 pluginを書いてみた

こんにちは、タイミーのデータエンジニアリング部 データサイエンス(以下DS)グループ所属のYukitomoです。

今回はPythonのLinterとしてメジャーなflake8のプラグインの作り方を紹介したいと思います。

コードの記述形式やフォーマットを一定に保つため、black/isort/flake8などのformat/lintツールを使うことはpythonに限らずよく行われていますが、より細部のクラス名や変数名を細かく規制したい(例:このモジュールのクラスはこういう名前付けルールを設定したい等)、けれどコードレビューでそんな細かい部分を目視で指摘するのは効率的でない、といったケースはありませんか?そんな時、flake8のプラグインを用意して自動検出できるようにしておくと便利です。

ネット上には公式サイトを含めいくつかプラグイン作成の記事があるのですが、我々の想定ケースと微妙に異なる部分がありそのままでは利用できなかったため、

  • 最新のflake8(2024/7現在, 7.1.0)を用い
  • 比較的新しいパッケージマネージャーであるpoetry(1.8.3を想定)を利用して
  • 2種類のプラグインのそれぞれの作り方

を改めてここにまとめます。

準備するもの

  • Python: Versionは特に問いませんが、3.11.9で動作確認しています。
  • Poetry: 1.8 以上 (後述しますが1.8より導入されたpackage-mode = falseを指定しているため)。この記述を変えることで1.8以前のバージョンでも動くとは思いますが、この記事では1.8を前提としています。

上記が利用可能な環境をvenvやコンテナを利用して作成しておいてください。flake8本体はpyproject.tomlの依存モジュールとして導入されるため事前に準備する必要はありません(3.8以降で動作するはずですが、本記事では7.1を利用します)。

全体の構成

サンプルで利用するファイル群は以下の通りです。構文木を利用するタイプと1行ずつ読み込んでいくタイプと2種類あるため、それぞれをtype_a、type_bとしてサンプルを用意し、それら2つのサンプルを束ねる上位のプロジェクトを一つ用意しています。本来なら各プラグイン毎にユニットテスト等も実装すべきですが、本記事ではプラグインの書き方自体の紹介が目的のため割愛しています。なお、type A, type Bの呼称はflake8プラグインにおいて一般的な呼び名ではなく、本記事の中で2つのタイプを識別するために利用しているだけなので注意してください。

# poetry.lock 等本記事の本質と関係のないものは省略しています
(.venv) % tree .  # この位置を$REPOSITORY_ROOTとします。
.
├── pyproject.toml
└── plugins
    ├── type_a
    │   ├── pyproject.toml
    │   └── type_a.py
    └── type_b
        ├── pyproject.toml
        └── type_b.py

${REPOSITORY_ROOT}/pyproject.tomlは以下の通り。

# cat ${REPOSITORY_ROOT}/pyproject.toml
[tool.poetry]
name = "flake8 plugin samples"
version = "0.0.1"
description = "A sample project to demonstrate flake8 plugins"
authors = ["timee-datascientists"]
package-mode = false # この記述を外せばきっとpoetry 1.8より前でも動くはず

[tool.poetry.dependencies]
python = ">=3.11.9"

[[tool.poetry.source]]
name = "PyPI"
priority = "primary"

[tool.poetry.group.dev.dependencies]
# flake8を利用するので一緒によく利用されるblack/isortも導入
flake8 = "~7.1.0"
isort = "~5.13.2"
black = "~24.4.0"

# プラグインはローカルからeditable modeで登録
type_a = { path="./plugins/type_a", develop = true}
type_b = { path="./plugins/type_b", develop = true}

[build-system]
requires = ["poetry>=1.8"]
build-backend = "poetry.masonry.api"

Type A: AST Treeを利用する場合

Python codeの1ファイルをparseして抽象構文木(AST)として渡すタイプのプラグインです。ネットでflake8のプラグインを検索した時、こちらのタイプの実装例が出てくることが多く、また、構文木の処理が実装できるなら、こちらの方が使いやすいです。

構文木で渡されたpython ファイルを巡回し、その過程で違反を発見するとエラーを報告しますが、本記事のサンプルでは構文木の巡回結果は無視し、巡回後必ずエラーを報告しています。詳細はast.NodeVisitorを参照いただきたいのですが、各ノードを巡回する際に呼ばれるvisit()だけでなく、visit_FunctionDef() などファイル内で関数定義された場合、など個別の関数が用意されているので、これらを適切に上書きすることで、目的の処理を実現していくことになります。

なお、プラグインのコンストラクタには抽象構文木(ast)の他、lines, total_lines等公式ドキュメントのここに記述されているものを追加することができます。

以下にサンプルの実装(type_a/type_a.py)とプロジェクトの定義ファイル(type_a/pyproject.toml)を示します。

# type_a/type_a.py
import ast
from typing import Generator, List, Tuple

# プラグインの本体
class TypeAPluginSample:
    def __init__(
        self, tree: ast.AST  #, lines, total_lines: int = 0
    ) -> None:
        self.tree = tree

    def run(self) -> Generator[Tuple[int, int, str, None], None, None]:
        visitor = MyVisitor()
        visitor.visit(self.tree)
        # サンプルでは常にエラーを報告するが本来ならvisitorに結果を溜め込んで
        # 結果に応じてエラーをレポート
        if True:
            yield 0, 0, "DSG001 sample error message", None

# プラグインから利用する構文木の巡回機
class MyVisitor(ast.NodeVisitor):
    # visit() やvisit_FunctionDef()を目的に応じて上書き
    pass

# 他のサンプルでは必須っぽく書いてあるが、pyproject.tomlのentry-points
# の指定と被ってるなぁと思ってコメントアウトしても動いたので今はいらない気がする。
# def get_parser():
#    return TypeAPluginSample
(.venv) % cat plugins/type_a/pyproject.toml
# 親プロジェクトから直接ロードするため [project]の記述もしていますが
# プラグイン単体で独立したプロジェクトとするなら不要。
[project]
name = "type_a"
version = "0.1.0"
description = "Sample type a plugin"
authors = [{name = "timee-datascientists", email = "your.email@example.com"}]

[tool.poetry]
name = "type_a"
version = "0.1.0"
description = "Sample type-a plugin"
authors = ["timee-datascientists"]

[build-system]
requires = ["setuptools", "wheel", "poetry>=1.8.3"]
build-backend = "setuptools.build_meta"

[tool.poetry.dependencies]
python = ">=3.11.9"
flake8 = ">=7.1.0"

# ここでプラグインのクラス名を登録
[project.entry-points."flake8.extension"]
DSG = "type_a:TypeAPluginSample"

Type B: 1行ずつ処理する場合

対象となるpython ファイルを1行ずつ処理していくタイプのプラグインです。公式ドキュメントにある通り、歴史的な経緯で2種類あるようですが、こちらの1行ずつ処理するタイプを使ったサンプルを見かけたことがありません。特に非推奨とされているわけでもないですし、実装したいルール自体がシンプルであればこちらの方法で実装するのもありだと私は思います。physical_lineもしくはlogical_lineを第一引数に設定し、physical_lineの場合はファイルに書かれている1行ずつ、logical_lineの場合はpython の論理行の単位で指定した関数が呼ばれます。physical_line, logical_lineの両方を同時に指定することはできず、他の変数を追加する場合もphysical_line/logical_lineは第一引数とする必要があります。

以下にサンプルの実装(type_b/type_b.py)とプロジェクトの定義ファイル(type_b/pyproject.toml)を示します。

# type_b/type_b.py
from typing import Optional

# プラグイン本体
def plugin_physical_lines(
    physical_line: Optional[str] = None,
    line_number: Optional[int] = None,
    filename: Optional[str] = None,
):
    if line_number == 2:
        yield line_number, "DSG002 sample error message"
(.venv) % cat plugins/type_b/pyproject.toml

# type_aのものとほぼ同じ。project.nameおよびtool.poetry.nameをtype_bに書き換えた後、
# 差分は以下。プラグイン本体の関数を指定してやれば良い。

:
[project.entry-points."flake8.extension"]
DSG = "type_b:plugin_physical_lines"

実行結果

以下のようなサンプルファイルを用意し、flake8を実行した結果を示します。

# sample.py
def main():
    print(
        'Hello, World!'
    )

if __name__ == '__main__':
    main()

実行結果

% flake8 sample.py
sample.py:0:1: DSG001 sample error message
sample.py:2:3: DSG002 sample error message

注意点

Type A, Type B両方とも公式ドキュメントに書いてある変数は全てコンストラクタに追加できるのですが、それぞれのタイプにおいて意味のあるものは限られるため、必要なもののみを追加すれば良いです。

まとめ

flake8 のプラグインの定義方法を2通りご紹介しました。

タイミーのデータサイエンスグループでは通常のformat/lintだけでカバーできない(けれど少しの工夫により機械作業で抽出できる)運用ルールを本記事のようなflake8プラグインを用いてCIで事前に検出することで、コードレビューはできるだけ本質的な部分に集中できるよう取り組んでいます。

We’re Hiring!

タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!!

現在募集中のポジションはこちらです!

「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう!

References

開発生産性カンファレンス2024に参加しました

タイミー QA Enabling Teamのyajiriです。

去る6月28日〜29日の2日間、ファインディ様主催の「開発生産性カンファレンス2024」に参加してきました。

(タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があり、今回もこれを利用して新潟からはるばる参加してきました。) productpr.timee.co.jp

タイミーでは弊社VPoE(VP of ええやん Engineering)の赤澤の登壇でもご紹介した通り、チームトポロジーを組織に適用し、プロダクト組織の強化と改善にチャレンジしています。 speakerdeck.com

この登壇でも紹介されておりますが、私自身もイネイブリングチームの一員として、プロダクト組織全体のQA(品質保証)ケイパビリティの向上や、障害予防プロセスの改善に取り組んでいます。

開発生産性の観点から考える自動テスト

まずQAの視点で最も印象に残ったのは、皆さんもご存知のt_wadaさんによる「開発生産性の観点から考える自動テスト(2024/06版)」です。 speakerdeck.com

なぜ自動テストを書くのか?

この問いに対してt_wadaさんは「コストを削減するためではなく、素早く躊躇なく変化し続ける力を得るため」そして「信頼性の高い実行結果に短い時間で到達する状態を保つことで、開発者に根拠ある自信を与え、ソフトウェアの成長を持続可能にすること」と表現されていました。

(ここまで一言一句に無駄のない文章は久々に見た気がします)

タイミーでもアジャイル開発の中で高速なテストとフィードバックのサイクルを意識し、自動テストを含むテストアーキテクチャの強化に取り組んでいます。しかし、活動がスケールすると共にテストの信頼不能性(Flakiness)や実行時間の肥大化、費用対効果などの問題が発生します。

これらの問題に対する合理的な対応策を検討する上で、各々のテストの責務(タイプ)や粒度(レベル)を分類し、費用対効果と合目的性の高いものから重点的に対応していく必要があります。

そのためのツールとして「アジャイルテストの四象限」や「テストピラミッド」「テスティングトロフィー」などを活用し、テストレベルを整理し、テストのポートフォリオを最適化するアプローチを取っていましたが、具体的なアーキテクチャに落とし込んだ際に「これってどのテストレベルなんだっけ?」といった想定と実態の乖離がしばしば発生していました。

サイズで分類しテストダブルでテスト容易性を向上する

それを解決する手段として、テストレベルではなくテスト「サイズ」で整理する方法が提唱されました。

テストサイズの概念は古くは「テストから見えてくる グーグルのソフトウェア開発」、最近では「Googleのソフトウェアエンジニアリング」で紹介されていました。今回紹介されたのは、テストピラミッドにおいても具体的なテストタイプではなく「サイズ」で分類し、テストダブル(実際のコンポーネントの代わりに使用される模擬オブジェクト)を積極的に利用することでテスタビリティを向上させ、テストサイズを下げ、速度と決定性の高いテストが多く実装される状態を作るというアプローチです。

このアプローチは、タイミーのDevOpsカルチャーにも親和性が高く、ぜひ自動テスト戦略に取り入れたいと感じました。

おわりに

他にも魅力的で参考になる登壇が盛りだくさんで、丸々2日間の日程があっという間に過ぎる素晴らしいイベントでした。

主催のファインディ様やスポンサー、登壇者の皆さまに感謝するとともに、来年の開催も心より楽しみにしています。

H3を使用した BigQueryでの空間クラスタリングについて検証してみた

こんにちは、タイミーのデータエンジニアリング部データサイエンス(以下DS)グループ所属の菊地です。

今回は、H3を使用したBigQueryでの空間クラスタリングについて検証した内容を紹介したいと思います!

BigQueryでの空間クラスタリングとは

BigQueryにはクラスタリングという機能があり、うまく活用すると、クエリのパフォーマンスを向上させ、クエリ費用を削減できます。

クラスタリングは空間データにも適用でき、BigQuery がデフォルトで使用するS2インデックス システムを使用して、空間クラスタリングを行うことができます。

また、H3やGeohashなどの他の空間インデックスに対しても空間クラスタリングを行うことができ、今回はタイミーでも良く使用しているH3を使用して、空間クラスタリングを行う方法を検証してみました。

BigQueryでのクラスタリング及び空間クラスタリングについては、下記の記事が参考になるかと思います。

cloud.google.com cloud.google.com

H3を使用した BigQueryでの空間クラスタリングの検証

上記の参考記事でも挙げましたが、基本的にこちら記事の内容に沿いつつ、一部具体の実装が記載されていない箇所を補完しながら検証を行いました。 cloud.google.com

1. 検証用のテーブル作成

検証用のテーブルとして、経度と緯度のランダムポイントを、H3セルID(解像度13)に変換したテーブルを作成します。

DECLARE H3_INDEX_RESOLUTION INT64 DEFAULT 13;

-- 連番を格納しておくためだけのテーブル
-- CTEだと後続のテーブル作成が遅かったので実テーブルにしてます
CREATE OR REPLACE TABLE `tmp.tmprows` as
SELECT x FROM UNNEST(GENERATE_ARRAY(1, 10000)) AS x;

-- 経度と緯度のランダムポイントを、H3セルID(解像度13)に変換したテーブル
DROP TABLE IF EXISTS `tmp.h3_points`;
CREATE OR REPLACE TABLE `tmp.h3_points`
CLUSTER BY h3_index
AS 
WITH points AS (
  SELECT 
    `carto-os`.carto.H3_FROMLONGLAT(RAND() * 360 - 180, RAND() * 180 - 90, H3_INDEX_RESOLUTION) AS h3_index
    -- 後の検証のために追加
    , RAND() AS amount
  FROM 
    `tmp.tmprows` AS _a
    CROSS JOIN `tmp.tmprows` AS _b
)
select 
  h3_index
  , amount
FROM points

テーブルのストレージ情報と内容は以下のようになります。

2. クラスタリングによる絞り込みが効かないクエリ例

次に、参考記事で紹介されているように、親セルID(今回は解像度7)をWHERE句で指定してクエリを実行してみましたが、このクエリはテーブルをフルスキャンしてしまいます。

DECLARE PARENT_CELL_ID STRING DEFAULT '870000000ffffff'; -- H3解像度7のセルID

SELECT
    ROUND(SUM(amount), 6) AS sum_amount
FROM 
    `tmp.h3_points`
WHERE `carto-os`.carto.H3_TOPARENT(h3_index, 7) = PARENT_CELL_ID

ジョブ情報と結果

H3インデックスでクラスタリングを行っているにもかかわらず、テーブルをフルスキャンしてしまう理由としては、

H3_ToParentにはビット演算が関係し、複雑すぎて BigQuery のクエリアナライザが、クエリの結果がクラスタ境界にどのように関連しているかを把握できないために発生します。

参考記事では言及されています。

3. クラスタリングによる絞り込みが効くクエリ例

次に、クラスタリングによる絞り込みが適用されるクエリを検証してみます。

「2. クラスタリングによる絞り込みが効かないクエリ例」との違いとしては、低解像度の親セルに含まれる、高解像度セルの開始IDと終了IDを取得し、WHERE句で指定していることです。

DECLARE H3_PARENT_ID STRING DEFAULT '870000000ffffff';  -- H3解像度7のセルID
DECLARE H3_INDEX_RESOLUTION INT64 DEFAULT 13;

DECLARE RANGE_START STRING;
DECLARE RANGE_END STRING;

-- 低解像度の親セルに含まれる、高解像度セルの開始IDと終了IDを取得しセットする
SET (RANGE_START, RANGE_END) = (
  SELECT AS STRUCT
    `carto-os`.carto.H3_TOCHILDREN(H3_PARENT_ID, H3_INDEX_RESOLUTION)[0],
    ARRAY_REVERSE(`carto-os`.carto.H3_TOCHILDREN(H3_PARENT_ID, H3_INDEX_RESOLUTION))[0]
);

SELECT
  ROUND(SUM(amount), 6) AS sum_amount
FROM
  `tmp.h3_points`
WHERE 
  h3_index BETWEEN RANGE_START AND RANGE_END

ジョブ情報と結果は以下のようになっており、スキャン量が削減され、クエリのパフォーマンスも向上しています。クエリ結果も「2. クラスタリングによる絞り込みが効かないクエリ例」の結果と合致しています。

ジョブ情報と結果

まとめ

H3を使用した BigQueryでの空間クラスタリングについて検証してきました。

タイミーでは位置情報を活用した分析を行うシーンが多く、うまく活用することで機械学習時の特徴量生成や、BIツールからのクエリ最適化に繋げることができる可能性があるので、今後のデータ分析に活かしていきたいと思います。

We’re Hiring!

タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!!

現在募集中のポジションはこちらです!

「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう!

【イベントレポート】Kotlin Fest 2024に参加しました

2024年6月22日(土)にKotlin Fest 2024が開催されました。Kotlin Festは「Kotlinを愛でる」というビジョンを掲げた技術カンファレンスです。タイミーのAndroidエンジニアはエンジニアの成長を支援する制度の一つであるKaigi Passを利用して参加しました。

本投稿では、Kotlin Fest 2024に参加したメンバー(中川、haru、みかみ、しゃむむらたtick-tack)が気になったセッションや感想のレポートします!

メンバーによるレポート

中川編

効果的なComposable関数のAPI設計

私が気になったセッションは、haru067さんによる「効果的なComposable関数のAPI設計」です。このセッションでは、Composable関数を書くときに引数をどのように定義すべきかという、現場で直面する具体的な疑問に対して、様々なケーススタディを通じて考察が行われました。

セッションでは以下のプラクティスに触れられました:

  • State hoisting
  • Slot API
  • DSLでのslot APIの活用
  • デフォルト引数
  • Property drilling

特に印象的だったのは、これらのプラクティスが常に最適な解決策とは限らないという点が強調されていたことです。むやみに使うのではなく、適切な場面で使うことが重要であるという、現場での経験に裏打ちされた具体的なアドバイスが参考になりました。

haru編

Kotlinで愉しむクリエイティブコーディング

まず最初にご紹介するのは、畠山 創太 さんによる Kotlinで愉しむクリエイティブコーディング です。

私はクラブイベントにたまに行くので、VJさんという存在を元々知っていたのですが、そんなVJさんの中でもジェネ系と呼ばれる画面をリアルタイムに生成するライブコーディング的なアプローチのVJさんとプライベートで繋がりがあり、それに利用されているフレームワークなどを知っていました。

そんな中、このセッションではKotlinでリアルタイムにグラフィックスを処理できて、ジェネ系VJにも使えそうなOPENRNDRが紹介されていました。

OPENRNDRはProcessingやTouch Designerなどのジェネ系VJで使われるフレームワークとよく似たフレームワークで、KotlinベースのDSLでグラフィックス処理を記述することができます。

このセッションでは、OPENRNDRで書かれたいくつかのデモ(Boidsなど)が紹介され、OPENRNDRでできることの自由度や簡単に記述できることを紹介していました。

OpenGLベースのグラフィックスバックエンドをもち、RealSense, Kinect, TensorFlow, DMXなど多種多様な連携先が存在しており、これらを使えばセッションで紹介されていた以上のこともできそうだなと感じました。

Okioに愛を込めて

次にご紹介するのは、RyuNen344さんによるOkioに愛を込めてです。

OkioはBlock社が開発しているKotlin向けのI/O ライブラリで、OkHttpやMoshiのベースにも使われているライブラリです。

まず、Kotlinの標準ライブラリが充実しているのに、なぜOkioを採用するのかという話から始まりました。

いくつかの理由を紹介されていましたが、地味に落とし穴だなと思ったのは、Kotlinが元々JVMをターゲットとした言語としてスタートしているが故にJava標準ライブラリを呼び出しているところが多々あったり、それをKMPから使えなかったりするというところでした。

そんな中、OkioはJava標準ライブラリなどへの依存がなく、それでいて使い勝手の良いI/Oライブラリになっているということで、これから直接的・間接的問わず利用する頻度は増えていきそうでした。

これからKotlin向けのライブラリを作る上では、JVM以外のターゲットで使われることも前提として考えないといけないと思いました。

そして、綺麗なダジャレでセッションは終了。お見事でした。

みかみ編

例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する

「例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する」というセッションを紹介します。例外設計の重要性とプロダクト開発に与える影響について深く掘り下げ、KotlinとArrowライブラリを活用した柔軟な例外設計の実践方法が詳しく説明されていた発表でした。

特に印象的だったのは「例外設計とモデリング」についてです。このセッションでは、例外を「技術的例外とビジネス例外」および「予期する例外と予期しない例外」の組み合わせで大きく4つに分類できるという説明がありました。そしてそれぞれの例外に対して、ドメイン駆動設計(DDD)の考え方を基に、具体的にどのようにコードに反映させるかが紹介されました。例外をドメインに結びつけて考えることにより、プロダクト開発に良い影響を与える例外設計を行うことができると感じました。

例外自体は普段の実装でも意識しますが、その複雑さのため設計に関しては深く意識できていないことが多いと感じています。本セッション内容を通してプロダクト開発をより良くしていくための例外設計の考えた方と実践に挑戦していきたいと感じました。

しゃむ編

しゃむ(@arus4869)です。FF16を最近ようやくクリアできたので、FFVIIリバースやり始めました。最高ですね。

KotlinのLinterまなびなおし2024

私が気になったセッションは「KotlinのLinterまなびなおし2024」です。このセッションでは、各種Lintツールの紹介だけでなく、Lintツールを効果的に活用するための実践的なアドバイスも多数紹介されました。

中でも特に気になったのはkonsistです。konsistは、標準セットルールがなく、各プロジェクトの特性に合わせたルール設定が可能である点が魅力的でした。また、テスト環境やユニットテストでの動作が主な特徴で、アノテーションを活用することで特定の用途に応じたルール設定ができる点も興味深かったです。

またセッションの中で、Lintルールを段階的に導入することでチームの負担を軽減しつつ、徐々にコード品質を向上させるアプローチも印象的でした。

このセッションを通じて、KotlinのLintの効果的な使い方について多くの知見を得ることができ、学び直しの良い機会になりました。ありがとうございました。

むらた編

むらた(@orerus)です。最近夫婦でカイロソフトさんのアプリにハマっています。

withContextってスレッド切り替え以外にも使えるって知ってた?

さて、早速ですが私が気付きを得たセッションとしてT45Kさんによる「withContextってスレッド切り替え以外にも使えるって知ってた?」 を紹介させていただきます。スライドも公開されています。

Kotlin coroutinesを使っていると頻繁に登場する withContext ですが、セッションタイトルでズバリ指摘されている通り、私もスレッドの切り替え用関数であるかのように意識してしまっていたことに気づきました。

使い方が間違っているわけではありませんが、セッションで紹介されている通り、withContextの挙動は正確にはスレッド切り替えではなく「CoroutineContextを切り替える」(厳密には既存のCoroutineContextと引数で渡されたCoroutineContextをマージする)ことにあります。そのうえで、渡されたブロックをcoroutineContextで指定されているcoroutineDispatcherにて実行するという形になります。(詳細については是非T45Kさんのスライド資料を参照ください)

そのため、 withContext(Dispatchers.IO) のように切り替え先のスレッド (厳密には CoroutineDispatcher ) を指定するだけでなく、 withContext(Job() + Dispatchers.Default + CoroutineName("BackgroundCoroutine")) のように、複数のCoroutineContextを合成する形で引数を指定することができるんですね。(CoroutineContextの要素についてはこちらを参照ください

なお、 withContext 以外のコルーチンビルダー( launchasync など)についても、引数で指定されたCoroutineContextと既存のCoroutineContextをマージして用いる挙動は同じです。

今回のセッションを通じて、Kotlin coroutinesへの理解がさらに深まりました。とても良いセッションをありがとうございました!

tick-tack編

まだ JUnit を使ってるの? kotest を使って快適にテストを書こう

Kotest についての HowTo を熱く語っておられるセッションで Kotest への愛を感じました。最近よく名前を聞くライブラリな気がします。

タイミーでも hamcrest を採用していますが Java 向けのテストライブラリは Kotlin の予約語が使われていてエスケープしないととても見づらいことがあります。やっぱり Kotlin first に書けるのは非常に気持ちがいいですね。Kotest は Runner が JUnit で安定した環境で動かせるのもグッド。

個人的にセッション内で刺さったポイントとしては EventuallyProperty Based Testing です。

Eventually

内部で非同期処理を実行するメソッドのテストを書くときに実行しても assertion のタイミングが変更前で失敗するといったケースはよくあります。そういう時に eventually を使うと一定時間評価しつづけ期待する結果に変わったら成功と見なしてループを抜けてくれます。めちゃめちゃかしこい。逆に一定時間変更がないことを評価する continually もあるそうです。

Property Based Testing

都度実行する度に自前で用意しなくても、ランダムに自動生成された property を利用して複数回テストするといったことができます。境界値テストを用意する場合に役立ちそうです。


さっそく assertion だけですが触ってみました。

記述方法だけでも inifix で name shouldBe "tick-taku" みたいに書けて最高にワクワクします。楽しくテストが書けそうですね。

触ってみていいなと思ったのが、例えばインスタンスが別だけど中の property が同じな事だけ確認したい場合はこんな感じに書けました。1つずつ取り出して equals とかしなくてもスッキリしていいですね。

data class User(val id: Long, val name: String, val age: Int)

checkAll(
    iterations = 3,
    Arb.long(),
    Arb.string(1..10, Codepoint.katakana()),
    Arb.int(1..100)
) { id, name, age ->
    val user = User(id = id, name = name, age = age)
    repository.save(user)

    repository.getUser() shouldBeEqualToComparingFields user
}

一応あまり有用な例ではないですが上で紹介した property testing の checkAll や property のランダム生成もせっかくなので書いてみました。

個人的には Google の Truth が好きでしたが推し変しそうです。Android プロジェクトに導入するのもよさそうでした。

まとめ

Kotlin Fest 2024はKotlinという言語の可能性を改めて再認識するとともに熱意と活気に満ちたイベントでした。また、普段リモートワークで働くタイミーのエンジニアにとってもチームメンバーと対面で交流する貴重な機会でした。今回得られた知見を活かして今後のプロダクト開発にもさらに力を入れていきたいと思います、次回のKotlin Festも楽しみにしています!