Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
コード分割から始める複雑さの解消に向けたkintoneのアーキテクチャ改善 / アーキテクチャ...
Search
hirokuni-maeta
November 20, 2025
0
10k
コード分割から始める複雑さの解消に向けたkintoneのアーキテクチャ改善 / アーキテクチャConference 2025
hirokuni-maeta
November 20, 2025
Tweet
Share
More Decks by hirokuni-maeta
See All by hirokuni-maeta
複雑性に立ち向かうためのサーバーサイドコード分割 / JJUG CCC 2023 Spring
hirokunimaeta
2
2.1k
Featured
See All Featured
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
17k
Git: the NoSQL Database
bkeepers
PRO
432
66k
Learning to Love Humans: Emotional Interface Design
aarron
274
41k
Speed Design
sergeychernyshev
33
1.3k
Stop Working from a Prison Cell
hatefulcrawdad
273
21k
Writing Fast Ruby
sferik
630
62k
Being A Developer After 40
akosma
91
590k
How To Stay Up To Date on Web Technology
chriscoyier
791
250k
Rails Girls Zürich Keynote
gr2m
95
14k
Automating Front-end Workflow
addyosmani
1371
200k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
1.6k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
46
2.6k
Transcript
コード分割から始める複雑さの解消に向けた kintoneのアーキテクチャ改善 前田 浩邦 (サイボウズ株式会社) アーキテクチャConference 2025 1
自己紹介 ▌前田 浩邦 / Hirokuni Maeta ▌2014年4月 サイボウズ入社 ▌kintone開発チーム /
プロダクトエンジニア ▌サーバーサイドとフロントエンド、最近はサーバーサイドの改善を担当 2
kintone ▌業務システムを作成するためのノーコード・ローコードツール ⚫ 仕事に必要なデータを管理、チームに共有 ⚫ 案件管理や顧客管理など ▌アクセス権、業務プロセスを設定 ▌外部システムとも連携 3
本発表の内容 ▌機能に沿ったパッケージへのコード分割 ▌パッケージ内での機能とwebの分離 ▌AIによる支援 ▌まとめ 4
機能に沿ったパッケージへのコード分割 5
コード分割の背景 ▌長年開発を続ける中でコードが肥大化、複雑化してしまったこと ⚫ src/main/java下の総行数 ≧ 350,000 ⚫ クラスの役割が曖昧で、一つのクラスを把握するためには、呼び出し元 や呼び出し先をたくさん追っていく必要がある ▌開発が非効率に
⚫ 妥当な修正箇所が探しづらい、想定外の箇所に影響が出る ⚫ 新メンバーがキャッチアップしづらい 6
コード分割の背景 ▌要因: ⚫ ローコード・ノーコードツールという柔軟性を持ち、複雑化しやすい ⚫ 初期のモノリシックな作りが見直されず、開発が継続 ▌ボトムアップな改善活動にも限界を感じていた ⚫ 目についたコードをリファクタリングしたところで、残りは膨大 ⚫
このようなリファクタリングを、あと何回やればいいのか? ▌抜本的な改善として、コード分割という取り組みを開始 7
コード分割 ▌コードを以下のような状態にすること ⚫ 機能ごとに、その機能のためのコードを専用パッケージに分割 ⚫ 機能間で関わらないなら、コード間でも独立 ⚫ 機能間に関わりがあるなら、専用にインターフェイスを公開して依存 8
kintoneのアプリ ▌データを管理、共有するための簡単 な“データベース” ▌アプリを使って業務システムを作り、 業務改善を進める ⚫ 案件管理には案件管理アプリ、 顧客管理には顧客管理アプリと いった使い方をする 9
アプリ機能とアプリ設定機能 ▌アプリ機能: “データベース“にあるデータ (レコード) の入力、表示 10
アプリ機能とアプリ設定機能 ▌アプリ設定機能: “データベース”の設定 11 フォーム設定: 文字列や数値等のフィールドを配置して レコードを入力する画面を作成 フィールドの一覧
機能についての考え方: PdMのメンタルモデル ▌メンタルモデル: どのような部分から構成され、どう関係し合ってるかという捉え方 ▌PdMが持つメンタルモデルが、コード分割にも適用できるものだった ⚫ アプリを使う人と作る人のように、異なる属性のユーザーが存在している ⚫ それぞれの属性で違った使い方をしている ⚫
属性に合わせてアプリ機能、アプリ設定機能と分けて考えると、見通しがよい 12 アプリ機能 アプリ設定機能 レコードを 閲覧する フィールドを 設定する アプリを使う人 (現場で業務をする人) アプリを作る人 (情シス・業務リーダー)
コード分割で目指す構成 13 com.cybozu.kintone.appsettings アプリ機能 アプリ設定機能 com.cybozu.kintone.app FieldSetting Controller FieldSetting Service
Field Repository RecordController RecordService RecordData RecordRepository .export FieldSetting ServiceExport
コード分割で目指す構成 14 com.cybozu.kintone.appsettings アプリ機能 アプリ設定機能 com.cybozu.kintone.app FieldSetting Controller FieldSetting Service
Field Repository RecordController RecordService RecordData RecordRepository .export FieldSetting ServiceExport 機能に対応するパッケージを作成し、 機能に属する全コードをパッケージに配置
コード分割で目指す構成 15 com.cybozu.kintone.appsettings アプリ機能 アプリ設定機能 com.cybozu.kintone.app FieldSetting Controller FieldSetting Service
Field Repository RecordController RecordService RecordData RecordRepository .export FieldSetting ServiceExport 必要なら専用インターフェイスを 外部に公開
コード分割で目指す構成 16 com.cybozu.kintone.appsettings アプリ機能 アプリ設定機能 com.cybozu.kintone.app FieldSetting Controller FieldSetting Service
Field Repository RecordController RecordService RecordData RecordRepository .export FieldSetting ServiceExport 内部実装への依存はNG exportへの依存はOK
外部には専用インターフェイスを公開し、内部実装を隠ぺい public interface FieldSettingServiceExport { FieldSettingDto get(long appId); record FieldSettingDto(List<Field>
fields, …) {} record Field(long fieldId, FieldType type, …) {} } 17 アプリ側が機能実装する際、 アプリ設定側の情報についてはこれらにのみ依存
パッケージ間の依存も制限 ▌外部に公開しているからといっても誰でも依存できるわけではない 18 com.kintone.appsettings アプリ機能 アプリ設定機能 com.kintone.app ブックマーク機能 com.kintone.bookmark 機能間に関わりがないなら、
コードにおいても関わらせない .export
依存の管理 ▌ArchUnitを使って依存を定義 ⚫ 内部実装に依存していないか、exportに依存しているか ⚫ パッケージ間の依存としては問題ないか ▌JUnitと組み合わせて利用し、単体テストとしてCIに組み込む ⚫ 意図しない依存が入り込めばテストが失敗し、修正 19
機能の特徴を捉えた上で分割する ▌既存のコードを基に分割しても、複雑さを解消できるとは考えづらい ▌例えば、複数のgetメソッドがあって、同じクラスを返している場合を考える ⚫ そのようなgetメソッド群を切り出し、共通化されるかもしれない ⚫ それでも動くからというだけで、偶然そのクラスを返すようにした可能性もある ⚫ 既存のコードを基にした分割は、偶然を基にした分割にもなりうる ▌機能に沿った分割では、複雑さの解消を期待することができる
⚫ コードは機能を実現するためにあるので、コードと機能は対応付けが可能 ⚫ 機能の分類に沿うことで、独立したパッケージに実装を分割、複雑さを隠ぺい ⚫ kintoneの全体の複雑さを、個々の機能の複雑さに分解 20
コード分割の現在の状況 ▌アプリ設定機能の分割が完了 ⚫ 2022年11月から2024年8月にかけて分割 ▌他機能も分割が進行中 ▌新たな機能は最初から分割された状態で開発 21
コード分割をしてみて ▌機能ごとの開発が容易になった ⚫ 機能に対応するパッケージのコードを読めばよいので、範囲が明確 ⚫ 他機能には影響が出るのか、どのような影響が出るのか、出さないようにするにはど うすればいいか、検討しやすくなった ▌複雑さの要因が一つ見つかった ⚫ 要因:
複数のアクター (アプリを使う人、作る人) の処理が、一つのクラスに混在 ⚫ 詳しくは「複雑性に立ち向かうためのサーバーサイドコード分割」として発表 ⚫ https://blog.cybozu.io/entry/2023/03/14/110000 ⚫ https://speakerdeck.com/hirokunimaeta/jjug-ccc-2023-spring 22
パッケージ内での機能とwebの分離 23
webアプリケーションとしての設計 ▌Service ⚫ ビジネスロジック ⚫ 例: ブックマークの追加、取得 ▌Data ⚫ 主要な情報
⚫ 例: ブックマーク ▌Controller (SpringのController) ⚫ リクエストを受け取り、Serviceを呼び出し、DataをResponseに変換 24 Bookmark Service Bookmark Controller Bookmark Data List Response
背景 ▌Controllerでの、DataからResponseへの変換処理が曖昧だった ⚫ 基本的には単純なメンバ変数のマッピングで、機能の知識を必要としない ⚫ 実際は、単純なマッピングを超えた機能寄りな処理も混ざっていた ⚫ 例: DBを参照してアプリIDからアプリ名を引く、アクセス権が無ければ除去 ⚫
機能寄りの処理はどこまで書いていいのか? ▌曖昧なまま開発を続けることで、開発が非効率に ⚫ どこに書くか迷いながら開発 ⚫ 書き手のさじ加減で決まることで、可読性の低下 ⚫ どこでアプリ名を引いているのかなかなか見つからない 25
Controllerも”外部”とみなし、分割 26 ブックマーク機能 他機能 Service Bookmark ServiceExport .export Bookmark ApiService
.web Bookmark Controller Bookmark Service Bookmark Data
機能の振る舞いをApiServiceに記述 public interface BookmarkApiService { ListResponse list(long userId); record ListResponse(List<App>
apps, …) {} record App(String name, …) {} void add(…); } 27 Bookmark ApiService Bookmark Controller
ControllerはApiServiceを呼ぶだけ public class BookmarkController { ListResponse list() { return bookmarkApiService.list(getUserId());
} } 28 Bookmark ApiService Bookmark Controller
ApiServiceを導入してみて ▌曖昧だったControllerの役割が明確になった ⚫ 機能寄りの処理はどこまで書いていいのか? → 書かない ⚫ ApiServiceを呼ぶのみ ⚫ 必要なら、
SpringやHTTPリクエスト等のweb層のオブジェクトから情報を取り出す ⚫ 機能についての知識は持たず、フロントエンドと機能とを繋げることが役割 ▌さらに、下のような効果もあった ⚫ Dataクラス、Serviceクラスの肥大化が抑制された ⚫ Javaのinterfaceで機能を定義できるようになった 29
肥大化していたDataクラス ▌フィールドの値を保持するRecordDataクラスが肥大化していた ⚫ 各フィールドの値 (例: 1000) ⚫ アプリ設定に従い、画面表示用にフォーマットした値 (例: “1,000”)
30 Record Service Record Data Record Controller Record Response ・ 1000 ・ “1,000” ・ 1000 ・ “1,000” FieldSetting ServiceExport
ApiServiceの導入による肥大化の解消 ▌ApiServiceがフォーマットできるようになった ⚫ なぜなら、ApiServiceはResponseの生成と返却が役割で、Response の生成にはフォーマット処理が必要 ▌RecordDataは永続化の値だけ保持すればよい 31 Record Service ・
1000 Record Controller Record Data Record ApiService Record Response ・ 1000 ・ “1,000” FieldSetting ServiceExport
ApiServiceの導入で判明した複雑さ ▌アプリ名やフォーマットされた値は、 JSONにのみ必要な情報 ⚫ view modelに近い概念 ⚫ 返却されるJSONをviewと解釈としたときに、viewを生成するためのmodel ▌view modelの生成箇所が不適切で、これが複雑化につながっていた
⚫ Controllerでアプリ名を引く → Controllerで生成 ⚫ Controllerではどこまでが変換処理なのか曖昧に ⚫ Dataにフォーマットした値も置く → Serviceで生成 ⚫ Dataクラスが肥大化、Dataクラスを生成するServiceクラスも肥大化 32
ApiServiceの役割 ▌ApiServiceは以下の特徴を持つことから、ユースケースを実装する役割 ⚫ 常に機能の外から利用され、機能内の再利用は想定されない ⚫ 外部からの入力と、出力 (view model) を定義している ⚫
したがって、外部が期待する振る舞いを実現する責任を持つ ▌ユースケースの考えを導入して実装を見直すことで、複雑さの解消が見込める ⚫ 外部が期待する振る舞いを分析し、責務を割り出してクラスに定義する等 ⚫ ApiServiceがResponseを生成する際にフォーマットさせるのはこれに該当 ⚫ DataからResponseへの変換処理が曖昧になっていることの見直しも 33
Javaのinterfaceで機能を定義できるようになった ▌.exportを中心として図示 34 他機能 Service Bookmark ServiceExport .export Bookmark ApiService
.web Bookmark Controller Bookmark Service Bookmark Data
Javaのinterfaceで機能を定義できるようになった ▌他機能部分や.web下に、ブックマーク機能に関するコードは存在しない 35 他機能 Service .web Bookmark Controller 機能沿ったパッケージへのコード分割により、 他機能部分にブックマーク機能はない
機能とwebの分離により、 .web層にはブックマーク機能に関する知識がなくなった
Javaのinterfaceで機能を定義できるようになった ▌機能に関するコードは、残りの部分に存在する 36 Bookmark ServiceExport .export Bookmark ApiService Bookmark Service
Bookmark Data
Javaのinterfaceで機能を定義できるようになった 37 ▌外部から機能として認識できるのは.export部分のみ ⚫ 外部からの依存は.exportに制限されているため ⚫ ServiceやDataは外部から認識できず、それらは exportされているメソッドの振る舞いに表れている ▌外部から機能として依存しているのは、.exportにある interfaceと、メソッドが機能として期待
(契約) 通りに 振る舞うことのみ ⚫ ServiceやDataの特定のあり方には依存していない ⚫ あり方が何であっても、メソッドが期待通りに振る舞う ならば、それは機能になっている Bookmark ServiceExport .export Bookmark ApiService
Javaのinterfaceで機能を定義できるようになった 38 ▌.exportにあるinterfaceが機能を定義している ⚫ ServiceやDataは機能を実現するための内部 実装で、その詳細や複雑さは隠蔽されている ⚫ 機能を抽象データ型で表現 ▌応用 ⚫
結合テストの対象 ⚫ 機能開発の際、修正を考える起点になる Bookmark ServiceExport .export Bookmark ApiService
応用: 結合テストの対象 39 ▌機能のテストは.exportにあるinterfaceの振る舞 いのテストとして書くことができる ⚫ 機能の振る舞いは全て.exportに定義されてい るため ⚫ addした項目がlistで返ってくる等のテストになる
▌内部実装に依存せず、機能を外側から観測できて いるので、堅牢なテストになる Bookmark ServiceExport .export Bookmark ApiService
応用: 機能開発の際、修正を考える起点になる 40 Bookmark ServiceExport .export Bookmark ApiService ▌ApiServiceやResponseはどうなるのか、そのために Service、Dataはどう変えるのか、というトップダウンな考
え方が可能になる ⚫ 機能の一番外側から、全体を俯瞰した検討が可能 になる ▌今までは、ServiceとDataをどう変えるか、というボトム アップな考え方がほとんど ⚫ ServiceやDataに詳しくないと修正箇所を見つけら れず、Serviceの役割も曖昧なので考慮漏れが起き やすい
ApiServiceを起点に機能の修正を考える ▌性能カスタマイズオプション: アプリの用途に適したレコードを実装したい ⚫ 後からでもフィールドを追加できる、柔軟なレコード (← 既存実装) ⚫ 大量のデータを高速に扱うことのできるレコード (←
実装したい) ▌今までは、既存のServiceやDataをどう修正するかを考えがち ⚫ 既存実装にも影響し、影響範囲が広がりすぎてしまう 41 Record Service Record Data
ApiServiceを起点に機能の修正を考える ▌RecordResponseを返しさえすれば、内部実装はなんでもよい ⚫ 極端な話、実装をもう一つ追加しても機能的には問題ない ▌永続化部分(FastRecordService) のみを新規実装 ⚫ こちらの案で検討を継続中 42 Record
ApiService IRecord Service FastRecord Service FastRecord Data Record Service Record Data Record Response
AIによる支援 43
重量級なインターフェイスの自動生成 ▌コード分割やwebとの分離のためには、公開 インターフェイスが必要 ▌kintoneでは公開するインターフェイスが重量 級になりがちで、用意が大変 ⚫ フィールドごとクラスが10個以上 ⚫ 各フィールドそれぞれに専用メソッド ▌生成AIで省力化が可能だった
⚫ インターフェイスは既存の実装と似た構成 ⚫ クラスを最初に一つだけ人手で定義、これ をお手本として残りをAIで自動生成 44
よりフィットするインターフェイスの探索も可能に ▌できれば、インターフェイスもいくつかの パターンを試しみて、一番フィットするも のを選択したい ⚫ 生成AIにより、これも可能だった ⚫ 人手で試すのはほぼ不可能 ▌開発がある程度進んでしまうと、分割 に伴うインターフェイスの整備はかなりの
負担となり、手を入れづらい ⚫ 生成AIはここを可能にしていそう 45
まとめ 46
まとめ ▌PdMのメンタルモデルを基にして“外側から”分割していくアプローチ ⚫ 1. 機能に沿ったパッケージへのコード分割 ⚫ 2. パッケージ内での機能とwebの分離 ▌肥大化、複雑化したコードを改善するアプローチとして有効 ▌開発が進んでしまうと、分割に伴うインターフェイスの整備は大きな負担
になりうるが、生成AIによる支援により挑戦可能に 47