Redshiftのデータをサービス改善に役立てるデータ転送システム Queuery

こんにちは、技術部データ基盤グループの佐藤です。この記事では最近業務として主に取り組んでいたDWHから外部へのデータ転送基盤であるQueuery(きゅうり)について、OSSとしてGitHubへの公開しましたのでこの記事でご紹介をします。

github.com

Queueryというシステムは2017年の春頃にid:koba789の手により作られ、クックパッドのデータ基盤における重要な立ち位置を担っています。

背景

従来、RedshiftでSELECT文などの取得系クエリを実行するためにはRedshiftに直接接続してクエリを発行していました。この方法ではクエリ結果が巨大な場合にクライアント側のリソースを逼迫させることがありました。

しかし、それを避けるためにカーソルを使おうものなら今度はたちまちRedshiftのリーダーノードの具合が悪くなってしまいます。Redshiftから巨大な結果を得るクエリを外部から実行するためには様々な工夫が必要でした。

さらに通常の(PostgreSQLプロトコルを使った)接続方式では遠隔地(別AWSリージョン)からの接続が難しかったり、よくコネクションが切れたり、コネクションが切れると結果が取得できなかったりします。AWSのSecurity Groupの設定も忘れがちです。 また、セットアップがActiveRecord経由になるため単純に設定が面倒です。しかもActiveRecordを使ったあらゆる使い方ができてしまうため、標準化も困難です。

Queueryはこれらの問題を解決するためにあります。Queueryを使うことで、クライアントはRedshiftに直接接続せずHTTP APIで取得系クエリを実行できるようになります。

f:id:ragi256:20211202115017p:plain
アプリケーションからRedshiftへ接続する手法

仕組み

QueueryはRedshiftへUnload文を投げる役割を持つAPIサーバーと、Unload結果をS3から取得するクライアントに分かれています。クライアント側から投げられたSELECTクエリをHTTP API側で受け取り、Unload文へラップしてRedshiftに投げます。クライアント側はその結果をポーリングし続け、Unloadが完了したらS3へアクセスして結果を取得するようになっています。

できうる限りQueuery利用者の開発を単純化するため、クライアントはgem化されており、Gemfileに追加して設定ファイルを追加すればすぐ利用できるようになっています。

クライアントのサンプルコード

下記のコードをクライアント側でジョブに書き、必要なタイミングでバッチ実行するだけでRedshiftにあるデータを扱えるようになります。

Queueryの設定ファイル

# configuration
RedshiftConnector.logger = Logger.new($stdout)
GarageClient.configure do |config|
  config.name = "queuery-example"
end
QueueryClient.configure do |config|
  config.endpoint = 'queuery_api_server_host'
  config.token = 'XXXXXXXXXXXXXXXXXXXXX'
  config.token_secret = '*******************'
end

Queueryのクライアントコード

select_stmt = 'select column_a, column_b from the_great_table; -- an awesome query shows amazing fact up'
bundle = QueueryClient.query(select_stmt)
bundle.each do |row|
  # do some useful works
  p row
end

コンソール

また、簡易的なものではありますがWebコンソールも付属で用意してあり、コンソールではクライアント側の認証に必要なトークンを発行・無効化したり、直近でQueueryに投げられたクエリの様子を確認できます。

f:id:ragi256:20211202111451p:plain
QueueryのWebコンソール画面の例

QueueryサーバーのAPI側はシンプルなRailsで作られており、コンソールのフロントエンドはTypeScriptとReactでSPAにしています。

最近の改修内容

Queueryを紹介するついでに今年自分が改修を行った箇所について書いておきます。

QueueryアカウントとRedshiftユーザーの紐付け

以前はQueueryのコンソールから好きな名前のQueueryアカウントを誰でも作ることができ、既存Queueryアカウントの認証用トークンを誰でも有効・無効切り替えができる仕様になっていました。また、RedshiftでのUnload文実行はQueuery専用に用意された1つのRedshiftユーザーによって行われていました。

