Spring Modulith란?
Spring Modulith는 Spring Boot 애플리케이션을 모듈러 모놀리스(Modular Monolith)로 설계하게 돕는 공식 프로젝트입니다. 마이크로서비스의 장점(모듈 독립성, 명확한 경계)을 모놀리스의 단순함(단일 배포, 로컬 트랜잭션) 안에서 구현합니다. 모듈 간 의존성을 자동 검증하고, 이벤트 기반 비동기 통신을 통해 느슨한 결합을 강제합니다.
왜 모듈러 모놀리스인가?
| 항목 | 전통적 모놀리스 | 모듈러 모놀리스 | 마이크로서비스 |
|---|---|---|---|
| 배포 단위 | 하나 | 하나 | 서비스별 |
| 모듈 경계 | 없음/느슨 | 강제 | 네트워크 |
| 트랜잭션 | 로컬 | 로컬 | 분산 (Saga) |
| 운영 복잡도 | 낮음 | 낮음 | 높음 |
| MSA 전환 | 어려움 | 용이 | — |
프로젝트 구조
// build.gradle.kts
dependencies {
implementation("org.springframework.modulith:spring-modulith-starter-core")
implementation("org.springframework.modulith:spring-modulith-starter-jpa")
// 이벤트 저널링 (Transactional Outbox)
implementation("org.springframework.modulith:spring-modulith-starter-jdbc")
// 모듈 문서 자동 생성
testImplementation("org.springframework.modulith:spring-modulith-docs")
}
Spring Modulith는 패키지 구조 = 모듈 경계로 인식합니다:
com.example.shop/
├── ShopApplication.java # @SpringBootApplication
├── order/ # 📦 주문 모듈
│ ├── Order.java # 내부 엔티티 (패키지 접근)
│ ├── OrderService.java # 공개 API (public)
│ ├── OrderCreatedEvent.java # 공개 이벤트 (public)
│ └── internal/ # 🔒 내부 구현 (외부 접근 금지)
│ ├── OrderRepository.java
│ └── OrderValidator.java
├── inventory/ # 📦 재고 모듈
│ ├── InventoryService.java # 공개 API
│ └── internal/
│ ├── Stock.java
│ └── StockRepository.java
├── payment/ # 📦 결제 모듈
│ ├── PaymentService.java
│ ├── PaymentCompletedEvent.java
│ └── internal/
│ └── PaymentGateway.java
└── notification/ # 📦 알림 모듈
├── NotificationService.java
└── internal/
└── EmailSender.java
모듈 의존성 검증
@Test
void verifyModularStructure() {
ApplicationModules modules = ApplicationModules.of(ShopApplication.class);
// 모듈 구조 출력
modules.forEach(System.out::println);
// 순환 의존성, 잘못된 접근 검증
// order → inventory.internal 접근 시 실패!
modules.verify();
}
verify()는 다음을 자동 검증합니다:
- 순환 의존성 금지: order → inventory → order 불가
- 내부 접근 금지:
internal패키지의 클래스를 다른 모듈에서 참조 시 실패 - 공개 API만 허용: 모듈 루트 패키지의 public 클래스만 외부에 노출
이벤트 기반 모듈 통신
모듈 간 직접 호출 대신 Application Event로 느슨하게 연결합니다.
// order 모듈 — 이벤트 발행
public record OrderCreatedEvent(
String orderId,
String customerId,
BigDecimal totalAmount
) {}
@Service
@RequiredArgsConstructor
public class OrderService {
private final ApplicationEventPublisher events;
private final OrderRepository orderRepo;
@Transactional
public Order createOrder(CreateOrderCommand cmd) {
var order = Order.create(cmd);
orderRepo.save(order);
// 이벤트 발행 — 트랜잭션 커밋 후 전달
events.publishEvent(new OrderCreatedEvent(
order.getId(), cmd.customerId(), cmd.totalAmount()
));
return order;
}
}
// inventory 모듈 — 이벤트 수신
@Service
@RequiredArgsConstructor
public class InventoryEventHandler {
private final StockRepository stockRepo;
@ApplicationModuleListener // Spring Modulith 전용
void onOrderCreated(OrderCreatedEvent event) {
// 재고 차감 로직
stockRepo.decreaseStock(event.orderId());
}
}
// notification 모듈 — 비동기 이벤트 수신
@Service
public class NotificationEventHandler {
@ApplicationModuleListener
@Async
void onOrderCreated(OrderCreatedEvent event) {
// 이메일 발송 (비동기)
sendOrderConfirmation(event.customerId(), event.orderId());
}
}
Event Publication Registry
Spring Modulith의 핵심 기능인 이벤트 저널링은 Transactional Outbox 패턴을 자동 구현합니다.
# application.yml
spring:
modulith:
events:
jdbc:
schema-initialization:
enabled: true # event_publication 테이블 자동 생성
republish-outstanding-events-on-restart: true
동작 방식:
- 이벤트 발행 시
event_publication테이블에 같은 트랜잭션으로 저장 - 트랜잭션 커밋 후 리스너에게 이벤트 전달
- 리스너 처리 완료 시
completion_date업데이트 - 앱 재시작 시 미완료 이벤트 자동 재발행
-- 자동 생성되는 테이블
CREATE TABLE event_publication (
id UUID PRIMARY KEY,
listener_id TEXT NOT NULL,
event_type TEXT NOT NULL,
serialized_event TEXT NOT NULL,
publication_date TIMESTAMP NOT NULL,
completion_date TIMESTAMP -- NULL이면 미처리
);
이로써 이벤트 유실 없는 신뢰성 있는 모듈 간 통신이 보장됩니다.
모듈 테스트 — @ApplicationModuleTest
// order 모듈만 격리 테스트 — 다른 모듈 빈은 로드하지 않음
@ApplicationModuleTest(mode = BootstrapMode.DIRECT_DEPENDENCIES)
class OrderModuleTest {
@Autowired OrderService orderService;
@Autowired AssertablePublishedEvents events;
@Test
void createOrder_publishesEvent() {
var cmd = new CreateOrderCommand("cust-1", BigDecimal.valueOf(30000));
orderService.createOrder(cmd);
// 발행된 이벤트 검증
events.assertThat()
.contains(OrderCreatedEvent.class)
.matching(e -> e.customerId().equals("cust-1"))
.matching(e -> e.totalAmount().compareTo(BigDecimal.valueOf(30000)) == 0);
}
}
// Scenario API — 이벤트 체인 통합 테스트
@ApplicationModuleTest
class OrderIntegrationTest {
@Autowired Scenario scenario;
@Test
void orderCreation_triggersInventoryAndNotification() {
scenario.publish(new OrderCreatedEvent("ord-1", "cust-1", BigDecimal.valueOf(30000)))
.andWaitForEventOfType(InventoryDecreasedEvent.class)
.matching(e -> e.orderId().equals("ord-1"))
.toArriveAndVerify(e -> {
assertThat(e.success()).isTrue();
});
}
}
모듈 문서 자동 생성
@Test
void generateDocumentation() {
var modules = ApplicationModules.of(ShopApplication.class);
// PlantUML + Asciidoc 문서 자동 생성
new Documenter(modules)
.writeModulesAsPlantUml() // 모듈 의존성 다이어그램
.writeIndividualModulesAsPlantUml() // 개별 모듈 다이어그램
.writeModuleCanvases(); // 모듈별 API/이벤트 캔버스
}
Named Interface — 세밀한 공개 범위
// package-info.java — 특정 패키지를 명시적 공개 인터페이스로 선언
@org.springframework.modulith.NamedInterface("api")
package com.example.shop.order.api;
@org.springframework.modulith.NamedInterface("spi")
package com.example.shop.order.spi;
// 다른 모듈에서 명시적 의존 선언
@org.springframework.modulith.ApplicationModule(
allowedDependencies = { "order :: api", "inventory :: api" }
)
package com.example.shop.payment;
MSA 전환 준비
모듈러 모놀리스의 최대 장점은 필요할 때 MSA로 점진적 전환이 가능하다는 점입니다.
- 1단계: 모듈러 모놀리스 — 이벤트 기반 통신, 모듈 격리 검증
- 2단계: 특정 모듈 추출 — Spring Event를 Kafka/RabbitMQ로 교체
- 3단계: 독립 서비스 배포 — 이벤트 계약이 이미 정의되어 있으므로 변경 최소화
운영 팁
- CI에 verify() 포함: 모듈 구조 위반 시 빌드 실패로 아키텍처 퇴행 방지
- 이벤트 직렬화: Jackson으로 JSON 직렬화 — record 타입 권장
- 미완료 이벤트 모니터링:
event_publication테이블의completion_date IS NULL건수를 Prometheus 메트릭으로 수집 - 패키지 명명: 모듈 루트 = 기능명,
internal= 구현 상세
정리
Spring Modulith는 마이크로서비스의 설계 원칙을 모놀리스 안에서 실현하는 프레임워크입니다. 패키지 기반 모듈 경계 강제, 이벤트 기반 느슨한 결합, Transactional Outbox 자동화, 모듈 격리 테스트를 통해 — 운영 복잡도는 낮추면서 아키텍처 품질은 높일 수 있습니다. MSA가 필요한 시점에 자연스럽게 전환할 수 있는 최적의 출발점입니다.