LayerX エンジニアブログ

LayerX の エンジニアブログです。

LayerX インボイスの技術スタック〜分野横断で開発するためのSchema Driven Development〜

DX事業部の @yyoshiki41 です。

今回は、今年1月にリリースしたLayerX インボイス のアプリケーション開発についてです。 インフラ構成については、昨日のLayerX インボイスのインフラアーキテクチャで紹介されています。

tech.layerx.co.jp

LayerX インボイスというプロダクトの特徴

LayerX インボイス は、主に経理業務を行う方を対象ユーザーにした SaaS プロダクトです。 サービス内で以下の業務を行うことがメインになっています。

  1. 請求書の受領
  2. 請求書から仕訳を行う
  3. オンラインバンキングに取り込むためのデータを出力する
  4. 会計ソフトに仕訳データを連携する

また上記の他にも

  • マスタデータ(例えば、仕訳データの入力に使う取引先や部門、税区分など)の管理や会計ソフトへの連携
  • 源泉税の支払管理
  • 請求書の社内での確認

などを行う機能が実装されています。 エンジニアの普段の業務としては、馴染みのないものがほとんどですね、、

前置きが長くなりましたが、経理業務や会計からは少し離れ、本題のアプリケーション開発の話に移っていきます。

横断的な開発チーム

チームの特徴としてはフロントエンド開発だけを行うメンバーはおらず、

  • バックエンドエンジニアがフロントエンド開発を行う
  • UIデザイナーもフロントエンド開発を行う

という体制で開発をしています。

担当の領域を明確化するのではなく、機能単位で実装者をアサインして進めており、 プロダクトが立ち上げ期においては(必要な機能数が多いこともあり)非常にワークしていると思います。 プロジェクト初期に、サービス立ち上げを行った経験値を持つエンジニアが揃っていたこともうまくハマった点でもあります。

Frontend <=> Backend に限らず、Backend <=> Ops などをきれいに線引くことは難しく、 両者を行き来できるとコミュニケーションコストや機能提供スピードの面でもメリットを享受出来ると考えています。

フロントエンド開発

現在は、PCブラウザでのみサービス提供をしており、 Nuxt + TypeScript で、SPA という見慣れた構成かと思います。 Cloudfront + S3 での静的配信を行う形を取っています。

バックエンド開発

バックエンドは、Go がメインで使われています。 使っているライブラリなどは下記です。

  • フレームワーク:go-swagger
  • ORM:gorm(v1)
  • Logger: zerolog
  • DB Migration: dbmate
  • Model Generator from DB Schema: xo(fork したものを使っています)
  • Job Worker: Machinery

選択の理由としては、以下のような理由が挙げられます。

  1. まず薄く入れれるもの
  2. PR / Fork してメンテして運用出来るもの
  3. 静的型付けでコーディング出来るもの
  4. Goプログラムと外部とのデータ I/O の変換(marshalling/unmarshalling)部分を自動化出来る(e.g. HTTP API での json, DB)

以降では4番目の観点であり、チームとしての開発方針である Schema Driven を支えるものをメインに紹介していきます。

横断的な開発と Schema Driven Development

先程も述べたとおり、開発者はフロントエンドとバックエンドの双方を開発するため、 クライアントサイドエンジニアにAPIサーバーのI/Fを共有してモックを作って実装開始してもらうなどのコミュニケーションは行ってはいません。 それでも Schema Driven で開発するメリットとして享受したかったものに以下があります。

  • スキーマを先に宣言として書き、それがドキュメンテーションになる
  • レビュー工程を設計段階で行える
  • レビュー後、スキーマを「真」とした生成が行われた状態(強制された状態)で実装を始めることができる

また、HTTP API だけでなく DBレイヤでも取り入れている Schema Driven の開発プロセスをご紹介します。

■ go-swagger: HTTP API Schema 編

  1. Swagger 2.0 で定義を書く
  2. レビュー
  3. go-swagger でコードジェネレート (Swagger定義から、API Router, Req/Res Structs を生成)
  4. 作成された scaffold から、内部実装を書き進める

Swagger 2.0 (OpenAPI 2.0) しか使えない点はデメリットですが、それ以外に困る箇所は今の所ありません。 複数APIサーバーをモノレポで管理しており、Swagger 定義は以下のようなディレクトリで管理しています。

swagger
├── api                # サービスAPI
│   ├── gen.yaml       # 生成後の swagger定義 yaml
│   ├── meta.yaml      # swagger meta データ
│   └── paths          # URLパス毎にファイルを分割
│       └── users.yaml # /users の定義
├── api-admin          # 管理画面API
├── api-internal       # 内部サービス通信用API
└── definitions        # 全APIでの共通 model 定義
    └── users.yaml

API毎にディレクトリがあり、 definitions でモデル定義を共通化させて管理させています。 meta.yaml + paths/ + definitions/ から、swagger/api/gen.yaml を生成して、それを go-swagger にくわせてコード生成しています。

■ xo: DB Schema 編

  1. migration ファイルをSQLで書く
  2. レビュー
  3. ローカル上でマイグレーション適応
  4. xo コマンドでコードジェネレート (DB Schema から Go Structs を生成)

xo の使い方は 本家のREADME に譲りますが、Go Structs や enum、テーブルのPK/UK/FKからのクエリ関数生成、また生成テンプレート(Go の "text/template" 形式)をカスタマイズして任意のコードを生成することも可能です。

現状は主にモデル生成部分で使っており、CI上でテストを走らせ、DBスキーマがコードに反映されていないことを検知できるようにしています。

手を入れている点としては、アプリケーションから変更を加えないカラム(e.g. created_at, updated_at, MySQL の Generated Columns)を無視する設定を入れていたり、テンプレート部分を変更してテーブルやカラム名の const 化を行ったりしています。 DB アクセスする関数では gorm を使用していますが、生クエリではなく生成された Struct を使って書くケースが大半です。

おわりに

プロジェクト初期から Schema Driven を取り入れていたため、開発フローのベース部分での手戻りやカオス化などは避けられたと考えています。 とは言えまだまだスピードも開発機能も必要とするプロダクトなので、今後も変化をしていくことにはなります。

採用もオープンになっていますので、少しでも興味ありましたら、一度話を聞きに来てください!

herp.careers

LayerX インボイスでは、Schema を先に書いて開発を始めるスタイルを導入しました。 使用するプログラミング言語の実装から開始する(コードからスキーマを生成する)アプローチも、言語の特性とツール選択からは可能かと思います。 GraphQLで schema-first vs code-first が話にあがることもあり、 今後も開発スタイルをプロジェクトにあった形で始める点は動向には注目していきたい次第です