Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

gRPC × Go × Node.js におけるエラーハンドリングの実現方法

この記事は「Gaudiy Advent Calendar 2022」の19日目の記事です。

Web3 スタートアップの Gaudiy でソフトウェアエンジニアをしている Namiki ( @ruwatana ) です 🙂

ワールドカップを楽しみすぎていたら(日本代表もそして数時間前の決勝戦も本当に最高でした 🙌)、あっという間に自分の番が来てしまったわけですが、先月個人にフォーカスした 入社エントリ を書かせていただいたのもあり、今回は直近取り組んでいた gRPC周りのエラーハンドリングで得た技術的なナレッジ を共有できればと思います。

みなさんエモさ全開の記事を書いているところ、ただ実直にTechでコアな話をしていきます。

ちなみに弊社 Tech Blog への寄稿は初となりますので、お手柔らかにお願いします。誤ったことを書いていましたらこそっとご指摘いただけますと幸いです 🙇

1. gRPC について

現在、Gaudiy のプロダクトにおけるバックエンドサーバー間の API 通信のプロトコルとして gRPC を採用し始めています。

そもそも gRPC とは?の詳細については今回は割愛させていただきますが、簡単にいうとインターフェースを定義した言語(IDL)である Protocol Buffers をベースとしたエコシステムを用いることで、クライアント・サーバー間のメッセージのやり取りを効率的に実現することができる仕組みです。

詳しくは、公式 docs をご参考いただければと思います。 grpc.io

Gaudiy のプロダクトにおけるシステム構成は執筆現在では以下の図のようになっており、Go 製のマイクロサービス間での一部やりとりや、Node.js 製の BFF と Go サービス間の一部やりとりなどに gRPC を徐々に採用していっています。

この辺りの初期導入は、Dev Enabling チームのメンバーを中心に行っています。取り組み内容の詳細については、また別途記事になった時にご覧いただければと思います(誰か書きましょう)。

GaudiyのTech stack

2. 独自のパターンを用いたエラーハンドリングの実現

自分の所属している Stream Aligned な開発チームでは、直近で決済基盤のリニューアル開発を行ってきましたが 、その実装をしていく上で、gRPC サーバーにて複数の独自エラーパターンの返却を行い、フロントエンドで表示を制御したいというユースケースに直面しました。

これはさまざまなシステムにおいても、ごく一般的なユースケースかと思うのですが、gRPC でどのように実現したのかを詳しく説明していきたいと思います!

2-1. gRPC で推奨されているエラーモデル

まず、gRPC 公式の docs を見ると、Error handling についての Guide が提供されていることがわかります。2つの Error model についての説明がなされていますが、どちらかに沿って実現していくことが提唱されています。 grpc.io

2-1-1. Standard error model

抽象的かつシンプルなエラーハンドリングに関しては、docs にある通り Standard error model での表現が可能です。

codemessage という文字列が参照可能となっており、サーバー側で任意の情報を詰め、クライアント側でそれをもとにハンドリングするといったやり方で実現が可能です。

詳しい実装方法については、公式 docs に記載されている下記リポジトリにて言語ごとにサンプルが公開されているので割愛します(とても参考になります)。 github.com

2-1-2. Richer error model

非常にシンプルなエラーハンドリングであれば Standard error model で事足りますが、オブジェクトの中にデータも詰めてエラー返却をしたいなどのケースを考慮すると、Richer error model での表現がベターになってきます。

今回、Gaudiy で実際に直面したユースケースでは、カスタムのエラーコードとそれに付随する任意の情報を含めたいといったものでした。その点、Standard error model でも上手くやりくりすれば実現自体はできたかもしれないのですが、より拡張性や共通フォーマットの利用を意識して、こちらのモデルにて実装を行いました。

2-2. Richer error model の実装について

それでは、実際にどのように実装していったか、サンプルコードとともに順に見ていきます。

2-2-1. Protocol Buffers の定義

まずは、エラーの汎用的な独自の message を Protocol Buffers に定義し、protoc で各々の言語のコードを自動生成します。

今回は以下のような感じで、独自の message を定義し、サーバー側ではこの型を用いて任意の情報を詰め、クライアント側でデシリアライズ (パース) して受け取るという流れで実装しました。

独自 message の各フィールドには docs でも推奨されている、google が提供している error_details.proto の各messageを採用し、汎用的かつ条件によってさまざまな情報を詰め込むことができるようにしています。

syntax = "proto3";

package gaudiy.apierror;

import "google/api/field_behavior.proto";
import "google/protobuf/any.proto";
import "google/rpc/error_details.proto";
import "google/rpc/status.proto";

~~~

