クックパッドマートにおけるアカウント統合

こんにちは、買物プロダクト開発部の岸谷です。 クックパッドマートという生鮮 EC サービスのバックエンドエンジニアをやっています。

この記事は「クックパッドマートを支えるアカウントたち」の連載記事4日目です。

今回は、2-3 日目の記事にあったログイン機能にまつわるバックエンドの仕組みについて紹介していきます。

ユーザー登録無しでもアプリが使える、けど…

クックパッドマートのアプリはユーザー登録しなくても、アプリをインストールして、商品を選んで、受け取り場所を選択して、購入ボタンを押せば商品を購入することができます。 そしてこの情報は揮発することなく、前回買った商品を確認したり、次回のお買い物で同じ受け取り場所を使ったりすることができます。 当然ながらこういったユーザー情報はサーバーに格納されており、そのユーザー情報は iOS/Android の端末と紐付けられています。

端末と直接紐付いたユーザー

この認証の仕組みはユーザーにとって手軽なのがいいところですが、ユーザーが機種変更するときに情報を引き継ぐことができないという問題がありました。 そこでユーザーに明示的にアカウントを作成してもらい、そのアカウントで認証することによって、端末と切り離したユーザー情報を保持できるようにしました。 この仕組みをログインと呼んでいます。

ログインユーザー

ログインとアカウント統合

ユーザーに作成してもらったアカウントにユーザー情報を紐付けることによって、ユーザーは機種変更してもユーザー情報を持ち越すことができるようになりました。 ですが、ログインするとき、ログインするまで使っていた「端末と紐付いていた、ユーザー登録なしで使えていたときのユーザー情報」は揮発してしまうのでしょうか? それでは不便ですよね。 ここで出てくるのが「アカウント統合」という処理です。このアカウント統合は、

  1. 端末で認証していたユーザーが、
  2. 初めてログインしたとき、
  3. これまで使っていたユーザー情報を、ログイン後のユーザー情報にマージする

という処理です。これにより、購入履歴や受け取り場所といったログイン前のユーザー情報を、ログイン後も引き続き利用できるようになります。

ログイン処理の流れ

また、このアカウント統合処理は同じユーザー情報に対して複数回発生することがあります。 あるユーザーが複数台の端末を持っており、それぞれでクックパッドマートを利用していたケースです。 この場合、ユーザーはまず端末 A を使ってログインし、端末 A のユーザーと、ログイン後ユーザーが統合されます。 次にユーザーは端末 B を使ってログインし、端末 B のユーザーと、ログイン後ユーザーが統合されます。

複数端末でログインする場合

このようなケースも考慮してアカウント統合の処理を実装する必要があります。

アカウント統合のデータ処理

こう説明していくとなんだか難しく聞こえてきますが、実際のアカウント統合処理の骨子は以下のようにシンプルです。

  1. ユーザーモデルへの参照を持っているモデルを列挙する
  2. 各モデルについて、参照先ユーザーを統合元ユーザーから統合先ユーザーへ付け替える
  3. 統合元ユーザーを削除する

クックパッドマートのサーバー処理は主に Rails で書かれており、当然データモデルも ActiveRecord で記述されています。

# ユーザー
class User < ApplicationRecord
  has_many :orders
  has_one :user_location, dependent: :destroy
end

# 注文履歴
class Order < ApplicationRecord
  belongs_to :user
end

# ユーザーの受け取り場所
class UserLocation < ApplicationRecord
  belongs_to :user
end

アカウント統合処理は、ユーザーモデルへの参照を持っているモデルクラスに migrate_user_account というクラスメソッドを定義し、順番に呼び出して処理するような形にしました。 上記のモデル定義を例に取り、どのような処理になるかを見ていきます。

単純にユーザーを付け替えるだけの場合

たとえば注文履歴といったデータは、単純に持ち主のユーザーを付け替えるだけの処理で完了します。

class Order < ApplicationRecord
  belongs_to :user
  
  def self.migrate_user_account(source_user:, destination_user:)
    source_user.orders.update_all(user: destination_user)
  end
end

ユーザーを付け替えるだけでは済まない場合

たとえばユーザーが設定している受け取り場所は、1 ユーザーにつき 1 つしか設定できません。 このため単純な移行ができず、何らかのロジックを使ってどちらかのみを残す必要があります。 以下の例では、最終更新時刻が新しい方を残す、つまり最後に設定した方を残すというロジックで、統合後にどちらの受け取り場所を利用するかを選んでいます。

class UserLocation < ApplicationRecord
  belongs_to :user
  
  def self.migrate_user_account(source_user:, destination_user:)
    if source_user.user_location.updated_at > destination_user.user_location.updated_at
      destination_user.user_location.update!(location_id: source_user.user_location.location_id)
    end
  end
end

モデルによってはもう少し難しい場合もあり、たとえば以下のようなケースです。こういったものもモデルごとに一つ一つ検討し、実装していきます。

クックパッドマートには「リスト」という機能があり、複数の商品をリストにまとめ、そのリストに名前をつけて保存することができる。統合処理時、両ユーザーが同じ名前のリストを持っていた場合、リストに含まれる商品を突き合わせて、重複が無いようリストを合体させる。 クックパッドマートのクーポンには利用回数制限のあるものがある。統合処理時、両ユーザーが同じ回数制限クーポンを持っていた場合、それぞれのクーポンの利用回数を足して、そのクーポンの残り利用可能回数を算出する。

