はじめに: 増分プロセスとしての移行
従来のモノリスからモジュラーモノリスへの移行は、数か月かかるビッグバン プロジェクトではありません 書き換えと危険な展開の可能性があります。そして、 増分プロセス、安全かつ 可逆的であり、新機能の開発と並行して進めることができます。のすべてのステップ 移行により、動作するテスト可能なシステムが生成され、反復ごとにリスクが軽減されます。
この記事では、 4 ステップの移行プレイブック: 監査から コードの境界の特定、モジュールの物理的な分離から導入まで イベント駆動型コミュニケーションのこと。現実的なタイムライン、テスト戦略、 避けるべきアンチパターン。
この記事で学べること
- ストラングラーフィグパターンを内部モジュール化に適用
- フェーズ 1: DDD によるコード監査と境界の特定
- フェーズ 2: パッケージとモジュールへの物理的な分離
- フェーズ 3: API の抽出と内部コントラクトの定義
- フェーズ 4: イベント駆動型通信への移行
- 各フェーズのテスト戦略
- ロールバック戦略とリスク管理
- 現実的なスケジュール: リスクを管理しながら 2 ~ 6 か月
- 避けるべきアンチパターンとよくある間違い
ストラングラーパターン図
Il ストラングラーフィグパターン、元々はモノリスからモノリスに移行することを考えられていました。 マイクロサービスは、内部モジュール化にも効果的に適用されます。アイデアはシンプルです。 すべてを書き直すのではなく、 包む 徐々にレガシーモノリスの一部に 従来のコードが完全に置き換えられるまで、適切に構造化されたモジュールを使用します。
プロセスは次のように機能します。
- レガシーモノリスの機能領域を特定します
- 同じ機能を実装する明確な境界を持つ新しいモジュールを作成する
- トラフィックをレガシーコードから新しいモジュールに段階的にルーティングします。
- 新しいモジュールが安定したら、レガシーコードを削除します。
- 次の機能領域についても繰り返します
ストラングラー フィグの主な利点
システムは残ります 常に働いている 移行中。ありません システムが「半分移行され、機能していない」瞬間。増分ごとに、 完全でテスト可能なシステムであり、即時ロールバックが可能です。
フェーズ 1: コードの監査と境界の特定
最初のフェーズは純粋に分析的です。コードを変更するのではなく、コードを分析します。目標は モノリスの現在の構造を理解し、モジュールの自然な境界を特定します。
1.1 依存関係の分析
静的分析ツールを使用して、クラスとパッケージ間の依存関係をマッピングします。ツール どうやって J依存, アーチユニット、 または 構造101 彼らはできます 自然なクラスターと問題のある結合を明らかにする依存関係グラフを生成します。
// ArchUnit: analizza le dipendenze esistenti
@Test
void analyzeDependencies() {
JavaClasses classes = new ClassFileImporter()
.importPackages("com.legacy.app");
// Identifica i cicli di dipendenze
SliceRule noCycles = SlicesRuleDefinition
.slices()
.matching("com.legacy.app.(*)..")
.should().beFreeOfCycles();
// Questo test probabilmente fallirà nel monolith legacy
// Il report mostra esattamente dove sono i cicli
try {
noCycles.check(classes);
} catch (AssertionError e) {
// Analizza i cicli per identificare i confini
System.out.println("Dependency cycles found:");
System.out.println(e.getMessage());
}
}
1.2 チームによるイベント突撃
セッションを企画する イベントストーミング 開発者や関係者とともに 境界のあるコンテキストを識別するビジネス。このセッションではドメイン マップが生成されます モジュール境界の基礎となるビジネスの。
1.3 優先順位付け
すべてのフォームに同じ緊急性があるわけではありません。以下に基づいて優先順位を付けます。
- 変更の頻度: 頻繁に変更されるモジュールは、より早くモジュール化の恩恵を受けることができます。
- 複雑: より複雑なモジュールには、他のモジュールの前に明確な境界が必要です
- カップリング: モジュールから開始します 外部依存関係が少なくなる (抽出しやすくなります)
- ビジネス価値: ビジネスクリティカルなモジュールは優先的に注目する必要があります
フェーズ 2: 物理的な分離
このフェーズでは、ソースコードを技術層ごとの組織ごとに再編成します。 機能モジュールごとに 1 つの組織に割り当てられます。これは最も目に見える変更であり、次のことが可能です。 完全に下位互換性のある方法で作られています。
// Migrazione della struttura dei package
// FASE 2.1: Crea la nuova struttura
// PRIMA (layer-based):
// com.app.controller.OrderController
// com.app.service.OrderService
// com.app.repository.OrderRepository
// com.app.model.Order
// DOPO (module-based):
// com.app.order.api.OrderModuleApi
// com.app.order.internal.OrderController
// com.app.order.internal.OrderService
// com.app.order.internal.OrderRepository
// com.app.order.internal.domain.Order
// FASE 2.2: Sposta le classi una alla volta
// Usa il refactoring "Move Class" dell'IDE
// Il compilatore segnala immediatamente le dipendenze rotte
// FASE 2.3: Verifica che il build funzioni dopo ogni spostamento
// ./gradlew build
// Se il build fallisce, correggi le dipendenze o
// rollback lo spostamento
フェーズ 2 のテスト戦略
物理的に離れたからといって行動が変わるわけではありません。テスト戦略は次のとおりです。
- 回帰テスト: 各移動の後にテストスイート全体を実行します。
- コンパイルテスト: コンパイラと最初のテスト、ビルドが機能することを確認します
- エンドツーエンドのテスト: 主要なユーザー フローが機能していることを確認します
- 頻繁なコミット: 各クラスの移動はコミットであり、詳細なロールバックが可能です。
フェーズ 3: API の抽出
コードがモジュールごとに編成されたら、フェーズ 3 で インターフェース
公共の (API) モジュール間。各モジュールはパッケージ内のインターフェイスを公開します api
パッケージ内の実装を非表示にします internal.
// Fase 3: Estrazione dell'API dal codice esistente
// PASSO 1: Identifica i metodi chiamati da altri moduli
// Cerca tutti gli usi di OrderService al di fuori del package order
// grep -r "OrderService" --include="*.java" | grep -v "order/"
// PASSO 2: Crea l'interfaccia pubblica con solo i metodi necessari
package com.app.order.api;
public interface OrderModuleApi {
// Solo i metodi usati da altri moduli
OrderDto createOrder(CreateOrderCommand cmd);
Optional<OrderDto> findById(UUID id);
List<OrderDto> findByUserId(UUID userId);
}
// PASSO 3: Implementa l'interfaccia nel servizio esistente
package com.app.order.internal;
@Service
class OrderService implements OrderModuleApi {
// Il codice esistente non cambia
// Aggiungi solo "implements OrderModuleApi"
// e i metodi toDto() per le conversioni
@Override
public OrderDto createOrder(CreateOrderCommand cmd) {
// Logica esistente...
Order order = new Order(cmd);
orderRepository.save(order);
return OrderDto.from(order);
}
}
// PASSO 4: Aggiorna i chiamanti per usare l'interfaccia
// Prima: private final OrderService orderService;
// Dopo: private final OrderModuleApi orderModule;
フェーズ 4: イベント駆動型のコミュニケーション
最後のフェーズでは、 イベントベースのコミュニケーション モジュール間、 デカップリングが有利な場合には、直接同期呼び出しを徐々に置き換えます。 すべてのインタラクションがイベント駆動型になる必要はありません。同期呼び出しは引き続き有効です。 強い一貫性を必要とするクエリや操作に適しています。
// Fase 4: Da chiamata sincrona a evento
// PRIMA: accoppiamento sincrono
@Service
class OrderService {
private final NotificationService notificationService;
private final InventoryService inventoryService;
public void createOrder(CreateOrderCommand cmd) {
Order order = Order.create(cmd);
orderRepo.save(order);
// Chiamate sincrone accoppiate
notificationService.sendConfirmation(order);
inventoryService.reserveStock(order.getItems());
}
}
// DOPO: disaccoppiamento con eventi
@Service
class OrderService implements OrderModuleApi {
private final ApplicationEventPublisher events;
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
Order order = Order.create(cmd);
orderRepo.save(order);
// Pubblica evento: i consumatori reagiscono autonomamente
events.publishEvent(new OrderCreatedEvent(
order.getId(), order.getUserId(), order.getItems()
));
return order.toDto();
}
}
// Il modulo Notification reagisce all'evento
@Service
class NotificationHandler {
@TransactionalEventListener(phase = AFTER_COMMIT)
void onOrderCreated(OrderCreatedEvent event) {
notificationService.sendConfirmation(event.userId());
}
}
// Il modulo Inventory reagisce allo stesso evento
@Service
class InventoryHandler {
@TransactionalEventListener(phase = AFTER_COMMIT)
void onOrderCreated(OrderCreatedEvent event) {
inventoryService.reserveStock(event.items());
}
}
現実的なタイムライン
平均的なモノリス (50 ~ 100,000 LOC、3 ~ 5 人の専任開発者) の現実的なタイムラインは次のとおりです。
- 1~2週目: フェーズ 1 - コード監査、イベント ストーミング、優先順位付け
- 3~8週目: フェーズ 2 - 物理的な分離、一度に 1 つのモジュール
- 9~14週目: フェーズ 3 - API 抽出、コントラクト定義
- 15~20週目: フェーズ 4 - イベント駆動型通信
- 第 21 ~ 24 週目: 安定化、テスト、ドキュメント化
合計: 4~6ヶ月 システムを常に機能させた状態での完全な移行の場合 そして、移行中に機能をリリースする機能。
ロールバック戦略
移行の各段階は元に戻せる必要があります。各フェーズのロールバック戦略は次のとおりです。
- フェーズ 1: コードは変更されず、ロールバックも必要ありません
- フェーズ2: 各クラスの移動はコミットです。ロールバック =
git revert - フェーズ 3: インターフェースは付加的です。ロールバック = インターフェイスを削除し、直接呼び出しに戻ります
- フェーズ4: イベントは追加的です。ロールバック = リスナーを削除し、同期呼び出しに戻ります
避けるべきアンチパターン
最も一般的な移行の間違いとその回避方法は次のとおりです。
1.ビッグバンリライト
間違い: 新しいプロジェクトですべてを最初から書き直してから切り替えます 生産中です。 解決: Strangler パターンを使用した増分移行 図。
2. 早期摘出
間違い: 境界を明確にする前に、モジュールをマイクロサービスとして引き出します。 解決: 抽出を検討する前に、内部モジュール化を完了してください。
3. 共有可変状態
間違い: メモリ内の可変オブジェクトを共有するモジュール。 解決: 不変の DTO およびイベントを通じてのみ通信します。
4. 循環依存関係
間違い: モジュール A はモジュール A に依存するモジュール B に依存します。 解決: イベント バスを導入してループを解消するか、共有カーネルを作成します。
5. 不十分なテスト
間違い: 適切なテスト スイートなしで移行します。 解決: 移行を開始する前に、エンドツーエンドのテストを行っていることを確認してください。 主な流れを網羅しています。これらのテストはセーフティネットです。
次の記事
シリーズの次の最終記事では、 完全なケーススタディ: モジュラーモノリスに移行した 12 のマイクロサービスを備えたスタートアップ。指標を見ていきます 使用前/使用後、定量化された ROI、削減されたコスト、および 4 か月間で学んだ教訓 移住のこと。







