Slide 1

Slide 1 text

© LayerX Inc. ⻑期運⽤プロダクトの開発速度を維持し続け るためのリファクタリング実践例 2024/08/27

Slide 2

Slide 2 text

© LayerX Inc. 2 ● バクラク事業部 請求書受取チーム ● 2023年8⽉⼊社 ● バックエンドの開発が中⼼ 靴、服 旅⾏ 祭り 過去の経歴 ネイティブゲームのクライ アント開発 ニュースアプリのiOS、広 告配信システムのバックエ ンド開発 趣味 wataru ⾃⼰紹介

Slide 3

Slide 3 text

© LayerX Inc. 3 ⻑期運⽤しているプロダクトの直⾯している課題と、どのように向き合ったか 話すこと 実際に⾏ったリファクタリングをgolangのコードも交えて紹介 話すこと

Slide 4

Slide 4 text

⽬次 Agenda ● プロダクト紹介 ● 課題 ● やったこと ● まとめ

Slide 5

Slide 5 text

⽬次 Agenda ● プロダクト紹介 ● 課題 ● やったこと ● まとめ

Slide 6

Slide 6 text

© LayerX Inc. 6 バクラクシリーズの全体像 バクラクは、企業取引の前段となる「稟議の統⼀」と「債権‧債務の⼀元管理」が可能。 従業員‧経理のそれぞれが係る業務領域において、なめらかな業務連携により企業経営を加速させます。 仕訳データ 振込データ ⼊⾦データ 取引先 発注 請求 発注 請求 債権管理 債務管理 従業員 経理 ※ 開発予定の機能を含む 銀⾏ 会計ソフト 請求書 処理 経費 精算 振込 稟議 法⼈ カード 請求書 発⾏ 仕訳 (※) ⼊⾦消込 (※) 仕訳 © LayerX Inc.

Slide 7

Slide 7 text

⽬次 Agenda ● プロダクト紹介 ● 課題 ● やったこと ● まとめ

Slide 8

Slide 8 text

どんな状況? 結構あるあるな気がする

Slide 9

Slide 9 text

© LayerX Inc. 9 ● initial commitは4年前 ● 開発初期は速く出すことが重要、変化も多い ● 初期のコードもまだまだ現役 ● 変化の代償として負債が残るのは当然 状況 仕様が複雑 ● そもそもドメインが複雑、開発者に馴染みがない領域 ● 仕訳や源泉税 ● 振込データ ● 様々な会計ソフトとの連携 ● などなど 課題 請求書受取はバクラク最初期のプロダクト

Slide 10

Slide 10 text

© LayerX Inc. 10 ● ⼟台を⼤きくかえずに既存コードの上に実装してきた ● 機能開発すると意図してない既存機能が壊れたりする ○ 売上が⼀定あり、お客様も多くいる状況で壊すのは不味い ● 問い合わせ、インシデント対応のコストが⾼い ○ 1⽇潰れることも ● 正しい仕様がだれもわからない箇所がある ● この先も⼤きな機能開発がいくつか控えているが、変更箇所や影響範囲がすぐに分からなかったり ⼤きすぎたり… 課題 課題

Slide 11

Slide 11 text

リファクタリング前の構造

Slide 12

Slide 12 text

Presentation Tier Service Tier Repository Tier Repository S3 Repository Imp xo model REST Handler GraphQL Resolver Connect Usecase proto buf repositoryの実態はトランザクションスクリプト+DAOになっており、 操作ごとにメソッドを作成していたので請求書関連で100以上あった

Slide 13

Slide 13 text

© LayerX Inc. 13 ● 閲覧制限(β版として⼀部のお客様に限定公開している機能) ○ ユーザーが閲覧できる書類を制限する機能 ○ 既存のデータフェッチしている箇所ほぼすべてに影響がありそう ○ repository層のメソッドが多すぎて、全部書くのがキツイ ● 外貨請求書対応 ○ 多通貨対応や、⼩数対応などで様々な箇所を触る必要がありそう ○ 影響範囲が読めない ● mysql 5.7 -> 8 ○ 安全のためunit testは書いておきたい ○ repository層のメソッドが多すぎて、全部書くのがキツイ ● GORM V1->V2化 ○ 同上 控えていた⼤きな開発 課題

Slide 14

Slide 14 text

リファクタリングしたい! みんな思ってはいる