ユーザー情報の削除

最後に、統合元となったユーザー情報を消去します。

class User < ApplicationRecord
  def self.migrate_user_account(source_user:, destination_user:)
    source_user.destroy!
  end
end

これでアカウント統合処理が完了し、ログイン前のユーザー情報は無事ログイン後に引き継がれました。 実際には、クックパッドマートの User モデルは数十のモデルから参照されており、モデル一つ一つに対してデータ統合の方針を検討していく必要がありました。 どのみち数が多いので大変ではあるのですが、このようにモデルごとに分解して考えられるような設計にすると、処理全体の見通しは立てやすいのかなと思います。

クックパッドのアカウント基盤

「ユーザー登録をすることなく、端末で認証してユーザー情報と紐付ける」「ユーザーにメールアドレスとパスワードを使ったアカウントを作ってもらう」といった機能は、クックパッドマートで独自に開発したわけではなく、クックパッドのアカウント基盤を利用しています。 どういうことかというと、レシピサービスに登録しているメールアドレスとパスワードを使って、クックパッドマートにログインすることができます。(もちろん逆も然りです)

このような挙動は、クックパッドユーザー情報とクックパッドマートのユーザー情報を紐付けることで実現しています。 つまりクックパッドマートアプリでログインを行う際、実際にはクックパッドユーザーを使って認証を行い、それと紐付いたクックパッドマートのユーザー情報の利用を認可しています。

クックパッドユーザーと、クックパッドマートのユーザー

また、アカウント統合もクックパッドのアカウント基盤に実装されている機能です。 アカウント基盤は初回ログインを検知すると、次の流れでアカウント統合を実施します。

  1. クックパッドユーザー情報をアカウント統合する
  2. 他サービス (ここではクックパッドマート) にアカウント統合イベントを通知する
  3. 通知を受け取ったサービスは、自サービス内のユーザー情報をアカウント統合する

ログイン時のアカウント統合の流れ

このアカウント統合イベントは Amazon SNS のトピックとして実装されています。 社内基盤のジョブキューシステムである Barbeque は SNS トピックの購読機能があるため、通知が発生し次第 ECS タスクを立ち上げて、アカウント統合処理を開始することができます。 このようなアカウントシステムが整っている社内基盤を用いることで、社内で新しいサービスを立ち上げる際にも、認証・認可などの難しい設計に立ち入ることなく、サービスに関する設計・実装に集中することができています。

ログインの高速化

上記のアカウント統合イベントの通知の仕組みは、実装が簡単なためログイン機能の早期開発には役立ったのですが、問題がありました。 ジョブキューシステムを用いていることから本質的に処理が非同期であること、そして ECS タスクは起動に時間がかかってしまうことです。 実際にログイン機能の導入当初、クックパッドマートアプリでは初回ログインする際に 1 分程度「データ移行をしています」という画面を出し続ける状態になっていました。(このあたりの細かい話過去記事で解説しておりますので、ぜひご覧ください) この挙動はユーザー体験も悪く、またアプリの設計としても都度サーバーをポーリングしてアカウント統合の処理進捗を確認する、という難しい実装になってしまっていました。

これを解決するため、クックパッドのアカウント基盤を改修し、クックパッドマートアプリで初回ログインが発生した場合は、クックパッドマートに対してアカウント統合を実行する API を直接リクエストするようにしました。 幸いクックパッドマートのアカウント統合処理自体は 1 秒もかからないものであるため、ジョブキューシステムを用いた非同期的な処理から API のリクエストという同期的な処理に変更することにより、クックパッドマートマートの初回ログイン処理も、現在は一瞬で完了するようになりました。 このように、必要に応じてアカウント基盤自体の改修が実施できることも、社内基盤を用いている利点の一つです。

おわりに

クックパッドマートのログイン機能にまつわるサーバーサイドの処理を紹介しました。 アカウントの統合はあまり一般的な機能ではないかもしれませんが、クックパッドマートのようにログインレスとログインを両立させようとするとどうしても必要になってきます。 何気なく使ってるログイン機能でも、裏側ではこういう地道な処理が動いているんだなという雰囲気でも感じ取っていただければ幸いです。

最後になりますが、クックパッドマートでは一緒にサービスを開発してくれる仲間を絶賛大募集しています。 今回取り上げたログイン機能についても、ソーシャルログインの導入や、クックパッドのアカウント基盤を使ったコミュニケーションチャネルの整備など、まだまだやりたいことはたくさんあります。 EC サービスの開発って聞くとお堅いイメージがあるかもしれませんが、クックパッドマートは EC 機能を止めずに日に何度もデプロイが走るようなアグレッシブな開発環境で、きっと飽きさせることはないと思います。 EC サービスの開発に一家言ある人、認証・認可基盤の構築に興味がある人、とにかくユーザーに使ってもらうためのサービスの開発が好きな人等々、もし興味がございましたら以下から是非ご連絡お待ちしております。

https://cookpad.careers/services/cookpad-mart/

アカウント連載の記事一覧