← 개념 목록

모듈러 모놀리스 (Modular Monolith)

개념

단일 배포 단위를 유지하면서 내부적으로는 명확한 도메인 경계와 캡슐화된 모듈로 구성하여, 모놀리스의 운영 단순성과 마이크로서비스의 구조적 이점을 절충하려는 소프트웨어 아키텍처 스타일이다.

개요

모듈러 모놀리스는 애플리케이션 전체를 하나의 배포 단위로 유지하면서, 내부 코드베이스는 비즈니스 도메인을 기준으로 느슨하게 결합되고 응집도 높은 모듈로 분리하는 소프트웨어 아키텍처 스타일이다. 각 모듈은 자신이 담당하는 도메인 로직과 데이터를 캡슐화하고, 잘 정의된 인터페이스를 통해서만 다른 모듈과 상호작용한다. 이런 구조는 전통적인 모놀리스가 흔히 빠지는 뒤엉킨 결합 문제를 피하는 동시에, 마이크로서비스가 요구하는 네트워크 통신·분산 트랜잭션·개별 배포 파이프라인 같은 운영 부담을 지지 않아도 된다는 절충안으로 제시된다.

이 개념은 2010년대 중반 마이크로서비스 아키텍처가 업계 표준처럼 여겨지던 시기에, 소프트웨어 아키텍트 Simon Brown이 자신의 강연과 저술에서 대중화한 “패키지 바이 컴포넌트(package by component)” 코드 조직 방식에서 출발했다. 이후 Shopify, Basecamp 같은 실제 서비스의 아키텍처 전환 사례와, Martin Fowler의 모놀리스 퍼스트 전략, Kamil Grzybek의 체계적인 정리 작업을 거치며 하나의 독립된 아키텍처 스타일로 자리 잡았다. 최근에는 마이크로서비스를 과도하게 세분화했다가 운영 복잡도만 늘어난 조직들이 다시 통합된 구조로 회귀하는 사례가 이어지면서 재조명받고 있다.

등장 배경

전통적 모놀리스와 빅 볼 오브 머드

모놀리식 시스템은 오랫동안 소프트웨어 개발의 기본값이었다. 프레임워크가 강제하지 않는 한 대부분의 신규 프로젝트는 자연스럽게 모놀리스 형태로 시작되며, 초기에는 빠른 개발 속도와 단일 저장소·단일 배포 파이프라인·단일 데이터베이스라는 단순함이 큰 장점으로 작용한다. 문제는 시스템과 팀 규모가 커지는 과정에서 발생한다. 모듈 사이의 경계가 명시적으로 관리되지 않으면 서로 다른 도메인의 코드가 뒤섞이고 상호 참조하게 되는데, 이런 상태를 소프트웨어 공학에서는 흔히 빅 볼 오브 머드라고 부른다.

Shopify 엔지니어링 블로그는 이 문제를 겪은 대표적인 사례로 자주 인용된다. Shopify는 배송비 계산 로직과 세금 계산 로직이 서로 얽혀 있어, 세금 계산 방식을 바꾸는 작업이 의도치 않게 배송비 계산 결과에 영향을 주는 상황을 겪었다고 설명한다. 새로 합류한 개발자가 배송 관련 기능 하나를 수정하기 위해 결제, 주문 생성 등 시스템 전반을 이해해야 했고, 이로 인해 온보딩 기간이 길어지고 변경의 파급 범위를 예측하기 어려워졌다는 것이다.

다만 여러 저자들은 모놀리스라는 용어 자체가 나쁜 설계를 의미하지는 않는다는 점을 분명히 한다. Kamil Grzybek은 자신의 블로그 시리즈에서, 흔히 통용되는 모놀리스 정의들이 “단일 배포 단위”라는 사실과 “모듈화되지 않은 뒤엉킨 코드”라는 품질 판단을 하나로 묶어버리는 오류를 범한다고 지적한다. 그의 관점에서 모놀리스는 오직 배포 단위가 하나라는 사실만을 의미할 뿐, 내부 구조의 좋고 나쁨과는 무관한 중립적인 개념이다.

마이크로서비스의 부상과 한계

2010년대 들어 Netflix, Amazon 등 대규모 서비스 기업들이 마이크로서비스 아키텍처를 채택해 독립적인 개발·배포·확장이라는 성과를 알리면서, 마이크로서비스는 업계 전반에서 사실상의 표준 권장 사항처럼 확산되었다. 그러나 서비스를 여러 개의 독립 배포 단위로 쪼개는 순간 조직은 분산 시스템 고유의 문제들을 함께 떠안게 된다. 서비스 간 호출은 네트워크를 경유하므로 지연 시간과 실패 가능성이 늘어나고, 여러 저장소와 배포 파이프라인을 유지·관리해야 하며, 여러 서비스에 걸친 리팩터링은 모놀리스보다 훨씬 번거로워진다.

