소개: 증분 프로세스로서의 마이그레이션
레거시 모놀리스를 모듈식 모놀리스로 마이그레이션하는 것은 몇 달이 걸리는 대규모 프로젝트가 아닙니다. 재작성 및 위험한 배포. 그리고 증분 프로세스, 안전하고 새로운 기능의 개발과 병행하여 진행될 수 있습니다. 모든 단계는 마이그레이션은 작동하고 테스트 가능한 시스템을 생성하여 반복할 때마다 위험을 줄입니다.
이 글에서 우리는 4단계 마이그레이션 플레이북: 감사에서 코드의 경계 식별, 모듈의 물리적 분리부터 도입까지 이벤트 중심 커뮤니케이션의 우리는 현실적인 일정, 테스트 전략 및 피해야 할 안티 패턴.
이 기사에서 배울 내용
- 내부 모듈화에 적용된 Strangler Fig 패턴
- 1단계: DDD를 통한 코드 감사 및 경계 식별
- 2단계: 패키지와 모듈로의 물리적 분리
- 3단계: API 추출 및 내부 계약 정의
- 4단계: 이벤트 중심 커뮤니케이션으로 마이그레이션
- 각 단계별 테스트 전략
- 롤백 전략 및 위험 관리
- 현실적인 타임라인: 통제된 위험에서 2~6개월
- 피해야 할 안티 패턴과 일반적인 실수
교살자 패턴 무화과
Il 교살자 무화과 패턴, 원래는 모놀리스에서 다음으로 마이그레이션하기 위해 고안되었습니다. 마이크로서비스는 내부 모듈화에도 효과적으로 적용됩니다. 아이디어는 간단합니다. 모든 것을 다시 작성하는 대신 포장하다 점차적으로 레거시 모놀리스의 일부 레거시 코드가 완전히 대체될 때까지 잘 구조화된 모듈을 사용합니다.
프로세스는 다음과 같이 작동합니다.
- 레거시 모놀리스의 기능 영역을 식별합니다.
- 동일한 기능을 구현하는 명확한 경계를 가진 새 모듈을 만듭니다.
- 레거시 코드에서 새 모듈로 트래픽을 점진적으로 라우팅합니다.
- 새 모듈이 안정되면 레거시 코드를 제거합니다.
- 다음 기능 영역에 대해 반복합니다.
Strangler Fig의 주요 장점
시스템은 남아있다 항상 일하고 마이그레이션 중. 없다 시스템이 "절반 마이그레이션되었으며 작동하지 않는" 순간입니다. 각 증분은 즉시 롤백이 가능한 완벽하고 테스트 가능한 시스템입니다.
1단계: 코드 감사 및 경계 식별
첫 번째 단계는 순전히 분석입니다. 코드를 변경하지 않고 분석합니다. 목표는 모놀리스의 현재 구조를 이해하고 모듈의 자연스러운 경계를 식별합니다.
1.1 의존성 분석
정적 분석 도구를 사용하여 클래스와 패키지 간의 종속성을 매핑합니다. 도구 어떻게 JDepend, 아치유닛, 또는 구조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단계: 물리적 분리
이 단계에서는 기술 계층별로 조직별로 소스 코드를 재구성합니다. 기능 모듈당 하나의 조직에 할당됩니다. 이는 가장 눈에 띄는 수정 사항이며 완전히 이전 버전과 호환되는 방식으로 만들어졌습니다.
// 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단계 - 물리적 분리, 한 번에 하나의 모듈
- 9~14주차: 3단계 - API 추출, 계약 정의
- 15~20주: 4단계 - 이벤트 기반 커뮤니케이션
- 21~24주: 안정화, 테스트, 문서화
총: 4~6개월 완전한 마이그레이션을 위해 시스템은 항상 작동합니다. 마이그레이션 중에 기능을 릴리스하는 기능도 있습니다.
롤백 전략
마이그레이션의 각 단계는 되돌릴 수 있어야 합니다. 각 단계의 롤백 전략은 다음과 같습니다.
- 1단계: 코드 변경 없음, 롤백 필요 없음
- 2단계: 각 클래스 이동은 커밋입니다. 롤백 =
git revert - 3단계: 인터페이스는 추가됩니다. 롤백 = 인터페이스 제거, 직접 통화로 돌아가기
- 4단계: 이벤트는 추가됩니다. 롤백 = 리스너를 제거하고 동기 호출로 돌아갑니다.
피해야 할 안티패턴
가장 일반적인 마이그레이션 실수와 이를 방지하는 방법은 다음과 같습니다.
1. 빅뱅 리라이트
실수: 새 프로젝트에서 모든 것을 처음부터 다시 작성한 다음 전환합니다. 생산 중. 해결책: Strangler 패턴을 사용한 증분 마이그레이션 Fig.
2. 조기 추출
실수: 경계가 명확해지기 전에 모듈을 마이크로서비스로 끌어내세요. 해결책: 추출을 고려하기 전 내부 모듈화를 완료합니다.
3. 공유 변경 가능 상태
실수: 메모리에서 변경 가능한 객체를 공유하는 모듈입니다. 해결책: 변경할 수 없는 DTO 및 이벤트를 통해서만 통신합니다.
4. 순환 종속성
실수: 모듈 A는 모듈 A에 의존하는 모듈 B에 의존합니다. 해결책: 루프를 끊거나 공유 커널을 생성하기 위해 이벤트 버스를 도입합니다.
5. 불충분한 테스트
실수: 적절한 테스트 스위트 없이 마이그레이션합니다. 해결책: 마이그레이션을 시작하기 전에 엔드투엔드 테스트를 완료했는지 확인하세요. 주요 흐름을 다루고 있습니다. 이 테스트는 안전망입니다.
다음 기사
시리즈의 다음 기사이자 마지막 기사에서는 완전한 사례 연구: 모듈식 모놀리스로 마이그레이션된 12개의 마이크로서비스를 갖춘 스타트업입니다. 측정항목을 살펴보겠습니다. 전후, 정량화된 ROI, 비용 절감, 4개월 동안 얻은 교훈 이주.







