Software Architecture/Architectural Styles
Architectural Styles - 01. 모놀리스
모놀리스가 다시 출발점이 된 이유
2015년 무렵 한 스타트업이 하루 처리 건수 10만을 넘기며 모놀리스를 마이크로서비스로 쪼갰다. "모놀리스는 스케일이 안 된다"는 조언을 따른 선택이었다. 6개월 뒤 그 팀이 직면한 것은 트래픽 처리의 성공이 아니라 배포 파이프라인 8배, 분산 트랜잭션 버그, 그리고 "어느 서비스에서 로그가 끊겼는지"를 찾느라 보내는 새벽이었다. 모놀리스 시절 한 번에 끝나던 배포가 여덟 개 서비스의 버전 정합성을 맞추는 일로 바뀌었다. 그들이 진짜 필요했던 건 "모놀리스를 버리는 것"이 아니라 "모놀리스 안의 모듈 경계를 명확히 하는 것"이었다.
흥미로운 건 마이크로서비스를 대중화한 책 의 저자 Sam Newman이 2판(2021)에서 오히려 "모놀리스 먼저(monolith first)"를 강조한다는 점이다. 마이크로서비스의 대가가 모놀리스를 추천하는 모순. 이 글이 파고드는 질문은 그 모순의 배경이다 — 모놀리스는 왜 실패의 반대말이 아니라 출발점으로 재평가받고 있나.
비유로 감 잡기 — 한 주방 식당
한 주방에서 파스타·피자·샐러드·디저트를 전부 만드는 식당을 상상하자. 재료 찬장은 하나, 배관은 하나, 환기는 하나. 파스타 코너 셰프가 불판을 독점하면 피자 코너는 기다려야 한다 (스레드 경합). 주방 수도가 망가지면 모든 요리가 중단된다 (장애 전파). 하지만 이 식당엔 장점이 있다 — 셰프 한 명이 아침에 와서 "오늘 메뉴 바뀌었어"라고 외치면 그날로 전체가 새 메뉴로 바뀐다 (단일 배포). 재료 찬장을 들여다보면 무엇이 있는지 한눈에 안다 (단일 상태).
마이크로서비스는 이 식당을 파스타 매장·피자 매장·샐러드 매장으로 분리하는 일이다. 각 매장은 자기 배관·자기 찬장·자기 셰프를 갖는다. 한 매장이 문을 닫아도 다른 매장은 영업한다 (장애 격리). 하지만 각 매장엔 따로 전기·수도·임대료가 나가고, "고객이 파스타를 시키면 빵이 무료인가"를 매장 간에 무전으로 확인해야 한다 (네트워크 통신).
모놀리스는 이 비유에서 "한 주방"이다. 비유가 가르치는 핵심은 — 한 주방이 나쁜 식당이 아니라는 점. 문제는 한 주방인 게 아니라 주방 안에 구획이 없고, 셰프들이 서로의 찬장을 뒤지고, 불판 위에 뭐가 올라갔는지 아무도 모르는 카오스일 때 생긴다. 그 카오스의 학명은 큰 진흙 덩어리(Big Ball of Mud)다 (Foote, Yoder 1999). 모놀리스의 진짜 문제는 "모놀리스임"이 아니라 "경계 없는 모놀리스"임이다.
비유의 한계 — 식당 비유는 '물리적 분리'를 강조하지만, 소프트웨어 모놀리스와 마이크로서비스의 결정적 차이는 프로세스 경계(별도 주소 공간)와 배포 독립성이다. 주방은 물리적 공간이지만, 프로세스는 주소 공간이다. 비유는 여기서 무너진다.
모놀리스는 스타일이다, 실패가 아니다
모놀리스(monolith)를 다루는 글은 종종 "낡은 구조"라는 전제로 시작한다. 그 전제부터 다듬어야 한다. 모놀리스는 아키텍처 스타일(style)이다. Mark Richards와 Neal Ford가 (2020)에서 잡는 정의에 따르면, 모놀리스 스타일은 "단일 배포 단위(single deployment unit) 내에 모든 기능을 담는 구조"다. 구체적으로 세 특성으로 드러난다.
- 단일 프로세스 — 모든 코드가 하나의 실행 단위(주소 공간)에서 돈다. 함수 호출이 곧 모듈 간 통신이다.
- 공유 상태 — 일반적으로 단일 데이터베이스를 쓴다. 여러 모듈이 같은 테이블에 접근한다.
- 단일 배포 파이프라인 — 변경사항이 있으면 전체를 다시 빌드하고 배포한다.
이 세 특성은 단점이 아니라 특성이다. 단일 프로세스는 함수 호출의 속도(나노초)를 누린다. 단일 DB는 트랜잭션 하나로 여러 테이블을 원자적으로 바꾼다(ACID). 단일 배포는 "이 버전에서 결함이 났다"면 그 버전 하나만 보면 된다. 이런 특성이 주는 단순함이 모놀리스의 진짜 강점이다. 한 팀이 하루 만에 프로덕션에 올릴 수 있는 건, 배포 파이프라인이 하나이기 때문이다.
모놀리스가 실패와 동의어로 쓰이게 된 건 이 특성들이 특정 규모에서 병목으로 돌변하기 때문이다. 그 병목의 경계를 명확히 하는 게 다음 몇 절의 일이다.
큰 진흙 덩어리 — 모놀리스가 실패로 불리는 진짜 이유
Foote와 Yoder가 1999년에 이름 붙인 큰 진흙 덩어리(Big Ball of Mud)는 "모든 모듈이 모든 모듈을 직접 부르는" 카오스 구조다. 놀랍게도 그들은 이 구조가 "상당히 성공적인 시스템에서 자주 나타난다"고 관찰했다. 초기엔 빠른 변경을 허용하기 때문이다 — 어디든 직접 손대면 되니 개발 속도가 빠르다. 모놀리스의 실패담 대부분은 사실 모놀리스가 아니라 이 진흙 덩어리의 이야기다.
진흙 덩어리의 증상은 모놀리스 여부와 무관하다. 같은 프로세스 안에서도, 다른 프로세스에 나눠도, 서로의 내부(private)를 직접 건드리면 진흙이다. 마이크로서비스로 쪼갰어도 각 서비스가 서로의 DB를 직접 읽거나(shared database 안티패턴), 동기 호출 체인으로 엮이면(분산 모놀리스) 결과는 진흙이다. "모놀리스라서 망했다"는 진단은 대개 표면이고, 진짜 원인은 "경계(boundary)가 없었다"는 데 있다.
그래서 Newman이 2판에서 강조하는 출발점은 "모놀리스에서 시작하되, 경계를 코드 안에 새겨라"다.
모듈형 모놀리스 — 경계를 프로세스 밖이 아니라 코드 안에
모듈형 모놀리스(modular monolith)는 모놀리스의 단일 프로세스·단일 배포 장점을 유지하면서, 코드를 명확한 모듈 경계로 자르는 구조다. Newman이 2판에서 가장 강조하는 출발점이며, 핵심은 — 모듈은 단순한 '폴더 분류'가 아니라 강제된(enforced) 경계여야 한다는 점이다.
flowchart TD
subgraph MM["모듈형 모놀리스 (단일 프로세스)"]
ORD["orders 모듈"]
PAY["payments 모듈"]
SHP["shipping 모듈"]
ORD -.->|"public API 호출만"| PAY
ORD -.->|"public API 호출만"| SHP
DBO[(orders 스키마)]
DBP[(payments 스키마)]
DBS[(shipping 스키마)]
ORD --> DBO
PAY --> DBP
SHP --> DBS
end
ORD x--x|"내부 클래스 직접 접근 금지"| PAY
PAY x--x|"내부 클래스 직접 접근 금지"| SHP
강제된 경계란 무엇인가. orders 모듈이 payments 모듈의 PaymentInternalHelper 클래스를 직접 import하지 못하게 막는 것이다. 이 강제는 언어 수준(Java 모듈 시스템, Kotlin internal, Rust crate), 프레임워크(ArchUnit 같은 아키텍처 테스트), 또는 코드 리뷰 규칙으로 달성한다. 단순히 "패키지를 나눴다"는 것만으로는 부족하다 — 개발자가 바쁜 오후에 한 줄 import로 경계를 뚫을 수 있으므로.
모듈형 모놀리스의 진짜 가치는 "지금 잘 돌아가게"가 아니라 "나중에 쪼갤 수 있게"에 있다. Newman은 이 속성을 분해 가능한 모놀리스(decomposable monolith)로 부른다. 코드 안의 경계가 명확하면, 어느 모듈이 트래픽 폭증이나 장애 빈도의 주범인지 드러났을 때 그 모듈만 별도 프로세스로 빼낼 수 있다. 마이크로서비스로의 전환 비용이 "전체 재작성"이 아니라 "한 모듈 추출"이 되는 것이다.
모놀리스 먼저 — 마이크로서비스 대가가 모놀리스를 권하는 이유
Martin Fowler는 2015년 블로그 글 "MonolithFirst"에서 새 시스템을 마이크로서비스로 시작하지 말 것을 권했다. Newman의 2판(2021)은 이 입장을 더 강화한다. 그 논리는 단순하다.
마이크로서비스의 핵심 가치인 '서비스 경계'는 도메인을 깊이 이해한 뒤에야 그어진다. 프로젝트 시작 시점엔 어느 바운디드 컨텍스트(bounded context)가 어디서 끝나는지 아무도 모른다. 이때 경계를 강제(별도 프로세스)로 그어버리면, 두 가지 실패가 겹친다. 첫째, 잘못 그은 경계를 옮기는 비용이 어마어마하다 (데이터 이전, API 재설계, 클라이언트 마이그레이션). 둘째, 분산 시스템의 복잡성(네트워크 장애, 최종 일관성, 분산 트랜잭션)이 도메인 이해보다 먼저 쏟아진다.
모놀리스 먼저의 역설은 — 모놀리스 안에서 모듈 경계를 제대로 그을 수 있어야 마이크로서비스로 넘어갈 자격이 있다는 점이다. 모놀리스 단계에서 "경계 짓기"를 못 하는 팀이 마이크로서비스로 가면 경계가 더 잘 그어지는 게 아니다. 프로세스 경계라는 강제가 있어도, 그들은 여전히 DB를 공유하고 동기 호출로 엮어 분산 모놀리스를 만든다. Newman이 2판에서 가장 경계하는 안티패턴이 이것이다 — 쪼갰지만 여전히 결합된 구조. 모놀리스의 단점(배포 의존성)과 마이크로서비스의 단점(분산 복잡성)만 겹친 결과.
Conway의 짝 — 모놀리스가 적합한지는 팀 구조와 뗄 수 없다. Matthew Skelton과 Manuel Pais가 (2019)에서 정리한 바에 따르면, 한 팀(또는 stream-aligned 팀 소수)이 다루는 시스템에 모놀리스가 자연스럽다. 조직이 여러 팀으로 쪼개져 있고 각 팀이 독립 배포를 원할 때 비로소 마이크로서비스의 가치가 나타난다. Conway의 법칙 — "시스템 구조는 조직의 의사소통 구조를 따른다" — 을 거스르는 아키텍처는 결국 조직을 아키텍처에 맞추거나, 아키텍처가 조직에 깨진다.
모놀리스와 마이크로서비스의 트레이드오프 매트릭스
모놀리스가 "좋은지"는 질문이 성립하지 않는다. Richards와 Ford가 제1법칙으로 놓은 "소프트웨어 아키텍처의 모든 것은 트레이드오프다"가 여기서도 참이기 때문이다. 물어야 할 질문은 "주어진 품질 속성 요구(확장성·가용성·독립 배포·팀 규모)에서 어느 쪽이 맞나"다.
| 품질 속성 | 모놀리스 | 마이크로서비스 |
|---|---|---|
| 단순함(초기 진입) | 높음 — 단일 프로세스, 단일 배포 | 낮음 — 분산 시스템 복잡성 즉시 부담 |
| 확장(부하 분산) | 전체가 함께 커짐 | 서비스별 독립 확장 가능 |
| 장애 격리 | 한 모듈 장애가 전체 중단 유발 가능 | 서비스별 격리 가능(단, 동기 체인이면 전파) |
| 배포 속도 | 전체 재배포 (느리지만 단순) | 서비스별 독립 배포 (잘 짜였을 때만 빠름) |
| 데이터 일관성 | 단일 트랜잭션(ACID) 자연스러움 | 분산 트랜잭션 어려움(Saga·최종 일관성) |
| 관측성(observability) | 단일 로그·메트릭 — 추적 단순 | 분산 추적(trace) 인프라 필수 |
| 팀 독립성 | 한 팀이 전체 담당 시 자연스러움 | 다수 팀이 독립 작업 시 효과 |
| 기술 다양성(polyglot) | 단일 스택으로 수렴 | 서비스별 언어·프레임워크 가능 |
이 표의 핵심은 "어느 쪽이 이기는가"가 아니라 "현재 팀 규모·트래픽·품질 요구에서 어느 속성이 결정적인가"를 묻는 것이다. 하루 1만 건·한 팀 다섯 명이면 모놀리스가 압도적으로 합리적이다. 하루 1억 건·다섯 팀이면 모듈형 모놀리스에서 일부 모듈을 서비스로 추출하는 단계로 넘어간다.
모놀리스가 적합한 조건 — 언제 맞나
모놀리스가 적합한 조건을 하나로 요약하면 "단일 배포 단위가 병목이 아닌 상태"다. 구체적으로 다음 조건이 겹칠 때 모놀리스(특히 모듈형)는 합리적 출발점이다.
- 팀이 하나이거나, 여러 팀이더라도 긴밀히 협업하는 단계
- 도메인 경계가 아직 명확하지 않은 초기 제품
- 트래픽이 단일 인스턴스(또는 수평 복제)로 처리 가능한 수준
- 배포 빈도가 주/일 단위이고, 전체 재배포가 부담이 아닌 경우
- 강한 일관성(ACID)이 비즈니스 요구인 경우 (예: 금융 원장)
반대로 다음 신호가 뚜렷하면 모놀리스에서 모듈 추출을 진지하게 검토할 시점이다.
- 한 모듈의 트래픽이 전체의 70% 이상을 차지해 확장이 비효율적
- 서로 다른 팀이 같은 모놀리스의 서로 다른 부분을 자주 변경해 배포 충돌
- 일부 모듈은 99.99% 가용성이, 다른 모듈은 99%면 충분한데 한 배포 단위라 함께 끌려다님
- 도메인 경계가 명확해져 특정 모듈이 독립 서비스로 자연스럽게 떨어지는 시점
이 신호들이 뚜렷하지 않은 상태에서 "마이크로서비스로 전환해야 한다"는 압박이 오면, Newman의 조언은 단호하다 — 먼저 모놀리스 안에서 경계를 그어라. 그게 안 되면 분산 환경에서는 더 안 된다.
설계 사례 — 진흙 덩어리에서 모듈형 모놀리스로
주문·결제·배송을 다루는 작은 전자상거래를 예로, 모놀리스 안의 경계 짓기를 단계별로 보인다. 코드는 Java이지만, 핵심은 언어가 아니라 경계 강제 방식이다.
1단계 — 진흙 덩어리 (경계 없음)
// 모든 서비스가 서로의 데이터에 직접 접근
class OrderService {
void placeOrder(Order order) {
// orders 테이블 직접 insert
jdbc.update("INSERT INTO orders ...", order);
// payments 테이블까지 직접 insert
jdbc.update("INSERT INTO payments ...", order);
// shipping 테이블까지 직접 insert
jdbc.update("INSERT INTO shipping ...", order);
}
}
이 코드는 '모듈'이란 게 없다. OrderService가 orders·payments·shipping의 DB 스키마를 다 알고 직접 건드린다. payments 스키마가 바뀌면 OrderService도 고쳐야 한다 — 결합도가 최대인 상태.
2단계 — 패키지 분리 (경계 신호만)
package com.shop.orders;
class OrderService {
void placeOrder(Order order) {
orders.save(order);
// 다른 패키지지만 public 메서드라 직접 호출
com.shop.payments.PaymentService ps = new PaymentService();
ps.charge(order);
com.shop.shipping.ShippingService ss = new ShippingService();
ss.dispatch(order);
}
}
패키지를 나눴지만 강제가 없다. OrderService가 PaymentService의 인스턴스를 직접 생성하고, payments 패키지의 모든 public 클래스에 닿을 수 있다. 경계를 '권장'만 하고 '강제'하지 않는 단계다. 바쁜 오후에 한 줄 import로 경계가 뚫린다.
3단계 — 모듈형 모놀리스 (경계 강제)
// module-info.java — Java 9+ 모듈 시스템
module com.shop.orders {
exports com.shop.orders.api; // API 패키지만 export
// com.shop.orders.internal 은 export하지 않음 → 외부 접근 컴파일 에러
requires com.shop.payments.api; // payments 모듈의 API 패키지만 의존
requires com.shop.shipping.api;
}
package com.shop.orders.api;
public interface OrderOperations {
void placeOrder(OrderRequest req);
}
package com.shop.orders.internal;
// 구현체. 외부 모듈에선 접근 불가.
class OrderService implements OrderOperations {
private final PaymentGateway paymentGateway; // 다른 모듈의 API 인터페이스만 앎
private final ShippingGateway shippingGateway;
@Override
public void placeOrder(OrderRequest req) {
Order order = orders.save(req);
// payments 모듈의 public API만 호출, 내부 구현은 모름
paymentGateway.charge(new PaymentCommand(order.id(), order.total()));
shippingGateway.dispatch(new ShippingCommand(order.id(), order.address()));
}
}
핵심은 Java 모듈 시스템(module-info.java)의 exports 제한이다. com.shop.orders.internal 패키지는 export하지 않기 때문에, payments 모듈에서 OrderService의 내부 구현을 import하면 컴파일 에러가 난다. 언어 수준에서 경계가 강제된다.
Java 모듈 시스템이 무겁거나 레거시 프로젝트라면 ArchUnit 같은 아키텍처 테스트로 같은 효과를 낸다.
// ArchUnit으로 모듈 경계 위반을 자동 감지
@AnalyzeClasses(packages = "com.shop")
class ModularMonolithArchitectureTest {
@ArchTest
static final ArchRule orders_모듈은_payments_internal_에_접근_불가 =
noClasses().that().resideInAPackage("..orders..")
.should().dependOnClassesThat().resideInAPackage("..payments.internal..");
@ArchTest
static final ArchRule payments_모듈은_shipping_internal_에_접근_불가 =
noClasses().that().resideInAPackage("..payments..")
.should().dependOnClassesThat().resideInAPackage("..shipping.internal..");
@ArchTest
static final ArchRule 모든_모듈은_다른_모듈의_api_패키지만_참조 =
classes().that().resideInAPackage("..orders..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..orders..", "..payments.api..", "..shipping.api..", "java..");
}
이 테스트가 CI를 통과해야 빌드가 된다. 누군가 OrderService에서 payments 내부를 import하면 빌드가 깨진다. 경계가 '규칙'이 아니라 '기계적 강제'가 된다.
모듈형 모놀리스 위반 감지 — 코드에서 어떻게 알아내나
| 신호 | 의미 |
|---|---|
한 모듈이 다른 모듈의 *Repository를 직접 import |
데이터 스키마 공유 — 강결합 |
internal 패키지가 다른 모듈에서 보임 |
모듈 시스템/가시성 규칙 누락 |
| ArchUnit 테스트가 실패 | 경계 위반이 기계적으로 감지됨 |
| 두 모듈이 같은 DB 테이블을 동시에 읽고 쓰기 | 데이터 결합 — 쪼갤 수 없는 신호 |
| 한 모듈 변경이 다른 모듈의 테스트 실패로 이어짐 | 경계가 코드에 새겨지지 않은 징후 |
| "이거 바꿨는데 저쪽이 왜 깨지지?" 반복 | 암묵적 의존성 = 진흙 덩어리 진행 중 |
이 표의 신호들이 많으면, 그 모놀리스는 "모듈형"이 아니다. 패키지만 나눈 진흙 덩어리다. 마이크로서비스로 넘어가기 전에 먼저 이 신호들을 줄여야 한다.
모놀리스가 출발점인 이유
모놀리스가 "다시 좋은 구조"로 불리는 건 유행의 회귀가 아니다. 그동안 마이크로서비스 도입 실패 사례가 쌓이면서 분산의 비용을 직시하게 된 결과다. 모놀리스의 단점(확장 한계, 배포 단위 의존)은 진짜지만, 마이크로서비스의 단점(분산 트랜잭션, 장애 전파, 관측성 부담, 데이터 일관성 약화)도 진짜다. Newman이 2판에서 가장 강조하는 통찰은 — 모놀리스의 단점은 모듈 경계 짓기(modular monolith)로 완화되지만, 마이크로서비스의 단점은 분산이라는 본질에서 오기 때문에 완화가 아니라 감수여야 한다는 점이다.
모놀리스 먼저(monolith first)의 실용적 가치는 "지금 단순하게 시작하되, 나중에 쪼갤 수 있는 선택지를 열어둔다"는 데 있다. 이 선택지를 열어두는 도구가 모듈형 모놀리스이고, 그 도구가 제대로 작동하는지 확인하는 도구가 ArchUnit 같은 경계 강제 테스트다. 모놀리스를 버려야 할 시점은 이 도구들이 한계에 다다랐을 때 — 특정 모듈의 확장 요구가 전체를 끌고 가거나, 팀 구조가 분산 배포를 요구할 때 — 이다. 그 전까지 모놀리스는 실패가 아니라 가장 합리적인 출발점이다.
참고
- Newman — (2nd ed, O'Reilly, 2021), Ch.1·2·3 (특히 modular monolith → extract 경로) — 접근 2026-07-20
- Richards, Ford — (O'Reilly, 2020), Ch.2·3 (아키텍처 스타일 개관, 모놀리스)
- Foote, Yoder — "Big Ball of Mud" (1999), http://www.laputan.org/mud/ — 접근 2026-07-20
- Fowler — "MonolithFirst" (martinfowler.com, 2015), https://martinfowler.com/bliki/MonolithFirst.html — 접근 2026-07-20
- Skelton, Pais — (IT Pro Press, 2019), Conway 관련 절
- Bass, Clements, Kazman — (4th ed, Addison-Wesley, 2021), 아키텍처 스타일 장
'Software Architecture > Architectural Styles' 카테고리의 다른 글
| Architectural Styles - 06. 마이크로서비스의 함정 (0) | 2026.07.20 |
|---|---|
| Architectural Styles - 05. 마이크로서비스 (0) | 2026.07.20 |
| Architectural Styles - 04. 클린 아키텍처 (0) | 2026.07.20 |
| Architectural Styles - 03. 헥사고날 아키텍처 (0) | 2026.07.20 |
| Architectural Styles - 02. 계층형 아키텍처 (0) | 2026.07.20 |