このままでは社員の誰かが他チームのQueueryアカウントに手を加えてしまう恐れがあります。また、Unloadに使うユーザーの権限をQueueryアカウント毎、個別に分けることもできません。DWHに関するDevOpsを進めていく一環として、利用者の権限をきちんと分離し、Queueryアカウントをそのアカウント所有者・所有チームのRedshiftユーザーにしか扱えないようにする必要がありました。

そこで、Queueryアカウントについて、作成・認証用トークン作成、削除のタイミングでRedshiftユーザーの認証を求めるようにしました。認証作業自体はRedshiftにチェック用の単純なクエリを直接を投げるのみとし、本人確認がとれればユーザー名のみ記録することとします。 その後、実際のUnload文実行時には登録されたユーザー名を使ってGetClusterCredentials APIで一時的なユーザーを作成することにしました。

temporal_credential = Aws::Redshift::Client.new.get_cluster_credentials({
  db_user: redshift_user,
  db_name: database_name,
  cluster_identifier: cluster_identifier,
  auto_create: false
})

ds.config.merge!(username: temporal_credential.db_user, password: temporal_credential.db_password)
export_execute(datasource: ds, query_statement: sql, logger: logger)

こうすることでQueueryアカウントの管理は所有者であるRedshiftユーザーのみが行え、アカウント毎のクエリ実行はそのアカウントに紐付けられたRedshiftユーザーに基づいて実行されるようになりました。

ただし、現状ではRedshiftユーザーとQueueryアカウントとの2重管理になっており、権限管理が無用に複雑化しているという問題も抱えています。この点については今後Queuery側でのアカウント管理をやめ、RedshiftユーザーをそのままQueuery側のアカウントとして扱えるようにしようかと検討しています。

Unload文のmanifest.jsonを使った型キャスト

これまでQueueryにより出力されたファイルは全て圧縮&分割されたCSVとしてS3に出力されており、Queueryクライアントではその型を自動判別することができませんでした。そのため、Queueryクライアントを利用する開発者は取得した結果に対して手動で型キャストを行うコードを書く必要がありました。

RedshiftのUnload文には様々なオプションがあり、その中にはUnload結果に関するメタ情報を出力する、MANIFESTオプションがあります。
https://docs.aws.amazon.com/ja_jp/redshift/latest/dg/r_UNLOAD.html

このオプションにより出力されるJSON形式のマニフェストファイルの中には列名とデータ型に関する情報が含まれています。このマニフェストファイルを読み、自動で各カラムの型を判別して型キャストができるよう、QueueryサーバーとQueueryクライアントの両方に改修を行いました。

sql = "selectt 1, 1::bigint, 1.0, 'hoge', false, date '2021-01-01', timestamp '2021-01-01 00:00:00', null"

bundle1 = QueueryClient.query(sql) # 従来
bundle1.each do |row|
  p row # => ["1", "1", "1.0", "hoge", "f", "2021-01-01", "2021-01-01 00:00:00", ""]
end

bundle2 = QueueryClient.query(sql, enable_cast: true) # 型キャストオプション追加
bundle2.each do |row|
  p row # => [1, 1, 1.0, "hoge", false, Fri, 01 Jan 2021, "2021-01-01 00:00:00", nil]
end

また、副産物として従来では文字列型の空文字列と区別しづらかったnullをきちんと区別できるようになりました。

BarbequeからRedshift DataAPIへの非同期処理移行

QueueryではSQLを受け付けてからUnload文の実行結果を返却するまで、処理時間はSQLの内容に依存しています。SQLによっては非常に時間がかかってしまうため、非同期化をする必要がありました。そこで、元々はBarbequeというキューシステムを利用してジョブの非同期化をしていました。BarbequeはDockerとSQSを利用したジョブキューシステムです。

以前はこれでうまくいっていたのですが、2020年4月に起きたSQS障害で影響を受けたことや、Queueryの構成が複雑化していたことなどもあり、もっとシンプルで頑健性の高い仕組みにできないかと考えられていました。 そこで、2020年にRedshift Data APIが発表され、そのAPIに含まれるexecuteStatementdescribeStatementを利用すればBarbeque依存を外せそうだという案が上がりました。調査したところ非同期処理の周辺をこちらで保つ必要がなく、Queueryの構成をシンプル化できそうだということがわかりました。

f:id:ragi256:20211202115057p:plain
移行によるQueuery構成の変化

移行後は特に問題らしい問題が発生すること無く安定して稼働し、無事Barbequeからの依存を取り除くことができました。

Queueryと弊社データ基盤の構成

そもそもRedshift Data APIが扱えるのであれば、各開発者が自由にexecuteStatementをし、各自がUnloadをすればよいのではないか? そうすればこのシステムと運用は不要になるのではないかという意見もあるかと思います。

「背景」に書いたような理由から単にデータ取得をUnload文に絞りたいというのも理由にありますが、本当はもっと根本的な理由もあります。 弊社データ基盤では権限管理やデータガバナンスなどの運用観点から、設計思想にもとづくいくつかのポリシーがあります。(下記は一部抜粋で、他にもこういったポリシーもあります)

  1. Redshift内部がカオス化するのを避けるため、Redshiftへの書き込みはDWHチームが管理しやすいよう手段を限定する
  2. Redshiftへのバルク&ストリーミングロード、DWH内部のETLバッチ(集計処理など)、外部へのデータ転送は各種専用ツールを使ってワークフローを分ける
  3. できる限り自動化を進め、権限を移譲できる部分はできる限り強い権限を各チームに移譲し、各自でやってもらう

弊社がQueueryやBricolageといったDWH用ツールを作り、運用している理由はここにあります。DWHチームによる中央集権ではなく、できる限り民主的なデータ活用を推進していくにあたって、無秩序や混沌を避けるための必要な施策がDWH周辺ツールの充実でした。Queueryもまたその1つです。

Queueryを扱うことで社内の開発者誰もが気軽にRedshiftを活用できるようにしつつも、DWHチームによるデータフロー把握や障害時対応がしやすくなります。Redshiftからのデータ取得手段をQueueryに絞ってしまうことで何か不便であったり問題が発生するようなことがあれば、その都度上記のポリシーを考慮しつつチームで解決策を考え、実装していけばいいという方針です。

つまり、利用者の権限を緩めて自由に利用してもらいつつも、必要なところは手段を固定し、DWHチームによる運用負荷を減らすために必要だったということです。

DWH基盤を整えるためのエコシステムとQueuery

2021年を通して上記のような改修作業を続け、活発な開発が行われてきたQueueryでしたがOSSとしてGitHubに公開されていたのはクライアント側の実装のみでした。開発を続けてきて構成もシンプル化できたこともあり、今回OSSとしてサーバー側の実装も公開することとしました。これで、Redshiftに対するbatchシステム用ツールファミリー bricolages以下にQueuery周辺ツールが全て揃いました。

(※ redshift_connectorはRedshiftからデータを取得した後、ActiveRecordを利用してRDBMSのテーブルを簡単に更新できるようにするgemです)

[2021-12-09 追記] Python版クライアントも公開されました。PyPiから利用できます。 https://github.com/bricolages/queuery_client_python

Techlifeでも何度かご紹介している(2017年版2019年版2020年版)通り、弊社データ基盤グループはRedshiftを中心としてDWHとその周辺システムを構成しています。Redshiftを活用したデータ基盤構築するために必要なツール群のほとんどは内製であり、ツールを組み合わせて運用しています。

今回、また1つQueueryというデータ基盤を構築するエコシステムの一部を新たに公開することができました。クックパッドではDWHだけに留まらず、bdash-serverDmemoなどの多くのデータ関連ツールをOSSとして開発し、公開しています。これらのツールがより多くの人に使われ、活発な開発のもと相互に連携し扱いやすいエコシステムを形成する未来が訪れれば良いと考えています。

