この記事は、Sansan Data Intelligence開発Unitブログリレーの Vol.11 です。

こんにちは、技術本部Data Intelligence Engineering Unit Data Intelligence Groupでインターンをしている仲野です。
今回のブログでは、インターンとしてアサインされた私がキャッチアップを行い、すぐに軌道に乗ることができた、Sansan Data Intelligence(SDI)のアプリケーションサイドの開発フローを紹介します。
Sansan Data IntelligenceのArchitecture
SDIの開発において、Architectureと開発フローには密接な関係があります。そのため、まずはArchitectureのお話から始めます。
SDIのBackendは、Domain-Driven Design(DDD)をベースに、大きく3段階のスコープで構造化されています。
- Bounded Context(BC):知識・語彙の境界。チームの境界とも対応する
- Deployment Unit(DU):デプロイ単位。1プロセス・1コンテナに対応する
- Aggregate:トランザクション境界。ドメインモデルの単位
Deployment UnitとはSDI独自の概念で、一般的なマイクロサービスに相当するデプロイ単位です。関連するAggregateをまとめ、更新・デプロイの単位として定義されています。BCよりも細かい粒度で、非機能要件(スケーラビリティ等)を加味して設計されます。
また、各AggregateがPresentation / Application / Domain / Infrastructureの4層構造を包含しており、機能追加の際は、既存コードへの影響を最小限に抑えつつ、新しい「ブロック」を積むように実装を追加することができるようになっています。
また、SDIはcontracts, coreの2構成のリポジトリで実装されています。
- contracts : gRPCのAPI Interfaceをprotoファイルで定義するリポジトリ
- core : Interfaceに沿ってビジネスロジック(すなわちcoreとなる部分)を実装するリポジトリ
リポジトリを分けることで、contractsの定義とcoreの実装を独立して進められるようになっています。またcontractsのPRがレビュー中であっても、core側は開発ブランチのcommit hashを参照して実装を並行して進めることができ、レビュー待ち時間を無駄にしない開発ができるようになっています。
この設計思想が、後述する開発フローにも一貫して反映されています。
開発フロー全体像
SDIの機能開発は、大きく以下のStepで進みます。
- Command/Queryの設計
- チームレビュー
- contracts(gRPC/proto)の定義
- coreの実装
設計の合意を先に固めることで実装の手戻りを防ぎ、PRレビュー時もレビュアーがContextを持った状態で臨めるため、全体がスムーズに進みます。
ここでは、Tenant Userを追加するAdd Commandを例に、一連のフローを追ってみます。
設計
まずFigJamで以下を整理します。
- Aggregate:
tenant/tenant/tenant_user - Command:
Add - Publish event:
added

