Slide 1

Slide 1 text

Sansan技術本部 設計の世代交代を乗り越える、 14年続くAndroidアプリの開発戦略 Sansan株式会社 技術本部 技術本部 Eight Engineering Unit Mobile Applicationグループ 荒翔太 若⽥直希

Slide 2

Slide 2 text

荒 翔太 技術本部 Eight Engineering Unit Mobile Applicationグループ - 新卒⼊社7年⽬ - 普段は関⻄に⽣息しています - 今年から通信制⼤学に通っています

Slide 3

Slide 3 text

若⽥ 直希 技術本部 Eight Engineering Unit Mobile Applicationグループ - Sansan株式会社に2025/11に中途⼊社 - 北海道⽣まれ北海道育ちの左利き - マラソンにハマっています

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

長く運用されているアプリで、 設計由来の負債にどう向き合うか

Slide 6

Slide 6 text

アジェンダ 1. Eightアプリに積み重なった設計 2. なぜ開発しやすくならないのか 3. 運⽤とリファクタで課題を解決する a. 新たに積み重ねない b. 積み重なった設計を減らす 4. 次の世代に向けて 5. まとめ

Slide 7

Slide 7 text

Eightアプリに積み重なった設計

Slide 8

Slide 8 text

Eightアプリに積み重なった設計 14年の歴史を持つ⼤規模なコードベース Initial commit Java/Kotlin lines 設計パターン 2012-11-20 450,000+ 7世代

Slide 9

Slide 9 text

設計パターンの変遷 初期 1. MVC 2. MVP Rx基盤 3. 独⾃Flux 4. RecyclerView + 独⾃Flux 5. 独⾃Flux + ViewModel MVVM 6. Activity + MVVM + UseCase 7. Compose + MVVM

Slide 10

Slide 10 text

これまで⾏ってきた改善 最新の設計への改善は続けてきた - RxからCoroutinesベースの設計への移⾏ - Composable対応 - Fluxベースの独⾃アーキテクチャからMVVMへの変更

Slide 11

Slide 11 text

Eightアプリに積み重なった設計 しかしその結果として、7世代の設計が 同じコードベース上に混在している

Slide 12

Slide 12 text

Eightアプリに積み重なった設計 設計パターンが増えたことで認知負荷も 上がっていった

Slide 13

Slide 13 text

設計が積み重なったことによる認知負荷の上昇 開発時に、影響範囲にある全ての設計パターンに ついて考慮する必要がある - 開発速度の低下 - 調査コストの増加 - 修正難易度の上昇 - オンボーディング速度の低下

Slide 14

Slide 14 text

なぜ開発しやすくならないのか

Slide 15

Slide 15 text

改善の落とし⽳ 多くの改善はプレゼンテーション層に閉じていた プレゼンテーション層 MVC MVP Flux MVVM トレンドへの追従 モデル層/データ層 以前からの実装が参照され続けた

Slide 16

Slide 16 text

なぜデータ層への改善が遅れたのか - データ層はわかりやすいトレンドがなく、 外部要因で刷新されづらい - データの永続化を担っており、 改善や刷新のコスト, リスクが⼤きい

Slide 17

Slide 17 text

過去のデータ層が参照され続けると 新設計に閉じた開発ができず、 認知負荷の上昇に繋がる

Slide 18

Slide 18 text

課題が顕在化した理由

Slide 19

Slide 19 text

Eightの過去のデータ層が抱えていた問題 - 広範なオフライン対応 - 特定ライブラリへの依存が表出している - ⼀つのデータ更新が複数の機能に伝播する

Slide 20

Slide 20 text

広範なオフライン対応 - Eightは紙の名刺管理に劣らない体験を提供する ため、ローカルDBを利⽤している機能が多い - 開発初期には基準が曖昧で、アプリの多くの モデルがローカルDBで管理されていた

Slide 21

Slide 21 text

広範なオフライン対応 新機能の実装時に、データの更新やモデルの 変更と、アプリ内 DBとの整合性を取るために、 古い実装を参照する必要がある

Slide 22

Slide 22 text

特定ライブラリへの依存が表出している - Repositoryの引数や返り値に、ライブラリや フレームワークの型が指定されている - Deprecated化やライブラリの置き換え後も、 古い型を扱う必要がある

Slide 23

Slide 23 text

課題が顕在化した理由 - 広範なオフライン対応 → 新規実装をする際に、以前のデータ層を 参照する必要がある - 特定ライブラリへの依存が表出している → 以前のデータ層を参照すると過去世代と 切り離せなくなる

Slide 24

Slide 24 text

課題が顕在化した理由 1. 以前の設計思想への依存が増え続ける構造

Slide 25

Slide 25 text

⼀つのデータ更新が複数の機能に伝播する 名刺に関連するデータを参照する画⾯が多く、 データの更新、削除などで多数の画⾯が 影響範囲になる

Slide 26

Slide 26 text

⼀つのデータ更新が複数の機能に伝播する - 主要機能は影響範囲が絡まり合っている → ⼩さな単位でのリファクタリングが難しい