既存実装を活用しつつJetpack Composeを用いてクックパッドAndroidアプリの買い物機能を高速に開発している話

こんにちは、クックパッド事業本部 買物サービス開発部の佐藤(@n_atmark)です。 2019年新卒で入社後、クックパッドの新規サービスである「クックパッドマート」の開発に従事しており、2020年からはレシピサービス「クックパッド」iOSアプリの買い物機能を開発していました。

レシピサービス「クックパッド」の買い物機能は、iOSアプリ(以下: クックパッドiOS)で先行リリースしており、現在Androidアプリ(以下: クックパッドAndroid)でも同様の機能開発を行っています。 本稿ではクックパッドAndroidの買い物機能の開発について紹介します。

買い物機能とは

生鮮食品ECサービス「クックパッドマート」の仕組みと連携し、レシピサービス「クックパッド」のアプリから食材を注文できます*1

詳しくはiOSアプリ向けの買い物機能の開発について紹介している​​ SwiftUI を活用した「レシピ」×「買い物」の新機能開発 をご覧ください。

画面スクリーンショット
買い物機能の画面例

なぜやるのか

クックパッドで目指している「毎日の料理を楽しみにする」の実現のためです。

現状のレシピサービスでは、料理に制約があります。冷蔵庫にある材料からレシピを探し、冷蔵庫にあるもので料理をつくる ━━ この流れは非常によくある形ですが、「冷蔵庫にある食材」に制約された窮屈な体験とも言えます。

あり物の食材からレシピを探している図
冷蔵庫にある材料からレシピを探す体験

楽しみが広がる、まったく新しい「買い物」を

クックパッドマートは、流通の仕組みから開発しているまったく新しい生鮮食品ECサービスです。

  • 1品からでも送料無料
  • 価格も割安
  • 新鮮
  • 品揃えも豊富
  • 受け取りも楽にできる

cookpad-mart.com

━━ そのような新たな買い物手段があることで、これまでになかった料理の選択肢や楽しみが広がります。

この新たな「買い物」をレシピサービスにうまく融合させることで、今まで以上に料理を楽しみにできると考えています。

例えば、「めずらしい野菜で作るハロウィン料理、気になる!」「バターナッツかぼちゃ美味しそう!食べてみたい!」のように、食べたいものを起点に料理を作りたくなる体験もその一つです。

ハロウィン料理にぴったりの珍しい野菜が載っているスクリーンショット
食べたいものを起点に料理を作りたくなる体験

この新しい「レシピ」×「買い物」の体験は現在クックパッドiOSでのみ提供していますが、既に多くのユーザーから好評をいただいています。もっと多くのユーザーさんに利用していただけるようにするため、今回クックパッドAndroidにも買い物機能を追加することになりました。

買い物機能を最速でリリースするために選択したこと

「レシピ」×「買い物」の新しい体験はまだまだ成熟しておらず、日々チーム内で新しいアイデアを議論したり、ユーザーインタビューを繰り返しています。

note.com

チームとしてはまだまだ体験を突き詰めたいフェーズなので、利用できるユーザーを増やすためのAndroidアプリ開発だけに全リソースを投入できる状況ではありません。そのため、最低限のリソースで最速にリリースを行い、リリースされてからの体験改善に時間やリソースを使いたいと考えています。

再利用可能なAndroid実装の活用

冒頭でも紹介した通り、今回開発中の買い物機能は生鮮食品ECサービス「クックパッドマート」の仕組みを利用しています。

クックパッドマートの利用に特化した専用アプリ(以下: マートAndroid)も既に開発されています。

下のスクリーンショットは、マートAndroidとクックパッドAndroidの買い物機能のそれぞれの商品詳細画面です。

マートAndroid
クックパッドAndroid

画面の表示内容や方法はデザイン上、似通っていることがわかると思います。 画面構成が同じ箇所に関しては実装の使い回しができるのではないかと考えました。

