소개: 하이브리드 아키텍처로서의 모듈형 모놀리스
이전 기사에서는 마이크로서비스 위기와 이것이 나타내는 경고 신호를 분석했습니다. 유통이 문제가 될 때. 이제 해결책을 모색할 때입니다. 모듈형 모노리스, 단일 아키텍처의 운영 단순성을 결합한 아키텍처입니다. 마이크로서비스의 전형적인 모듈성과 논리적 독립성을 갖춘 배포 가능한 프로세스입니다.
모듈식 모노리스는 하향 절충안이 아니라 의식적인 건축 선택 이를 통해 두 세계의 장점을 모두 얻을 수 있습니다. 이번 글에서는 디자인하는 방법을 알아보겠습니다. 모듈 경계, 내부 API 정의 및 프레임워크가 다음과 같은 방법을 정의합니다. 스프링 계수 시간이 지나도 아키텍처 무결성을 유지하는 데 도움이 될 수 있습니다.
이 기사에서 배울 내용
- 모듈형 모놀리스 아키텍처의 기본 원리
- 명확하고 유지 관리 가능한 모듈 경계를 정의하는 방법
- 내부 API의 역할과 모듈 간 계약
- Spring Modulith: 컴파일 타임에 경계를 적용하는 프레임워크
- 구조적 비교: 모놀리스, 모듈식 모놀리스, 마이크로서비스
- 다이어그램과 코드가 포함된 참조 아키텍처
모듈형 모노리스의 기본 원리
모듈식 모놀리스는 네 가지 아키텍처 원칙으로 인해 기존 모놀리스와 구별됩니다. 이는 코드 구조와 구성 요소 간의 상호 작용을 제어합니다.
1. 단일 배포 가능, 다중 논리 모듈
애플리케이션은 다음과 같이 컴파일되고 배포됩니다. 단일 아티팩트 (하나의 JAR, 하나의 WAR, 바이너리)이지만 내부적으로는 독립적인 논리 모듈로 구성됩니다. 각 모듈에는 고유한 패키지, 자체 클래스 및 잘 정의된 공개 API 표면이 있습니다. 모듈은 통신합니다 공개 인터페이스를 통해서만 독점적으로 다른 모듈의 내부 클래스에 액세스하지 않습니다.
2. 캡슐화 및 정보 은닉
각 모듈은 공개 인터페이스 뒤에 내부 구현을 숨깁니다. 수업 도메인, 저장소, 내부 서비스는 외부에서 접근이 불가능한. API, DTO 및 이벤트만 노출됩니다. 이는 모듈이 다음을 수행할 수 있도록 보장합니다. 소비자를 방해하지 않고 내부적으로 진화합니다.
3. 데이터 소유권
각 모듈은 독점 소유자 당신의 데이터. 물리적 데이터베이스인 경우에도 공유할 수 있지만 각 모듈은 자체 저장소를 통해서만 자체 테이블에 액세스합니다. 어떤 모듈도 다른 모듈의 테이블을 직접 읽거나 쓸 수 없습니다. 공개 API를 통해.
4. 노골적인 의사소통
모듈 간의 상호 작용은 다음을 통해 발생합니다. 노골적인 채널: 메소드 호출 인터페이스, 도메인 이벤트 또는 명령/쿼리를 통해. 숨겨진 종속성도 없습니다. 다른 모듈의 데이터베이스에 직접 액세스합니다. 모든 종속성은 표시되고 추적 가능합니다.
주요 차이점
전통적인 모놀리스에서는 모든 클래스가 다른 클래스를 호출할 수 있습니다. 모듈식으로 모놀리스, 모듈은 통신할 수 있습니다 공개 인터페이스를 통해서만. 이 제한 사항과 모듈을 마이크로서비스로 추출하는 것이 가능한 이유를 알아보세요.
참조 아키텍처
모듈식 모노리스가 실제로 어떻게 구성되어 있는지 살펴보겠습니다. 다음 예에서는 애플리케이션을 보여줍니다. 4개의 모듈을 갖춘 전자상거래: 주문하다, 목록, 사용자 e 지불.
// Struttura del progetto modular monolith
// com.ecommerce/
// ├── order/
// │ ├── api/
// │ │ ├── OrderModuleApi.java (interfaccia pubblica)
// │ │ ├── OrderDto.java (DTO pubblico)
// │ │ └── OrderCreatedEvent.java (evento pubblico)
// │ └── internal/
// │ ├── OrderService.java (logica privata)
// │ ├── OrderRepository.java (accesso dati privato)
// │ └── Order.java (entità privata)
// ├── catalog/
// │ ├── api/
// │ │ ├── CatalogModuleApi.java
// │ │ └── ProductDto.java
// │ └── internal/
// │ ├── CatalogService.java
// │ └── Product.java
// ├── user/
// │ ├── api/
// │ │ └── UserModuleApi.java
// │ └── internal/
// │ └── UserService.java
// └── payment/
// ├── api/
// │ └── PaymentModuleApi.java
// └── internal/
// └── PaymentService.java
내부 API의 정의
각 모듈은 다음을 정의하는 인터페이스를 노출합니다. 공공 계약 형태의.
이 인터페이스는 다른 모듈에 대한 유일한 액세스 포인트입니다. 구현이 숨겨져 있습니다.
패키지에 internal.
// API pubblica del modulo Order
package com.ecommerce.order.api;
public interface OrderModuleApi {
/**
* Crea un nuovo ordine.
* Pubblica OrderCreatedEvent al completamento.
*/
OrderDto createOrder(CreateOrderCommand command);
/**
* Recupera un ordine per ID.
* @throws OrderNotFoundException se l'ordine non esiste
*/
Optional<OrderDto> findById(UUID orderId);
/**
* Lista ordini di un utente con paginazione.
*/
Page<OrderDto> findByUserId(UUID userId, Pageable pageable);
/**
* Annulla un ordine esistente.
* Pubblica OrderCancelledEvent al completamento.
*/
void cancelOrder(UUID orderId);
}
// DTO pubblico - solo dati, nessuna logica di dominio
public record OrderDto(
UUID id,
UUID userId,
List<OrderItemDto> items,
BigDecimal total,
OrderStatus status,
Instant createdAt
) {}
모듈의 내부 구현
모듈 구현은 완전히 숨겨져 있습니다. 패키지에 포함된 클래스 internal
외부에서는 접근할 수 없습니다. 이를 통해 구현을 자유롭게 리팩토링할 수 있습니다.
소비자에게 영향을 주지 않고.
// Implementazione privata del modulo Order
package com.ecommerce.order.internal;
@Service
class OrderServiceImpl implements OrderModuleApi {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
private final CatalogModuleApi catalogModule; // dipendenza da altro modulo
OrderServiceImpl(OrderRepository orderRepository,
ApplicationEventPublisher eventPublisher,
CatalogModuleApi catalogModule) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
this.catalogModule = catalogModule;
}
@Override
@Transactional
public OrderDto createOrder(CreateOrderCommand command) {
// Verifica prodotti tramite API pubblica del modulo Catalog
List<ProductDto> products = command.getItemIds().stream()
.map(catalogModule::findById)
.map(opt -> opt.orElseThrow(ProductNotFoundException::new))
.toList();
// Crea l'entità di dominio (classe privata)
Order order = Order.create(command.getUserId(), products);
orderRepository.save(order);
// Pubblica evento per altri moduli interessati
eventPublisher.publishEvent(
new OrderCreatedEvent(order.getId(), order.getUserId())
);
return order.toDto();
}
}
스프링 계수: 컴파일 타임의 경계
스프링 계수 그리고 다음을 위한 도구를 제공하는 공식 Spring 프레임워크입니다. Spring Boot에서 모듈식 모놀리스를 구조화하고 검증하고 문서화합니다. 그 특징 메인과 능력 컴파일 타임에 모듈 간의 경계를 확인하세요., 패키지 간의 무단 액세스를 방지합니다.
스프링 모듈리스 구성
<!-- pom.xml - Dipendenze Spring Modulith -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
경계 검증 테스트
Spring Modulith는 모듈 간의 액세스 규칙을 확인하는 자동 테스트를 제공합니다. 존경받습니다. 모듈이 다른 모듈의 내부 클래스에 액세스하면 테스트가 실패합니다.
// Test che verifica l'integrita delle boundaries
@Test
void verifyModularStructure() {
ApplicationModules modules = ApplicationModules.of(
EcommerceApplication.class
);
// Verifica che nessun modulo acceda a classi interne
// di un altro modulo
modules.verify();
// Genera documentazione automatica dei moduli
new Documenter(modules)
.writeModulesAsPlantUml()
.writeIndividualModulesAsPlantUml();
}
// Output del test in caso di violazione:
// Module 'payment' depends on non-exposed type
// com.ecommerce.order.internal.Order
// from module 'order'
Spring Modulith를 사용한 이벤트 기반 통신
Spring Modulith는 기본적으로 이벤트 기반 모듈 간 통신을 지원합니다. 이벤트
를 통해 게시됩니다. ApplicationEventPublisher 그리고 함께 소비
@ApplicationModuleListener, 생산자와 소비자 간의 분리를 보장합니다.
// Modulo Order: pubblica un evento
@Service
class OrderServiceImpl implements OrderModuleApi {
private final ApplicationEventPublisher events;
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);
events.publishEvent(new OrderCreatedEvent(
order.getId(), order.getUserId(), order.getTotal()
));
return order.toDto();
}
}
// Modulo Payment: reagisce all'evento
@Service
class PaymentEventHandler {
@ApplicationModuleListener
void onOrderCreated(OrderCreatedEvent event) {
// Processa il pagamento quando un ordine viene creato
paymentService.processPayment(
event.userId(), event.total()
);
}
}
// Modulo Notification: reagisce allo stesso evento
@Service
class NotificationEventHandler {
@ApplicationModuleListener
void onOrderCreated(OrderCreatedEvent event) {
notificationService.sendOrderConfirmation(event.userId());
}
}
비교: 모놀리스 vs 모듈형 모놀리스 vs 마이크로서비스
건축 파노라마에서 모듈식 단일체의 위치를 이해하기 위해 다음을 비교해 보겠습니다. 주요 차원에 대한 세 가지 아키텍처:
운영 복잡성
- 전통적인 단일체: 낮은. 하나의 배포, 하나의 프로세스, 중앙 집중식 로그. 하지만 내부 코드가 엉킬 수 있습니다.
- 모듈형 모노리스: 낮은. 구조화되고 유지 관리 가능한 내부 코드를 갖춘 모놀리스와 동일한 운영상의 이점
- 마이크로서비스: 높은. N 파이프라인, N 배포, 분산 추적, 서비스 메시, 네트워크 정책
확장성
- 전통적인 단일체: 수평(여러 인스턴스). 95%의 경우에 충분함
- 모듈형 모노리스: 수평적 + 중요 모듈을 독립된 서비스로 추출하는 기능
- 마이크로서비스: 서비스별로 독립적입니다. 매우 비대칭적인 하중에만 필요함
팀 자율성
- 전통적인 단일체: 제한적입니다. 모두가 명확한 경계 없이 동일한 코드베이스에서 작업합니다.
- 모듈형 모노리스: 보통의. 명확한 인터페이스, 공유 배포를 갖춘 모듈별 팀
- 마이크로서비스: 높은. 완전히 독립적인 팀, 독립적인 배포
소송 비용
- 전통적인 단일체: 최소. 간단한 인프라
- 모듈형 모노리스: 최소-보통. 모놀리스와 동일한 인프라 비용
- 마이크로서비스: 높은. 업계 추정에 따르면 모놀리스 대비 6배
모듈형 모노리스의 장점
모듈형 모노리스는 최적점 대다수의 경우 조직: 모놀리스의 운영 비용을 낮게 유지하고 필요한 모듈성을 제공합니다. 중간 규모 팀을 위한 것이며 향후 마이크로서비스 추출 기능을 보존합니다. 데이터가 그것을 정당화할 때.
모듈을 마이크로서비스로 추출해야 하는 경우
모듈형 모노리스가 반드시 최종 지점은 아닙니다. 그리고 꽤 출발점 솔리드 필요한 경우 마이크로서비스를 추출할 수 있습니다. 하지만 적절한 때가 언제인지 어떻게 알 수 있습니까?
모듈 추출을 정당화하는 표시는 다음과 같습니다.
- 비대칭 스케일링: 이 모듈은 다른 모듈보다 10배 더 많은 리소스를 필요로 합니다.
- 전담팀: 5명 이상의 팀이 해당 모듈에서만 작업하며 독립적인 배포가 필요함
- 다양한 기술 스택: 모듈은 다른 언어나 런타임의 이점을 누릴 수 있습니다.
- 출시 빈도가 다름: 모듈은 매일 릴리스가 필요하고 나머지 시스템은 매주 릴리스됩니다.
- 격리 실패: 모듈의 버그로 인해 전체 시스템이 충돌할 수 있습니다.
실제 구현: 첫 번째 단계
기존 모놀리스가 있고 모듈식 모놀리스로 전환을 시작하려는 경우 먼저
단계와 기능별로 코드를 패키지로 구성 기술 계층이 아닌.
따로 분리하는 대신 controller, service, repository,
로 구분하다 order, catalog, user.
// PRIMA: organizzazione per layer tecnico (sbagliato)
// com.app/
// ├── controller/
// │ ├── OrderController.java
// │ ├── UserController.java
// │ └── CatalogController.java
// ├── service/
// │ ├── OrderService.java
// │ ├── UserService.java
// │ └── CatalogService.java
// └── repository/
// ├── OrderRepository.java
// ├── UserRepository.java
// └── CatalogRepository.java
// DOPO: organizzazione per feature/modulo (corretto)
// com.app/
// ├── order/
// │ ├── api/OrderModuleApi.java
// │ └── internal/
// │ ├── OrderController.java
// │ ├── OrderService.java
// │ └── OrderRepository.java
// ├── user/
// │ ├── api/UserModuleApi.java
// │ └── internal/
// │ ├── UserController.java
// │ ├── UserService.java
// │ └── UserRepository.java
// └── catalog/
// ├── api/CatalogModuleApi.java
// └── internal/
// ├── CatalogController.java
// ├── CatalogService.java
// └── CatalogRepository.java
다음 단계
이 기사에서 우리는 모듈식 모놀리스의 기본 원리를 정의하고 어떻게 작동하는지 살펴보았습니다. 명확한 경계로 코드를 구성하십시오. 다음 기사에서는 각 측면에 대해 더 자세히 살펴보겠습니다.
- 제3조: 도메인 중심 설계를 사용하여 제한된 컨텍스트를 식별하고 모듈 경계를 정의하는 방법
- 제4조: 공유 및 개별 스키마에 대한 패턴을 사용하여 모듈식 단일체용 데이터베이스를 설계하는 방법
- 제5조: 모듈 간 통신 패턴: 동기식, 비동기식, 이벤트 중심
다음 기사
다음 기사에서는 도메인 중심 설계 그리고 그것을 사용하는 방법 식별하기 위해 제한된 컨텍스트 귀하의 신청서. 개념을 살펴보겠습니다. 유비쿼터스 언어, 컨텍스트 매핑, 집계 디자인 및 이를 경계로 변환하는 방법 등 코드의 구체적인 모듈.