특히 서비스 경계가 잘못 설정된 상태에서 마이크로서비스로 전환하면, 서비스는 물리적으로 분리되어 있지만 여전히 동기적으로 강하게 결합된 “분산 모놀리스”라는 안티패턴에 빠지기 쉽다. 이 경우 마이크로서비스의 운영 비용은 그대로 떠안으면서 독립적인 개발·배포라는 핵심 이점은 얻지 못하는 최악의 결과로 이어진다.

모놀리스 퍼스트 전략

Martin Fowler는 2015년에 발표한 글 “MonolithFirst”에서, 성공적으로 마이크로서비스를 도입한 사례 대부분이 처음부터 마이크로서비스로 설계된 것이 아니라 지나치게 커진 모놀리스를 나중에 분리한 경우였다는 관찰을 소개한다. 반대로 처음부터 마이크로서비스로 시스템을 구축한 사례들은 대체로 심각한 어려움을 겪었다고 언급한다. 그 근거로 그는 두 가지를 든다. 첫째, 마이크로서비스는 상당한 운영 비용, 즉 “마이크로서비스 프리미엄”을 수반하므로 이 비용을 정당화할 만큼 시스템이 복잡해지기 전까지는 손해에 가깝다는 것이다. 둘째, 서비스 경계를 안정적으로 나누려면 바운디드 컨텍스트를 정확히 파악해야 하는데, 숙련된 아키텍트라도 프로젝트 초기에 올바른 경계를 예측하기는 매우 어렵다는 것이다. 그래서 그는 모놀리스로 시작해 도메인 이해가 쌓이고 경계가 안정된 뒤에 마이크로서비스로 옮겨가는 전략을 제안하되, 이 접근이 성공하려면 애초에 모놀리스 내부를 API 경계와 데이터 저장 방식 모두에서 신중하게 모듈화해 두어야 한다고 강조한다. 다만 그는 이 반대 의견, 즉 처음부터 여러 개의 작은 서비스로 팀을 나누어 시작하는 편이 조직 확장에는 더 유리할 수 있다는 주장도 함께 소개하며 균형 잡힌 시각을 제시한다.

Shopify 역시 자사의 사례를 Fowler의 “설계 스태미나 가설”과 연결 지어 설명한다. 시스템 초기에는 설계 품질에 크게 투자하지 않고 빠르게 기능을 추가하는 편이 합리적이며, 기능 개발 속도가 눈에 띄게 느려지는 시점, 즉 설계 투자 대비 손익분기점을 지나는 시점이 되어서야 아키텍처 재설계에 자원을 투입하는 것이 타당하다는 논리이다.

정의

모놀리스: 단일 배포 단위

Kamil Grzybek은 모놀리스를 “정확히 하나의 배포 단위를 갖는 시스템”으로 간결하게 정의한다. 이 정의에는 내부 구조에 대한 어떠한 가치 판단도 포함되지 않는다. 즉 모놀리스라는 용어는 실행 파일이나 배포 아티팩트가 하나라는 물리적 사실만을 가리키며, 그 내부가 잘 정리되어 있는지 뒤엉켜 있는지는 별개의 문제라는 것이다.

모듈성의 세 가지 조건

Grzybek은 이어서 “모듈성”이 성립하기 위한 조건을 세 가지로 정리한다.

첫째, 모듈은 독립적이고 상호 교체 가능해야 한다. 완전한 독립은 불가능하지만, 의존성의 개수, 의존성의 강도(하나의 메서드만 가끔 호출하는지, 여러 메서드를 빈번히 호출하는지), 그리고 의존 대상 모듈의 변경 빈도라는 세 가지 척도로 독립성의 정도를 가늠할 수 있다. 만약 두 모듈이 지나치게 강하고 빈번하게 서로를 호출한다면, 이는 애초에 모듈 경계를 잘못 나눈 것이므로 차라리 하나의 모듈로 합치는 편이 낫다.

둘째, 모듈은 원하는 기능을 스스로 완결적으로 제공할 수 있어야 한다. 흔히 표현 계층, 서비스 계층, 데이터 접근 계층처럼 기술적 관심사를 기준으로 나눈 것도 넓은 의미의 모듈이라 부를 수 있지만, 이런 기술 중심 모듈은 하나의 비즈니스 기능을 추가하거나 변경할 때마다 여러 계층에 걸쳐 함께 수정되어야 한다는 한계가 있다. 반면 모듈러 모놀리스가 지향하는 모듈은 주문, 결제, 배송처럼 비즈니스 기능 단위로 나뉘며, 하나의 기능 변경이 원칙적으로 하나의 모듈 안에서 끝나도록 설계된다. 이런 방식은 “수직 슬라이스”라 불리며, 여러 수직 슬라이스를 하나의 모듈로 묶는 형태를 취한다.

셋째, 모듈은 명확한 인터페이스, 즉 계약을 가져야 한다. 이 계약은 동기 호출을 위한 퍼블릭 메서드나 REST 엔드포인트일 수도 있고, 비동기 통신을 위해 발행하는 이벤트일 수도 있다. 외부에 노출하는 모든 것이 곧 그 모듈의 공개 API가 되므로, 캡슐화는 모듈성과 분리될 수 없는 요소이다.

패키지 바이 컴포넌트

