トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。

トレタに入社してから3ヶ月間で感じたこと

はじめに

はじめまして。2021年の10月から株式会社トレタでサーバーサイドエンジニアとして働いている神山です。

実務としては、トレタO/Xの認証/権限管理サービス周りを担当しています。

なおトレタO/Xについては代表のnoteをご覧ください。 note.com

今回はトレタに入社してから感じたことや、どのような業務をしているかなど記載していこうと思います。

トレタのエンジニアはこんな感じなんだなぁ、というのが伝われば幸いです。

簡単な経歴と転職の経緯

大学を卒業後、新卒で入社したのは産業電気機器のメーカーでハードウェアエンジニアとして4年半ほど勤務していました。

作る仕事がしたいという思いから入社したのですが、新規プロジェクトが中々立ち上がらないことから悩み、ソフトウェアエンジニアへの方針転換を決めました。

2020年1月からは受託開発企業に入社し、金融機関関連サービスの開発や、交通関係の予約システムの開発に従事していました。

サービス視点を持って開発したい、もっとスキルを伸ばしていきたいという思いから転職活動を開始、2021年10月にトレタに入社しました。

入社の決め手

1. トレタO/Xのエンハンスのために設計機会が豊富

トレタO/Xはローンチされてからまだ日の浅いサービスであることを知りました。

エンハンスのための設計余地がまだまだあるのではないかと考えていました。

加えて、マイクロサービスはモノリスを分散していく用途で採用している会社が多いかとおもいます。

新規サービスとしてマイクロサービスに携わることができるのは非常に貴重な経験なのではと考えました。

2. より難易度の高い設計能力を求められる

トレタO/XはSaaSであり、それを実現するためにマイクロサービスの構成となっています。

SaaSであるということは当然ながら、ユーザーの様々なユースケースをクリアするように機能設計をしていかなければなりません。

加えてマイクロサービスによる機能実現を図るため、各サービスにおけるデータ整合性などを考慮し設計をする必要があります。

以上を踏まえると、難易度の高い設計が必要となるのではと考えていました。

マイクロサービスに関する詳しい構成はこちらで解説していますのでご覧ください。

https://blog.hatena.ne.jp/toreta-dev/toreta-dev.hatenablog.com/edit?entry=13574176438044016049

3. 労働環境が良い

コロナの影響もあり、かなり早い段階からフルリモートを取り入れている会社であることを知りました。

フルリモートであることを必須の条件とは考えていなかったのですが、移動時間があまり好きではない僕としては出勤時間を0にできるというのは大きな魅力でした・・。

4. 心理的安全性が高い

面接の段階から、穏やかな方々が多いのではないかという印象がありました。

加えて様々な媒体でトレタのことを調べていたのですが、自社の利益だけを追求する、という方針を取らない印象が強くありました。

サービスを利用してくださる飲食店の方々のことを考え、どうすれば業界全体がよくなるのかという視点を常に持ち続けていることが凄く印象に残ったのを覚えています。

実際に入社して感じたこと

1. トレタO/Xのエンハンスのために設計機会が豊富

こちらは入社前に予想していた通りでした。

僕が担当しているのは認証/権限管理サービスというトレタO/Xの中のごく一部ではあるのですが、入社してすぐに機能設計を行うこととなりました。

トレタの設計はかなり綿密で、機能のユースケース洗い出しから一つ一つのデータの概念、またその概念の定義から考えていくこととなります。

この時CTOとともにミーティングを重ねつつ設計を進めたのですが、

自身よりも圧倒的に技術力のある方からレビューを受けられるというのは、かなり嬉しいカルチャーショックでした。

現段階では設計フェーズは終わり、2022年の早い段階で機能リリースできるように実装を進めています。

2. より難易度の高い設計能力を求められる

こちらに関してもサービスの難易度の高さを痛感することとなりました。

マイクロサービスを採択していることにより、当然ながら各サービスで保持している機能、保持しているデータが存在します。

これらのデータ整合を保ちつつ今後のエンハンスを見据えた機能設計をするためには、様々な前提条件を把握していかなければなりません。

ただ前提条件を抑えるだけでもかなり四苦八苦しており、そこからどうすればより良いユーザー体験を作り出すことができるのかというのは非常に難しく、その一方で技術者としては良い環境だと感じています。

3. 労働環境が良い

こちらも入社前伺っていた通り、エンジニアは全員フルリモートに対応して業務に当たっています。

