이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Monolith가 합리적인 선택인 조건은 무엇인가?
  • 단일 app과 단일 DB 구조는 성능, 정합성, 장애 격리, 운영 복잡도를 어떻게 바꾸는가?
  • Monolith의 한계 신호는 코드 크기가 아니라 어떤 운영 지표에서 드러나는가?

개요

Monolith는 하나의 application이 여러 업무 기능을 함께 배포하고 운영하는 구조다. 주문, 결제, 배송, 정산 코드가 같은 process에서 실행되고 같은 DB transaction을 사용할 수 있다. 이 구조는 나쁜 출발점이 아니다. 오히려 초기 제품, 작은 팀, 강한 transaction, 빠른 도메인 변경에서는 가장 안전한 선택일 수 있다. 문제는 규모가 커질 때 단일 배포와 공유 데이터가 변경 충돌과 장애 영향 범위를 키운다는 점이다. 따라서 Monolith 판단은 “낡았는가”가 아니라 “지금의 요구사항에 단순함이 더 큰 이득인가”를 묻는 일이다.

이 문서가 바꾸는 설계 축

바뀌는 내용
성능network hop이 적고 단일 transaction이 빠르지만 전체 app을 함께 scale해야 한다.
정합성단일 DB ACID를 쓰기 쉬워 강한 정합성 구현이 단순하다.
장애 격리process, DB, 배포가 하나라 한 기능 장애가 전체 영향으로 커질 수 있다.
운영 복잡도배포, 관측, rollback은 단순하지만 release 충돌과 blast radius가 커진다.

Monolith가 강한 상황

Monolith의 장점은 단순함이다. 다음 조건에서는 분리보다 유지가 낫다.

조건이유
팀이 작다API 계약과 service 운영 비용이 과하다.
도메인이 자주 바뀐다경계를 너무 빨리 고정하면 다시 나누는 비용이 크다.
하나의 transaction이 핵심이다주문 생성과 재고 차감 같은 불변식을 DB가 보장하기 쉽다.
트래픽이 한 application scale로 충분하다기능별 독립 scale이 필요 없다.
운영 인력이 적다tracing, service alert, on-call 분리 비용을 감당하기 어렵다.
이 조건에서는 MSA보다 잘 구조화된 Monolith가 더 신뢰할 수 있다.

대표 시나리오

초기 커머스 서비스가 있다고 하자. 주문 생성은 재고 차감, 쿠폰 사용, 결제 요청 준비를 함께 처리한다.

POST /orders
-> validate cart
-> decrease stock
-> use coupon
-> create order
-> return orderId

이 단계들이 같은 DB transaction에 있으면 실패 처리가 단순하다. 재고 차감 후 주문 생성이 실패하면 rollback하면 된다. 사용자는 즉시 성공 또는 실패를 본다. 초기에는 이 단순성이 큰 장점이다.

단일 트랜잭션의 장점

Spring에서는 transaction 경계가 명확하다.

@Transactional
public OrderId placeOrder(PlaceOrderCommand command) {
    Cart cart = cartRepository.get(command.cartId());
    Stock stock = stockRepository.getForUpdate(cart.productId());
    stock.decrease(cart.quantity());
    Order order = orderRepository.save(Order.create(cart, command.memberId()));
    return order.id();
}

이 코드는 한 DB 안에서는 이해하기 쉽다. 동시성 제어, rollback, unique constraint, foreign key를 DB에 맡길 수 있다. 서비스를 나누면 같은 코드는 API 호출, event, 보상 transaction, idempotency로 바뀐다. 정합성 비용을 감당할 이유가 없다면 나누지 않는 것이 맞다.

Monolith의 한계 신호

한계는 파일 수보다 변경과 운영 지표에서 드러난다.

신호의미
release train이 길어진다작은 변경도 전체 QA와 배포 대기를 요구한다.
기능 변경마다 여러 팀 코드가 충돌한다모듈 경계와 owner가 흐리다.
결제 장애가 주문 조회까지 막는다장애 격리가 부족하다.
특정 기능만 scale하고 싶은데 전체 app을 늘린다비용과 capacity가 낭비된다.
DB table owner가 없다schema 변경과 데이터 품질 책임이 흐려진다.
build/test 시간이 급증한다feedback loop가 느려진다.
이 신호가 있더라도 바로 MSA로 가지 않는다.
먼저 모듈 경계를 세우고, 실제로 분리할 책임이 있는지 확인한다.

변경 결합도 측정

Monolith의 크기보다 같이 바뀌는 정도가 중요하다.