Slide 27

Slide 27 text

課題が顕在化した理由 1. 以前の設計思想への依存が増え続ける構造 2. 以前の設計思想への依存が減らせない構造

Slide 28

Slide 28 text

Eightアプリで起きていた問題

Slide 29

Slide 29 text

Eightアプリで起きていた問題 - 考慮するべき設計の世代が重なると、 認知負荷が増⼤し開発が難しくなる - 逐次的な改善では考慮するべき設計の世代を 減らせない構造になっていた

Slide 30

Slide 30 text

運⽤とリファクタで課題を解決する

Slide 31

Slide 31 text

問題のおさらい

Slide 32

Slide 32 text

Eightのアプリに設計が積み重なった原因 データ層を変更しづらい 新設計が古いデータ層に依存 MVC MVP Flux MVVM データ層 設計が世代の数だけ積み重なる

Slide 33

Slide 33 text

設計が重なった原因に対する打ち⼿ 2つのアプローチで、設計の積み重なりに対処する データ層を変更しづらい 設計を新たに積み重ねない 新設計が古いデータ層に依存 設計が世代の数だけ積み重なる 積み重なった設計を減らす

Slide 34

Slide 34 text

設計を新たに積み重ねない

Slide 35

Slide 35 text

具体的なシチュエーションを想像 - 新しい画⾯を新しいアーキテクチャで作ることを想定 - 古いデータ層の情報が欲しい場合 UI Layer MVC MVP Flux MVVM Domain Layer(optional) データ層 Data Layer 引用:アプリ アーキテクチャ ガイド 別の⾮同期処理 / ローカルDB

Slide 36

Slide 36 text

具体的なシチュエーションを想像 直接依存は設計が積み重なってしまう MVC MVP Flux MVVM データ層 UI Layer Domain Layer(optional) Data Layer 引用:アプリ アーキテクチャ ガイド 作り直しは影響範囲が広すぎる 過去実装に依存してしまう

Slide 37

Slide 37 text

古いデータ層への依存が増えないように、境界を設計する 1. 境界を作る 新旧の実装を橋渡しする仕組みを設計 2. 境界を守る 決めた境界が破られない仕組みを作成 新 UI Layer Domain Layer(optional) 旧 データ層 Data Layer 引用:アプリ アーキテクチャ ガイド

Slide 38

Slide 38 text

境界を作る 新旧の橋渡しをする中間層を置く 新 UI Layer Domain Layer(optional) 旧 Bridge データ層 Data Layer 引用:アプリ アーキテクチャ ガイド

Slide 39

Slide 39 text

境界を作る Bridgeが古いデータ層に直接依存するのはNG❌ 新実装から旧実装を剥がせなくなる 新 UI Layer Domain Layer(optional) 旧 Bridge データ層 Data Layer 引用:アプリ アーキテクチャ ガイド

Slide 40

Slide 40 text

境界を作る - 古い実装には直接依存させない 依存性を逆転させる!! 新 UI Layer 旧 BridgeImpl データ層 Domain Layer(optional) Bridge Interface Data Layer 引用:アプリ アーキテクチャ ガイド

Slide 41

Slide 41 text

境界を作る - 古い実装には直接依存させない 依存性を逆転させる!! 腐敗防⽌層として機能させる!! - Realmのモデル → Bridgeの型 - RxJava → Coroutines / Flow 新 UI Layer 旧 BridgeImpl データ層 Domain Layer(optional) Bridge Interface Data Layer 引用:アプリ アーキテクチャ ガイド

Slide 42

Slide 42 text

境界を守る 境界を作っても無意識的にルールを破ってしまう レビューでも⾒逃してしまう OK 旧 新 新 interface 旧 NG 新 旧 静的に検査し、機械的に保証する - 依存グラフを検査するGradleプラ グイン - 違反があれば失敗させCIで検出

Slide 43

Slide 43 text

設計を新たに積み重ねない ― まとめ 新しい世代を⾜しても、古いデータ層へ直接設計が 積み重ならない状態を整えた - 境界を作る:中間層を置き、新→旧の依存の⽮印そのも のをなくした - 境界を守る:依存グラフを静的に検査するGradleプラグ インで、ルール違反をCIで機械的に検出できるように した

Slide 44

Slide 44 text

積み重なった設計を減らす

Slide 45

Slide 45 text

次に、⼟台の上で積み重なった設計を減らす データ層を変更しづらい 設計を新たに積み重ねない 新設計が古いデータ層に依存 設計が世代の数だけ積み重なる 積み重なった設計を減らす

Slide 46

Slide 46 text

増えた設計パターンを減らす 開発速度の低下‧調査コストの増加、修正難易度の上昇、 オンボーディング速度の低下 を解消するために統⼀ UI Layer MVC Flux MVP MVVM Domain Layer(optional) Data Layer 引用:アプリ アーキテクチャ ガイド

Slide 47

Slide 47 text