また通勤時間がない分、副業をしている社員もいるようです。

僕自身も副業や勉強の時間をしたり、自身の趣味に時間を割くことができるようになりました。

4. 心理的安全性が高い

こちらも入社前に予想していた通りでした。

業務の中でわからない部分や、設計のために意見や意図を収集したりするのですが、質問した全員が快く相談に乗ってくださります。

やはり穏やかな社員が多く、プライベートな話題でもちょっとした笑いを混ぜつつ会話をする方が多いです。

まとめ

入社して何より感じるのは、今トレタは転換期を迎えているということです。

コロナによる飲食業界の意識変革、今までのモノリシックな技術からの脱却、O/Xのリリースなど新しい挑戦の場が多く用意されています。

だからこそ、事業の成長とともにエンジニアとしてもスキルアップを図ることができるのではないかと考えています。

さいごに

トレタでは一緒に開発する仲間を募集しています。
興味がある方は是非カジュアル面談へお越しください!

www.wantedly.com

トレタO/Xの開発にジョインしてやってきたこと

こんにちは、パパエンジニアのkentaroです.

本記事はトレタ Advent Calendar 2020の 24 日目の記事です。
去年の終わりにO/Xの開発に携わるようになってから様々な経験を積むことができたので、今回はそれを振り返ってみたいと思います。

なお、「トレタO/X」については代表のnoteをご覧ください。

note.com

O/Xジョイン前

トレタの主力商品である予約/顧客台帳サービスのiOSアプリ開発を担当していました。

toreta.in

トレタ入社以前も2014年8月から Swift / Objective-C によるiOS開発をずっとやってきていた、というキャリアです。
(なお、その前は営業等をやっておりエンジニアではありませんでした。これはこれで語れる話なのですが、別の機会に譲ります。)

O/Xジョイン

2020年11月中旬、当時開催されていたネイティブ定例にkitagawaさんが参加し、「O/Xでエンジニアが足りていないんだけど、やってみたい人いませんか?」というのに手を挙げたのがきっかけです。 まずはワンダーテーブルさまのよなよなビアワークス向けのモバイルオーダーアプリ開発を担当することになりました。

yonayona

技術スタックとしてはフロントエンドがFlutter Web、バックエンドがFirebaseという構成でした。

firebase

ジョインしたはいいがしんどい

11月中に諸々のインプットをしてもらい、12月入ったところで環境構築を行い実際に開発を開始しました。
今振り返ってみてもこの頃はめちゃくちゃしんどかったです。

しんどさの原因は「なんもわからん」状態だったこと。

技術面ではそれまで慣れ親しんできたSwiftからDartに変わり、Flutterもしくは採用しているpackageが提供する機能などとにかくキャッチアップすることだらけでした。
公式ドキュメントが充実していること、SwiftUIの経験があったので宣言的UIへの抵抗感がなかったことは救いでしたが、開発効率はiOSのときとは比べ物にならないほど落ちていた実感がありました。

また、モバイルオーダーという文脈だと予約事業とは異なったドメイン知識が要求されるのですが、それが圧倒的に不足していたことも辛かったです。

しんどさをなくす

これはもう特効薬はなく、愚直に「わかるようになる」しかありませんでした。

技術面では同僚エンジニアとビデオ会議をしまくり、実装で詰まっているところの相談をしたり、PRの意図の確認をさせてもらったりしていました。
その後は試行錯誤しながらコードを書いては動作確認をしたり、PR作ったり、うまくいかなかったらまた相談したり…と、ひたすらプロダクトと向き合うしかありませんでした。

ドメイン知識に関しては社内のドキュメントを読み漁り、わからんことがあれば有識者に質問をしたり…のような当たり前のことを継続し続けていました。

2021年2月に自分が担当した比較的大きめな機能追加がリリースされた頃にはしんどさはほとんど無くなっていたように記憶しています。

共通基盤対応

この頃のバックエンドはPoC期間のプロトタイピング用のものだったため、製品版として共通基盤を利用する対応が必要となってきていました。
全体像としてはマイクロサービスへの挑戦とその結果を参照ください。

micro-service

自分はフロントエンド側の以下対応を担当しました。

  • ユーザー向けアプリの共通基盤対応
    • 共通基盤に向けてのAPIリクエスト周りの実装
  • スタッフ向けアプリのリプレイス
    • 共通基盤に向けてのAPIリクエスト周りの実装
    • Flutter2系を採用
    • Riverpodによる状態管理