チームレビュー
設計が終われば、チーム内で同期レビューを行います。
Product Backlog Item(PBI)の内容を押さえた上で以下のような点について認識合わせを行います。
- BC, DU, Aggregateの設計は妥当なものか
- DB 設計のTable, Indexは妥当なものか
- 要件を網羅できているか
次に開発に移っていくのですが、contractsで先にSchemaを定義し、コードレビュー後にcoreの実装に進んでいきます。
contracts実装
contractsの実装では雛形を生成するCLIが用意されており、以下のように実行することでprotoファイルの雛形が自動生成されるようになっています。
sci gen contract command \ --bounded-context tenant \ --deployment-unit tenant \ --aggregate tenant_user \ --command add \ --event added
雛形生成のCLIの大きなメリットは、実装の揺れをなくせることだと思っています。
gRPCのCommand/Queryを新たに追加する際、protoファイルの命名・ディレクトリ構造・サービス定義の書き方といった定型部分を毎回手動で揃えるのはコストがかかります。実装されているCLIはこれらをプロジェクトの規約に沿って自動生成するため、以下のような揺れが起きません。
- メッセージ名の命名規則(XxxRequest/XxxResponseの形式)
- サービス定義のファイル分割粒度
- BC・DU・Aggregateごとのディレクトリ配置
また、protoファイルから Go・TypeScriptのコードが自動生成されるため、手書きで合わせる必要がなく、Backend・Frontend間の型の不一致をコンパイル時に検出できます。
結果として、Command/Queryの追加にかかる時間の大半を「Interfaceの設計」と「ビジネスロジックの実装」に費やすことができ、最速で開発を行うことができます。
実際に以下のファイル階層で生成されます。
tenant_user/v1
├── tenant_userv1connect
├── tenant_userv1fixture # fixture
├── service.pb.go # no edit
├── service.proto # RPC methodの定義
├── add_message.pb.go # no edit
├── add_message.proto # command/queryのreq/resの定義
├── added_event.pb.go # no edit
├── added_event.proto # eventの定義
└── added_event.go # no edit
# 同時にTypeScript(Frontendで用いる)の型生成も行われるが、省略
以下のようなTODOの部分を埋めてSchema定義を実装し、生成。PRを出します。
syntax = "proto3"; package tenant.tenant.tenant_user.v1; option go_package = "tenant/tenant/tenant_user/v1"; message TenantUserServiceAddRequest { // TODO: Add fields here } message TenantUserServiceAddResponse { string id = 1; // TODO: Add fields here }
core実装
coreの実装もCLIの実行から始めます。以下のように実行すると、DDDの層構造に沿ったファイル群の雛形が自動生成されます。
sci gen template command \ --bounded-context tenant \ --deployment-unit tenant \ --aggregate tenant_user \ --command add \ --event added
ファイルの階層は以下のようになっており、それぞれのファイルがCLIによって雛形として生成されます。
tenant_user/
├── handler_v1.go # API endpoint
├── add_v1_handler.go # Request
├── add_v1_application_service.go # usecase
├── add_v1_test.go # test コード
└── internal/core/
├── entity.go # ドメインロジック
├── vo.go # Value Object
├── repository.go # データ永続化
├── add_v1_adapter.go # データ変換
└── added_event.go # event 定義
雛形の例
// TODO: Application Serviceのロジックを実装してください。 // TODO: Implement the logic for the Application Service. // サービスロジック以外の処理 (値の変換、DB からのデータ取得等) は internal/core パッケージに実装してください。 // エラーは下記ドキュメントを参考に ex.WrapAsFoo を使用して適切にラップしてください。 // https://github.com/sansan/... func (s *addApplicationServiceV1) execute(ctx context.Context, req *addV1Args) (*addV1Result, error) { // TODO: req から必要な情報を取得して新しい RootEntity を作成する // TODO: Extract necessary information from req and create a new RootEntity _ = req newRootEntity := core.NewRootEntity(core.NewCreatedAt(s.clock.Now())) ...
雛形が生成されればTODOコメントに沿ってロジックを実装していきます。Application Service・Aggregate・Repositoryそれぞれの責務が雛形の時点で決まっているため、ロジックの実装だけに集中して進めることができます。
contractsと同様に、CLIによって実装の構造が規約に従って強制されます。そのため、誰が実装を行なっても同じ層構造・命名規則のコードが生まれるため、コードレビューの負荷を減らすことにもなっています。
この開発フローを踏んだことで感じたメリット
- チームレビューの重要さ
設計レビューをチームで同期的に行うことで、コードレビュー時にレビュアーが実装の背景を把握済みであるため、説明コストを省くことができ、また、認識のズレを設計段階で解消できるため、実装後の手戻りが発生しにくくなります。実際に、私もアサインされて半日でcontractsのSchema実装PRを上げ、チームの合意を得ることができました。
- CLIによってArchitectureの制約がコードに強制される
ファイル構成・命名規則・層の責務分担がCLIによる自動生成によって規約化されているため、構造ではなくロジックの正しさにコードレビューの焦点を絞れます。また、DDDのArchitectureさえ把握していれば既存コードと一貫した構造で実装を始められるため、オンボーディングコストも低くなっています。
- 雛形PRと実装PRの分離でレビューの認知負荷を下げられる
雛形生成直後のPRと、ロジックを実装したPRを分けることでレビュアーにとってどこが実装した部分なのか明確に理解することができます。このPRを分けることの徹底によりチーム内のコードレビューがスムーズに進んでいるように思います。
最後に
この明確な開発フローの浸透により、アサイン直後の私でも即座に開発サイクルに合流し、2週間で1機能を実装することができました。今ではSDI開発の機能をチーム単位ではなく、個人単位で実装することができるようになっています。
今後、SDIの機能拡大、チームメンバーの増員に伴っても、この開発フローを踏むことで、スムーズに開発を続けられると考えています。
Sansan技術本部ではカジュアル面談を実施しています
Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。
エンジニア採用説明会を開催します
3月31日(火)に採用説明会を行います。今回の記事で触れたSDI開発の実際について、現場のエンジニアやProduct Ownerから直接お話しします。興味のある方はぜひご参加いただければと思います。