Simon Brown은 자신의 강연과 글에서 하나의 기능, 예컨대 “주문 조회” 기능을 구현할 때 코드를 조직하는 네 가지 방식을 비교한다.

패키지 바이 레이어는 웹 계층, 서비스 계층, 저장소 계층처럼 기술적 역할에 따라 코드를 수평으로 나누는 가장 전통적인 방식이다. 이 방식은 빠르게 구현할 수 있지만, 서로 다른 비즈니스 도메인이라도 웹·서비스·저장소라는 동일한 구조를 반복하게 되어 코드가 어떤 도메인을 다루는지 드러내지 못한다.

패키지 바이 피처는 기능이나 바운디드 컨텍스트를 기준으로 코드를 수직으로 묶는 방식이다. 이 경우 하나의 패키지 안에 관련 클래스가 모두 모이므로 어떤 도메인을 다루는지는 명확해지지만, 패키지 바깥에서 이 도메인의 정보에 접근하려면 반드시 하나의 진입점(예: 컨트롤러)을 거치도록 강제할 것인지 여부가 별도로 결정되어야 한다.

포트 앤 어댑터(헥사고날 아키텍처)는 프레임워크나 데이터베이스 같은 기술적 세부 사항을 “바깥”에, 도메인 로직을 “안쪽”에 두고 의존성이 항상 안쪽을 향하도록 강제하는 방식이다.

Brown이 제안하는 네 번째 방식인 패키지 바이 컴포넌트는 하나의 굵직한 컴포넌트에 관련된 책임, 즉 비즈니스 로직과 영속성 코드를 모두 하나의 패키지에 묶어 서비스 지향적인 관점에서 시스템을 바라보는 하이브리드 방식이다. 이렇게 하면 주문과 관련된 무언가가 필요할 때 “OrdersComponent” 하나만 알면 되고, 그 내부 구현(비즈니스 로직과 저장소가 어떻게 분리되어 있는지)은 컴포넌트 이용자가 알 필요가 없는 구현 세부 사항이 된다.

