🛫

Turborepoライクで始めるモジュラーモノリス

2022/10/31に公開

こんにちは、アルダグラムの開発ユニット長の田中です。

アルダグラムはノンデスクワーカー向けプロジェクト管理アプリ「KANNA」を提供しています。

https://lp.kanna4u.com/

KANNAのフロントエンドはモノリスなNext.jsで構築されています。

最近ではプロダクトや開発組織のスケールに伴い、モノリス構成がゆえの課題が多く発生してきました。

  • 予想外の箇所でCIが落ちている・・・
  • この変更による副作用が怖い・・・
  • この機能に修正入れていいんだっけ?別のチームに確認しないと・・・

など開発速度の低下や不具合発生の頻度が増加していました。

これら課題に対して、モジュラーモノリスという手法を用いて(ちょっとだけ)改善したので、ご紹介します。

KANNAの主要機能

KANNAは大きく分けると2つの機能から構成されています。

1つは「プロジェクト管理機能」です。

「プロジェクト管理機能」の中には

  • 報告管理機能
  • 写真管理機能
  • 資料管理機能

…etc

など現場のプロジェクト管理に必要な様々な機能が備わっています。


プロジェクト管理機能


チャット機能

モノリス構成の課題

現在のアルダグラムの開発組織のグループ構成は以下のようになっています。

グループ名 ミッション
グロースグループ 全ユーザー向けの基本機能開発・改修
基盤グループ 大手企業向けの機能開発・改修 / SRE
グローバルグループ 海外企業向けの機能開発・改修

「開発する機能群」でグループ構成されているというわけではなく、「対象顧客」によるグループ構成を行なっています。

グループの構成の仕方としては、機能群による分割、職種領域による分割、事業による分割・・・・など様々な分け方が存在しますが、2022年10月時点では「対象顧客」が最適だと考えています。

そのため、同じ機能に対して複数のグループが変更を加えるといったケースが発生します。

この課題を解決するために、複数のグループでコンセンサスを形成しないといけないため、コミュニケーションの増加に繋がったり、意思決定までに時間がかかり生産性低下を招いています。

また機能開発時に別のグループ起因の不具合が発生すると、リリースが滞ってしまい、デプロイ頻度の低下も顕著になっていました。

以前にプロダクトの将来像として、アーキテクチャをマイクロサービス移行することを検討しており、「マイクロサービス化してみますか!」などの議論はありましたが

  • リモデリングに膨大な時間がかかりそう
    • サービス境界を間違えると現状より悪化しそう
  • CI / CDの再整備やリポジトリ構成の変更が必要になる
  • 純粋なエンジニアリソースの不足

など様々な問題があり、時期尚早であると判断しました。

そこで今回は、モノリス内で明確に境界を設けるモジュラーモノリスの手法を採用することで、問題の解決を図りました。

モジュラーモノリスとは

モノリス内で明確なモジュール分割を行うことにより、モノリスのようなシンプルなアーキテクチャを維持しながらも、マイクロサービスのようなモジュールの独立性を維持する手法です。

またモジュールに分割されているため、将来的にモジュール単位でマイクロサービス化を進めることが容易になります。

そしてモデリングや境界の決定に失敗があった場合でもマイクロサービスと比較して低コストで修正が可能である点が、組織スケール中のスタートアップには最適なメリットだと考えています。

さて「モジュラーモノリスで行こう!」と決まりましたが、どのようにディレクトリ構成を行えば良いのでしょうか?

将来のマイクロサービス化を見据えているため、その仕組みに簡単に移行できるディレクトリ構成にしようと考えました。

そこでmonorepo構成の管理ツールであるTurborepoのディレクトリ構成を採用しました。

https://turbo.build/repo

Turborepoライクで始めるモジュラーモノリス

Turborepoのディレクトリ構成は以下のようになっています。

├── README.md
├── apps
│   ├── docs (docsという名前の独立したNext.jsアプリケーションです)
│   │   ├── package.json
│   │   └── pages
│   │         └── index.tsx
│   └── web (webという名前の独立したNext.jsアプリケーションです)
│       ├── package.json
│       └── pages
│             └── index.tsx
├── package.json
├── packages (docsとwebで共通で使うUIパーツ・ロジック・ライブラリ)
│   ├── config
│   │   ├── eslint-preset.js
│   │   └── package.json
│   ├── tsconfig
│   │   ├── README.md
│   │   ├── base.json
│   │   ├── nextjs.json
│   │   ├── package.json
│   │   └── react-library.json
│   └── ui
│       ├── Button.tsx
│       ├── index.tsx
│       ├── package.json
│       └── tsconfig.json
├── turbo.json
└── yarn.lock