Slide 15

Slide 15 text

© LayerX Inc. 15 リファクタリングしたいけど ● 正しい仕様がわからん ○ そういう仕様なのか、たまたまそうなってるのか ○ ドメイン知識も要求されるしなんか壊れそうだから触りたくない 障壁 課題 ● 事業優先度の問題 ○ 機能開発でやりたいことがたくさんある ○ リファクタリングのビジネス上の価値は算定しづらい

Slide 16

Slide 16 text

リファクタリングできる?

Slide 17

Slide 17 text

できる(こともある)

Slide 18

Slide 18 text

© LayerX Inc. 18 リファクタリングしたいけど ● 正しい仕様がわからん ○ 💡PDMや関係者と相談して仕様整理から始める ○ 💡なければ⾃分で仕様書を書く気概でやる 障壁 課題 ● 事業優先度の問題 ○ 機能開発でやりたいことがたくさんある、余裕がない ○ リファクタリングのビジネス上の価値は算定しづらい

Slide 19

Slide 19 text

© LayerX Inc. 19 リファクタリングしたいけど ● 正しい仕様がわからん ○ そういう仕様なのか、たまたまそうなってるのか ○ ドメイン知識も要求されるしなんか壊れそうだから触りたくない 障壁 課題 ● 事業優先度の問題 ○ 💡 機能開発の速度は落とさなければ問題ない ○ 💡 機能開発のためのリファクタリング

Slide 20

Slide 20 text

© LayerX Inc. 20 ● フルリプレイスやそれくらいの規模のリファクタリングでは短期的な開発速度は落ちるしその実施 判断は難しいので今回のスコープ外 ● 今回は開発期間として最低でも数週間~の状況、あまりにも短いと厳しいかも ● リファクタリングの⽬的が明確にあると良い ○ ステークホルダーからの理解が得られやすい ■ やる場合、やらない場合のpros/consを⾔語化して伝える ○ キレイにしたいからという⾃⼰満⾜にならない ● 開発期間の前半にリファクタリングをしておけば、機能開発の効率は⼤幅に上がると判断 ○ リファクタパート、開発パート合わせて当初の予定通り出せそう ● 機能開発と同時にはリファクタリングしない ○ リファクタリングだけした状態でtestやQAを通しておきたい ● 直接今回の機能開発に関係ないことはやらない 機能開発とセット 課題

Slide 21

Slide 21 text

⽬次 Agenda ● プロダクト紹介 ● 課題 ● やったこと ● まとめ

Slide 22

Slide 22 text

© LayerX Inc. 22 ● やりたいことは⼭ほどあるが、全部やるのは厳しい ● 開発速度に直結するような費⽤対効果が良いものをやる 対象を決める やったこと

Slide 23

Slide 23 text

© LayerX Inc. 23 ● 閲覧制限(β版として⼀部のお客様に限定公開している機能) ○ ユーザーが閲覧できる書類を制限する機能 ○ 既存のデータフェッチしている箇所ほぼすべてに影響がありそう ○ repository層のメソッドが多すぎて、全部書くのがキツイ ● 外貨請求書対応 ○ 多通貨対応や、⼩数対応などで様々な箇所を触る必要がありそう ○ 影響範囲が読めない ● mysql 5.7 -> 8 ○ 安全のためunit testは書いておきたい ○ repository層のメソッドが多すぎて、全部書くのがキツイ ● GORM V1->V2化 ○ 同上 控えていた⼤きな開発(再掲) 課題

Slide 24

Slide 24 text

© LayerX Inc. 24 repository層を中⼼としたリファクタすることに 複雑なドメインに⽴ち向かうためにDDDも⼀部取り⼊れる ● 今後の機能開発を眺めてみると、repository層のリファクタリングが⼀番効果がありそう 対象を絞る やったこと

Slide 25

Slide 25 text

© LayerX Inc. 25 service層のI/O変更 ● handler層から呼ばれるserviceのI/Oは変更しない ● serviceレベルで、外から⾒た振る舞いに変更はない ● service層のすでに存在するunit testが通れば安⼼ やらないこと やらないこと 既存テーブルの設計変更 ● 影響が⼤きすぎる ● 振る舞いを変えずに変更することが難しい

Slide 26

Slide 26 text