Brown이 강조하는 핵심은, 이 네 가지 방식이 개념적으로는 전혀 다르게 보이지만 모든 타입을 public으로 선언해 버리면 코드 상으로는 사실상 구분이 사라진다는 점이다. 패키지는 캡슐화 수단이 아니라 단순한 폴더 수준의 정리 수단으로 전락하고, 접근 제어자를 활용하지 않는 한 어떤 아키텍처 스타일을 표방하든 실질적으로는 동일한 결과가 된다. 그래서 그는 언어가 제공하는 접근 제어자(Java의 패키지 프라이빗, C#의 internal 등)를 최대한 활용해 컴파일러 수준에서 아키텍처 규칙을 강제할 것을 권장하며, 정적 분석 도구나 사후 검증에만 의존하는 방식은 차선책으로 본다.

패키지 바이 컴포넌트

OrdersComponent

패키지 바이 레이어

OrdersController

OrdersService

OrdersRepository

아키텍처 설계 원칙

경계 설정: 도메인 주도 설계와 바운디드 컨텍스트

실무에서 모듈 경계는 대개 도메인 주도 설계의 서브도메인 혹은 바운디드 컨텍스트를 기준으로 설정된다. Chris Richardson은 모듈러 모놀리스의 핵심 아이디어를 “컴포넌트를 조직하는 최상위 원칙이 도메인이어야 한다”는 말로 요약하면서, 웹·도메인·영속성 같은 기술 계층이 아니라 고객·주문 같은 도메인이 최상위 구조가 되고, 그 아래에 각 도메인별로 필요한 기술 계층이 존재하는 구조를 제안한다. 이렇게 도메인을 기준으로 구조를 잡으면 아키텍처 구조와 그 도메인을 담당하는 팀의 조직 구조가 자연스럽게 정렬된다는 이점이 있다.

모듈 간 통합 스타일

Kamil Grzybek은 Gregor Hohpe와 Bobby Woolf의 저서 “Enterprise Integration Patterns”의 분류를 모듈러 모놀리스 맥락에 적용해, 결합도·복잡도·데이터 적시성이라는 세 가지 기준으로 모듈 간 통합 방식을 비교한다.

파일 전송

한 모듈이 파일을 내보내고 다른 모듈이 이를 가져오는 방식이다. 결합도는 매우 낮지만 데이터 적시성이 대개 하루 단위 이상으로 느려서, 동일한 프로세스 안에서 실행되는 모놀리스 내부 모듈 간 통합에는 사실상 쓰이지 않는다.

공유 데이터베이스 데이터

여러 모듈이 같은 데이터베이스의 특정 데이터를 공유하는 방식이다. 구현이 가장 간단하고 데이터가 항상 즉시 일관되지만, 모듈 간 결합도가 매우 높아진다는 치명적인 단점이 있다. 한 모듈이 테이블 구조나 데이터를 변경하면 이를 참조하는 다른 모듈이 예고 없이 깨질 수 있고, 결과적으로 모든 데이터베이스 변경에 관련 모듈 전체의 합의가 필요해져 데이터베이스 자체가 변경의 병목이 된다.

다이렉트 콜

한 모듈이 다른 모듈의 메서드를 캡슐화된 인터페이스를 통해 직접 호출하는 방식이다. 호출하는 쪽은 상대 모듈의 내부 상태가 아니라 공개된 메서드만 알면 되므로 공유 데이터베이스 방식보다 결합도가 낮다. 다만 호출은 동기적이므로 호출자와 피호출자가 같은 시점에 함께 사용 가능해야 하는 시간적 결합이 발생하고, 호출하는 모듈은 상대 모듈에 대한 직접적인 의존성을 가지게 된다. 분산 시스템에서 이 방식에 해당하는 것이 원격 프로시저 호출이며, 마이크로서비스에서 이를 남용하면 앞서 언급한 분산 모놀리스 안티패턴으로 이어질 수 있다.

메시징

이벤트 기반으로 비동기 통신하는 방식이다. 모듈 간 직접적인 의존성이 사라지고 성능·안정성·확장성 면에서 이점이 있지만, 시스템 전체가 결과적 일관성 상태에 놓이게 되고 이벤트 버스 같은 인프라를 갖추어야 하므로 구현 복잡도가 올라간다. 여기서 오가는 이벤트는 하나의 바운디드 컨텍스트 안에 갇혀 있어야 하는 도메인 이벤트와는 구별되는 “통합 이벤트”로, 계약의 안정성을 위해 꼭 필요한 정보만 담아 최소한으로 유지하는 것이 바람직하다.

Grzybek은 이 네 방식 중 정답은 없으며, 시스템의 본질적 복잡도가 낮다면 단순한 공유 데이터베이스 방식도 무방하지만, 복잡도가 높아질수록 다이렉트 콜과 메시징을 상황에 맞게 혼용하는 편이 현실적이라고 결론짓는다.

데이터 소유권과 저장 전략

모듈 간 데이터 결합을 낮추는 핵심은 각 모듈이 자신의 데이터를 배타적으로 소유하는 것이다. Shopify는 컴포넌트마다 고유한 인터페이스를 정의하고 관련 데이터를 배타적으로 소유하도록 하는 방향으로 전환했다. 물리적으로 하나의 데이터베이스를 쓰더라도 모듈별로 스키마를 분리해 두면, 다른 모듈이 테이블을 직접 조인하거나 참조 제약을 거는 일을 막을 수 있다. 이 원칙은 자기완결형 시스템(SCS) 방법론에서도 강조되는데, 같은 데이터베이스를 여러 시스템이 함께 쓰더라도 스키마를 공유하지 않아야 한다는 규칙이 명시되어 있다. 스키마 공유는 그 스키마를 참조하는 모든 구성 요소 간의 조율을 필요로 하는 강한 결합을 만들기 때문이다.

경계 강제 기법

경계를 설계 문서로만 남겨두면 시간이 지나면서 예외가 쌓여 흐지부지되기 쉽다. 이를 막기 위한 강제 기법은 대체로 세 단계로 나뉜다.

첫째는 언어 자체의 접근 제어 기능을 활용하는 방법으로, Simon Brown이 가장 선호하는 방식이다. Java의 패키지 프라이빗 접근 제어자, C#의 internal 키워드, TypeScript/JavaScript의 모듈 export 여부처럼, 컴파일 단계에서 원천적으로 위반이 불가능하도록 만드는 접근이다.

둘째는 정적 분석 및 아키텍처 테스트 도구를 이용하는 방법이다. Java 진영의 ArchUnit이 대표적이며, 이를 기반으로 만들어진 Spring Modulith는 모듈 간 순환 의존을 감지하고, 명시적으로 허용하지 않은 모듈 간 참조나 모듈 내부로 정의된 타입에 대한 외부 접근을 자동으로 검출해 테스트 실패로 보고한다.

셋째는 런타임에서 실제 호출 그래프를 관측해 위반을 찾아내는 방법이다. Shopify는 “Wedge”라는 내부 도구를 만들어, Ruby 트레이스포인트를 이용해 CI 과정에서 전체 호출 그래프를 수집하고, 컴포넌트 경계를 넘는 호출·연관·상속 관계 중 허용되지 않는 것을 찾아내 컴포넌트별 위반 점수를 계산하도록 했다.

모놀리스, 모듈러 모놀리스, 마이크로서비스 비교

구분 전통적 모놀리스 모듈러 모놀리스 마이크로서비스
배포 단위 하나 하나 서비스 수만큼 다수
내부 구조 계층 중심, 경계 불명확 도메인 중심 모듈, 명확한 경계 서비스 자체가 경계
모듈(서비스) 간 통신 인메모리 직접 호출, 구분 없음 인메모리 다이렉트 콜 또는 인프로세스 메시징 네트워크 기반 RPC 또는 메시지 브로커
데이터 저장 대개 완전히 공유 스키마 분리 또는 완전 분리 권장 서비스별 독립 데이터베이스
장애 격리 낮음(단일 프로세스) 낮음(단일 프로세스, 논리적으로만 격리) 높음(프로세스 단위 격리)
개별 확장 불가(전체 단위로만 확장) 불가(전체 단위로만 확장) 가능(서비스별 독립 확장)
기술 다양성 사실상 단일 스택 사실상 단일 스택 서비스별 폴리글랏 가능
배포 파이프라인 하나 하나 서비스 수만큼 다수
팀 자율성 낮음 모듈 소유권을 통해 어느 정도 확보 가능 높음(서비스 단위 독립 배포)
운영 복잡도 낮음 낮음 높음(오케스트레이션, 관측 가능성 필요)
마이크로서비스로의 전환 용이성 낮음(경계 재파악 필요) 상대적으로 높음(경계가 이미 존재) 해당 없음

관련 및 인접 아키텍처

장엄한 모놀리스

Ruby on Rails의 창시자이자 Basecamp의 공동 창업자인 David Heinemeier Hansson(DHH)은 2015~2016년 무렵 “장엄한 모놀리스(Majestic Monolith)“라는 표현으로, 작은 팀이 의도적으로 모놀리스를 선택하고 이를 자랑스럽게 여기는 태도를 제시했다. 그의 논지는, 마이크로서비스 지향 아키텍처가 요구하는 인력과 운영 부담은 웹·모바일·데스크톱·이메일을 아우르는 광범위한 기능을 극소수의 인원으로 유지해야 하는 Basecamp 같은 조직에는 적합하지 않다는 것이다. 장엄한 모놀리스는 불필요한 개념 모델과 추상화를 최대한 걷어내고, 시스템을 여러 곳에 분산시키지 않는다는 원칙을 명시적으로 표방한다는 점에서, 뒤엉킨 채로 방치된 모놀리스와는 구분된다.

자기완결형 시스템

자기완결형 시스템(Self-contained Systems, SCS)은 독일 컨설팅 회사 InnoQ가 제안한 아키텍처로, 큰 시스템을 여러 개의 독립적인 시스템으로 나누되 각 시스템이 자신만의 UI, 비즈니스 로직, 데이터 저장소를 모두 갖추고 하나의 팀이 전담하도록 하는 방식이다. 각 SCS는 다른 SCS와 되도록 비동기적으로만 통신하며, 데이터베이스를 물리적으로 공유하더라도 데이터 자체는 공유하지 않는다. 모듈러 모놀리스와 지향점은 비슷하지만, SCS의 각 단위는 그 자체로 독립 배포되는 애플리케이션이라는 점에서 마이크로서비스에 가까운 굵은 단위의 분산 아키텍처에 해당하며, 하나의 배포 단위 안에 모듈을 담는 모듈러 모놀리스와는 이 지점에서 구분된다.

수직 슬라이스 아키텍처

수직 슬라이스 아키텍처는 Jimmy Bogard가 제안한 코드 조직 원칙으로, 계층이 아니라 하나의 요청이나 유스케이스를 기준으로 코드를 묶는 방식이다. 이는 앞서 설명한 패키지 바이 피처, 그리고 모듈 내부를 완결적인 기능 단위로 구성해야 한다는 모듈성의 두 번째 조건과 밀접하게 맞닿아 있으며, 모듈러 모놀리스에서 하나의 모듈 내부를 조직하는 방식으로도 자주 함께 언급된다.

생태계와 도구 지원

자바 생태계: 스프링 모듈리스와 아크유닛

스프링 프레임워크 팀은 2022년 실험적 프로젝트로 Spring Modulith(과거 명칭 Moduliths)를 발표했다. 이 프로젝트는 ArchUnit을 내부적으로 활용해 애플리케이션 모듈 구조를 분석하고, ApplicationModules.verify() 호출 하나로 모듈 간 순환 의존, 허용되지 않은 모듈 간 참조, 내부 전용 타입에 대한 외부 접근을 검증해 위반 시 테스트를 실패시킨다. 또한 이벤트 기반의 모듈 간 통신을 지원하며, 이벤트 발행 내역을 데이터베이스에 기록해 재시작 이후에도 최소 한 번 전달을 보장하는 이벤트 발행 레지스트리를 제공하고, 모듈 구조를 PlantUML 다이어그램으로 자동 문서화하는 기능도 갖추고 있다.

닷넷 생태계

.NET 진영에서는 ABP.io 같은 프레임워크가 모듈러 모놀리스 구축을 위한 도구와 가이드를 제공하며, Steve Smith(Ardalis)를 비롯한 여러 아키텍트들이 Clean Architecture 계열 템플릿과 Microsoft의 eShopOnWeb 참조 애플리케이션을 통해 모듈러 모놀리스 구현 사례를 공유하고 있다. Kamil Grzybek이 GitHub에 공개한 “Modular Monolith with DDD” 예제 저장소 역시 .NET 진영에서 널리 참조되는 구현 사례이다.

Node.js/TypeScript 생태계: 네스트제이에스

Node.js/TypeScript 환경에서는 NestJS가 모듈 시스템을 프레임워크 차원에서 기본으로 제공한다. NestJS의 모듈은 @Module() 데코레이터가 붙은 클래스로 표현되며, 해당 모듈이 소유하는 컨트롤러와 프로바이더(서비스)를 선언하고, 다른 모듈에서 가져다 쓸 수 있도록 공개할 프로바이더만 exports 배열에 명시한다. exports에 포함되지 않은 프로바이더는 모듈 바깥에서 주입받을 수 없으므로, 이 구조 자체가 앞서 설명한 패키지 바이 컴포넌트의 캡슐화 원칙을 의존성 주입 컨테이너 수준에서 구현한 형태라고 볼 수 있다.

// orders/orders.module.ts
import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';

@Module({
  controllers: [OrdersController],
  providers: [OrdersService],
  exports: [OrdersService], // 다른 모듈에 공개할 대상만 명시적으로 노출
})
export class OrdersModule {}
// customers/customers.module.ts
import { Module } from '@nestjs/common';
import { OrdersModule } from '../orders/orders.module';
import { CustomersService } from './customers.service';

@Module({
  imports: [OrdersModule], // OrdersModule이 exports 한 대상만 접근 가능
  providers: [CustomersService],
})
export class CustomersModule {}

이런 방식으로 도메인마다 OrdersModule, CustomersModule, ShippingModule처럼 기능 모듈을 나누고, 모듈 간에는 명시적으로 공개된 프로바이더를 통해서만 상호작용하도록 구성하면, 하나의 Node.js 프로세스로 배포되는 모놀리스이면서도 도메인 경계가 코드 구조에 그대로 드러나는 모듈러 모놀리스를 구현할 수 있다.

구글 서비스 위버

구글은 2023년 3월 Service Weaver라는 오픈소스 프레임워크를 공개했다. Go 언어 기반의 이 프레임워크는 개발자가 애플리케이션을 하나의 모듈화된 바이너리로 작성하고 컴포넌트 간에는 일반적인 메서드 호출만 사용하도록 하되, 배포 시점에는 각 컴포넌트를 서로 다른 마이크로서비스로 분리해 배치할 수 있게 해주었다. 컴포넌트가 같은 프로세스에 배치되면 메서드 호출 그대로 실행되고, 서로 다른 프로세스로 분리되면 프레임워크가 이를 원격 프로시저 호출로 자동 변환해 개발자가 이 차이를 신경 쓰지 않도록 하는 것이 핵심 아이디어였다.

다만 구글은 2024년 12월 5일부로 Service Weaver의 활발한 개발을 중단한다고 공식 발표했다. 공개된 사유는, 기존 애플리케이션을 Service Weaver 방식으로 옮기려면 상당 부분을 다시 작성해야 했기 때문에 실제 채택 사례가 충분히 늘지 않았다는 것이었다. 이 사례는 논리적 모듈성과 물리적 배포 단위를 자동으로 분리해 주려는 야심 찬 시도가 실제 도입 장벽 앞에서 어떻게 좌초할 수 있는지를 보여주는 사례로 남아 있다.

핀란드 오울루 대학교 연구진 Ruoyu Su와 Xiaozhou Li가 2024년 발표한 그레이 문헌 리뷰 논문은 Service Weaver와 Spring Modulith를 포함한 프레임워크들, 그리고 Shopify·Appsmith·Gusto·Playtech 등 모듈러 모놀리스를 채택한 기업 사례를 조사해, 모듈러 모놀리스가 마이크로서비스의 대안이자 마이크로서비스로 가는 전 단계로서 모두 기능할 수 있다고 결론지었다.

산업 사례

쇼피파이: 컴포넌트화

Shopify는 십수 년에 걸쳐 천 명이 넘는 개발자가 참여해 온 대규모 Ruby on Rails 코드베이스로, 2016년 무렵 모놀리스의 한계가 뚜렷해졌다. 개발자 설문을 통해 문제를 데이터 기반으로 파악한 뒤, 2017년 초 “Break-Core-Up-Into-Multiple-Pieces”라는 이름으로 시작된 프로젝트는 이후 “컴포넌트화(Componentization)“로 발전했다.

가장 먼저 진행한 작업은 코드 조직 방식의 전환이었다. 기존에는 모델·뷰·컨트롤러 같은 기술적 개념으로 코드를 나누던 전형적인 Rails 구조였는데, 이를 주문·배송·재고·결제 같은 실제 비즈니스 개념 중심으로 재편했다. 약 6,000개에 달하는 Ruby 클래스를 어느 컴포넌트에 속하는지 하나하나 분류해, 코드 변경 없이 파일 위치만 옮기는 대규모 작업을 단 한 번의 풀 리퀘스트로 처리했다.

다음 단계는 의존성 격리였다. 각 컴포넌트가 공개 API를 통해서만 다른 컴포넌트와 상호작용하고, 자신이 다루는 데이터를 배타적으로 소유하도록 만드는 작업이다. 이를 지원하기 위해 Shopify는 “Wedge”라는 도구를 자체 개발해 컴포넌트 경계를 넘나드는 호출과 위반 사례를 지속적으로 추적했다. 이런 격리 작업 덕분에, 과거라면 손대기조차 어려�웠을 낡은 세금 계산 엔진을 새로운 시스템으로 교체하는 작업을 비교적 수월하게 해낼 수 있었다고 알려져 있다.

베이스캠프: 장엄한 모놀리스

Basecamp는 약 12명 규모의 개발 인원으로 웹, iOS, Android, 데스크톱, 이메일 연동까지 아우르는 제품을 유지해 왔다. DHH는 이 정도로 작은 조직이 마이크로서비스 지향 아키텍처가 요구하는 노동 집약적인 운영을 감당하는 것은 현실적이지 않다고 보고, 하나의 Rails 애플리케이션 안에 거의 모든 기능을 의도적으로 통합하는 방식을 택했다고 설명한다. 이는 단순히 분리를 게을리한 결과가 아니라, 팀 규모와 예산이라는 제약 조건 위에서 내린 명시적인 선택이라는 점이 이 사례의 핵심이다.

아마존 프라임 비디오: 재통합 사례

아마존 Prime Video의 영상 품질 분석(VQA) 팀은 스트림 품질 문제를 실시간으로 감지하는 모니터링 도구를 AWS Step Functions와 Lambda를 이용한 분산 서버리스 구조로 구축했다. 그러나 수천 개의 동시 스트림을 감당해야 하는 규모로 커지면서, 스트림 1초마다 여러 번의 상태 전이가 발생해 계정 한도에 빠르게 도달하는 문제와, 컴포넌트 사이에 프레임 데이터를 주고받기 위해 S3를 경유하는 비용이 과도하게 커지는 문제에 부딪혔다.

이에 따라 팀은 모든 컴포넌트를 하나의 프로세스로 통합해 데이터 전달을 메모리 안에서 처리하도록 재설계했다. 이전에는 여러 검출기(detector)를 독립된 서비스로 수평 확장했다면, 재설계 이후에는 하나의 인스턴스 안에서 수직으로 확장하고, 필요한 처리량을 맞추기 위해 서비스 자체를 여러 벌 복제하는 방식으로 전환했다. 그 결과 인프라 비용을 90% 이상 절감했고 확장 가능한 스트림 수도 늘어났다고 발표되었다.

이 사례는 마이크로서비스와 서버리스가 항상 정답은 아니며 사용 사례별로 판단해야 한다는 교훈으로 널리 인용되지만, 몇 가지 주의할 점도 함께 언급된다. 이 발표는 Prime Video 전체가 모놀리스로 회귀했다는 의미가 아니라, VQA 팀이 운영하던 하나의 모니터링 도구에 한정된 재설계였다는 점, 그리고 처음에 서버리스로 빠르게 구축한 선택 자체는 서비스 초기 단계에서는 합리적이었다는 점이 함께 지적된다.

비판과 논쟁

용어에 대한 반론: “모듈러 모놀리스는 존재하지 않는다”

“Microservices Patterns”의 저자이자 마이크로서비스 분야의 대표적 논자인 Chris Richardson은 모듈러 모놀리스라는 용어 자체에 문제를 제기한다. 그의 논지는 다음과 같다. 모놀리식 아키텍처란 시스템을 단일 컴포넌트, 즉 시스템 아키텍처 관점에서 하나의 블랙박스로 구조화하는 스타일을 가리키는 용어이다. 그런데 “모듈러 모놀리스”라는 표현으로 실제로 설명하려는 것은 그 하나뿐인 컴포넌트의 내부 구조이지, 시스템 전체의 아키텍처 스타일이 아니다. 마이크로서비스 아키텍처에 속한 개별 서비스 역시 여러 팀이 소유하는 여러 서브도메인을 담고 있다면 똑같이 내부적으로 모듈화될 필요가 있으므로, 모듈성이라는 속성은 모놀리스만의 전유물이 아니라는 것이다.

더 나아가 그는 전통적인 모놀리스라도 계층과 모듈로 이루어진 잘 정의된 구조를 가질 수 있으며, 진짜 문제는 그 계층과 모듈이 시스템의 서브도메인이나 이를 담당하는 팀과 정렬되어 있지 않다는 데 있다고 지적한다. 즉 모듈화의 유무가 아니라 정렬(alignment)의 유무가 핵심이라는 것이다. 이런 이유로 그는 “모듈러 모놀리스” 대신 “모듈러 컴포넌트” 혹은 더 정확하게는 “도메인 지향 컴포넌트 아키텍처”라는 이름이 더 적절하다고 제안한다.

실무상의 위험 요소

용어 논쟁과 별개로, 모듈러 모놀리스를 실제로 운영할 때 자주 지적되는 위험 요소들이 있다.

첫째, 경계는 시간이 지나면서 침식되기 쉽다. 강제 메커니즘 없이 팀의 규율에만 의존하면, 마감 압박 속에서 예외적인 지름길이 하나둘 생기다가 결국 경계가 유명무실해진다. 그래서 앞서 설명한 컴파일러·정적 분석·런타임 검증 같은 강제 기법이 사실상 필수적으로 여겨진다.

둘째, 장애 격리 수준이 마이크로서비스보다 낮다. 모든 모듈이 하나의 프로세스 안에서 실행되므로, 한 모듈의 심각한 버그나 자원 고갈이 전체 애플리케이션을 함께 무너뜨릴 수 있다.

셋째, 개별 모듈만 따로 확장할 수 없다. 특정 모듈에 트래픽이 집중되더라도 전체 애플리케이션 인스턴스를 통째로 늘리는 방식으로만 대응할 수 있어, 자원 사용의 효율이 마이크로서비스만큼 세밀하지 않다.

넷째, 기술 스택이 사실상 하나로 고정된다. 모듈마다 다른 언어나 런타임을 선택할 수 있는 마이크로서비스나 자기완결형 시스템과 달리, 모듈러 모놀리스의 모든 모듈은 대개 동일한 언어와 프레임워크 위에서 동작해야 한다.

다섯째, 배포가 한 덩어리로 결합되어 있다. 아무리 작은 변경이라도 전체 애플리케이션을 함께 재배포해야 하므로, 회귀 테스트 범위와 배포 리스크가 항상 시스템 전체를 기준으로 산정된다.

여섯째, 조직이 커질수록 팀과 모듈 소유권을 지속적으로 재정렬해야 하는 부담이 있다. 이는 Conway의 법칙이 지적하는 조직 구조와 시스템 구조 사이의 긴장과도 맞닿아 있으며, 모듈별 코드 소유권을 명시적으로 관리하는 절차가 별도로 필요하다.

도입 판단 기준

여러 전문가들의 견해를 종합하면, 모듈러 모놀리스를 채택할지 판단할 때 고려할 만한 기준은 대체로 다음과 같이 수렴한다. 도메인 경계에 대한 이해가 아직 무르익지 않은 프로젝트 초기 단계라면, Fowler가 제안한 모놀리스 퍼스트 전략에 따라 우선 모듈러 모놀리스로 시작해 경계가 안정되기를 기다리는 편이 안전하다. 반대로 팀별로 완전히 독립적인 배포 주기, 서로 다른 기술 스택, 극단적으로 상이한 확장 요구사항이 이미 명확하다면 마이크로서비스나 자기완결형 시스템처럼 물리적으로 분리된 아키텍처가 더 적합할 수 있다. 아울러 조직이 오케스트레이션, 관측 가능성, 온콜 운영 같은 분산 시스템 운영 역량을 충분히 갖추고 있는지도 중요한 변수이다. 이런 역량이 갖추어지지 않은 상태에서 마이크로서비스로 전환하면 앞서 설명한 분산 모놀리스 안티패턴에 빠질 위험이 커진다. Amazon Prime Video의 사례가 보여주듯, 이미 마이크로서비스로 세분화된 시스템이라도 특정 구성 요소가 그 세분화로 인한 이득보다 비용이 더 크다고 판단되면 해당 부분만 다시 통합하는 것도 유효한 선택지이며, 아키텍처 결정은 시스템 전체가 아니라 구성 요소 단위로도 개별적으로 내려질 수 있다.

경계 없는 성장

의도적 모듈화

팀 확장 및 독립 배포 필요

분산 비용이 이득을 초과할 때

전통적 모놀리스

빅 볼 오브 머드

모듈러 모놀리스

마이크로서비스


참고 자료

  • Brown, Simon, “Modular monolith”, simonbrown.je
  • Westeinde, Kirsten (2019), “Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity”, Shopify Engineering Blog
  • Grzybek, Kamil (2019), “Modular Monolith: A Primer”, kamilgrzybek.com
  • Grzybek, Kamil (2020), “Modular Monolith: Integration Styles”, kamilgrzybek.com
  • Fowler, Martin (2015), “MonolithFirst”, martinfowler.com
  • Hansson, David Heinemeier (2016), “The Majestic Monolith”, Signal v. Noise
  • Richardson, Chris (2024), “Architectural patterns for modular monoliths that enable fast flow”, microservices.io
  • Richardson, Chris (2024), “How modular can your monolith go? Part 7 - no such thing as a modular monolith?”, microservices.io
  • Spring 팀 (2022), “Introducing Spring Modulith”, spring.io
  • Petrovic, Srdjan; Sawhney, Garv (2023), “Introducing Service Weaver: A Framework for Writing Distributed Applications”, Google Open Source Blog
  • Service Weaver 팀 (2024), Service Weaver 유지보수 모드 전환 공지, serviceweaver.dev / GitHub
  • Prime Video 팀 (2023), “Scaling up the Prime Video audio/video monitoring service and reducing costs by 90%”, Prime Video Tech Blog
  • Su, Ruoyu; Li, Xiaozhou (2024), “Modular Monolith: Is This the Trend in Software Architecture?”, arXiv:2401.11867
  • Self-contained Systems, scs-architecture.org
  • Wikipedia, “Self-contained system (software)”
  • NestJS, “Modules”, docs.nestjs.com / nestjs.com
  • Smith, Steve (2024), “Introducing Modular Monoliths: The Goldilocks Architecture”, ardalis.com
  • DeviQ, “Modular Monolith”, deviq.com
  • Newman, Sam (2019), Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith, O’Reilly Media