https://github.com/vercel/turbo/tree/main/examples/basic の一部を抜粋しています

このディレクトリ構成に合わせて、チャット機能を分離しました。

移行前のディレクトリ構成(一部抜粋)

├── README.md
├── package.json
├── pages
│   ├── chats (今回分離するチャット機能のルーティングコンポーネント)
│   │   └── index.tsx
│   ├── projects
│   └── ....etc
├── src
│   ├── config
│   ├── core   (今回分離するチャット機能や他の機能のhooksなどを管理)
│   │   └── hooks
│   ├── lib
│   └── ui       (今回分離するチャット機能や他の機能のコンポーネントを管理)
│       ├── atoms
│       ├── molecules
│       ├── organisms
│       └── templates
└── tsconfig.json

移行後のディレクトリ構成(一部抜粋)

├── README.md
├── apps
│   └── chats
│       ├── pages
│       │     └── index.tsx
│       └── src
│           ├── config
│           ├── core
│           │   └── hooks
│           ├── lib
│           └── ui
│               ├── atoms
│               ├── molecules
│               ├── organisms
│               └── templates
├── package.json
├── pages
│   ├── chats (ルーティングコンポーネントはそのままだが、ロジックはapps/chats/pages/index.tsで管理)
│   │   └── index.tsx
│   ├── projects
│   └── ....etc
├── packages (共通で使うUIパーツ・ロジック)
│   ├── core
│   │   └── hooks
│   ├── lib
│   └── ui
│       ├── atoms
│       └── molecules
└── tsconfig.json

appsディレクトリ内にchatsのコンポーネントやhooksなどのロジックを移行しました。

pages内のルーティングコンポーネントはルーティングを作成するためそのままにしていますが、内部のロジックは apps/chats/pages/index.ts で管理するようにしています。

pages/chats/index.ts
import Page from '@chats/pages/groups/index'
import { AppPage, AppPageContext } from '@core/interfaces/AppPage'

type ChatGroupPageProps = {
  chatGroupUuid: string
  isServer: boolean
}

const ChatGroupPage: AppPage<ChatGroupPageProps> = ({
  chatGroupUuid,
  isServer
}) => {
  return <Page chatGroupUuid={chatGroupUuid} isServer={isServer} />
}

....
apps/chats/pages/index.ts
import useChatImageListDelete from '@chats/src/core/hooks/useChatImageListDelete'
import ChatGroupTemplate from '@chats/src/ui/templates/ChatGroupTemplate'
....

const ChatGroupPage: FC<ChatGroupPageType> = ({ chatGroupUuid, isServer }) => {
  const deleteImageList = useChatImageListDelete()

  ...

  return (
    <ChatGroupContext.Provider>
      <ChatGroupTemplate chatId={chatGroupUuid} isServer={isServer} />
    </ChatGroupContext.Provider>
  )
}

....
tsconfig.json
{
  "compilerOptions": {
		...
    "paths": {
      "@pages/*": [
        "pages/*"
      ],
      "@chats/*": [
        "apps/chats/*" // appsのパスを追加
      ],
      "@common-ui/*": [
        "packages/ui/*"
      ],
    },
  ...
  },
...
}

またpackagesのuiディレクトリで管理するコンポーネントはatoms, moleculesのみしています。

またロジックを注入せずに、プレゼンテーションコンポーネントにすることでUIの生成のみ責務を持つようにしています。

ロジックを持たないコンポーネントのため、各機能で安全に共通パーツとして利用できます。

このようにすることで、各機能を分離し、変更により他の機能に影響を及ぼすことがなくなり、テスト工数の削減につながりました。

またTurborepoのディレクトリ構成を採用しているため、将来のマイクロサービス化の際の移行コストも削減できたと思います。

最後に

モジュラーモノリス構成に変更し1ヶ月ほど経ちますが、今のところ大きな問題は発生していません。

比較的低コストでモジュールに分離できるため、マイクロサービス化の前段階の取り組みとしても最適なのではと考えています。

今後の課題としては、既存のatoms, moleculesコンポーネントにはhooksなどのロジックを扱ってしまっているコンポーネントがあるので、プレゼンテーションコンポーネントになるようにリファクタリングし、packagesで扱えるようにすることが挙げられます。

今後も新規機能開発や既存機能のモジュール化に取り組み、開発効率化や保守性の高いプロダクトに成長させていきます。

アルダグラム Tech Blog

Discussion