積み重なった設計を減らす移⾏戦略 プロジェクトの有無にかかわらず移⾏⽅針を決めることが ⼤事 01 02 03 04 対象を決める 小さく 統合する 段階的に 公開する 旧実装を 削除する 新旧の境界を設計し積み重ならない状態にする

Slide 48

Slide 48 text

対象を決める 全てを置き換えるのは⾮現実的。優先度の⾼い画⾯を選定 全画⾯ 定量評価 約200画⾯ 同じ状態‧旧DBを複数画⾯で参照しており、依存を広げている画⾯ 定性判断 末端の画⾯はやらない。今後変更する可能性が⾼い画⾯ 対象画⾯ 65画⾯

Slide 49

Slide 49 text

⼩さく統合する ― Feature Flagsで未完成機能を隠す ⻑命ブランチを避け、未完成の状態でもトランクへ⼩さく統合して いく - データ層‧UiState‧ViewModel‧UIなどに実装を分割し、⼩ さく実装‧⼩さくレビュー - Feature Flagsで未完成機能を隠し、トランクへの統合を可能 にする

Slide 50

Slide 50 text

段階的に公開する― Firebase Remote Configを利⽤ Firebase Remote Configを⽤いて段階的に公開を⾏いユーザーへ の影響を最⼩限に抑える 社内限定 特定のIDに 配信 1% 100% 設定削除 2⽇後 7⽇後 14⽇後 ロールバック:重⼤な不具合の発⽣時は旧実装へ戻す

Slide 51

Slide 51 text

旧実装を削除する 旧実装だけでなく、それと移⾏に付随したものを削除する 削除までを移⾏完了の条件とすることで、アプリ全体の複雑さを統⼀ できる 旧実装 Feature Flag Remote Config Bridge 削除 削除 削除 削除

Slide 52

Slide 52 text

まとめ ― 設計の積み重なりに対する2つの打ち⼿ データ層が変えづらいという制約の中で 新しい世代を⾜しても設計が積み重ならないようにし すでに積み重なった設計も着実に減らしている。 - 65画⾯中、30画⾯が開発完了。6画⾯がリリース済み。

Slide 53

Slide 53 text

次の世代に向けて

Slide 54

Slide 54 text

次の世代に向けて 今のアーキテクチャから移⾏する将来も 想定しなければいけない

Slide 55

Slide 55 text

次の世代に向けて - 想定できる将来 - 例えばKMPや、他のCompose-drivenな アーキテクチャなど - 避けるべき状態 - 古いアーキテクチャが切り離せず、複雑性が 溜まっていく状態

Slide 56

Slide 56 text

Eightが抱えていた問題を先回りして対応する

Slide 57

Slide 57 text

Eightが抱えていた問題を先回りして対応する 1. 複雑性の源だったオフライン要件の⾒直し 2. 適切なクラス間の境界の設計

Slide 58

Slide 58 text

複雑性の源だったオフライン要件の⾒直し モデル層の影響範囲を⼤きくしていた 仕様⾃体を整理する

Slide 59

Slide 59 text

複雑性の源だったオフライン要件の⾒直し Before 明確な基準がなく、利⽤可能な多くの場所で オフラインキャッシュが追加、利⽤されてきた After プロダクトオーナーと相談し、サービスに必須な オフライン対応の要件の線引きをする

Slide 60

Slide 60 text

複雑性の源だったオフライン要件の⾒直し 仕様⾯からのアプローチでユーザーのコア体験を 毀損せずに実装の複雑化の原因を軽減する

Slide 61

Slide 61 text

適切なクラス間の境界の設計 疎なインターフェースを意識して設計, 実装を⾏う

Slide 62

Slide 62 text

適切なクラス間の境界の設計 - レイヤーごとに独⽴した責務を持たせる - ライブラリの型を境界に使わない

Slide 63

Slide 63 text

適切なクラス間の境界の設計 Before - ライブラリの都合が、参照元に漏れ出していた - 古いライブラリの型を意識し続ける必要があった After - 独⽴したクラス, メソッド定義を⾏う - 特定のライブラリの型は表出させず、呼び出し元に影響を 与えず差し替え可能にする

Slide 64

Slide 64 text

次の世代に向けて - 疎結合なクラス設計を保つことは設計の世代を 乗り越えるのを容易にしてくれる - 複雑さの根元が仕様にある場合には 仕様に⼿を⼊れることも有効

Slide 65

Slide 65 text

まとめ

Slide 66

Slide 66 text

設計の世代が積み重なったことによる課題 実装時に過去の設計への考慮が必要だと、 コードベースの認知負荷が上がり、 開発速度の低下などの問題を引き起こす

Slide 67

Slide 67 text

課題に対して私たちがどのように向き合っているか 影響の境界を定め、リアーキテクチャは形骸化 しないよう筋道を⽴てて進める必要がある

Slide 68

Slide 68 text

課題を受けて、将来に向けて何を意識しているか 設計変更⾃体を避けるのではなく、 変わる可能性を想定してアプリの状態を 整えておく

Slide 69

Slide 69 text

No content