主な挑戦

APIクライアントの実装に挑戦してみました。
詳細は割愛しますが、リクエスト用抽象classをextendsしたリクエストclassを渡してあげると、リクエストclass側で期待している型のレスポンスが受け取れるような仕組みになっています。
SwiftではProtocolとGenericsで実装するイメージですが、同じ要領で抽象classとGenericsを使い実装しました。
Dartでも抽象的な処理が書けるんだなーと感動したのを覚えています。

6月リリース

こちらの共通基盤対応は無事6月にリリースされました!

議事録を書くようになった

この頃から日々のMTGやスタンドアップの議事録を書くようになりました。
もともと自分とは別の法人のプロジェクトを担当しているQAが取り組みをはじめていて「ええやん!」と思って真似したのがきっかけです。
話したのはいいんだけど、結局何が決まって何がNext Actionなんだっけ?というのがテキストとして残るとその場にいなかった人にも共有しやすいし、後から検索もできて便利だなと感じています。

tech.toreta.in

ちなみにNotionに各種議事録が作成されているのですが、会議体ごとにまとめて表示できるようにしてみたりもしました。
これを作ってたらNotion力が向上したような気がします。

notion

React化対応

PoCではFlutterで作っていたユーザー向けモバイルオーダーアプリをReactにリプレイスするプロジェクトです。
自分は2021年9月から本格的に参加しています。
こちらはまだリリースされておらず、絶賛開発中です。

Webフロントエンドへの挑戦

Flutter版もWebアプリではあったのですが、基本的にプラットフォームのことをあまり気にせず開発できるので、このプロジェクトでWebのフロントエンド開発に業務で初めて携わる感覚がありました。
技術スタックとしては、TypeScript・React・Next.jsを採用しています。

個人ブログを上記技術スタックで開発した経験はあったものの、業務での開発はやはり一味違いました。
TypeScriptの重鎮にPRをレビューしてもらうので色々と指摘されることはあるのですが、意図・根拠が明確でめちゃくちゃ勉強になりました。
まだまだ勉強することがたくさんありますが、楽しんでやれているのはいいことかなと思っています。

振り返ってみて

本当に色々なことやったんだなーと感じました。1年じゃなくて、2, 3年くらい経ってるんじゃねーの?という感覚です。
新しい環境に飛び込むとこんなにも密度が高いんだなーという気持ち…
まだ書きたいことはあったけど、キリがないので特に印象深いことに絞って書きました。

あと、SwiftもDartもTypeScriptも好きだなーということに気がつけました。みんないい言語!!

最後に

新しい技術に挑戦したい方も、得意な技術でバリューを発揮したいという方も、もし興味がありましたらお気軽にカジュアル面談等お申し込みください!

www.wantedly.com

RailsでBFFを構築した際、複雑にならないように気をつけたこと

はじめに

この記事はトレタ Advent Calendar 2021 の22日目の記事です。

こんにちは、昨年と同じ日の投稿になりました、サーバーサイドエンジニアの @shiroemons です。

GoとRuby on Railsを書いてます。

最近、RailsでBFF(Backends For Frontends)を構築しました。 BFFについての細かい説明は省略しますが、簡単に言いますとフロントエンドからのリクエストに応じて複数のバックエンドに対して、APIコールしデータを取得し内容を加工してフロントエンドに返却するAPIサーバーです。

構築した際に、複雑にならないように気をつけたポイントを3点紹介します。

バックエンド毎のClientをPOROで作成

データを取得するバックエンドは、GraphQLとRESTと異なるAPIが存在していました。

GraphQLと通信する際、graphql-clientを検討しましたが、AuthorizationヘッダーやIdempotency-Keyヘッダーをリクエスト毎に変える必要があったため、使い方がアンマッチでしたので不採用としました。 採用した方法は、HTTPクライアントライブラリの Faraday を使用し、各バックエンド用(GraphQL, REST)のClientをPORO(Plain Old Ruby Object) で作成して対応しました。

サンプルを以下に記載しています。

GraphQL用のClient

  • 親クラス