© LayerX Inc. 26 機能開発に影響を与えない部分のリファクタ ● 理想は全repository書き換えたいが、今回やりたい機能開発の開発速度が上がるわけではなく、⾃ ⼰満⾜になるかもしれない やらないこと やらないこと DDDの正しさを追い求めすぎない ● DDDのエッセンスは取り⼊れるが、原理主義にならない ● 正しいDDDを導⼊する⽬的でリファクタリングするわけではない

Slide 27

Slide 27 text

© LayerX Inc. 27 repository層のリファクタ ● 集約ルート単位でのやり取りに ● usecaseごとに作られていたメソッドの削除 具体的には やったこと domain層の導⼊ ● 集約に対するビジネスロジックをまとめる ● エンティティ、domain serviceの作成

Slide 28

Slide 28 text

© LayerX Inc. 28 具体的には やったこと repository層のリファクタ ● 集約ルート単位でのやり取りに ● usecaseごとに作られていたメソッドの削除 domain層の導⼊ ● 集約に対するビジネスロジックをまとめる ● エンティティ、domain serviceの作成

Slide 29

Slide 29 text

© LayerX Inc. 29 repositoryの設計思想 ● 集約内部の変更は必ず集約ルートを経由することで集約内を常に整合性が確保された状態にする ● 集約ルートの単位でデータの取得・永続化を行う ● 集約ルート:repositoryは1:1 ○ テーブル単位ではない ● 集約をまたいだ検索が必要な場合、 query serviceで書く ● ビジネスロジックを持たない ○ 指示(指定された引数)に従って CRUDするだけ ● repository同士で依存しない repository層のリファクタ

Slide 30

Slide 30 text

© LayerX Inc. 30 repository層のリファクタ ● 既存のモデルをすべて書き出し整理した ● 良い集約の範囲を決めるのは難しい ● やってみて違和感がないか確認したり、試⾏錯誤が必要 ● ドメインエキスパートに相談してもいいかも 集約を定義 請求書 (ルート) 請求書ファイル 請求書タグ

Slide 31

Slide 31 text

© LayerX Inc. 31 repository層のリファクタ ● 集約ルートのエンティティをdomainパッケージに作成した ● 集約ルート以外は既存の⾃動⽣成されたmodelを使⽤ 集約を定義 package domain type Invoice struct { *model.Invoice Files []*model.InvoiceFile Tags []*model.InvoiceTag } package model type InvoiceEmbedded struct { Invoice Journals []*Journal Client *Client Files []*Files … }

Slide 32

Slide 32 text

© LayerX Inc. 32 repository層のリファクタ ● 関連テーブルを全部まとめたような巨⼤structをあちこちで使⽤しており、集約単位に分解が必要 だった ● 不要な箇所でも巨⼤structを使⽤して関連テーブルをfetchしておりパフォーマンスも良くない ● 画⾯の描画に集約外の要素が必要であればpresentation層でくっつける ● 既存の巨⼤モデルでは19モデルembeddingされてる箇所も 既存のstructの分解

Slide 33

Slide 33 text

© LayerX Inc. 33 repository層のリファクタ ● 旧repositoryの⼀部、⼤量のメソッドがある ● 微妙に違うusecaseに対して違うメソッドが存在し、I/Oもバラバラ ● ⼤きな変更の際など、全て変更するのも、全てtestを書くのもつらい repositoryの再定義 package domain type InvoiceRepository interface { GetByID(ctx Context, id string) (*model.Invoice, error) GetFileByID(ctx Context, id string) (*model.InvoiceFile, error) UpdateForFooUseCase(ctx Context, value string) error } // 集約の一部を操作するようなrepoは削除 type InvoiceHogeRepository interface { UpdateStatus(ctx Context, status string) error }

Slide 34

Slide 34 text

© LayerX Inc. 34 repository層のリファクタ ● 集約ルート単位でデータのやり取りをする ● 基本的にはGet,GetMany,Saveのみ提供(例外はあるが) repositoryの再定義 package domain type InvoiceRepository interface { Get(ctx Context, id string) (*Invoice, error) GetMany(ctx Context, ids ...string) (*Invoice, error) Save(ctx Context, id string) error }

Slide 35

Slide 35 text