宣言的UIフレームワークの活用

SwiftUI を活用した「レシピ」×「買い物」の新機能開発 にもあるように、クックパッドiOSの買い物機能は SwiftUI を用いて開発されています。約1年半 SwiftUI を用いてサービス開発をしていたこともあり、チーム開発で宣言的UIフレームワークを活用するためのノウハウも溜まってきていました。

一方、Android界隈でも Jetpack Compose が stable release され、クックパッドAndroidの開発にも Jetpack Compose が既に利用され始めていました。

SwiftUI でのノウハウを踏まえて、Jetpack Compose を活用することで実装コード量を減らすことや、プレビューを用いてビルドサイクルを短くすることで、素早く開発を進められるのではないかと考えました。

方針

これらの状況を踏まえて、クックパッドAndroidの買い物機能では Jetpack Compose を利用することで

  • マートAndroidの既存リソースを再利用できる
  • クックパッドiOSの買い物機能で培った宣言的UIフレームワーク活用の知見を利用できる

というメリットを活かす形で開発を始めました。

Jetpack Compose にはナビゲーションを行うためのコンポーネントなどもありますが、それらは使わずに素朴なUIコンポーネントのみをView部分で利用し、画面遷移などは既存のVIPERアーキテクチャ*2にのっかる方針をとっています。

これはクックパッドiOSの買い物機能と同様の設計方針です。VIPERアーキテクチャの View 部分にのみ SwiftUI を利用する設計を1年半続けた上で大きなハマりもなくメリットを享受できているためです。(詳しくは: SwiftUI を活用した「レシピ」×「買い物」の新機能開発

また、今回開発速度を重視するに当たって、画面のUIを意図的にクックパッドiOSの買い物機能ではなくマートAndroidに寄せている部分があります。

マートAndroidはほぼ全ての画面で RecyclerView を利用しており、各アイテムは Layout XML を用いた従来の View で実装されています。(詳しくは: クックパッドマートAndroidアプリの画面実装を最高にした話【連載:クックパッドマート開発の裏側 vol.4】

この特徴を生かして、Jetpack Compose における相互運用 API として用意されているAndroidView/AndroidViewBinding*3を用いることで、マートAndroid で画面を構成している Layout XML をそのままクックパッドAndroidで利用することで開発速度の向上を図っています。

実装について

Jetpack Compose版VIPERベースのアーキテクチャ

Jetpack Compose版アーキテクチャに関して、2020年のクックパッドAndroidアプリのアーキテクチャ事情 から一部変更している箇所があるので、変更点について紹介します。

アーキテクチャの図
Jetpack Compose版アーキテクチャ

Presentationレイヤーについて

2020年のクックパッドAndroidアプリのアーキテクチャ事情 ではInteractor や Routing との連係は Presenter が行っており、ViewModel は View 実装の一部という扱いで Contract には処理を記載していませんでした。Jetpack Compose版アーキテクチャでは Presenter は廃止され、Interactor や Routing との連係は新たにContract に記載された ViewModel(実態は AAC の ViewModel)が行うようになっています。

データフローについて

2020年のクックパッドAndroidアプリのアーキテクチャ事情 では Rx を用いてレイヤー間の連係を行っていましたが、Jetpack Compose 版アーキテクチャでは Kotlin Coroutines を使用しています。

基盤実装としての ApiClient は Rx ベースで実装されているため、DataStore で kotlinx.coroutines.rx2.await を用いて Rx の Single を suspend 関数に変換しています

import kotlinx.coroutines.rx2.await
import javax.inject.Inject

class ApiProductsDataStore @Inject constructor(
    private val apiClient: ApiClient
) : ProductsDataStore {
    override suspend fun getProducts(): List<Product> {
        return apiClient.get(path = "/products")
            .await()
            .decodeJSONArray()
    }
}

UI層でのみJetpack Composeを活用する実装に関して

上述のようなアーキテクチャの差分を踏まえた上で、ViewとViewModelの連係について紹介します。

View層の概略図
Jetpack Compose を組み込んだVIPER View 層の概略図

Activity/Fragment が ComposeView として Screen-suffix の Composable 関数を持つような形をとっています。 Screen-suffix の Composable 関数には ViewModel を渡しており、Composable 関数からのクリックイベントなどは ViewModel の関数を呼び出すようにしています。また画面表示のために、ViewModel は StateFlow を公開しており、StateFlow の状態変化に応じて画面を更新できるようにしています(詳しくは後述)。

UILayerの実装に関して

クックパッドAndroidでは Activity/Fragment を用いた View ベースの設計になっています。そのため、Jetpack Composeの相互運用 API として用意されている setContent 関数を呼び出して、Compose ベースの UI を追加しています。

Activity

class ProductListActivity : RoboAppCompatActivity() {

    private val viewModel: ProductListCreateViewModel
        by lazy { ViewModelProvider(this).get(ProductListCreateViewModel::class.java) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ProductListScreen(viewModel)
        }
    }
}

Fragment

@AndroidEntryPoint
class ProductListFragment : Fragment() {
    @Inject
    lateinit var viewModelFactory: ViewModelFactoryProvider<ProductListViewModel>

    private val viewModel: ProductListViewModel by viewModels { viewModelFactory }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                ProductListScreen(viewModel)
            }
        }
    }

    companion object {
        fun newInstance() = ProductListFragment()
    }
}

