개요

Spring Modulith는 스프링 애플리케이션 내에서 논리적 모듈 경계를 명확히 정의하고 강제하는 프로젝트입니다. MSA의 복잡성 없이 모듈 간 느슨한 결합과 높은 응집도를 달성할 수 있는 "모듈러 모놀리스" 패턴을 공식 지원합니다. 2024년에 정식 GA를 달성했으며, 모놀리스에서 MSA로의 점진적 전환 경로를 제공합니다.

핵심 개념

Application Module은 Spring Modulith의 기본 단위입니다. 최상위 패키지의 직접 하위 패키지가 하나의 모듈이 됩니다. 예를 들어 com.example.order, com.example.inventory가 각각 하나의 모듈입니다.

모듈 간 통신은 Application Event를 통해 이루어집니다. 직접 메서드 호출 대신 이벤트를 발행하고 구독하는 방식으로, 모듈 간 결합도를 최소화합니다.

모듈 검증 기능은 아키텍처 규칙을 테스트로 강제합니다. 순환 의존성 감지, 허용되지 않은 모듈 간 접근 차단, API 표면(public 클래스) 검증 등을 자동으로 수행합니다.

Event Externalization은 모듈 내부 이벤트를 Kafka, RabbitMQ 등 외부 메시지 브로커로 자동 전파합니다. 이를 통해 모놀리스에서 MSA로의 전환이 자연스럽게 이루어집니다.

실전 예제

프로젝트 구조와 모듈 정의입니다.

com.example.shop/
├── ShopApplication.java
├── order/                    # 주문 모듈
│   ├── Order.java           # 공개 API (public)
│   ├── OrderService.java    # 공개 API (public)
│   ├── OrderCompleted.java  # 이벤트 (public)
│   └── internal/            # 내부 구현 (패키지 프라이빗)
│       ├── OrderRepository.java
│       └── OrderValidator.java
├── inventory/                # 재고 모듈
│   ├── InventoryService.java
│   └── internal/
│       └── InventoryRepository.java
└── notification/             # 알림 모듈
    ├── NotificationService.java
    └── internal/
        └── EmailSender.java

이벤트 기반 모듈 간 통신입니다.

// order 모듈 - 이벤트 정의
public record OrderCompleted(
    String orderId,
    String customerId,
    List<LineItem> items,
    BigDecimal totalAmount
) {}

// order 모듈 - 이벤트 발행
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {

    private final ApplicationEventPublisher events;
    private final OrderRepository orderRepository;

    public Order completeOrder(String orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow();
        order.complete();

        // 이벤트 발행 - 재고 차감, 알림 등은 각 모듈이 처리
        events.publishEvent(new OrderCompleted(
            order.getId(),
            order.getCustomerId(),
            order.getItems(),
            order.getTotalAmount()
        ));

        return orderRepository.save(order);
    }
}

// inventory 모듈 - 이벤트 구독
@Service
@RequiredArgsConstructor
public class InventoryService {

    private final InventoryRepository inventoryRepository;

    @ApplicationModuleListener
    void on(OrderCompleted event) {
        event.items().forEach(item ->
            inventoryRepository.decreaseStock(
                item.productId(), item.quantity()
            )
        );
    }
}

모듈 구조 검증 테스트입니다.

@SpringBootTest
class ModularityTests {

    ApplicationModules modules = ApplicationModules.of(ShopApplication.class);

    @Test
    void verifyModuleStructure() {
        // 순환 의존성, 잘못된 접근 등을 검증
        modules.verify();
    }

    @Test
    void createModuleDocumentation() {
        // 모듈 관계 다이어그램 자동 생성
        new Documenter(modules)
            .writeModulesAsPlantUml()
            .writeIndividualModulesAsPlantUml();
    }
}

이벤트 외부화 설정입니다.

// Kafka로 이벤트 외부화
@Configuration
public class EventExternalizationConfig {

    @Bean
    EventExternalizationConfiguration eventExternalization() {
        return EventExternalizationConfiguration.externalizing()
            .select(EventExternalizationConfiguration
                .annotatedAsExternalized())
            .build();
    }
}

// 외부화할 이벤트에 어노테이션 추가
@Externalized("orders.completed")
public record OrderCompleted(
    String orderId, String customerId, BigDecimal totalAmount
) {}

활용 팁

  • internal 패키지: 모듈의 구현 상세를 internal 하위 패키지에 넣으면, Spring Modulith가 다른 모듈의 접근을 자동으로 차단합니다.
  • Event Publication Registry: 이벤트 발행 실패 시 재시도를 위해 spring-modulith-events-jdbc를 추가하면 이벤트가 DB에 기록됩니다.
  • 개별 모듈 테스트: @ApplicationModuleTest 어노테이션으로 특정 모듈만 격리하여 빠르게 테스트할 수 있습니다.
  • Observability: Spring Modulith는 모듈 간 이벤트 흐름을 자동으로 추적하여 Micrometer Tracing과 연동됩니다.
  • 점진적 도입: 기존 모놀리스에 Spring Modulith를 적용할 때, 먼저 패키지 구조만 정리하고 verify() 테스트부터 시작하세요.

마무리

Spring Modulith는 "모놀리스냐 MSA냐"라는 이분법적 선택을 넘어, 모듈러 모놀리스라는 실용적 중간 지점을 제공합니다. 모듈 경계를 명확히 하고, 이벤트 기반 통신으로 느슨한 결합을 유지하면, 필요한 시점에 특정 모듈만 독립 서비스로 분리하는 것이 자연스럽습니다. MSA를 시작하기 전에, 먼저 모듈러 모놀리스로 아키텍처를 정리하는 것을 권장합니다.