# app/models/graphql_example_client/client.rb
module GraphqlExampleClient
  class Client
    def initialize(auth_token, **options)
      @auth_token = auth_token
      @options = options
    end

    def call
      response = run
      JSON.parse(response.body)
    rescue JSON::ParserError
      {
        errors: [
          {
            code: response.status,
            message: response.body
          }
        ]
      }
    end

    private

    def run
      Faraday.post(example_graphql_url, request_body, headers)
    end

    # https://example.com/graphql
    def example_graphql_url
      ENV['EXAMPLE_GRAPHQL_URL']
    end

    def request_body
      {
        query: query,
        variables: variables
      }.to_json
    end

    def query
      raise 'query called on parent.'
    end

    def variables
      raise 'variables called on parent.'
    end

    def headers
      {
        'Content-Type' => 'application/json',
        'Authorization' => @auth_token,
        'Idempotency-Key' => idempotency_key
      }
    end

    def idempotency_key
      ...
    end
  end
end
  • 親クラスを継承し、1クエリ単位でファイルを作成
    • プレフィックスにQueryMutationを付けておく
# app/models/graphql_example_client/query_locations.rb
module GraphQLExampleClient
  class QueryLocations < Client
    private

    def query
      <<~GRAPHQL.freeze
        query locations($where: LocationWhereClause!, $limit: Int, $offset: Int) {
          locations(where: $where, limit: $limit, offset: $offset) {
            locations {
              id
              name
              createdAt
              updatedAt
            }
          }
        }
      GRAPHQL
    end

    def variables
      {
        where: {
          createdAt: {
            gt: 0
          }
        },
        limit: limit,
        offset: offset
      }
    end
      
    def limit
      ...
    end
      
    def offset
      ...
    end
  end
end
  • 使い方
response_body = GraphQLExampleClient::QueryLocations.new(@auth_token, limit: params[:limit], offset: params[:offset]).call

REST用のClient

  • 親クラス
# app/models/rest_example_client/client.rb
module RestExampleClient
  class Client
    def initialize(auth_token, **options)
      @auth_token = auth_token
      @options = options
    end

    def call
      response = run
      JSON.parse(response.body)
    rescue JSON::ParserError
      {
        error: {
          code: response.status,
          message: response.body
        }
      }
    end

    private

    def rest_example_base_url
      ENV['REST_EXAMPLE_BASE_URL']
    end

    def run
      raise 'run called on parent.'
    end

    def request_body
      raise 'request_body called on parent.'
    end

    def headers
      {
        'Content-Type' => 'application/json',
        'Authorization' => @auth_token
      }
    end
  end
end
  • 親クラスを継承し1API単位でファイルを作成
    • プレフィックスにGetPostを付けておく
# app/models/rest_example_client/post_example.rb
module RestExampleClient
  class PostExample < Client
    private

    def run
      Faraday.post(url, request_body, headers)
    end

    def url
      "#{rest_example_base_url}/v1/example"
    end

    def request_body
      {
        hoge: hoge,
        ...
      }.to_json
    end
    
    def hoge
      @options[:hoge]
    end
  end
end
  • 使い方
response_body = RestExampleClient::PostExample.new(@auth_token, hoge: params[:hoge]).call

使い方は、newしてcallするだけなので簡単です✨

1リクエスト毎にファイルが別れているのでコードをスッキリ書くことができます✨

Interactor層の導入

BFFの特性上、クライアントからの1リクエストで同じバックエンドに対し別々のAPIを呼び出したり、他のバックエンドに対してもAPIを呼び出すことがあります。 愚直に書くとすぐに複雑になりテストも書きづらくなります。 そこで、複雑なビジネスロジックを解消するために、Interactor層を導入しました。 Interactor層を導入することで、ビジネスロジックをカプセル化することができます。

使用したgemは、 collectiveidea/interactor-rails です。 導入方法は、READMEを参照ください。

サンプルを以下に記載しています。 良いサンプルを用意できなかったため、具体例を使って紹介します。

  • 処理
    • 請求書から領収書IDを取得する処理(GraphQL)
    • 既存の領収書を無効化する処理(GraphQL)
    • 領収書を再発行する処理(REST)

Interactorのサンプル

  • 取りまとめ役のファイル
# app/interactors/invoice_receipt.rb
class InvoiceReceipt
  include Interactor::Organizer

  # 上から順番に実行されます。
  organize InvoiceReceipt::GraphqlExampleFetchInvoiceWithReceipt,
           InvoiceReceipt::GraphqlExampleVoidReceipts,
           InvoiceReceipt::RestExamplePostReissueReceipt
end
  • 請求書から領収書IDを取得する処理