ViewModelの実装に関して

ViewModel では Interactor/Routingの連係と、UI の状態と表示するコンテンツを管理しています。 実装クラスは ViewModel (Jetpack) が担当し、ViewModel interface に基づいた画面を実装します。 MutableStateFlow によって画面の状態を管理しますが、状態の変更は内部で行い外部には StateFlow を公開しています。

class ProductListViewModel @Inject constructor(
    private val interactor: ProductListContract.Interactor,
    private val routing: ProductListContract.Routing
) : ViewModel(), ProductListContract.ViewModel {

    private var _state = MutableStateFlow<ScreenState>(ScreenState.Loading)

    override val state = _state.asStateFlow()

    init {
        fetchProducts()
    }

    override fun onProductDetailPageRequested(productId: Long) {
        routing.navigateProductDetail(productId)
    }

    private fun fetchProducts() = viewModelScope.launch {
        runCatching {
            interactor.fetchProducts()
        }
        .fold(
            onSuccess = {
                _state.value = ScreenState.Idle(
                    ProductListContract.ViewModel.ScreenContent(
                        products = it
                    ) 
                )
            },
            onFailure = {
                _state.value = ScreenState.Error(
                    reason = it.errorStatus.reason,
                    reloadAction = ::reload
                )
            }
        )
    }
}

Screenの実装に関して

Activity/Fragment が ComposeView として表示している Composable 関数を Screen という命名にしています。

@Composable
fun ProductListScreen(
    viewModel: ProductListContract.ViewModel
) {
    val state = viewModel.state.collectAsState()
    AsyncLoadSurface(state = state.value) { content: ProductListContract.ViewModel.ScreenContent ->
        ProductListScreenContent(
            products = screenState.products,
            onTapProduct = viewModel::onProductDetailPageRequested
        )
    }
}
 
@Composable
private fun ProductListScreenContent(
    products: List<Product>,
    onTapProduct: (Long) -> Unit
) {
    LazyColumn {
        item {
            HeaderSection()
        }
        items(products) { product ->
            ProductSection(
                product = product,
                onTapProduct = onTapProduct
            )
        }
        item {
            FooterSection()
        }
    }
}

@Composable
private fun ProductSection(product: Product, onTapProduct: (Long) -> Unit) {
    AndroidViewBinding(
        factory = ViewProductSectionBinding::inflate,
        modifier = Modifier
            .background(CookpadColor.Ivory)
            .padding(horizontal = 20.dp)
    ) {
        textView.text = product.name
        button.setOnClickListener {
            onTapProduct(product.id)
        }
    }
}

