Upgrade to Pro — share decks privately, control downloads, hide ads and more …

複雑性に立ち向かうためのサーバーサイドコード分割 / JJUG CCC 2023 Spring

複雑性に立ち向かうためのサーバーサイドコード分割 / JJUG CCC 2023 Spring

hirokuni-maeta

June 04, 2023
Tweet

Other Decks in Programming

Transcript

  1. 複雑性に立ち向かうための
    サーバーサイドコード分割
    前田 浩邦
    1

    View Slide

  2. 自己紹介
    ▌前田 浩邦 / Hirokuni Maeta
    ▌2014年4月 サイボウズ入社
    ▌kintone開発チーム / ソフトウェアエンジニア
    ▌バックエンドとフロントエンド
    ▌最近はkintoneのインフラ移行プロジェクトに参加
    2

    View Slide

  3. kintone
    ▌業務システムを自分たちで作成
    ⚫案件管理、顧客管理、…
    ▌様々なアクセス権、ワークフロー
    ▌REST APIを使った他システムとの連携
    3

    View Slide

  4. 目次
    ▌コード分割とは
    ▌コード分割トライアルプロジェクト
    ▌コード分割を通して見えてきた複雑性
    ▌今後の課題とまとめ
    4

    View Slide

  5. コード分割とは
    5

    View Slide

  6. 背景
    ▌肥大化・複雑化するコード
    ⚫リリースして10年経過
    ⚫src/main/java下のファイル総行数 ≧ 35万
    ▌開発効率の低下
    ⚫どうリファクタリングすればいいか分からない、コードの可読性を上げづらい
    ⚫影響範囲の確認が難しい
    ⚫覚えることが多く、新メンバーがキャッチアップしづらい
    6

    View Slide

  7. コード分割
    ▌コードを以下のような状態にすること
    ⚫機能ごとにパッケージに分割
    ⚫機能間で関わらないなら、コード間も関わらない、依存しない
    ▌機能に沿っていて、独立して理解可能なコードの塊を増やしていくことで、開発の効率化を
    目指す
    7

    View Slide

  8. 機能の特徴を捉えた上で分割する
    ▌コードだけを基に分割しても、複雑性を解消できるとは考えづらい
    ⚫例えば、同じクラスを返り値にするメソッド、ということで共通化されるかもしれない
    ⚫その実装で動くからという理由だけで、偶然同じになってしまった可能性もある
    ⚫つまり、コードだけを基にした分割は、偶然を基にした分割になってしまうかもしれない
    ▌コードで実現したいところの機能の特徴を捉えておくことで、偶然を排した意思決定ができそう
    8

    View Slide

  9. kintoneのアプリ
    ▌業務の数だけ作成
    ⚫案件管理、顧客管理、…
    ▌データを蓄積する簡単な“データベース”
    ▌データを蓄積する以外にも
    ⚫ワークフロー、アクセス権、通知、など
    9

    View Slide

  10. アプリ機能とアプリ設定機能
    ▌アプリ機能: “データベース“にあるデータ (レコード) の表示、編集
    10

    View Slide

  11. アプリ機能とアプリ設定機能
    ▌アプリ設定機能: “データベース”の設定
    11
    フィールドの一覧
    登録するデータの形を設定
    (フィールドの設定)

    View Slide

  12. コード分割で目指す構成
    UpdateController
    GetController
    FieldService
    FieldRepository
    com.kintone.appsettings
    AppData FieldData CategoryData
    共有するデータ
    ArchUnitで
    依存を禁止
    アプリ機能 アプリ設定機能
    12
    com.kintone.app
    RecordData
    RecordService
    RecordRepository
    GetRecordController

    View Slide

  13. コード分割で目指す構成
    RecordData
    RecordService
    RecordRepository
    GetRecordController
    AppData FieldData CategoryData
    共有するデータ
    ArchUnitで
    依存を禁止 UpdateController
    GetController
    FieldService
    FieldRepository
    com.kintone.appsettings
    com.kintone.app
    機能の性質上関わらないならば、
    コードでも関わらせない
    13
    アプリ機能 アプリ設定機能
    機能に対応するパッケージを作成
    機能に属する全コードをパッケージに配置

    View Slide

  14. コード分割で目指す構成
    AppData FieldData CategoryData
    共有するデータ
    ArchUnitで
    依存を禁止 UpdateController
    GetController
    FieldService
    FieldRepository
    com.kintone.appsettings
    機能間で共有が必要なデータ
    現状は共有を許容
    14
    RecordData
    RecordService
    RecordRepository
    GetRecordController
    com.kintone.app
    アプリ機能 アプリ設定機能

    View Slide

  15. 依存の管理
    ▌ArchUnit
    ⚫JUnit等で使うライブラリ
    ⚫パッケージ間の依存関係をチェックすることができる
    ▌依存を監視する仕組みを整備
    ⚫ArchUnitを使って依存のルールを定義し、単体テストに組み込む
    ⚫意図しない依存が入り込めばテストが落ちる
    15

    View Slide

  16. なぜ機能単位でのコード分割をするのか?
    ▌機能に対応したパッケージ分けは、既にある程度されていた
    ⚫これをさらに推し進めることは次のステップとして自然
    ▌新規機能の開発と平行して進められる
    ⚫分割の前後でコードの中身はほとんど変わらない
    ⚫機能開発しているチームが「なにこのコード!?」と驚かない
    ▌分割の基準を一から考える必要が無かった
    ⚫Product Manager (PM) のメンタルモデルをそのまま適用できそうだった
    16

    View Slide

  17. コード分割の基準: PMのメンタルモデル
    ▌メンタルモデル: どのような部分から構成され、どう関係し合ってるかという捉え方
    ⚫PMのメンタルモデルがコード分割にも違和感なく適用できるものだった
    アプリを使う人
    (現場で
    業務をする人)
    アプリを作る人
    (情シス・業務リーダー)
    レコードを表示する フィールドを変更する
    kintone
    アプリ機能 アプリ設定機能
    17
    com.kintone.app com.kintone.appsettings

    View Slide

  18. PMのメンタルモデルはどのようにして得られた?
    ▌kintoneの機能提供を継続してきたことから
    ⚫リリースして10年の間、いろいろな立場の人に、新規機能の提供を継続
    ⚫共通の機能をいろいろな立場の人に提供する中で、一貫したモデルが形成
    ▌アプリとアプリ設定という捉え方も、このような経緯の中から
    ⚫もともとはあまり区別せず開発されてきた
    ⚫けれども、分けたほうが考えやすい
    18

    View Slide

  19. コード分割トライアルプロジェクト
    19

    View Slide

  20. コード分割はなかなか進まず
    ▌仕組みは整えたものの、2~3人のメンバが週に1時間ほど集まって行うのみ
    ⚫分割するのも小さな機能
    ▌チームのメンバにヒアリング
    ⚫やり方がイメージできてなくて手が出しづらい
    ⚫優先順位が下がりがち
    20

    View Slide

  21. プロジェクト
    ▌普段の開発では難しい大きめの改善やPoCなどを行う仕組み
    ⚫目的やゴール、参加メンバと期間を説明し、承認を受ける
    ⚫人と時間を確保できるので、集中的に取り組むことができる
    ▌コード分割が抱えていた課題の解消に使えそう
    ⚫時間が少ない、分割対象が小さめ → 時間を確保できる
    ⚫やり方がイメージできない → 参加メンバと分割に集中的に取り組むことができる
    21

    View Slide

  22. コード分割トライアルプロジェクト
    ▌目的・ゴール
    ⚫分割について学習し、プロジェクト終了後でも参加メンバが分割を行えるようになる
    ⚫(学習が目的であり、分割すること自体は目的ではない)
    ▌分割対象: アプリ設定機能
    ⚫kintoneのコア機能でありかつ、ある程度複雑
    ⚫機能的に他から依存が無いので、他の機能よりかは手間が少なくなりそう
    ⚫コードの分量的には小さくはなくちょうどよいサイズ
    → コード分割の経験を積むにはちょうどよさそう
    22

    View Slide

  23. コード分割トライアルプロジェクト
    ▌参加者
    ⚫コード分割に興味があるメンバを募集
    ⚫アプリやアプリ設定を担当している開発メンバ
    ▌期間
    ⚫2022年11月~2023年1月
    ⚫週4時間の分割作業を3ヶ月
    23

    View Slide

  24. 分割の進め方: モブプログラミング
    ▌コア機能の分割はこれが初めてだったので、いろいろ手探り状態
    ⚫一気に分割することは難しいため、どこから始めてどのあたりを目指すか
    ⚫分割後の命名について (巨大なサービスクラスの分割時など)
    ▌モブプログラミングだとやりやすかった
    ⚫同じコードを見ながら一緒に考える → 認識がブレづらい、分割に集中できる
    ⚫100~200ファイルものdiffが出るので、PRをレビューするやり方だと大変
    ▌知見の共有や学習にも効果的
    ⚫知っている人は序盤だけリードして終盤は観察、のような工夫
    24

    View Slide

  25. プロジェクトを終えて
    ▌フィールド設定、アクセス権設定など20ある項目のうち、5つ分を分割
    ▌コア機能を分割をしてみて、コードの可読性が向上しているのを実感
    ⚫コードの肥大化・複雑化に対してコード分割は効果的という確信が深まる
    ▌プロジェクト解散後、参加メンバは各自の担当領域の分割を推進中
    ▌ゴールは達成されたといってもよさそう
    25

    View Slide

  26. コード分割を通して見えてきた複雑性
    26

    View Slide

  27. APIトークンの分割
    ▌APIトークン
    ⚫REST APIを呼び出す際に、リクエストヘッダに付与する
    ⚫ユーザーを使わずにレコードの取得や更新ができる
    ▌アプリ設定のAPIトークン設定画面で設定する
    27

    View Slide

  28. 分割前のAPIトークンの仕組み
    ApiTokenService
    - getToken
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    設定を
    変更する
    28
    APIトークンを表現する
    データクラス
    APIトークンに関する処理
    を担当するサービスクラス
    アプリを
    作る人
    アプリを
    使う人

    View Slide

  29. 分割前のAPIトークンの仕組み
    ApiTokenService
    - getToken
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    29
    listで設定を取得
    updateで変更
    アプリを
    作る人
    アプリを
    使う人
    設定を
    変更する

    View Slide

  30. 分割前のAPIトークンの仕組み
    ApiTokenService
    - getToken
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    30
    getTokenで取得
    canViewを確認
    アプリを
    作る人
    アプリを
    使う人
    設定を
    変更する

    View Slide

  31. このまま分割することは不可能
    ApiTokenService
    - getToken()
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    com.kintone.appsettings
    31
    getTokenのある
    appsettingsに依存
    アプリを
    作る人
    アプリを
    使う人 ArchUnitで
    依存を禁止
    設定を
    変更する

    View Slide

  32. getTokenがappsettingsにあるのが変?
    ApiTokenService
    - getToken()
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    com.kintone.appsettings
    32
    - list, update, ApiToken
    → 変更する際に利用される
    - getToken
    → 変更することとは関係ない
    アプリを作る人が設定を
    変更する際に利用されるコード
    アプリを
    作る人
    アプリを
    使う人 ArchUnitで
    依存を禁止
    設定を
    変更する

    View Slide

  33. 分割後のAPIトークンの仕組み (サービス、データクラスを分割)
    ApiTokenService
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    com.kintone.appsettings
    ApiTokenRightService
    - getToken()
    ApiTokenRight
    - long appId
    - bool canView, canAdd
    Impl
    33
    アプリを
    作る人
    アプリを
    使う人
    設定を
    変更する

    View Slide

  34. Dependency Injectionによって実装への依存を避ける
    ApiTokenService
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    com.kintone.appsettings
    ApiTokenRightService
    - getToken()
    ApiTokenRight
    - long appId
    - bool canView, canAdd
    Impl
    34
    DIを使うことで
    appsettingsへの依存を回避
    アプリ設定のテーブルを参照
    するため、appsettings内
    アプリを
    作る人
    アプリを
    使う人
    設定を
    変更する

    View Slide

  35. データクラスの可読性の向上
    35
    ApiTokenService
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    com.kintone.appsettings
    ApiTokenRightService
    - getToken()
    ApiTokenRight
    - long appId
    - bool canView, canAdd
    Impl アプリを作る人向けの変更
    がしやすくなった
    (memoの追加など)
    不要なプロパティに依存
    しなくなった
    アプリを
    作る人
    アプリを
    使う人
    設定を
    変更する

    View Slide

  36. サービスクラスの可読性の向上
    36
    ApiTokenService
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    com.kintone.appsettings
    ApiTokenRightService
    - getToken()
    ApiTokenRight
    - long appId
    - bool canView, canAdd
    Impl
    アプリを
    作る人
    アプリを
    使う人
    機能と実装の対応が取れている
    - できること → ApiTokenService
    - 設定の表示 → #list
    - 設定の変更 → #update
    設定を
    変更する

    View Slide

  37. サービスクラスの可読性の向上
    37
    ApiTokenService
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    com.kintone.appsettings
    ApiTokenRightService
    - getToken()
    ApiTokenRight
    - long appId
    - bool canView, canAdd
    Impl
    getTokenとlistが分かれた
    - 同じApiTokenを返しているので、共通化できそう
    にすら思える
    - 事前のアクセス権チェックの有無などが違うため、
    実は大きく異なる アプリを
    作る人
    アプリを
    使う人
    設定を
    変更する

    View Slide

  38. コードを複雑にしていたもの
    ▌使う側と作る側で同じサービスクラス、データクラスを使用していたこと
    ▌APIトークン以外でも同様の現象が見られた
    ApiTokenService
    - getToken
    - list()
    - update()
    ApiToken
    - long appId
    - bool canView, canAdd
    - String memo
    レコードを
    取得する
    作る人のための情報、
    使う人には不要
    実行するのが使う人か作る人かで、
    必要な処理や情報が異なる
    → serviceの実装がちぐはぐに見える
    38
    アプリを
    作る人
    アプリを
    使う人
    設定を
    変更する

    View Slide

  39. 使う側と作る側で分けると、素直な構成になりそう
    ▌使う側から呼ばれるか、作る側からかで、必要とされる情報や処理が変わる
    ⚫パッケージやクラス分割することで、可読性の向上が期待できる
    ▌APIトークンの分割では効果を実感
    ⚫不要なプロパティのロード、ちぐはぐな実装の問題の解消
    ⚫機能と実装の対応をつけやすくなる
    ▌機能追加もしやすくなる
    ⚫作る人がより作りやすくする機能追加は、使う側に関係なく生じる
    ⚫分けておくことで、影響範囲を抑えるのが簡単になる
    39

    View Slide

  40. 使う側と作る側で分けるための実装
    ▌使う側と作る側でどう依存をコントロールするかは、やり方はいろいろありそう
    ⚫今回はDIを利用したが、もっとよい分け方もあるはず
    ▌機能的な要素だけでなく、技術的な要素も関わってきそう
    ⚫DIが使えるか
    ⚫DBまで分割したらどうか
    ▌技術的に何を選べば機能的にはどう変わるか、など、まだまだ未知
    ⚫いろいろと探求しがいがありそう
    40

    View Slide

  41. 今後の課題とまとめ
    41

    View Slide

  42. 今後の課題
    ▌コードを分割しきる
    ⚫現在、参加していたメンバは自チームに戻って分割を開始
    ⚫共通部分の分析、整理
    ▌さらなる分割
    ⚫DB分割、サービス分割
    ※ ユーザーやPMのメンタルモデルを気に留めるのは忘れない
    42

    View Slide

  43. まとめ
    ▌コードの複雑化と開発効率の低下が課題
    ▌コード分割の仕組みを整えた
    ⚫分割の基準はPMのメンタルモデル
    ▌コード分割の学習のため、アプリ設定を分割するプロジェクトに挑戦
    ⚫プロジェクト終了後、参加メンバは分割を開始
    ⚫可読性の向上を実感、複雑化の要因らしきものも発見
    ▌今後は更なる分割を目指す
    43

    View Slide