# /app/interactors/invoice_receipt/graphql_example_fetch_invoice_with_receipt.rb
class InvoiceReceipt::GraphqlExampleFetchInvoiceWithReceipt
  include Interactor

  def call
    result = fetch_invoice_with_receipt
    if result[:errors].present?
      context.fail!(error: result[:errors])
    else
      context.receipt_id = result[:data][:receipt_id]
    end
  end

  private

  # GraphQLから請求書を取得する処理(GraphQLのQueryを実行する処理)
  def fetch_invoice_with_receipt
    ...
  end
end
  • 既存の領収書を無効化する処理
# app/interactors/invoice_receipt/graphql_example_void_receipts.rb
class InvoiceReceipt::GraphqlExampleVoidReceipts
  include Interactor

  def call
    return if context.receipt_id.blank?

    result = void_receipts
    if result[:errors].present?
      context.fail!(error: result[:errors])
    else
      context.void_receipt = result[:data]
    end
  end

  # 途中で失敗した場合、rollbackメソッドが呼び出される
  def rollback
    ...
  end

  private

  # 既存の領収書を無効化する処理(GraphQLのMutationを実行する処理)
  def void_receipts
    ...
  end
end
  • 領収書を再発行する処理
# app/interactors/invoice_receipt/rest_example_post_reissue_receipt.rb
class InvoiceReceipt::RestExamplePostReissueReceipt
  include Interactor

  def call
    result = post_reissue_receipt
    if result[:error].present?
      context.fail!(error: result[:error])
    else
      context.receipt = result
    end
  end

  private

  # 領収書を再発行する処理(RESTのPostを実行する処理)
  def post_reissue_receipt
    ...
  end
end
  • 実際の使われ方
class Invoices::ReceiptsController < ApplicationController
  def create
    # 領収書再発行処理
    result = InvoiceReceipt.call(
      auth_token: @auth_token,
      location_id: params[:location_id],
      invoice_id: params[:invoice_id],
      mail_address: params[:email],
      name: params[:name]
    )

    if result.success?
      render json: ReceiptPresenter.new.as_json, status: :ok
    else
      render json: ErrorsPresenter.new(result.error), status: :bad_request
    end
  end
end

使い方は、取りまとめ役のファイルをcallするだけなので簡単です✨

個別の責務を1ファイルで書くことで、可読性も向上し複雑にならずコントローラーもスッキリ書くことができます✨

Presenter層の導入

BFFということで、クライアントが必要とする形でレスポンスを返さないといけません。 APIモードで動かしているためViewファイルでJSONの組み立ては行わず、Presenter層を導入して対応しました。 こちらもPORO(Plain Old Ruby Object)で実装しました。

サンプルを以下に記載しています。

Presenterのサンプル

  • 親クラス
# app/presenters/presenter.rb
class Presenter
  def initialize(object = nil)
    @object = object
  end

  def as_json
    raise 'as_json called on parent.'
  end
end
  • 親クラスを継承してファイルを作成
    • サフィックスにPresenterを付ける
# app/presenters/locations_presenter.rb
class LocationsPresenter < Presenter
  def as_json(*)
    {
      locations: locations
    }
  end

  private

  def locations
    @object&.map { |o| location(o) } || []
  end

  def location(object)
    {
      id: object[:id],
      name: object[:name],
      created_at: object[:created_at],
      updated_at: object[:updated_at]
    }
  end
end
  • 実際の使われ方
    • mergeメソッドを使用したりして整形しています
# app/controllers/locations_controller.rb
class LocationsController < ApplicationController
  def index
    response_body = GraphQLExampleClient::QueryLocations.new(@auth_token, limit: params[:limit], offset: params[:offset]).call
    status = :ok
    if response_body[:errors]
      response_json = ErrorsPresenter.new(response_body[:errors])
      status = :bad_request
    else
      locations_data = response_body[:data][:locations]
      total = locations_data[:total]
      locations = LocationsPresenter.new(locations_data[:locations])
      pagination = PaginationPresenter.new(Pagination.new(total, params[:per_page], params[:page]))
      response_json = locations.as_json.merge(pagination.as_json)
    end
    render json: response_json, status: status
  end
end

使い方は、newしてas_jsonするだけなので簡単です✨

おわりに

コードの紹介がメインとなりましたが、できるだけ複雑にならないように気をつけてコードを書いています。雰囲気だけでも読み取れてもらえたら幸いです。

トレタに少しでも興味を持っていただいた方がいれば、カジュアル面談などお気軽にご応募ください!

www.wantedly.com

© Toreta, Inc.

Powered by Hatena Blog