買い物機能は基本縦スクロールの画面構成となるため、画面実装は LazyColumn をベースにした画面実装にしています。 意味のあるまとまりで画面を要素分割し、LazyColumn の item に要素ごとのまとまりを入れています。この item の各要素をSection という単位で分割しています。

画面をSectionで分割しているスクリーンショット
LazyColumnのitem単位の分割

Section の中で実装している画面は、既存リソースがある場合は AndroidView/AndroidViewBinding を用いて再利用を行い、新規で実装が必要な部分に関しては Jetpack Compose を利用してレイアウトを作成しています。

実際どうだったのか

うまくいった点

  • Jetpack Compose ベースのUI構築をしたことで、RecyclerView を用いる際の Adapter/ViewHolder を書く必要がなくなり、記述量が削減されることで開発速度の向上に繋がった
  • LazyColumn の中身を Section という単位で分割する指針を早いうちに決められた
    • これによって、AndroidView/AndroidViewBinding を用いて Layout XML や Android View などの既存リソースを使う場合も、RecyclerView を利用する際のリストアイテムを苦労なくJetpack Composeベースのレイアウトに組み込むことができた
  • SwiftUI でチーム開発している時に課題となったコンポーネントの粒度について早めに共通認識をつくることで、読みやすく保守しやすい状態を維持して開発できた*4

うまくいかなかった点

  • Jetpack Compose に慣れてない段階では、既存の Layout XML をコピーして一部を変更する方が速かったので、AndroidViewBinding を活用してレイアウトできることがメリットになっていた
    • しかし、Jetpack Compose に慣れてくると、該当画面を探してコピーしてくるより、一から Jetpack Compose で書く方が速くなり、恩恵を受けていたのは最初のうちだけだった

まとめ

買い物機能を最速でリリースするために、利用可能な既存実装の再利用や Jetpack Compose を用いて、速度向上を図っている事例を紹介しました。買い物機能は2022年頭にファーストリリースを目指して絶賛開発中です。今後も開発状況を発信していきたいと思っていますので、ぜひ応援よろしくお願いします。 リリースされたらぜひ使ってみてくださいね!

クックパッドでは一緒に働く仲間を募集しています!

今回は買い物機能の開発にあたっての工夫や Jetpack Compose の活用事例についてご紹介しました。

新しいフレームワークを活用して、高速にサービス開発を進めることで事業をドライブしたいエンジニアを大募集しています。

カジュアル面談や学生インターンシップなども随時実施していますので、ぜひお気軽にご連絡ください。

info.cookpad.com

*1:近隣地域の生産者や市場直送の新鮮でおいしい食材を、1品から送料無料で購入できる。https://info.cookpad.com/pr/news/press_2020_1015

*2:クックパッドAndroidアプリはVIPERアーキテクチャを用いて開発されています。 詳しくは 2020年のクックパッドAndroidアプリのアーキテクチャ事情 を参照ください。

*3:AndroidView/AndroidViewBindingはJetpack Composeの相互運用 API として用意されているもので、AndroidView や XML Layout を Compose UI のレイアウト階層に含めることができる。

*4:コンポーネント粒度に関して共通認識をつくるための取り組みに関して チームでSwiftUIを書くために ~読みやすく保守しやすい設計について考えたこと~ にまとめていますので、こちらも参照ください。

DroidKaigi 2021 において、「2020年代の WebView 実装」というタイトルで発表しました

こんにちは。モバイル基盤部のこやまカニ大好きです。

先日行われた DroidKaigi 2021 で「2020年代の WebView 実装」というタイトルで発表させていただいたので、今日はその発表を簡単にまとめようと思います。 当日聴いて頂いた参加者の方、本当にありがとうございました。 流れていくコメントを眺めていると他社でも WebView 増殖はやっぱり発生するんだなあという気持ちが沸き起こり、胸が熱くなりました。