© LayerX Inc. 35 repository層のリファクタ ● 集約外のテーブルを使⽤したい場合 ● 複雑な条件の検索が必要な場合別途query serviceを作る 集約をまたぐ場合 package query type InvoiceQueryService interface { Find(ctx Context, params InvocieFindParams) (domain.Invoices, error) } // 検索条件 type InvocieFindParams { name *string status *model.InvoiceStatus }

Slide 36

Slide 36 text

© LayerX Inc. 36 具体的には やったこと repository層のリファクタ ● 集約ルート単位でのやり取りに ● usecaseごとに作られていたメソッドの削除 domain層の導⼊ ● 集約に対するビジネスロジックをまとめる ● エンティティ、domain serviceの作成

Slide 37

Slide 37 text

© LayerX Inc. 37 ● サービス層に書かれていたエンティティに関するビジネスロジックの移植 ● 内部状態の変更はエンティティのメソッド経由でしか⾏わない エンティティ domain層の導⼊

Slide 38

Slide 38 text

© LayerX Inc. 38 エンティティ domain層の導⼊ package serivce func (s Invoice) UpdateStatus(ctx context.Cotext, id string, status model.InvocieStatus) error { invoice := s.repo.GetByID(ctx, id) // statusを直接書き換える invoice.Status = status // 集約ルートを経由しないで書き換える invoice.Files[0].Status = hoge // 専用のメソッド return s.repo.UpdateStatus(id, status) } ● 古い実装 (極端な例)

Slide 39

Slide 39 text

© LayerX Inc. 39 エンティティ domain層の導⼊ package serivce func (s Invoice) UpdateStatus(ctx context.Cotext, id string, status model.InvocieStatus) error { invoice := s.repo.Get(ctx, id) invoice.UpdateStatus(ctx, status) // 必要ならvalidationとか return s.repo.Save(ctx, invoice) } ● リファクタリング後の実装

Slide 40

Slide 40 text

© LayerX Inc. 40 ● 複数のエンティティにまたがる場合や⾃然に表現できない場合 ● 多⽤はしない、どうしても必要なときのみ ○ ドメインモデル貧⾎症にならないように ● 例えば共通の採番ロジックなど、各エンティティに直接持たせるのが不⾃然な場合 domain service domain層の導⼊

Slide 41

Slide 41 text

© LayerX Inc. 41 ● 採番ロジックの例、実際は採番テーブルを使⽤しており、interfaceがdomain層にある ● 他にも、重複チェックなどが考えられる(エンティティ⾃⾝が⾃分が重複しているか知らないから) domain service domain層の導⼊ package domain func (s InvoiceService) CreateInvoice(ctx context.Cotext, …) (*domain.Invoice error) { invoice := NewInvoice(...) num = s.numberGenerator.Generate() // なんか処理 return invoice }

Slide 42

Slide 42 text

リファクタ後の構造

Slide 43

Slide 43 text

© LayerX Inc. 43 Before(再掲) domain層の導⼊ Presentation Tier Service Tier Repository Tier Repository S3 Repository Imp xo model REST Handler GraphQL Resolver Connect Usecase proto buf

Slide 44

Slide 44 text

© LayerX Inc. 44 domain層の導⼊ Presentation Tier Usecase Tier Domain Tier Infra Tier Entity Repository Domain Service Repository Imp xo model REST Handler GraphQL Resolver Connect Usecase proto buf S3 usecase model

Slide 45

Slide 45 text

⽬次 Agenda ● プロダクト紹介 ● 課題 ● やったこと ● まとめ

Slide 46

Slide 46 text

© LayerX Inc. 46 ● スコープを絞り、機能開発とセットでリファクタリングをすることで開発速度を落とさずにリファ クタリングできた ○ 👍その後の開発では恩恵だけただで受けれる ● DDDを⼀部取り⼊れ集約を定義し、⼤量にあったrepositoryのメソッドを整理した ○ 👍その後のrepositoryに対する変更が容易に ○ 👍 unit testも楽 ● サービス層かかれていたビジネスロジックの移植 ○ 👍 集約ルートのエンティティ経由でしか内部状態が変更されないことが保証されるため、変 更すべき箇所が明確に。不具合対応も楽に まとめ まとめ

Slide 47

Slide 47 text

© LayerX Inc. 47 ● リファクタリングによって、開発速度が上がったり、開発体験がよくなった実感はあるが、今回は その価値を評価まではしていない ● いい⽅法があれば懇親会で教えて下さい! さいごに まとめ