모듈러 모놀리스(Modular Monolith) 아키텍처
1. 개요
모듈러 모놀리스는 하나의 배포 단위(단일 프로세스) 안에서 업무 도메인을 명확한 경계를 가진 독립 모듈로 나누고, 모듈 간에는 공개 계약(public API)과 이벤트로만 상호작용하도록 강제하여 모놀리스의 단순한 운영과 마이크로서비스의 느슨한 결합을 동시에 추구하는 아키텍처 스타일이다.
지난 10여 년간 많은 조직이 확장성과 팀 자율성을 이유로 시스템을 마이크로서비스로 분해했으나, 상당수는 기대한 효과 대신 네트워크 호출의 지연과 실패, 분산 트랜잭션, 서비스 간 버전 호환, 관측·추적의 복잡성이라는 비용을 떠안았다. 특히 도메인 경계가 충분히 안정화되지 않은 초기 제품을 성급하게 쪼갠 경우, 하나의 기능 변경이 여러 서비스에 걸친 동시 배포로 번지는 '분산 모놀리스(distributed monolith)'로 귀결되는 사례가 빈번했다. 이러한 반성 속에서, 단일 배포의 단순함은 유지하되 내부 구조만큼은 마이크로서비스 수준으로 엄격히 모듈화하자는 접근이 재조명되었고 이것이 모듈러 모놀리스다.
전통적 모놀리스의 문제는 '단일 배포'가 아니라 '내부 경계의 부재'에 있다. 계층(layer)으로만 나뉜 코드베이스는 시간이 지나면 어느 기능이 어느 데이터를 만지는지 추적할 수 없는 '큰 진흙 공(big ball of mud)'으로 퇴화한다. 모듈러 모놀리스는 배포 경계를 하나로 두는 대신 논리적 경계를 도메인 단위로 세우고, 모듈 내부 구현을 외부로부터 은닉하여 결합도를 통제한다. 즉 '배포의 통합'과 '설계의 분리'를 분리해서 다루는 것이 핵심 사고방식이다.
이 접근의 필요성은 변화의 방향으로 판단한다. 도메인 경계가 아직 유동적이고, 팀 규모가 수십 명 이하이며, 운영 조직이 분산 시스템을 감당할 성숙도를 갖추지 못한 단계라면 모듈러 모놀리스가 합리적이다. 반대로 특정 모듈만 트래픽이 폭증해 독립 확장이 필수이거나, 서로 다른 언어·런타임·배포 주기가 강하게 요구되는 상황이라면 그 모듈만 서비스로 떼어내는 선택이 더 낫다. 기술사는 "모놀리스냐 마이크로서비스냐"의 이분법이 아니라, 변경 격리·확장 요구·조직 성숙도라는 축 위에서 어느 지점에 서야 하는지를 설명할 수 있어야 한다.
2. 전체 구조와 핵심 설계 원리
모듈러 모놀리스는 하나의 실행 파일(또는 컨테이너) 안에 여러 도메인 모듈이 공존하되, 각 모듈이 자신의 공개 계약 뒤로 내부를 숨기는 구조다. 외부에서 보면 하나의 애플리케이션이지만, 내부에서 보면 명확히 분리된 여러 개의 '서비스 후보'가 모여 있는 형태다. 아래 그림은 주문·결제·배송·재고 모듈이 하나의 프로세스 안에서 공개 API와 도메인 이벤트로만 연결되는 모습을 보여 준다.
flowchart TB
subgraph APP["단일 배포 단위 (하나의 프로세스)"]
direction LR
O["주문 모듈\nPublic API + 내부 구현 은닉"]
P["결제 모듈\nPublic API + 내부 구현 은닉"]
S["배송 모듈\nPublic API + 내부 구현 은닉"]
I["재고 모듈\nPublic API + 내부 구현 은닉"]
BUS["인프로세스 이벤트 버스"]
O -->|공개 API 호출| P
O -->|주문확정 이벤트| BUS
BUS -->|구독| S
BUS -->|구독| I
end
UI["웹·모바일 클라이언트"] --> O
O --> DBO[("주문 스키마")]
P --> DBP[("결제 스키마")]
S --> DBS[("배송 스키마")]
I --> DBI[("재고 스키마")]
위 구조에서 가장 중요한 규칙은 모듈이 다른 모듈의 내부 클래스나 테이블을 직접 참조하지 못한다는 점이다.
주문 모듈이 결제가 필요하면 결제 모듈의 공개 API(예: PaymentPort.authorize(...))만 호출하고, 결제 내부의 승인 로직이나 저장 구조는 알 수 없다.
이렇게 하면 결제 모듈을 전면 재작성하거나 훗날 별도 서비스로 분리하더라도 공개 계약만 유지하면 주문 모듈은 영향을 받지 않는다.
두 번째 원리는 데이터 소유권의 분리다. 모든 모듈이 하나의 데이터베이스를 공유하더라도, 각 모듈은 자신의 스키마(혹은 테이블 집합)만 쓰기 권한을 갖고 다른 모듈의 테이블을 직접 조인하지 않는다. 타 모듈의 데이터가 필요하면 조인 대신 공개 API를 통해 질의하거나, 이벤트로 받아 자신의 읽기 모델에 복제한다. 이 규칙이 무너지면 "DB 레벨에서 모든 것이 연결된" 전통적 모놀리스로 즉시 퇴행하므로, 모듈러 모놀리스의 성패를 가르는 가장 민감한 지점이다.
세 번째 원리는 의존성 방향의 통제다. 모듈 간 의존이 순환하면 분리가 불가능해지므로, 공유 커널(shared kernel)과 도메인별 모듈을 구분하고 의존 그래프가 비순환(acyclic)이 되도록 설계한다. 예컨대 공통 식별자·값 객체·이벤트 정의는 얇은 공유 모듈에 두되, 업무 규칙은 각 도메인 모듈에만 둔다.
2.1 모듈 경계의 식별
모듈 경계를 어디에 그을 것인가는 아키텍처가 아니라 도메인의 문제다. 도메인 주도 설계(DDD)의 경계 컨텍스트(bounded context)가 가장 유력한 후보이며, 같은 용어가 서로 다른 의미를 갖는 지점(예: 주문에서의 '고객'과 마케팅에서의 '고객')이 바로 모듈을 가르는 선이 된다. 경계를 잘못 그으면 한 기능 변경이 여러 모듈을 동시에 건드리게 되어 결합도가 오히려 커지므로, 변경이 함께 일어나는 응집된 개념을 한 모듈로 묶는 것이 원칙이다.
실무에서는 초기에는 경계를 느슨하게 잡고, 변경 로그와 커밋이 어느 모듈에 몰리는지를 관찰하며 경계를 재조정하는 진화적 접근이 안전하다. 한 번에 완벽한 경계를 설계하려는 시도는 도메인 이해가 부족한 초기에 오히려 잘못된 경계를 고착시킬 위험이 크다.
2.2 경계 강제(enforcement) 수단
경계는 문서나 구두 약속이 아니라 도구로 강제될 때만 유지된다.
Java 진영에서는 Spring Modulith나 ArchUnit으로 "A 모듈이 B 모듈의 internal 패키지를 참조하면 빌드 실패"와 같은 규칙을 테스트로 고정한다.
.NET에서는 프로젝트 참조를 모듈 단위로 쪼개고 내부 타입을 internal로 제한하며, Java 9 이상에서는 모듈 시스템(JPMS)이나 패키지 접근제어로 공개 범위를 좁힌다.
이러한 자동 검증이 없으면 바쁜 일정 속에서 개발자들이 손쉬운 지름길(직접 참조)을 택하게 되고 경계는 수개월 내에 소멸한다.
3. 모듈 간 상호작용과 내부 구조
모듈 간 통신은 크게 동기 호출과 비동기 이벤트로 나뉘며, 각각의 선택이 결합도와 일관성에 직접 영향을 준다. 아래 그림은 주문 생성 한 건이 결제·재고·배송 모듈로 전파되는 흐름을 동기·비동기 혼합으로 표현한 것이다.
sequenceDiagram
participant C as 클라이언트
participant O as 주문 모듈
participant P as 결제 모듈
participant B as 이벤트 버스
participant I as 재고 모듈
participant S as 배송 모듈
C->>O: 주문 생성 요청
O->>P: 공개 API 호출 (결제 승인)
P-->>O: 승인 결과 반환
O->>O: 주문 확정 저장
O->>B: OrderConfirmed 이벤트 발행
B->>I: 재고 차감 처리
B->>S: 배송 준비 처리
O-->>C: 주문 번호 응답
동기 호출은 즉시 결과가 필요한 경우에 쓴다. 주문 확정 전에 결제 승인 여부를 반드시 알아야 하므로, 주문 모듈은 결제 모듈의 공개 API를 인프로세스로 호출하고 결과를 기다린다. 인프로세스 호출은 네트워크를 거치지 않으므로 지연이 사실상 없고, 같은 트랜잭션 경계 안에서 처리할 수 있어 분산 트랜잭션의 복잡성을 피할 수 있다는 점이 마이크로서비스 대비 결정적 장점이다.
비동기 이벤트는 결과를 즉시 기다릴 필요가 없고, 발신 모듈이 수신 모듈의 존재를 몰라도 되는 경우에 쓴다.
주문 확정 후 재고 차감과 배송 준비는 주문 모듈의 관심사가 아니므로, 주문 모듈은 OrderConfirmed 이벤트만 발행하고 누가 그것을 구독하는지는 알지 못한다.
이렇게 하면 나중에 '포인트 적립' 모듈을 추가하더라도 주문 모듈 코드를 바꿀 필요 없이 이벤트를 구독하는 모듈만 더하면 되므로, 개방-폐쇄 원칙이 모듈 수준에서 실현된다.
여기서 주의할 설계 함정은 '같은 프로세스'라는 편리함에 기대어 이벤트를 단순 메서드 호출로 대체하는 것이다. 인프로세스 이벤트라도 발행-구독의 계약을 명시적으로 두어야, 훗날 그 모듈을 별도 서비스로 분리할 때 이벤트를 메시지 브로커(예: Kafka)로 바꾸는 작업이 국소적이게 된다. 반대로 이벤트를 남용해 모든 동기 흐름까지 비동기로 처리하면, 단일 프로세스에서 굳이 최종적 일관성을 감수하는 역설이 생기므로 균형이 필요하다.
3.1 트랜잭션과 일관성
모듈러 모놀리스의 큰 이점은 여러 모듈의 변경을 하나의 로컬 트랜잭션으로 묶을 수 있다는 점이다. 주문 저장과 결제 기록을 같은 트랜잭션에서 커밋하면 부분 실패로 인한 데이터 불일치를 원천적으로 막을 수 있어, 마이크로서비스에서 Saga·보상 트랜잭션으로 어렵게 달성하던 일관성을 간단히 얻는다. 다만 이벤트 기반 비동기 처리 부분은 여전히 최종적 일관성이므로, 이벤트 발행과 상태 변경의 원자성을 위해 트랜잭셔널 아웃박스(transactional outbox)와 유사한 패턴을 모듈 내부에 적용하는 것이 안전하다.
3.2 내부 패키지 구조
각 모듈은 내부적으로 api(공개 계약), domain(업무 규칙), infrastructure(저장·외부 연동), internal(은닉 구현)로 나누는 것이 일반적이다.
공개되는 것은 오직 api 패키지의 인터페이스와 이벤트·DTO뿐이며, 나머지는 모듈 바깥에서 참조 불가로 막는다.
이 구조는 헥사고날 아키텍처(포트와 어댑터)를 모듈 단위로 반복 적용한 것으로 볼 수 있으며, 한 모듈 안에서도 업무 규칙이 기술 세부에 오염되지 않도록 지킨다.
4. 마이크로서비스·전통적 모놀리스와의 비교
세 아키텍처는 흔히 우열의 관계로 오해되지만, 실제로는 서로 다른 제약을 최적화한 선택지다. 핵심 차이는 '배포 단위의 수'와 '경계 강제의 수단'에서 비롯되며, 그 차이가 운영 비용과 변경 유연성의 트레이드오프로 이어진다.
| 구분 | 전통적 모놀리스 | 모듈러 모놀리스 | 마이크로서비스 |
|---|---|---|---|
| 배포 단위 | 1개 | 1개 | N개 |
| 모듈 경계 | 모호(계층 위주) | 명확(도메인·도구 강제) | 물리적 분리 |
| 모듈 간 통신 | 직접 호출·공유 테이블 | 공개 API·인프로세스 이벤트 | 네트워크(REST·gRPC·메시지) |
| 트랜잭션 | 단일 로컬 | 단일 로컬 | 분산(Saga·보상) |
| 독립 확장 | 불가 | 불가(전체 확장만) | 모듈별 가능 |
| 운영 복잡도 | 낮음 | 낮음 | 높음 |
| 팀 자율성 | 낮음 | 중간 | 높음 |
표가 보여 주듯 모듈러 모놀리스는 운영 복잡도를 낮게 유지하면서도 경계를 명확히 한다는 점에서 전통적 모놀리스와 결정적으로 다르다. 반면 마이크로서비스가 제공하는 '모듈별 독립 확장'과 '언어·런타임 이질성'은 포기한다. 따라서 트래픽이 특정 모듈에 집중되어 독립 확장이 핵심 요구가 되는 순간이 바로 모듈러 모놀리스에서 마이크로서비스로의 전환 신호다.
실제 사례는 이 트레이드오프를 선명하게 보여 준다. 전자상거래 플랫폼 Shopify는 대규모 Ruby on Rails 코드베이스를 마이크로서비스로 쪼개는 대신, 경계가 강제된 모듈러 모놀리스로 재구성하여 단일 배포의 이점을 지키면서 수천 명 규모의 개발 조직이 병렬로 작업하도록 했다. 스트리밍 사업자 Netflix 계열이 아닌 Amazon Prime Video 팀은 2023년, 영상 품질 모니터링 파이프라인을 분산 마이크로서비스에서 단일 애플리케이션으로 통합하여 인프라 비용을 약 90% 절감했다고 공개했는데, 이는 과도한 분산이 오히려 비효율을 낳을 수 있음을 보여 준 대표 사례다. 고객 데이터 플랫폼 Segment 역시 수백 개로 늘어난 마이크로서비스의 운영 부담을 견디지 못하고 다시 모놀리스로 통합한 경험을 공유했으며, 이 전환들의 공통 교훈은 "분리는 공짜가 아니며 경계가 안정된 뒤에 미뤄도 늦지 않다"는 것이다.
5. 심화: 전환 전략과 최신 동향
모듈러 모놀리스가 특히 강력한 지점은 마이크로서비스로 가는 '중간 역'으로 기능할 수 있다는 점이다. 마틴 파울러가 제시한 '모놀리스 우선(Monolith First)' 원칙은, 도메인 경계가 불확실한 초기에 성급히 서비스를 쪼개면 잘못된 경계를 물리적으로 고착시키므로, 먼저 하나의 배포 단위 안에서 경계를 다듬고 그 경계가 안정된 뒤에 필요한 모듈만 서비스로 떼어내라고 권고한다. 모듈러 모놀리스는 이 전략의 자연스러운 토대가 된다. 모듈 간 통신이 이미 공개 API와 이벤트로 추상화되어 있으므로, 분리 시점에 인프로세스 호출을 원격 호출로, 인프로세스 이벤트를 메시지 브로커로 바꾸는 작업이 모듈 경계에 국한된다.
도구 생태계도 이 접근을 뒷받침하도록 성숙했다.
Spring 진영의 Spring Modulith는 모듈 경계를 명시하고 위반을 테스트로 검증하며 모듈 간 이벤트와 문서화를 지원한다.
ArchUnit은 패키지 의존 규칙을 코드로 단정하여 CI에서 경계 위반을 차단하고, .NET의 NDepend나 아키텍처 테스트 라이브러리도 유사한 역할을 한다.
이처럼 '경계를 깨면 빌드가 깨지는' 자동화가 보편화되면서, 모듈러 모놀리스는 더 이상 규율에만 의존하는 취약한 패턴이 아니라 도구로 지탱되는 실천 가능한 아키텍처가 되었다.
최근 클라우드 환경에서는 단일 배포라도 수평 확장(동일 이미지를 여러 인스턴스로)과 무중단 배포가 보편화되어, '모놀리스는 확장이 안 된다'는 통념이 상당 부분 완화되었다. 서버리스·컨테이너 오케스트레이션 위에서 모듈러 모놀리스를 운영하면, 전체 애플리케이션을 복제해 트래픽을 분산하는 방식으로 상당한 규모까지 감당할 수 있다. 결국 독립 배포·독립 확장이 정말 필요한 소수 모듈만 선별적으로 분리하는 '하이브리드' 형태가 현실적인 지배 구조로 자리 잡고 있다.
6. 고려사항 및 시사점
첫째, 경계 강제의 자동화가 성패를 가른다.
모듈러 모놀리스는 개발자의 선의가 아니라 빌드·테스트 도구로 경계를 지킬 때만 유지된다.
ArchUnit·Spring Modulith 같은 아키텍처 테스트를 CI 파이프라인의 필수 게이트로 두어, 경계 위반이 병합되기 전에 차단하는 체계를 아키텍처 설계와 동시에 마련해야 한다.
둘째, 데이터 소유권을 설계 단계에서 못 박아야 한다. 가장 흔한 실패는 "성능을 위해" 다른 모듈의 테이블을 직접 조인하면서 시작된다. 모듈별 스키마 분리와 교차 조인 금지를 규약으로 명문화하고, 타 모듈 데이터가 필요하면 공개 API 질의나 이벤트 기반 읽기 모델 복제로 해결하도록 가이드해야 장기적으로 분리 가능성이 보존된다.
셋째, 전환은 경계가 안정된 뒤로 미루는 것이 합리적이다. 초기부터 마이크로서비스로 가는 결정은 도메인 이해가 가장 부족한 시점에 가장 비싼 구조를 택하는 역설이다. 모듈러 모놀리스로 시작해 변경 패턴과 트래픽 특성을 관측하고, 독립 확장·독립 배포·기술 이질성이 실제로 요구되는 모듈만 선별적으로 분리하는 전략이 비용과 위험을 모두 낮춘다.
넷째, 조직 구조와 아키텍처를 함께 설계해야 한다. 콘웨이의 법칙에 따라 시스템 구조는 조직의 소통 구조를 닮는다. 모듈러 모놀리스에서 모듈 경계와 팀 책임 경계를 일치시키면 코드 소유권이 명확해지고 변경 충돌이 줄어들지만, 하나의 저장소·배포를 공유하므로 배포 조율과 코드 리뷰 프로세스를 팀 간에 합의해 두어야 통합의 이점이 유지된다.
다섯째, 관측 가능성과 모듈 단위 지표를 함께 설계한다. 단일 프로세스라도 모듈별 처리 시간·오류율·이벤트 적체를 분리해 측정하면, 어느 모듈이 병목이고 어느 모듈이 먼저 분리 대상인지 데이터로 판단할 수 있어 전환 의사결정의 근거가 된다.
참고자료
- Martin Fowler, MonolithFirst
- Spring, Spring Modulith Reference Documentation
- Amazon Prime Video Tech, Scaling up the Prime Video audio/video monitoring service and reducing costs
한 줄 요약: 모듈러 모놀리스는 단일 배포의 단순함을 지키면서 도메인 모듈을 공개 API와 이벤트로만 연결하고 경계를 도구로 강제하여, 마이크로서비스의 느슨한 결합을 저비용으로 얻고 필요한 모듈만 훗날 서비스로 분리할 수 있게 하는 아키텍처다.