message Details {
  google.rpc.ErrorInfo error_info = 1 [(google.api.field_behavior) = REQUIRED];
  google.rpc.BadRequest bad_request = 2;
  google.rpc.DebugInfo debug_info = 3;
  google.rpc.LocalizedMessage localized_message = 4;
  google.rpc.PreconditionFailure precondition_failure = 5;
  
   ...

また、任意のカスタムエラーコードなどの情報も Protocol Buffers で enum などを用いて定義しておくと、クライアント・サーバー双方から参照できるので良いと思います。

先述した通り、Gaudiy ではこの辺りの整備や方針は Dev Enabling チームが導入から利用までをサポートしてくれたので、現在は非常にシームレスな利用が可能になっています。フィーチャー開発がメインの我々にとっては本当に頼もしい限りです🙌 (大感謝)

2-2-2.サーバー側の実装

さて、サーバー側の実装ですが、Gaudiy ではシステム構成的にも基本的に Go での実装となります。

エラーの詰め方としては、 grpc/status パッケージの error インターフェースの実装型である status.Status ( google/rpc/status.proto の message に相当 ) の details フィールドに先ほど作成した独自の message をセットすることで実現しています。

下の例では、Required なフィールドとなる ErrorInfo の Reason に Protocol Buffers で定義しておいたカスタムエラーコード (enum) の文字列を詰めるような実装にしています。

もちろん、Metadata フィールドに customErrorCode のような key で格納しても良いと思いますし、カスタムエラーコードに依存した message を作ってしまうというのも良いと思います。この辺りは、開発メンバーの中で合意形成が必要でしょう。

以下が実装サンプルのコードになります。

import (
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
  "google.golang.org/protobuf/types/known/emptypb"

  "gaudiy/apierror"
  hogepb "gaudiy/hoge"
)

func (s *Server) Hoge() (*emptypb.Empty, error) {
  res, err := s.service.Hoge()
  if err != nil {
    // status.Statusを任意のcodeとerrから作成
    st := status.New(codes.InvalidArgument, err.String())

    // Status.detailsに定義した型を詰める
    st, _ = st.WithDetails(&apierror.Details{
      ErrorInfo: &errdetails.ErrorInfo{
        // pbに定義したカスタムのエラーコードをReasonに詰める
        Reason: hogepb.CustomErrorCode.Hoge.String(),
        Domain: "your.domain",
        Metadata: map[string]string{
          // その他任意の情報があれば詰める
          "key": "value",
        },
      },
      // その他の error_details フィールドにも任意の情報を詰められる
    })

    // statusからerrorを生成して返却
    // (wrapしてしまうとstatusが正しくセットされないのでその場合はUnwrapが必要)
    return nil, st.Err()
  }

  return &emptypb.Empty{}, nil
}

2-2-3.クライアント側の実装

Gaudiy ではクライアント側となりうるのは Go 製のマイクロサービスまたは Node.js 製の BFF です。それぞれの実装を見ていきます。

まず、Go で受け取る場合は先ほどの逆を同じパッケージを用いつつ行うだけなので非常にシンプルです 🙆

import (
  "log"

  "google.golang.org/grpc/status"
  "google.golang.org/protobuf/types/known/emptypb"

  "gaudiy/apierror"
)

func (c *Client) Hoge() (*emptypb.Empty, error) {
  res, err := c.service.Hoge()
  if err != nil {
    // errからstatus型への変換を試みる
    st, ok := status.FromError(err)
    if !ok {
      return nil, err
    }

    // status.detailsを取り出して独自の型へのキャストを試みる
    details := st.Details()
    if len(details) > 0 {
      switch d := st.Details()[0].(type) {
      case *apierror.Details:
        // expected case
        log.Printf("reason: %v", d.ErrorInfo.Reason)
      default:
        // unexpected case
      }
    }

  ~~~

さて、問題は Node.js での実装です。

gRPC は、各言語のライブラリがそれぞれ独立された思想で実装・提供されているため、status という構造1つとっても、Go と同じように Node.js でも簡単に扱えるというわけではありません。

Richer error model の説明にも下記のようにあり、Node.js ライブラリは非対応だったりします。

This richer error model is already supported in the C++, Go, Java, Python, and Ruby libraries, and at least the grpc-web and Node.js libraries have open issues requesting it.

Go の gRPC 公式ライブラリである grpc-go では、status.details の情報を gRPC HTTP Client / Server どちらの実装にも serialize / deserialize してくれるロジックが埋め込まれており、利用する側はあまり HTTP 通信の規格までを意識せずに非常にシームレスに扱うことができます。

しかし、Node.js の公式パッケージである grpc-js にはそうした実装が現在なく、Go 製のサーバーから status の情報が HTTP 通信に含まれてきたとしても、自分でクライアントのデシリアライズロジックを書く必要があります。

上記の grpc-go の実装の通り、status.details の情報は grpc-status-details-bin という HTTP header key に格納されていることがわかるので、そちらを取得しつつ手動でデシリアライズする必要があります。

この辺りを行ってくれる Node.js の OSS が下記のようにいくつか公開されていますが、そこまでヘビーなものではないので Gaudiy では自前で実装することにしました。

それでは、サンプルコードとともに紹介していきます。 やってることは Go と同じでリクエストのエラーオブジェクトから status オブジェクトとその中の details に格納された独自の message の型情報を取り出しています。

import { credentials } from '@grpc/grpc-js';

// protobufから自動生成された各種コードをimport
import * as GaudiyApiError from './gaudiy/apierror/apierror_pb';
import * as grpc from './gaudiy/hoge/hoge_grpc_pb';
import { HogeRequest, HogeResponse } from './gaudiy/hoge/hoge_pb';
import { Status } from './google/rpc/status_pb';

const client = new grpc.HogeClient('localhost:5001', credentials.createInsecure());

client.hoge(
  new HogeRequest(),
  function (error: ServiceError | null, response: HogeResponse) {
    if (!error) {
      // 正常系処理
      return;
    }

    // grpc-goのサーバー側で埋め込まれたkeyからstatus.detailsのバイナリ情報を取り出す
    const detailsBinary = error.metadata.get('grpc-status-details-bin');
    if (!detailsBinary) {
      throw error;
    }

    const buffer = detailsBinary[0];
    if (!Buffer.isBuffer(buffer)) {
      throw error;
    }

    // binaryをdeserializeして該当の型だった場合のみunpackする
    const details = Status.deserializeBinary(new Uint8Array(buffer))
      .getDetailsList()
      .map((element) => {
        // protobufで定義したtype名で格納されているので判定
        if (element.getTypeName() === 'gaudiy.apierror.Details') {
          return element.unpack(GaudiyApiError.Details.deserializeBinary, element.getTypeName())?.toObject();
        }
        return null;
      })
      .find((element) => element);

    if (!details) {
      throw error;
    }

    // detailsの中身が取れる
    console.log(details?.errorInfo?.reason);
  }
);

かなり取り回しにくい感じではありますが、これでようやく Go のように Node.js でも Richer error model の情報を取得することができました(Go に比べるとコード量が多いですね😅)。

いかがでしたでしょうか。
gRPC と Go の記事は多いものの、Node.js を駆使したエラーの実装方法やナレッジはなかなか探しても見つからなかったため、改めて全体感をまとめました。

サンプルで紹介したような実装に行き着く際にも、文献や実装を漁ったり、デバッグやトライアンドエラーを繰り返しして実現したので、苦労したポイントでした。
この周辺で悩む方がいましたらその助けになれば幸いです 🙏

3. おわりに

Gaudiy では今回紹介した gRPC をはじめ、新しい技術に対して積極的に投資・チャレンジしていくというスタイルで開発を行っています!

一緒に働くエンジニアも積極的に募集しておりますので、こうした技術や開発スタイルに興味をお持ちの方がいらっしゃいましたら、選考前にカジュアル面談も可能ですので、ぜひ採用ページからお気軽にご応募いただければと思います!

recruit.gaudiy.com

Gaudiy Advent Calendar 2022の20日目は、@yusukesatoo06 さんが担当です!
今年も残り少なくなってきましたね、身体にはお気をつけて良い年末をお過ごしください🎄
それでは、また 👋

Gaudiyのいちエンジニアが代表Dev(技術責任者)になって感じたこと

この記事は「Gaudiy Advent Calendar 2022」の18日目の記事です。

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyでエンジニアをしている勝又(@winor)です。11月初めごろから、いちエンジニアをしていた自分が、社内で「代表Dev」と呼ばれている技術責任者になりました。

今まで明確なマネジメント経験がない自分がストレッチなロールを任されたところもあり、苦戦しながらも日々を過ごしているので、今回はその振り返りができればと思います。

かなりリアルでハードな話になってはいるのですが、同じような境遇の方やこれから遭遇するかもしれないどなたかの参考になれば幸いです。

1. 代表Devが新設された背景

これまでのGaudiyは、CEO以外の役職や階層がないフラットな組織でした。プロダクト開発組織は、Stream aligned teamが複数存在するのみで、チーム内のロールはPdM、UI・UXデザイナー、エンジニアとシンプルなメンバー構成でした。この状況でもメンバーのオーナーシップは非常に高く、他職能メンバーとのコラボレーションを頻繁に行い、個々人が効率的に成果を生む自律的な組織だったと思います。

(その頃の組織体制については、こちらの記事をご参考ください。)

techblog.gaudiy.com

しかし、シリーズBを迎え急激に組織も拡大していった結果、頻繁なコラボレーションが認知負荷の増大を招いたり、合意形成に時間がかかったり、生産性が低下したりする問題が発生してきました。

この問題を解決するために、完全フラットな組織から、各チームの代表者を立てて意思決定の所在と責任を明確にし、よりスピーディな意思決定ができるような自律分散型組織へと移行しました。

この結果として、Stream aligned team以外にもDevやDesignなどの職能横断の組織と、責任者である「代表」という役割が誕生しました。その開発組織の代表が、自分が担う「代表Dev」になります。

また、この組織設計のGaudiyらしいユニークなところは、各組織の代表は各組織にいるメンバーから「選挙」で決定される点です。こうすることで、成果だけでなくメンバーからの信頼を得ないと続投ができない設計になっています。

この理由も詳しくあるのですが、少し本筋とズレてしまうので興味ある方は下記の記事を参考にしていただければと思います。

type.jp

2. 代表Devになる前はどんな役割だったか?

振り返りをする前に、前提情報として、代表Devになる前の自分はどんな役割だったか?について触れることで、なぜ後述する課題にぶち当たってしまったのかの参考になればと思います。

基本的には前述したStream aligned teamに所属をし、アウトカムのためのデリバリーを達成するためにはなんでもするようなエンジニアでした。他のメンバーからは、「不確実性に対して推進力がある」みたいなところは評価いただくこともありました。

チームでは暗黙的に開発のリーダーのような役割を担うことはありましたが、明確にマネージャー的な役割を経験したことは、前職も含め今までのキャリアでは一度もありませんでした。

3. 代表Devになって最初に躓いたこと

3-1. ボールを打ち返すことに精一杯になっている

最初に躓いた点は、様々な課題や要望に対するボールを打ち返すことに精一杯になってしまうことでした。

最初はなにかあったときの一次ボール受けになっていて、スピーディーに打ち返していくように頑張っていたのですが、当たり前ですがわりとすぐにこれだとスケールできないなということに気づきました。

この課題は、今では役割を分けて権限委譲したことによりある程度は解決できています。とはいえ、リソースが限られた開発組織ですべてのボールを打ち返せるかというとそれは難しく、ボールに対して優先順位をつけたり、まとめて解決できるようにしていくことなども大切でした。

3-2. ピープルマネジメントを重視せず、問題が大きくなって気づく

元々の意思決定では、ピープルマネジメントに時間を割かないようにしていました。理由としては、今思うと自分目線の考えだったなと思うのですが、Gaudiyの開発組織でそういった問題が発生しないだろうと考えていたためです。

ただ結果的には、自分が観測できていなかった開発組織内の課題が大きくなり、問題が発生してから気づくケースが複数件ありました。この結果を踏まえて、開発のメンバーと1on1を行うようになり、継続的に開発組織の課題を検知することに対して優先度を上げるようにしています。

失敗をして気づいたことでもありますが、「エンジニアリングマネジメントトライアングル」という、エンジニアリングマネジメントで必要な3つの役割(Technology、Product、Team)の軸をつないだ図から考えても、TeamとProductをつなぐTeam Development的な部分を軽視していたということがわかりました。

また、これを参考にすると、今なにが足りていないのか? 本当にそこが足りていなくて良いのか?という視点が得られ、それ以降の開発組織や仕組みの改善への参考となりました。

steam.place

4. 代表Devになって変化したこと

4-1. 開発組織と隣接組織の成果の総和という考え方を持てた

本や記事を読んで学んだことですが、EM的なポジションになりたての方などでよくありがちなのが、コードを書く時間が減って自分のアウトプット量が減ることにより、フラストレーションが貯まることです。この点に関しては、会社や事業の目標を達成するためにできることを行うという考え方を元から持っていたのでそこまで問題はなく、開発組織全体の成果を上げていくことが重要だと考えていました。

一方で、HIGH OUTPUT MANAGEMENTという本を読み、少し考えが変わったことがあります。それは開発組織だけでなく、開発に影響力が及ぶ隣接する組織の成果を挙げることも、代表Devのロールに対する成果として大事だということです。

最終的に会社として達成すべきことは、事業目標を達成することであり、Gaudiyで言えばミッションである「ファンと共に、時代を進める」ことが最も大事です。そして、この事業目標の達成に必要な価値は、各組織から直接的に生まれることはほぼなく、ある組織で生まれた価値を他の組織がまた違う形に変換をしていき、ユーザーに届くことによって生まれます

例えばGaudiyの開発組織でいえば、社内の管理画面開発を怠ることでコミュニティ内での施策を回すスピードが落ちてしまい、CS組織の成果を落とすような影響があったり、他にも、デザインシステムの運用を怠ることでUIデザインとプロダクトの乖離が発生し、デザインのチェックコストなどにリソースを使ってしまい、デザイン組織の成果を落とすような影響などがあったりします。

開発組織の影響は、直接的な事業への貢献と隣接する組織への貢献の2種類あり、これらを最大化していくことが大事であるという考え方を持てたし、課題が発生したときの優先順位の軸として重要な要素だと考えることができました。

4-2. 開発の役割を明確化し、様々な開発施策をクオリティ高く実行できるようになった

以前はボールが集中する課題もあったため、開発組織内の役割を新設して、開発メンバーを頼るような形に変えていきました。

プロセスとしては、開発、組織、プロダクトなどの課題などから必要な役割を逆算していきました。例えば、Stream aligned teamのデリバリーに責任をもつ開発リーダーや、BE・FEの各リーダーなどの役割を明示して、各々のメンバーに責任の範囲で自律的に動いてもらっています。

彼らのおかげで開発の施策や仕組みづくりがクオリティ高く行われるようになり、最もうまくいった改善点ではないかとも感じています。

5. 今後の課題と向き合い方

5-1. 中長期的な意思決定ができていない

代表Devとして改善しなくてはと思っている一番の課題は、中長期的な意思決定がうまくできていないという点かなと思います。この理由はいくつかあるのですが、

  1. 改善すべき大きな課題はありつつも、その中でどこがボトルネックなのか?が特定できていない
  2. ヒアリング過多になっていて、意思決定の収束までが遅い

が主な要因だと考えています。

ここは自分の力量が本当になかったなと思うところはありつつ、他の方からフィードバックをもらって変えていこうと思ったのは、不安なところがあると、逃げのヒアリングを行ってしまい、とりあえず課題を探ろうとしすぎている点です。

マーケティングやR&Dなどでも、「仮説を持たずに調査やヒアリングばかりを行うのは、すべての課題を揃えて解決策を出していくことになり、効率が悪い意思決定になってしまう」というアンチパターンが実際にあるといいます。

課題を揃えていくことも重要だとは思いますが、事業やプロダクトの不確実性が非常に高いスタートアップの環境では、意思決定を素早く行い、実際に検証をして改善していくことも非常に大切です。そのため、今後はこの考え方も取り入れて意思決定をしていくことが大事だと痛感しました。

5-2. 副業メンターの方がいることで、自分の行動やモヤを振り返られる

代表Devとして今までとだいぶ違った動きをしているところもあり、モヤや躓きが多かったのですが、こういった点を他の方に相談して意見をもらえるのは本当にありがたかったです。飲み会で率直に色々とダメ出ししてもらったりしたことは本当に感謝しています笑。

また、Gaudiyで副業をしていただいてるEMの@yowrb_0905さんと週1で1on1を行っているのですが、メンタリングをしていただいたり、EMとして働かれている経験から新しい考え方に気づかせていただくことが多く、1on1をしてもらう重要性も再認識できました。

6. まとめ

今回は代表Devになってみての振り返りをまとめてみました。良かったところもありましたが、全体的に反省点や自分の至らなさで失敗してしまったところはやはり多いなと感じました。

ただ個人的には、Gaudiyの「ファンと共に、時代を進める」という壮大なMissionに共感しており、この実現にはエンジニアリングの力が本当に大切だと考えています。そのためにも、選挙で変わる可能性はありますが、どんな形でも強い開発組織を作っていき、少しでもVisionの実現に近づけていきたいと思っています。

また、強い開発組織を作っていく文脈でもエンジニアの採用は非常に力を入れていますので、ぜひ興味ある方は下記リンクか自分(@winor)へ連絡でもいただければと思います!

recruit.gaudiy.com

明日のアドベントカレンダーはエンジニアの並木さん(@ruwatana)が担当します。

アウトカムの最大化へ。開発組織の変遷と向き合い方

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyでプロダクトマネージャーをしている@kaa_a_zuです。

開発組織は、ITサービスを提供している企業にとって「エンジン的な存在」であり、プロジェクトや各メンバーの生産性に大きな影響を及ぼします。そんなエンジンは、事業の成長に伴って柔軟に変化させていく必要があると考えています。

Gaudiyでも、これまでに数度、開発組織のアップデートを重ねてきました。今のエンジンは、「仕様策定から開発、リリース、効果測定までをひとつのチームが行い、そのチームメンバー全員が責任を持ってアウトカムの最大化を図ることができる」ものになっています。

そこで今回は、2022年7月に行われた開発組織の体制移行を中心に、これまでの開発組織の変遷から今回の体制に至った背景、体制移行にあたって考慮した点などについて書こうと思います。

事業や人員拡大に伴い開発組織をアップデートする必要性を感じている方や、メンバーが最大限の能力を発揮できていないという課題感のあるエンジニアの方のご参考になれば幸いです。

※少し長いので、現在の開発組織の設計・運用だけ知りたい方は、「3. 開発組織体制の変遷」の終わりまで読み飛ばしてください。

1. Gaudiyのプロダクトと技術

まずはじめに、Gaudiyについて簡単にご紹介します。何をやっているかわかりやすく言うと、Web3のコミュニティSaaSをつくっている企業です。

Web3時代のファンプラットフォーム「Gaudiy Fanlink」というプロダクトを、エンタメIP(知的財産コンテンツ)を有する企業に提供して、クライアントの方々と一緒にファンの熱量を最大化させるコミュニティ運営に日々勤しんでいます。

service.gaudiy.com

Gaudiyが提供しているのはこのワンプロダクトですが、各IPファンの熱量を最大化させるための手段として NFT(代替不可能なトークン)やDID(分散型ID)といったブロックチェーン技術を利用しています。

とはいっても、下図のように、いわゆるWeb2の企業が使っている技術と大きな差はあるわけではありません。

Gaudiyが利用している技術スタックについての説明です。Frontendでは Next.js Typescript, ApolloClientが使われていてVercelにホスティングされてます。BackendではKotlinとGolangが使われていてCloudRunにホスティングされています。FrontendとBackendの間にBFF層として、TypescriptとApolloServerがあります。BlockchainはEthreumとPolygonが使われています。
Gaudiy Fanlinkを構成している技術スタック

よく「ブロックチェーンのことをよく知らなくても大丈夫ですか?」と聞かれますが、Gaudiyでは多くのWeb2企業出身のエンジニアが活躍しています。

techblog.gaudiy.com

2. Gaudiyの開発組織

そんなGaudiyですが、昨今のWeb3やNFTへの注目もあいまって、下図のようにエンジニアメンバーがどんどん増えています。

先日、シリーズBの資金調達が完了したこともあり、今後さらにエンジニア採用を加速させていく予定です。

エンジニアの採用計画についての説明です。2018年は2人、2020年は9人、2022年は19人となっています。そして2023年に50人、2024年に100人を予定しています。
2020年時点では9人だったエンジニアが現在では2倍以上になっている

このような事業フェーズの前進や組織拡大に伴って、開発組織を新たな形に移行する必要がでてきました。

3. 開発組織体制の変遷

ここで一度、創業期からの開発組織の体制について振り返ります。

Gaudiyではこれまでに二度、下図のように組織をアップデートしてきました。それぞれ、どのような背景や意図で組織体制を変えてきたのかについて説明します。

Gaudiyの開発組織の変遷(人数はPO・PM・Designer含む)

3-1. 【第1フェーズ】創業〜2021年5月

がむしゃらにプロダクトをつくる開発組織

<特徴>

  • Engineerは必要最小限のスコープである「プロジェクト」という単位にわかれる
  • POとDesignerが仕様をつくり、各プロジェクトでそれらを開発する

この頃は、まさに0→1のフェーズ。創業初期はブロックチェーンの顕在的なニーズが世の中にあまりなく、先行事例も少なかったため、PMFをめざす上で重要だったのは「いかに関係者に価値を理解してもらえるか」でした。

そこでまずは、クライアント企業に深く入り込み、個別の状況に対して理解と共感を深め、エンタメ企業に共通するペインを見つけながら、それを解決するプロダクトをつくる。そのプロセスが必要であり、戦略的に「Sales-Led Growth(セールス主導の成長)」でのプロダクト開発をしていました。

プロダクトが小さくメンバーも少なかったため、開発スタイルとしては、POとメンバーが密にコミュニケーションを取りながら、アウトプットを重視する形です。不確実な課題に対しての突破力が高いメンバーで構成されており、早いスピードで確実にアウトプットを積み上げていました

この開発体制によって、複数のクライアント企業様と一緒にコミュニティを提供できたことで、プロダクトの大枠をつくることができました。同時に、後に想定される「SaaSとしての急成長に備えた地盤を整えること」が課題となってきました。

3-2. 【第2フェーズ】2021年5月〜2022年7月

その課題に対応するために、2021年5月頃から、「継続的にコミュニティの機能をつくるExperienceチーム」「決済システムや他のプラットフォームと連携するためのSDK、IDシステムなどの基盤をつくるPlatformチーム」の2チームに分かれました。

地盤を整えるための開発組織

<特徴>

  • Engineerは機能をつくるチーム(Frontend Engineer中心)と、基盤をつくるチーム(Backend Engineer中心)にわかれる
  • POとDesignerが仕様をつくり、各チームでそれらを開発する
  • 各チームの開発スケジュールを管理するProject Managerのポジションが生まれる

この時代も、継続して戦略的なSales-Led Growthでの開発をしていました。セールスのニーズにあわせた開発をしていたため、運用するコミュニティの数も着実に増えていきました。また、増えてきたメンバーをまとめるため、プロジェクトマネージャーのポジションが新設され、個の集合からチーム感が強くなっていきました。

ここで課題となってきたのが、「SaaSとしての汎用性」と「1つ1つの機能の質」です。

Product-Led Growth(プロダクト主導の成長)ではなく、スピードとアウトプットを重視したSales-Led Growthでの開発を戦略的に採用してきたために、すべてのコミュニティに汎用的な最大のアウトカムは出せていない状態でした。

また他にも、以下のような課題が浮き彫りになってきました。

  • Designerのリソース不足で、仕様策定者のPOとDesignerの間で機能の細部要件が定まらず、開発に手戻りが生まれる
  • メンバーの増加に伴い、コミュニケーションが複雑になり、よりトップダウン開発の色が濃くなってしまった
  • なぜその仕様でその機能を作るのかの共通認識をメンバー全員が取れていない

3-3. 【第3フェーズ】2022年7月〜現在

こうした課題に対応するために、個々のチームが仕様策定からユーザーへの提供までを一貫して担い、それらが並存する組織体制に移行しました。これが現在の開発組織体制です。

アウトカムを最大化する開発組織

<特徴>

  • 仕様策定からユーザーに体験を届けるまでを一貫して担うため、EngineerはFrontend、Backendという分かれ方はせず、クロスファンクショナルなチーム構成になる。
  • POが事業進捗をふまえた開発の優先順位を決定し、PdMを中心にチーム全員で仕様をつくり、それらを開発/提供/効果検証する。
  • 特定の開発領域をもたないため、チーム名のつけかたを「寿司ネタ」にする。(社内公募で決めました。)

ひとつのプロダクトに対して、2022年9月時点で開発チームは3つです。現在は、横断的なチームは存在していません。

Gaudiyは、2022年の5月にシリーズBの1st、8月に2ndの調達をしました。次はいよいよシリーズCになります。

次のマイルストーンとして、私たちは「シリーズCでユニコーン企業になる」を本気で目指しており、その目標達成のために今までの開発の仕方を大きく変更する必要が出てきました。

ここからは、どんなことを考えて現在の開発組織を設計したか、そしてどのように組織運営をしているかについてお伝えしていきます。

4. 今回の組織体制移行において特に意識したこと

メンバーが増え、組織が拡大するにつれて、組織体制を変えることにはリスクが生じます。社員数が50人に満たないGaudiyでさえ、私はこの難易度が高いと感じていました。大前提、すべての理想を叶えられる組織体制は存在しないと思っています。

また基本的に、恩恵を享受する時には痛みも伴います。このようなトレードオフの関係になり得る重要な意思決定においては、特に意識しないといけないことが2つあると考えています。

1つ目に、絶対に譲れないコアな考え方を決めること、2つ目に関わるメンバーの納得感を得ることです。それぞれ具体的にお伝えします。

4-1. 譲れないコアな考え方を決める

今回の開発組織の体制移行に際して、以下3つをコアな考え方として定めました。

  1. アウトカムに責任が持てるチームであること
  2. すぐに新たなポジションは作らないこと
  3. 文化が継承されること

これらについて、順番に説明します。

4-1-1. アウトカムに責任が持てるチームであること

以前までは、アウトカムよりもアウトプットを重視した開発をしていました。ここからのフェーズでは多少のスピードを犠牲にしてでも、アウトカムを重視した開発をしていく必要があります。

ユーザーに対するアウトカムを最大化するためには、ユーザーが本当にやりたいことを探り、提供し、改善していくことが必要だと考えています。つまり、Whyを意識しながら仕様策定や微小な軌道修正を繰り返すことのできる開発組織の体制が必要です。

そのためには、POが細部まで仕様策定したものをチームが開発するといった社内受託感をなくし、開発チームが能動的にそれらについて考えることができ、アジリティ高く、タスク完結性を持って遂行可能な状況をつくる必要がありました。

  • アジリティ…企業のビジョンと一致している方向に、各々が意思決定ができている度合い
  • タスク完結性…各々が作業工程の一部ではなく最初から最後までに取り組めること

この状況を作るために、変更したのは3つです。

  1. 今までのPjM(Project Manager)というポジションをPdM(Product Manager)に変え、チーム内での最終的な意思決定の責任者に。
  2. 各チームが開発するプロダクトバックログを決める会議体を新設。
  3. 仕様策定からリリースまでを1つのチームだけで一貫して行えるクロスファンクショナルなメンバー構成に変更。

図示すると下図のようになります。

能動的に仕様部分を考えることができ、アジリティ高く、タスク完結性を持って遂行できる開発組織体制

当然かもしれませんが、タスク完結性がなければその決定に対してチームや個人が感じる責任は小さくなり、考えるプロセスにおいて他責感を生んでしまいます。

新しい体制は、チームがタスク完結性を持つ構造にしているため、否が応でも決定に対しての責任を持たなくてはいけない構造です。結果的に、アウトカムについてチームや個人がより意識をするため、ユーザーにとってより良いものが提供できるという状況が生まれています。

4-1-2. すぐに新たなポジションは作らないこと

組織体制を変えるタイミングでやってしまいがちなことのひとつが、新たなポジションを作ることだと思っています。

特定の課題を解決することの単純解が、ポジションの新設です。今回の議論でも「QAエンジニア」「Engineering Manager」「スクラムマスター」などの新たなポジションをつくりたいという声が上がりました。

結論、今回の体制移行に伴って新たなポジションをつくることはしませんでした。なぜかというと、ポジションをつくることは簡単でも、減らすことはとても難しいと考えているからです。

一度ポジションをつくってしまうと、そこにアサインされた人が「今まで持っていた裁量を失うこと」を恐れたり、アサインから外れることがモチベーションを落とす要因にもなり得ます。そのため、ポジションの新設による課題解決は、安易に選択すべきではないという結論にいたりました。

この辺りの「裁量とモチベーションの関係」については、米・ハーバード大学で心理学の教授をされている ダニエル・ギルバート氏 の著書で分かりやすく説明されています。

www.amazon.co.jp

改めてになりますが、ポジションをつくりたいという議論が出てきた時には、第一に仕組みで解決することができないかを模索し、試行錯誤をした後で本当に必要だったらポジションを新しく設ける、という流れが大事だと思っています。

4-1-3. 文化が継承されること

スタートアップの成長過程でよく言われているのが「30人の壁」「50人の壁」「100人の壁」です。

これらは組織規模がおおよそその人数になった時に、組織に対する満足度が低下し、結果として離職率が高まることを指します。これらの障壁の原因は、「文化の希薄化」と「1人当たりが持つ影響力の低下」だと言われています。

創業まもない少人数の時はプロダクトも小さく、ステークホルダーも少ないため、全員で意思決定をしてつくることに必死になります。その後、企業が有名になり組織が拡大していくと、途中で入社してきた経験豊富な優秀な人中心の構造になり古参メンバーがモヤを感じてしまったり、今まで知れていたことが知れなくなったり、以前と比べて組織に対する影響力が小さくなってしまいます。

Gaudiyの場合、まだエンジニアメンバーが20人未満だということもあり、そのまま進めることも可能でした。しかしながら、拡大スピードを考えると、早めに次の一手を打たないといけないような状況でした。

そのために各チームに企業文化を持っていて浸透させることができる古参メンバーが在籍する構成にしたり、全ての情報をアクセス可能にしたり、チーム間でのコミュニケーションが取れる(極論、取らなければいけない)開発の流れにしています。

このあたりは、開発組織だけでなく、GaudiyのバリューのひとつでもあるDAO(Decentralized Autonomous Organization:自律分散型組織)がベースになっています。以下の記事で詳しく説明しているので、よければご覧ください。

seleck.cc

note.com

4-2. 関わるメンバーの納得感を得る

組織体制の変更は、今まで慣れ親しんだ開発手法や今後のキャリアにも影響が出ます。そして、メンバーはロボットではなく、感情をもつ "人" であり、その誰しもがコンフォートゾーン(居心地の良い環境)を探してしまうものです。

その前提のもと、今回の移行に際しては、全開発メンバーにヒアリングをして意見を聞き、できる限りのwillを反映させました

組織体制の変更について考えている時は、どうしても視点が組織に向いてしまいがちですが、その視点だけではロマン(表層)的にしか描くことができません。チームや個人といった現場(本質)的視点を入れることで、より現実的で納得感が得られる体制を描けるのだなと学びました。

実際に、全員と数回の1on1を行うことはかなり大変でした。ただ、これから強い組織が成長するためには、今までのGaudiyを築いてきたメンバーがとにかく大切です。そのメンバーが持つモヤをなくし、組織基盤となる文化や考え方を新たなメンバーに浸透させることは、1on1を行う大変さに比べて何十、何百倍も大事だと思っていました。

それらを経て、最終的には社内AMA(Ask Me Anything)も開き、納得感を得て体制移行を完了できたのではないかと思っています。

5. 組織をワークさせるための工夫

すでに3ヶ月弱、この開発組織体制で動いていますが、実際にうまくワークしているのか?について最後に触れておきます。

結論、課題は山積みですが、今のところうまくワークしていると感じています。今までは開発組織の体制づくりに着目して説明をしてきましたが、ここからはこの組織体制における運用の工夫についてご紹介します。

5-1. 定期的に組織課題を改善するMTGを開く

組織課題は業務を重ねるにつれて顕在化し、日々更新されていきます。そこで、約2週間に1回の頻度で組織課題を改善をするミーティングを開いています。

この会議体の責任者はPdMですが、実際にそれぞれの解決に責任を持って旗を振る人は、その課題を挙げた人になることが多いです。今後も継続的にこのミーティングを行っていく必要があるなと感じています。

組織課題の管理DB

5-2. いつどこのチームが何に着手するのかを可視化する

ワンプロダクトを複数チームで開発する体制なので、他のチームが取り組んでいる開発領域を知り、お互いにコミュニケーションを取りながら開発をしていく必要があります。

そこで、他のチームがやっていること/今後やることをタイムライン形式で表示し、簡単に知ることができる状態にしています。

タイムライン形式でのプロダクトバックログ

また下図のように、弊社のプロダクトバックログには、今後開発をしたい機能(正確にはアウトカムベースのストーリー)があります。

これらをカンバン形式でも見ることができるため、開発メンバーが将来的にどういった機能が載るのかをイメージしながら、現在の開発ができるような状態にしています。(= 拡張性の考慮)

カンバン形式でのプロダクトバックログ

5-3. 通常業務禁止DAYで横断的にDX改善を行う

繰り返しになりますが、Gaudiyでは現在、横断的なチームは存在しません。

一般的に、横断的なチームがやることとしては、技術選定基準や開発方針の決定、難易度が高い開発、横断的に行う必要があるメトリクス計測の開発などがあると思います。

横断的なチームには、上述したようなことにフォーカスして着手できるメリットがある一方で、生じ得るデメリットとしては、組織として階層構造になってしまったり、横断的なチームが社内受託的な組織になってしまうなどが考えられます。

今のフェーズにおいては、できるだけ全員がオーナーシップを持ってレバレッジの効くDX改善に取り組むことが大事だと思っています。これを実現可能にしているのが、毎週水曜日にある「EMPOWER-DAY」です。

この日は午前が休み、午後は通常業務を原則してはいけない決まりになっています。この曜日を使って、チームに関係ない開発メンバーとのDX向上に取り組んでいます。具体的には以下のような取り組みが行われています。

開発チームが抱えている課題管理DB

なお、事業フェーズが進み組織が拡大する中で、横断的なチームが今後誕生する可能性はあります。Empower-Dayが誕生した背景や具体的な内容については、以下の記事で詳しく説明しています。

note.com

6. 現状の課題

上述したように、様々な仕組み化によって改善はしているものの、まだまだ多くの課題があります。

ここでは、今後解決していきたい課題についてお伝えします。

6-1. 意思決定責任の所在が明確化されないこと

開発組織が階層構造になっていないことや、特定のことを行う横断的な組織がないことにはデメリットがあります。

そのひとつが、全員がオーナーシップを持っているが故に、開発時の意思決定スピードが落ちることです。これについては、取り組みごとに最終意思決定者(≒責任者)を明確にするような形で推進しており、徐々に解消している最中です。

6-2. 開発時のコミュニケーション過多、コンフリクトが生じる

現在のプロダクトは小さく、機能もそこまで多くないため、チームごとの担当領域を定めていません。つまり、ワンプロダクトにおける体験ごとにタスク完結性を持って開発する運用になっています。

この運用(体制)では、他のチームが取り組んでいる開発領域を知り、お互いにコミュニケーションを取る必要がありますが、これには一定のコストがかかります。そして、これらの課題はチームが増えるごとにさらに大きくなっていくことが予想されます。

このコミュニケーションコストが許容できなくなるタイミングでは、逆コンウェイの法則に従って、担当する領域を明確化した機能単位のチームに移行することが望ましいのかなと現時点では考えています。

7. 今後に向けて

やはり、プロダクトと組織は切っても切り離せない関係にあると思っています。つまり、強い組織をつくることができれば、おのずと良いプロダクトをつくれる可能性が高くなるはずだと考えています。

ここからGaudiyは、さらに大きくなっていきます。2022年時点で20人ほどの開発組織も、2023年には50人、2024には100人に大きくしていき、グローバルに向けた挑戦をしていきます。

すごくおもしろいフェーズだと思うので、少しでも興味持ってくださった方はぜひカジュアルにお話ししたいです。

meety.net

エンジニア向けの採用ページもできたので、貼っておきます!

recruit.gaudiy.com

Server-Driven UIの採用背景と実装について

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyで、フロントエンドエンジニアをしているkodai(@r34b26)です。

Gaudiyでは、Airbnbが採用していることで有名な「SDUI(Server-Driven UI)」という設計手法を取り入れています。

先月のTech Blogでは、ユーザーに対してファンダムな体験を届けるために実践している、スキーマ駆動開発についてお伝えしました。

techblog.gaudiy.com

今回は少し視点を変えて、顧客やユーザーと対峙する社内メンバーに対して、ファンダムな体験を届けるために実践している、SDUIについてまとめてみます。

GaudiyでSDUIを取り入れた理由や、その実装方法なども書いてみたので、一事例としてよければご参考ください。

1. SDUI(Server-Driven UI)とは

SDUIはおそらく、Airbnbが使用する設計手法として、その名称とともに広がったのが始まりです。

medium.com

UIの表現方法をすべてサーバーサイドで決定し、クライアントに返す、という手法になります。

通常のアプリケーション開発では、レンダリングロジック、ルーティングなどをクライアント側で決めますが、SDUIの手法ではこれらをサーバー側で決定しています。

日本語だとこちらのnoteがわかりやすいです。

note.com

2. SDUIのメリット

先述したAirbnbの記事や、一般的に説明されているメリットとしては、以下になります。

  1. UIの変更にアプリケーションのデプロイを必要としない

    サーバー側のロジックや設定を変更することで、クライアント側の表示を変更できるため、クライアントアプリのデプロイが必要ありません。そのため、デプロイまでの社内プロセスや実行時間を待つことなく、UIに対する変更が可能です。

  2. アプリケーションの審査を必要としない

    とくにネイティブアプリでのメリットになりますが、UI更新にデプロイを必要としないため、Apple, Googleの審査を毎回待つことなく細かい変更や巻き戻しができます。これにより、社内でコントロール不可能なタイムロスを圧縮することが可能です。

  3. 複雑な表示切り替えに拡張可能

    レコメンドエンジンを用いたユーザーごとの表示切り替えによる、行動促進や広告配信ができます。また、ABテストや限定リリースなど、リッチなロジックによる制御を組みやすくなります。

つまり、クライアントの都合による変更反映までのタイムラグをできる限り減らすことができる。それがSDUIの最大のメリットです。

3. なぜGaudiyでSDUIを採用したのか

Gaudiyの話をすると、少なくとも現状はWebアプリケーション(PWA)のみで実装を進めており、ネイティブアプリの実装はしていません。またレコメンドエンジンに関しても、将来展望としてはありますが、現在は実装を行っていません。

それなのに、なぜSDUIを取り入れたのか。それは、顧客に提供するアプリケーションのカスタマイズ性を担保するためです。

こちらは、実際にGaudiyが提供している、各アプリケーションのホーム画面です。

実際のアプリケーション画面

同一ページ、同一ソースコードで実現していますが、表現がだいぶ異なっているのがわかると思います。

Gaudiyでは、ブロックチェーンを活用した「Gaudiy Fanlink」というプロダクトを通じて、さまざまなファン体験を総合的に提供していますが、そのコミュニティアプリは基本的に個別カスタマイズを行っていません。

プロダクトのめざす方向性や汎用性などの観点から、必要と判断された機能を追加する形で開発を進めていますが、クライアント企業によってはすべての機能を必要としていなかったり、段階的に機能を増やしていきたかったりするため、一部機能のみを提供する場合があります。

たとえば、

  • 提供する機能を増やしたい/減らしたい
  • ホームに表示するコンテンツを施策に合わせて変更したい
  • IPの世界観を壊さないような形で表現したい

といったクライアントの要求に対して、Bizサイドのメンバーがエンジニアを常に頼ることなく扱えるようにしたい。それを実現するための手段として、SDUIを実践することに決めました。

4. SDUIの実装方法

ここからは、GaudiyでどのようにSDUIを実装しているかをお伝えしていきます。

元々、BFF-Client間通信に使用していたApollo GraphQLを生かしつつ、Airbnbの実装例を参考にしながら弊社のユースケースに沿うように、レイアウト、UIパーツをGraphQL Schemaに落とし込んで表現しました。

これによって、

  • Fragment Colocationによるフェッチ最適化
  • 宣言的データフェッチによるReactの宣言的UIとの対応
    • 将来的に、宣言的なフレームワークを用いてネイティブに拡張可能

といった、データ効率とDXが高い開発が可能になりました。

では実際に、どのようなSchemaになったかを数点ご紹介していきたいと思います。

4-1. レイアウトのSchema

Gaudiyの現状のアプリでは、モバイルとデスクトップで、大きくレイアウトを変えることがあります。

具体的には、「モバイルで1列」「デスクトップで1列」「デスクトップで2列」の3パターンと、それぞれのレイアウトに対してトップに特別なコンテンツを置く・置かないの出し分けがあり、すべてのレイアウトは3 x 2=6パターンで表現されています。

デスクトップ・2列レイアウト

モバイル・1列レイアウト

これらを表現でき、また将来的にパターンが増えたり、タブレットなどに細かく対応する余地を持たせたSchemaとして、以下のように表現しました。

type LayoutsPerFormFactor {
  mobile: Layout!
  desktop: Layout!
}

union Layout = SingleColumnLayout | TwoColumnLayout

type SingleColumnLayout {
  mainTop: SingleSectionPlacement
  main: MultipleSectionsPlacement...
}

type TwoColumnLayout {
  mainTop: SingleSectionPlacement
  main: MultipleSectionsPlacement!
  sub: MultipleSectionsPlacement!
    ... 
}

抽象化したパターンがSchemaから把握できる形になっていて、わかりやすいかと思います。

4-2. UIパーツのSchema

前述したコミュニティごとの特性の違いから、ある機能単位での表示・非表示と、UI上の表現の違いを数パターンで表したいという要望がありました。

たとえば、以下のような同じ”トピック”機能に対して、ランキング形式でユーザー行動をより強く促す形と、フラットに表現する形で使い分けています。

ランキング形式で表現するパターン

フラットに表現するパターン

ここでは、機能ごとに出し分けする点と、機能ごとのpreview的に複数項目を表現する点から、機能ごとにセクションを分け、Arrayで子を持つように表現しました。

type SectionContainer {
  id: ID!
  sectionComponentType: SectionComponentType
  section: Section
    ...
}

enum SectionComponentType {
  TOPIC_CAROUSEL
  TOPIC_TRENDING
  ...
}

union Section =
    ChatSection
  | TopicSection
  ...

type TopicSection {
  title: String!
  items: [Topic!]!
    ...
}

type Topic {
  id: ID!
  label: String!
  description: String!
  imageUrl: String!
  createdAt: Int!
  isDisabled: Boolean!
  ...
}

機能の増減は Sectionで受け止め、機能ごとの見た目や並びのブレに関しては SectionComponentTypeで受け止めることで、unionが膨大に増えることを抑制しています。また、UIごとに必要になるfieldの違いに関しては、clientでのcolocationで定義することによって、over/under fetchが起きないようにしています。

このように、プロダクトの性質や、設計の採用目的によって重きを置くところが違うので、SDUIの思想をベースにしつつ、ユースケースにあった調整をした上で使用すると効果的だと考えています。

5. まとめ

今回はSDUIに関して、Gaudiyで実際にどう取り組んでいるかをまとめてみました。

特にフロントエンジニアの視点で考えると、Schema駆動でバックエンドと協力して設計・実装するのと同じくらい、もしくはそれ以上に、宣言するUIがデザイナーと認識の合った形になるように、こちらからヒアリングして提案することが大事になると思いました。

これから運用と拡張のフェーズを経て、引き続きSchemaを育てていきたいと思っています。

興味ある人がいれば、ぜひお話ししたいですー。

meety.net

Gaudiyの技術選定について知りたい方は、以下の記事をご参考ください。

techblog.gaudiy.com

UXの向上と開発の生産性を両立するスキーマ駆動開発

こんにちは!ファンと共に時代を進める、Web3スタートアップのGaudiyでエンジニアをしている勝又(@winor30)です。

Gaudiyのプロダクト開発において、最も大切にしていることの1つに、ファンダムな最高のユーザー体験を提供することがあります。

今回は、このファンダムなユーザー体験を提供しながら、プロダクトの成長速度を落とさないために取り組んでいる、UX中心のスキーマ駆動開発についてまとめてみます。

具体的な開発フローや工夫している点についてまとめたので、同じような課題を抱えるチームのご参考になれば嬉しいです!

1. 取り組みの背景

UXに直結するエンジニアリングの領域は、ユーザーが実際に触るフロントエンドが中心になります。そのため、よいユーザー体験を提供するには、フロントエンド開発にしっかりとリソースを割ける状態を作ることが大事だと考えています。

また、プロダクトが順調に成長するためには、よい体験を実現しながらも、バックエンドの領域に近いビジネスロジックを正しく実行したり、機能の拡張性や汎用性を担保することも必要です。

それらの観点から、第一にユーザー体験を、第二にプロダクトの成長速度を落とさないという目的から、スキーマ駆動開発を中心とした開発フローを取り入れました。

2. Gaudiyのアーキテクチャ構成

Gaudiyが提供するプロダクトのアーキテクチャは、下記のようなFE → BFF → BEのような構成になっています。

エンジニアによる実装範囲は特に明確になっていませんが、基本的にFEをフロントエンドエンジニアが、BEをバックエンドエンジニアが開発しています。BFFに関しては、集約や整形ぐらいのロジックしかないため、ケースバイケースで開発をしています。

https://cdn-ak.f.st-hatena.com/images/fotolife/g/gaudiy/20220310/20220310234806.png

3. UX中心のスキーマ駆動開発の概要

基本的には、下記4つの開発アクティビティを大切にしています。

  1. すべてのインターフェースをスキーマ駆動で開発する
  2. スキーマのズレを検知できるようにする
  3. システム全体の結合は開発初期の段階で行う
  4. FEのブロックにならないようにモックを返す

この意図としては、「FEのブロックにならないようにモックを返す」を基本方針とすることで、UXの向上や検証のために必要なフロントエンド開発に、最大限のリソースを割くことができる状態を作るためです。

また、開発初期の段階でBEやBFFをFEときちんと結合させ、実際に結合させた状態を先に作るようにしています。これによって、実際の挙動に限りなく近い状態にすることができ、UXの細かい部分をいつでも確認することができます。

このような状態を維持しながらBEの開発も行えるように、スキーマ駆動開発と継続的なズレの検知により、BEとFEが自律的に開発できる状態を作っていくことが大切だと考えています。

3-1. すべてのインターフェースをスキーマ駆動で開発

Gaudiyでは現在FE → BFFはGraphQLを、BFF → BEはRESTを利用してアプリケーション間の通信を行っています。

そのため、GraphQLではシンプルにGraphQLスキーマを、RESTではOpenAPIを利用してスキーマ駆動開発を行っています。

各スキーマについてはFE、BEのエンジニアが議論しながら作成し、それをPRに上げるところから開発を始めています。これによって、それぞれのエンジニアが自律的に開発でき、開発の生産性をかなり高めることができています

実際に行う際はLive Shareを利用して、同時編集でGraphQLのスキーマを作成することがおすすめです。

3-2. スキーマのズレを検知できるようにする

GraphQLやRESTは、必ずサーバー側とクライアント側が存在します。

そのため、いくらスキーマを利用していても、サーバーとクライアント側で異なるバージョンのスキーマが利用されてしまうことは起こり得ます。これが生じると結局バグが発生してしまうため、プルリクエスト時のCIなどでズレを検知できるようにすることが大切です。

Gaudiyではアプリケーションのコードをmonorepoに配置しており、クライアント側とサーバー側のコードがすべて同じRepositoryに存在しています。

そのため、GitHub Actionでスキーマに変更があったのか? また変更があった場合、生成されたコードでビルドは可能か? をチェックしており、それによってインターフェースのズレを検知できるようにしています。

下記はGithub Actionのサンプルになります。

name: Check OpenAPI Schema for bff

on:
  pull_request:
    types: [opened, synchronize]
    paths:
      # bffのコードが変更されたケースとbeのopenapi.yamlのスキーマが更新されたタイミングで発火する
      - 'projects/bff/**'
      - 'projects/be/openapi.yaml'

defaults:
  run:
    working-directory: ./projects/bff

jobs:
  check-openapi-schema:
    name: check code
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
...
      # openapi.yamlからコードを生成し、その後のstepでbuildをし、成功するかチェック
      - name: Generate API Client by OpenAPI Schema
        run: npx @openapitools/openapi-generator-cli generate -i ../../projects/be/openapi.yaml -g typescript-axios  -o ./src/datasources/be/openapi/
      - name: Check build
        run: npm run build

3-3. システム全体の結合は開発初期の段階で行う

開発初期の段階でFE、BFF、BEが通信を行っている状態をつくることによって、開発初期段階でモックを使っていたとしてもリアルに近いプロダクトをチームに提供でき、UXを高めることに繋げられます。

Gaudiyは、体験の検証やQAを頻繁にする組織なので、最新の状態で触ることのできる環境を常に用意することは非常に大事です。そのため、結合自体も早めに実施するということをやっています。

体験の検証やQAについては以下のblogに詳しく書いてあるので、ぜひ読んでいただければと思います。

techblog.gaudiy.com

また、経験上の話をすれば、開発終盤になってFEとBFFや、BFFとBEのI/Fが合わないみたいなケースがかなり多いと思っています。そのため、先にこのリスクを潰しつつ継続的に維持した方が、大きくズレる前に修正もしやすく、トータルのコストが低くなると考えています。

3-4. FEのブロックにならないようにモックを返す

開発初期でシステムの全体の結合を作るには、BEがデータを返却する必要があります。BEでデータを返却できるような状態を作るには、DBとの接続や別サービスとの接続、BEのビジネスロジックの実装などかなり工数がかかります。

しかし、BEの開発をFEに待ってもらうと、それこそUXの最大化に繋がりません。そのため、どういうデータを返すべきかをFEエンジニアと相談しながら、モックデータを返却するような形で、BEの初期は開発するべきだと考えています。

こうすることで、FEが自律的に開発することができ、よりUXの最大化に繋げやすくなります。

また、モックが返却するデータは、FEエンジニアや他のロールの人に相談しながら実際に利用するようなデータを作ることによって、初期段階でもプロダクトを触ったときの体験を少しでもリアルなものに近づけることができます。

4. 開発フローの具体的な流れ

前述の4つの開発アクティビティをベースにしつつ、基本的には下記のようなフローで案件の開発を行っています。

今回の記事を読んで興味がある方は、具体的にどのように開発しているかの解像度を上げるためにも、ぜひ読んでいただければと思います。

4-1. 開発の基本フロー

1:開発案件のストーリーマップとDDDを終わらせる

完璧なものを作る必要はありませんが、全体像やどういう方針で開発するかの認識を揃えておくことで、その後の開発の認識ズレがなく開発しやすいです。

2:FE・BEエンジニアでGraphQLのスキーマを設計し、プルリクエストを上げる

Live Shareなどを使って同時編集でスキーマを作成できるようにするといい感じです。

3:GraphQLスキーマからモックレスポンスを返すような実装をし、最速でFEと結合できる状態を作る

4:FEはGraphQLスキーマからQueryを生成したりして、実際に結合できる状態を作る

このとき、仮にBFFと結合できない場合は、MSW(Mock Service Worker)を利用するとBFFを待たずにlocal開発ができます。(ただし、開発・本番などのリモート環境では確認できません。)

5. BEはOpenAPIを設計し、OpenAPIに沿ったモックレスポンスを返すBEアプリケーションを開発する

6. OpenAPIスキーマからクライアントを自動生成させ、すべてモックレスポンスの状態でBFF ← → BE間を結合させる

7. BEを開発していく

4-2. 継続的に行っていくこと

また、継続的に行っていくこととして、実装するなかでスキーマが変わるときは、必ず担当の別領域の(BE → FE、FE → BE)エンジニアに報告するようにしています。

レビュアーにもアサインするとベターです。

5. さいごに

今回の記事では、Gaudiyで実践している「UX中心のスキーマ駆動開発フロー」についてまとめてみました。

この開発フローを取り入れてから、UXを高めるための開発にリソースを割けるようになり、開発チーム全体の生産性もかなり上がったなと感じています。よければぜひご参考になさってみてください。

もっと詳しく知りたい方がいたら、ぜひMeetyで気軽にご連絡いただけたらと思います!他社さんの取り組みもお伺いしてみたいです。

meety.net