프로그래밍 PROGRAMMING/아키텍쳐

Spring Modulith로 모듈형 모놀리스 설계하기

매운할라피뇽 2026. 3. 4. 10:30
반응형

개요

모놀리스가 성장할수록 패키지 간 의존성이 복잡하게 얽혀 유지보수가 어려워지는 경험을 해보신 적 있으신가요? Spring Modulith는 단일 배포 단위를 유지하면서도 명확한 모듈 경계(Module Boundary)를 강제하는 모듈형 모놀리스(Modular Monolith) 아키텍처를 구현할 수 있도록 지원하는 Spring 공식 프레임워크입니다.
전통적인 모놀리스는 시간이 지날수록 도메인 간 직접 호출이 무분별하게 늘어납니다. 반면 모든 도메인을 마이크로서비스로 분리하면 운영 복잡도가 급증합니다. Spring Modulith는 이 두 극단 사이의 균형점을 제공합니다.
이 글에서는 Spring Modulith로 모듈 경계를 정의하고, 이벤트 기반 통신으로 모듈을 분리하며, 아키텍처 규칙을 자동 검증하는 방법을 단계별로 살펴봅니다.


Spring Modulith 모듈 구조 설계

Spring Modulith에서 모듈은 애플리케이션 루트 패키지 직하의 최상위 패키지 단위로 자동 인식됩니다. 각 모듈의 루트 패키지에 위치한 클래스만 외부에 공개되고, internal 하위 패키지는 프레임워크 수준에서 자동으로 은닉됩니다.

먼저 pom.xml에 의존성을 추가합니다.

<!-- spring-modulith BOM은 Spring Boot 3.x에 포함되어 있습니다 -->
<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>

아래는 주문(order)과 재고(inventory) 모듈로 분리한 패키지 구조입니다.

com.example.shop
├── order                          ← 주문 모듈 공개 API
│   ├── Order.java                 ← 외부 모듈에서 참조 가능
│   ├── OrderService.java
│   └── internal                   ← 모듈 내부 구현 (외부 접근 불가)
│       ├── OrderRepository.java
│       └── OrderValidator.java
└── inventory                      ← 재고 모듈
    ├── InventoryService.java
    └── internal
        └── InventoryRepository.java

모듈 경계를 코드 컨벤션이 아닌 프레임워크 수준에서 강제한다는 점이 Spring Modulith의 핵심입니다. order.internal의 클래스를 inventory 모듈에서 직접 참조하면 아키텍처 위반으로 즉시 감지됩니다. 팀 내 암묵적 규칙에 의존하지 않고, 잘못된 의존성이 코드베이스에 스며드는 것을 사전에 차단할 수 있습니다.


모듈 간 이벤트 기반 통신

모듈 간 직접 의존을 줄이기 위해 Spring Modulith는 Application Event 기반의 통신 패턴을 권장합니다. 주문 완료 시 재고를 차감하는 시나리오를 예시로 살펴보겠습니다.
주문 모듈에서 이벤트를 발행하는 코드입니다.

// order/OrderService.java
// InventoryService를 직접 주입하지 않고 이벤트로 의존성을 역전시킵니다.
@Service
@RequiredArgsConstructor
public class OrderService {

    private final ApplicationEventPublisher eventPublisher;
    private final OrderRepository orderRepository;

    @Transactional
    public Order placeOrder(OrderRequest request) {
        Order order = Order.create(request);
        orderRepository.save(order);

        // 재고 모듈을 직접 호출하지 않고 이벤트 발행으로 결합도를 낮춥니다
        eventPublisher.publishEvent(new OrderPlacedEvent(order.getId(), order.getItems()));
        return order;
    }
}

재고 모듈에서 이벤트를 수신하는 코드입니다.

// inventory/InventoryService.java
// @ApplicationModuleListener는 트랜잭션 커밋 후 이벤트를 안전하게 처리합니다.
@Service
@RequiredArgsConstructor
public class InventoryService {

    @ApplicationModuleListener
    void onOrderPlaced(OrderPlacedEvent event) {
        event.getItems().forEach(item ->
            decreaseStock(item.getProductId(), item.getQuantity())
        );
    }
}

실행 결과:

[INFO] Order saved: ORDER-001
[INFO] Transaction committed successfully
[INFO] Inventory decreased: PROD-A (qty: 2)
[INFO] Inventory decreased: PROD-B (qty: 1)

@ApplicationModuleListener는 Spring의 @TransactionalEventListener(phase = AFTER_COMMIT)를 기반으로 동작합니다. 트랜잭션이 성공적으로 커밋된 이후에만 이벤트를 처리하므로, 주문 저장 실패 시 재고가 잘못 차감되는 데이터 불일치 문제를 예방할 수 있습니다. 이벤트를 DB에 저장하는 이벤트 퍼블리케이션 레지스트리 기능을 활성화하면 장애 상황에서도 이벤트 재처리가 가능합니다.


아키텍처 규칙 자동 검증

Spring Modulith의 두드러진 기능 중 하나는 테스트 코드 한 줄로 모듈 경계 위반을 자동 감지하는 것입니다. 잘못된 의존성이 코드 리뷰를 통과하더라도 CI/CD 파이프라인에서 차단할 수 있습니다.
아키텍처 검증 테스트를 작성하는 방법입니다.

// ApplicationModularityTests.java
// ApplicationModules는 루트 패키지를 기준으로 전체 모듈 구조를 정적 분석합니다.
class ApplicationModularityTests {

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

    @Test
    void verifyModularStructure() {
        modules.verify(); // 위반 발견 시 AssertionError 발생
    }

    @Test
    void printModuleOverview() {
        modules.forEach(System.out::println); // 모듈 의존 관계 출력
    }
}

검증 통과 시 출력:

. 2 modules detected
   order     - Package: com.example.shop.order
   inventory - Package: com.example.shop.inventory
✓ No violations found

경계 위반 시 출력 (inventoryorder.internal을 직접 참조한 경우):

Module 'inventory' depends on non-exposed type
  com.example.shop.order.internal.OrderValidator
in module 'order'.

modules.forEach(System.out::println) 한 줄만으로 각 모듈의 공개 API, 발행/수신 이벤트, 외부 의존 관계를 텍스트로 확인할 수 있습니다. 별도의 아키텍처 문서 없이도 코드에서 직접 설계 의도를 파악할 수 있다는 점에서 팀 온보딩 비용을 줄이는 데 유리합니다.


맺음말

Spring Modulith는 모놀리스의 고질적인 문제인 경계 없는 의존성을 패키지 컨벤션과 테스트 자동화로 해결합니다. 코드 컨벤션에만 의존하던 모듈 설계를 프레임워크가 강제하도록 위임함으로써, 팀 규모가 커져도 아키텍처 품질을 유지할 수 있습니다.

다음 상황이라면 도입을 검토해 보시기 바랍니다:

  • 단일 서비스 내 도메인 경계가 불분명하게 얽혀 있는 경우
  • 팀 간 코드 변경이 서로에게 영향을 미쳐 배포 충돌이 잦은 경우
  • 마이크로서비스 전환 전 도메인 단위를 먼저 정리하고 싶은 경우


Spring Modulith는 공식 문서에서 지속적으로 발전하고 있고, Spring ApplicationEvent 심화 활용법도메인 주도 설계(DDD) 패키지 구조 설계를 함께 살펴보시면 모듈형 아키텍처 설계에 대한 이해를 한층 높이면서 모듈간 의존성을 심플하게 정리할 수 있습니다.

반응형