last_30_days:
  order_files_changed = 42
  payment_files_changed_together = 31
  shipping_files_changed_together = 6
  deploy_blocked_by_other_team = 8

주문 변경 때 결제 코드가 거의 항상 함께 바뀐다면 아직 경계가 불안정할 수 있다. 반대로 결제 변경이 주문과 독립적으로 반복된다면 분리 후보가 될 수 있다. 이 숫자는 분리 결론이 아니라 모듈 정리 필요성을 보여주는 근거다.

장애 영향 범위

Monolith는 process 하나가 여러 기능을 담는다. 결제 provider timeout이 thread pool을 점유하면 주문 조회도 느려질 수 있다. 하지만 이것이 반드시 MSA 필요성을 뜻하지는 않는다. 먼저 내부 bulkhead, timeout, circuit breaker, pool 분리를 적용할 수 있다.

payment:
  timeout-ms: 800
  max-concurrent-calls: 40
order-read:
  timeout-ms: 200

같은 process 안에서도 리소스 격리를 일부 만들 수 있다. 분리는 내부 격리로 부족할 때 검토한다.

유지 판단

다음에 해당하면 Monolith 유지가 합리적이다.

  • owner team이 하나이고 release 충돌이 작다.
  • 사용자 핵심 경로가 강한 transaction을 요구한다.
  • 기능별 트래픽 차이가 크지 않다.
  • 장애가 대부분 코드 품질이나 query 문제에서 나온다.
  • 운영팀이 service 단위 관측과 on-call을 아직 감당하기 어렵다. 이때 할 일은 MSA 전환이 아니라 package, transaction, DB owner를 정돈하는 것이다.

전환 판단

다음 신호가 쌓이면 Modular Monolith나 MSA를 검토한다.

  • 결제, 정산, 알림처럼 release cadence가 확실히 다르다.
  • 특정 기능 장애가 전체 SLO를 반복해서 깨뜨린다.
  • 특정 팀이 독립 배포하지 못해 business delivery가 막힌다.
  • 기능별 scaling 비용 차이가 크다.
  • data owner와 schema 변경 책임이 이미 나뉘어 있다. 전환의 첫 단계는 “서비스를 만든다”가 아니라 “현재 Monolith 안에서 경계를 증명한다”이다.

개인 프로젝트 기준

  • Monolith를 선택했다면 단일 transaction이 주는 장점을 설명한다.
  • package 구조와 DB table owner를 최소한 문서로 남긴다.
  • 결제, 알림 같은 외부 의존성에는 timeout과 fallback을 둔다.
  • 포트폴리오에서 “MSA를 안 쓴 이유”를 운영 복잡도와 정합성 기준으로 말한다.
  • 작은 서비스에서는 Monolith가 더 좋은 설계일 수 있음을 숫자로 설명한다.

기업 운영 기준

  • 변경 결합도, deploy 대기 시간, build/test 시간, 장애 영향 API를 주기적으로 본다.
  • 단일 app이라도 기능별 metric과 owner를 둔다.
  • 외부 dependency별 timeout, retry, circuit breaker를 분리한다.
  • DB schema 변경은 expand-contract와 rollback 조건을 가진다.
  • MSA 전환 전 module boundary와 data owner를 먼저 검증한다.

위험 신호

  • “Monolith라서 나쁘다”는 이유만으로 service를 나눈다.
  • 단일 transaction이 필요한 기능을 검토 없이 event 기반으로 바꾼다.
  • 모든 기능이 같은 DB table을 수정하지만 owner가 없다.
  • 결제 timeout이 전체 thread pool을 점유하는데 내부 격리를 하지 않는다.
  • 배포 충돌 문제를 조직/모듈 경계가 아니라 service 수로만 해결하려 한다.
  • Monolith 유지 결정을 했지만 모듈 경계와 전환 조건을 적지 않는다.

확인 질문

확인 질문

  • 확인 질문: Monolith의 가장 큰 장점은 무엇인가?
    • 답변: 단일 process와 단일 DB transaction으로 정합성, 배포, 관측, rollback을 단순하게 유지할 수 있다는 점이다.
  • 확인 질문: Monolith의 한계는 코드 줄 수에서 결정되는가?
    • 답변: 아니다. 변경 결합도, 배포 대기, 장애 영향 범위, 기능별 scale 요구, 데이터 owner 부재에서 드러난다.
  • 확인 질문: Monolith 한계가 보이면 바로 MSA로 가야 하는가?
    • 답변: 아니다. 먼저 Modular Monolith로 경계와 owner를 검증하고, 분리 비용을 감당할 근거가 있을 때 MSA를 검토한다.