Spring Modulith 모듈러 모놀리스

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

동작 방식:

  1. 이벤트 발행 시 event_publication 테이블에 같은 트랜잭션으로 저장
  2. 트랜잭션 커밋 후 리스너에게 이벤트 전달
  3. 리스너 처리 완료 시 completion_date 업데이트
  4. 앱 재시작 시 미완료 이벤트 자동 재발행
-- 자동 생성되는 테이블
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가 필요한 시점에 자연스럽게 전환할 수 있는 최적의 출발점입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux