
마이크로서비스가 주목받으면서 많은 팀이 분산 시스템으로 전환을 시도했지만, 운영 복잡도와 네트워크 오버헤드로 인해 어려움을 겪는 경우가 적지 않습니다. 모듈러 모놀리스 아키텍처(Modular Monolith Architecture)는 단일 배포 단위의 단순함을 유지하면서도 명확한 모듈 경계를 통해 확장성과 유지보수성을 확보할 수 있는 현실적인 대안입니다. 이 글에서는 Java 프로젝트에 모듈러 모놀리스를 적용하는 방법을 패키지 구조 설계부터 모듈 간 의존성 관리까지 단계별로 살펴봅니다.
모듈러 모놀리스란 무엇인가
모듈러 모놀리스는 하나의 배포 아티팩트(JAR, WAR 등) 안에서 도메인별로 명확히 분리된 모듈로 코드를 구성하는 아키텍처 패턴입니다. 전통적인 레이어드 모놀리스가 controller → service → repository 계층으로 코드를 분리하는 것과 달리, 모듈러 모놀리스는 도메인 중심으로 수직 분리(vertical slicing)를 적용합니다.
핵심 원칙은 세 가지입니다.
- 높은 응집도(High Cohesion): 관련 기능은 같은 모듈 안에 모읍니다.
- 낮은 결합도(Low Coupling): 모듈 간 직접 의존은 최소화하고 공개 API를 통해서만 소통합니다.
- 명시적 경계(Explicit Boundaries): 모듈 내부 구현은 외부에 노출하지 않습니다.
마이크로서비스와 비교하면, 네트워크 분리 없이도 논리적 경계를 유지할 수 있어 개발 초기 단계나 팀 규모가 작은 프로젝트에 특히 적합합니다. 나중에 트래픽이나 팀이 성장하면 각 모듈을 독립 서비스로 추출하기도 수월합니다.
패키지 구조 설계
모듈러 모놀리스의 출발점은 패키지 구조입니다. 기존 레이어드 구조와 비교하면 차이가 명확합니다.
변경전 — 기술 계층 기준 패키지 구조:
com.example.app
├── controller
│ ├── OrderController.java
│ └── UserController.java
├── service
│ ├── OrderService.java
│ └── UserService.java
└── repository
├── OrderRepository.java
└── UserRepository.java변경후 — 도메인 모듈 기준 패키지 구조:
com.example.app
├── order
│ ├── api ← 외부에 공개하는 인터페이스
│ │ └── OrderFacade.java
│ ├── application ← 유스케이스 로직
│ │ └── OrderService.java
│ ├── domain ← 도메인 모델
│ │ └── Order.java
│ └── infrastructure ← DB, 외부 API 어댑터
│ └── OrderRepository.java
├── user
│ ├── api
│ │ └── UserFacade.java
│ ├── application
│ │ └── UserService.java
│ ├── domain
│ │ └── User.java
│ └── infrastructure
│ └── UserRepository.java
└── shared ← 모듈 공통 유틸리티
└── Money.java이 구조에서 각 모듈은 api 패키지만 외부에 공개하고, application, domain, infrastructure는 모듈 내부에서만 사용합니다. 이 규칙을 아키텍처 테스트로 강제하면 더욱 효과적입니다.
모듈 간 의존성 관리
모듈러 모놀리스에서 가장 중요한 것은 모듈 간 직접 참조를 차단하는 것입니다. order 모듈이 user 모듈의 내부 서비스를 직접 호출하면, 경계가 흐려져 일반 모놀리스와 다를 바 없어집니다.
아래 예시는 order 모듈이 user 모듈의 공개 인터페이스만 사용하도록 설계하는 방법입니다.
user 모듈이 외부에 공개하는 파사드(Facade) 인터페이스를 정의합니다.
// user/api/UserFacade.java — user 모듈의 공개 계약
public interface UserFacade {
UserInfo getUserInfo(Long userId);
}
// UserInfo는 모듈 간 데이터 전달용 DTO (도메인 객체 직접 노출 금지)
public record UserInfo(Long id, String name, String email) {}order 모듈은 인터페이스에만 의존하고, 구현체는 알지 못합니다.
// order/application/OrderService.java
@Service
@RequiredArgsConstructor
public class OrderService {
private final UserFacade userFacade; // 인터페이스만 참조
private final OrderRepository orderRepository;
public OrderResult placeOrder(Long userId, OrderRequest request) {
// user 모듈 내부 구현에 의존하지 않음
UserInfo user = userFacade.getUserInfo(userId);
Order order = Order.create(user.id(), request.items());
orderRepository.save(order);
return new OrderResult(order.getId(), user.name());
}
}Output (로그 예시):
[OrderService] 주문 생성 완료 — userId=42, orderId=1001, userName=홍길동이렇게 UserFacade 인터페이스를 경계로 두면, user 모듈 내부를 어떻게 리팩터링해도 order 모듈에는 영향이 없습니다. Spring의 @Bean 등록을 통해 구현체를 주입하므로, 실제 코드 결합은 런타임에만 발생합니다.
ArchUnit으로 경계 강제하기
규칙을 문서화하는 것만으로는 부족합니다. ArchUnit 라이브러리를 사용하면 모듈 경계 위반을 자동으로 감지하는 아키텍처 테스트를 작성할 수 있습니다.
// test/ArchitectureTest.java
class ModularBoundaryTest {
@ArchTest
static final ArchRule orderMustNotAccessUserInternals =
noClasses()
.that().resideInAPackage("..order..")
.should().accessClassesThat()
.resideInAnyPackage(
"..user.application..", // user 내부 서비스 접근 금지
"..user.domain..", // user 도메인 객체 직접 접근 금지
"..user.infrastructure.." // user DB 계층 직접 접근 금지
)
.as("order 모듈은 user.api 패키지만 참조해야 합니다.");
}Output (위반 시 테스트 실패 메시지):
Architecture Violation [Priority: MEDIUM]:
Rule 'order 모듈은 user.api 패키지만 참조해야 합니다.' was violated (1 times):
Method <com.example.order.application.OrderService.placeOrder>
calls method <com.example.user.application.UserService.findById>이 테스트가 CI 파이프라인에 포함되면, 팀원이 실수로 모듈 경계를 넘는 의존성을 추가했을 때 즉시 빌드가 실패합니다. ArchUnit 공식 문서에서 더 다양한 규칙 정의 방법을 확인할 수 있습니다.
맺음말
모듈러 모놀리스 아키텍처는 도메인 중심 패키지 구조, 공개 인터페이스를 통한 모듈 간 소통, 아키텍처 테스트를 통한 경계 강제라는 세 가지 실천으로 요약할 수 있습니다.
마이크로서비스 전환을 고려 중이라면, 먼저 모듈러 모놀리스로 도메인 경계를 명확히 정의하는 것이 안전한 선택입니다. 경계가 잘 정의된 모듈은 나중에 독립 서비스로 분리할 때도 인터페이스 계약만 HTTP 또는 메시지 큐로 교체하면 되므로 마이그레이션 비용이 크게 줄어듭니다.
반면, 팀 전체가 하나의 도메인만 담당하거나 서비스 규모가 충분히 작다면 굳이 이 패턴을 도입할 필요는 없습니다. 아키텍처는 팀과 제품의 현재 상황에 맞게 선택하는 것이 중요합니다.
관련하여 도메인 주도 설계(DDD)의 바운디드 컨텍스트 개념과 헥사고날 아키텍처(Ports & Adapters)를 함께 학습하면 모듈 경계를 더 체계적으로 정의하는 데 도움이 됩니다.
'프로그래밍 PROGRAMMING > 아키텍쳐' 카테고리의 다른 글
| Event Sourcing 아키텍처 패턴 적용하기 (0) | 2026.03.23 |
|---|---|
| BFF 패턴으로 API 게이트웨이 설계하기 (0) | 2026.03.19 |
| Cell-based Architecture 적용하기 (0) | 2026.03.17 |
| CQRS 패턴 적용법: 명령과 조회 분리하기 (0) | 2026.03.13 |
| 분산 트랜잭션을 안전하게 처리하는 Saga 패턴 설계 가이드 (1) | 2026.03.11 |