現時点で話せる内容は大体話せたと思うので、この記事では DroidKaigi公式の動画と発表資料の共有、当日うまく答えられなかった質問への回答だけ記載しようと思います。 最高の WebView を作る作業はまだ継続中ですので、完全版 WebView に関しては完成し次第別記事でお知らせいたします。

動画

https://www.youtube.com/watch?v=IOnpHyOg5sc

資料

https://speakerdeck.com/nein37/saikou-no-webview-2021

補足

動画(とコメント)を見直していて、なぜそこまで WebView が必要なのかという部分についてうまく説明できていなかったと思ったので少し補足します。

クックパッドアプリの WebView の用途は大きく分けて3つ存在します。

  1. 新規登録などの一部の特殊な画面
    • 新規登録画面は過去ネイティブで実装されていましたが、2019年に Web ページ側の新規登録フローを改善した際にアプリでも WebView から新規登録するように変更しました
    • この部分の WebView は役割が非常にはっきりしていて分かり易く、クックパッドアプリの WebView の中でもかなり良い使い方だと思います
  2. 課金導線(LP)の表示
    • LPはキャンペーン施策によって画面表示を一斉に、大きく切り替えたい場合があります
    • 利用規約、プライバシーポリシーの変更や無料期間の修正などは即時切り替える必要があります
  3. APIが存在しない機能の表示
    • 古いサービスなのでそういうページもあります…
    • これはリソースを割けばネイティブなUIに置き換えられますが、リソース配分など様々な理由で現在でも WebView として提供しています

このうち、 1. の用途についてはこれまでほとんど問題なく動作していました。 もともとここだけ Kotlin + VIPER で実装されていたという事情もありますが、ネイティブ実装された画面に遷移しづらい新規登録画面ということも大きいと思います。 この画面は WebView でかなりうまく動いているので、これからも WebView のままになると思います(10年間 WebView のままかどうかはわかりませんが)

2. の課金導線ページ表示はかなり複雑です。 アプリ内の様々な箇所から課金導線ページへの遷移があり、課金導線ページ内からもレシピページなどネイティブ実装された画面への遷移が存在します。 課金導線ページはコンテンツの表示をサーバサイドで細かくコントロールしたい事情があるので、これからも WebView で実装され続けることでしょう。 この課金導線ページの表示をできるだけ簡単に実装できるようにするのがクックパッドアプリにおける最高の WebView への道だと考えています。

3. が存在する理由は単純で、 WebView でしか表示できないコンテンツがあるため WebView を使って表示しています。 このパターンのWebページからはネイティブ実装されたレシピ詳細画面や検索結果画面への遷移が頻繁に発生するため、最も複雑な実装になっています。 今回の発表にあった Routing に実装したネイティブ画面への遷移処理のほとんどは、このパターンのWebページの表示のために必要になった実装です。 WebView 以外の解決方法としてネイティブ画面で再実装することもできますが、アプリ開発に使えるリソースは有限です。 アプリが大きくなるにつれて、大きくコストを割けない機能というものはどうしても出てきます。 そういったコンテンツの表示をサポートし、ユーザーに価値を届けられる仕組みとして WebView は必要だと考えています。

クックパッドアプリの実装では 3. 用途の WebView に若干 2. の機能が混ざったりしているので、今後の改修でどんどんシンプルで使いやすい WebView にしていく予定です。

最後に

発表の中でもお伝えしましたが、クックパッドではAndroidエンジニアを募集しています!

  • WebView 大好きなエンジニア
    • 一緒に最高のWebViewにしましょう!!!
  • WebView が好きじゃないエンジニア
    • 社内のほとんどのモバイルアプリエンジニアはそうなので大丈夫です!
  • WebView にできれば触りたくないエンジニア
    • こやまカニ大好き以外のメンバーは今ほとんど WebView の実装には触ってないので大丈夫です!

上記に該当しない方でも募集中なので気軽にご応募下さい

https://info.cookpad.com/careers/