본문으로 건너뛰기
목록으로

모놀리스 vs MSA — 언제 무엇을, 그리고 MSA를 지탱하는 것들

Johny Cho
Software Engineer @ Kurly

"스타트업 초기 멤버로 합류했다면 모놀리스와 MSA 중 무엇을 고르겠습니까?" 면접에서 흔히 나오는 질문입니다. "대세는 MSA죠", "넷플릭스도 쓰니까요" 같은 답은 절반만 맞습니다. 선택 기준과 MSA를 지탱하는 요소들을 정리합니다. (이러한 구조를 다이어그램으로 표현하는 법은 C4 모델 참고.)

핵심 명제 하나 — 기술은 비용입니다. MSA는 공짜로 주어지지 않고, 복잡성이라는 값을 감수해야 합니다.

모놀리스 vs MSA — 무엇이 다른가​

  • 모놀리스(Monolith) — "거대한 단일 어플리케이션". 하나의 어플리케이션 안에 비즈니스 로직과 DB가 모두 들어갑니다. 쇼핑몰이면 인증·주문·장바구니·결제가 한 어플리케이션에.
  • 마이크로서비스(MSA) — 기능별로 나누고 DB까지 독립시킵니다. 유저·주문·결제 서비스가 각자 DB를 갖고, 앞단에 API 게이트웨이를 배치합니다. 핵심 3속성은 독립 배포 · 장애 격리 · 독립 확장입니다.

컴퓨터에 빗대면 이해가 쉽습니다. 모놀리스는 올인원(일체형) PC — 하나로 완결돼 간편하지만, 그래픽카드만 교체하려 해도 본체 전체를 손봐야 합니다. MSA는 조립식 PC — 그래픽카드만 따로 교체·업그레이드할 수 있지만, 부품을 각각 고르고 호환을 맞추고 조립·관리하는 수고가 듭니다.

관점모놀리스MSA
배포단순(하나만 배포)복잡(단, CD로 자동화)
확장전체를 한꺼번에 → 비효율특정 서비스만 독립 확장
장애 격리하나가 멈추면 전체 영향다른 서비스로 전파 안 됨
기술 복잡도낮음(구조가 단순)높음(특히 트랜잭션)

확장성이 MSA의 큰 장점입니다. 주문에만 트래픽이 몰리면 MSA는 주문 서비스만 스케일 아웃하지만, 모놀리스는 유저·결제까지 한꺼번에 확장해야 합니다.

MSA의 대가 — 복잡성​

여기서 복잡성은 내부 로직이 아니라 아키텍처 구조의 복잡성입니다.

  • 네트워크 분리 — API가 나뉘면 서비스 간 통신이 네트워크를 타고, 그만큼 장애 지점이 늘어납니다.
  • 분산 트랜잭션 — 하나의 DB면 주문+결제를 한 트랜잭션으로 묶기 쉽지만, DB가 분리되면 어렵습니다(그래서 Saga·Outbox 같은 패턴이 필요 — 메시지 큐 비교 참고).
  • 관측성 — 여러 서비스를 거치니 로그 추적이 복잡해집니다.

MSA는 최신 기술이라기보다 조직 구조 관점의 아키텍처입니다. 콘웨이 법칙은 "시스템 구조는 그것을 만드는 조직의 소통 구조를 닮는다"고 말합니다. 팀이 하나면 시스템도 하나, 팀이 기능별로 나뉘면 시스템도 그렇게 나뉩니다. 넷플릭스·아마존도 필요해진 뒤에 MSA를 택했습니다. 조직이 준비되지 않으면 MSA도 실패합니다.

언제 무엇을 — 모놀리스로 시작해 모듈러 모놀리스를 거쳐​

  • 모놀리스가 맞는 경우 — 초기 스타트업, 작은 팀, 도메인이 아직 불확실할 때. 초기 단계에서 더 큰 문제는 확장성 부족이 아니라 과도한 복잡성입니다.
  • MSA를 고려할 때 — 서비스가 충분히 크고, 조직이 기능 단위로 나뉘고, 배포 병목·장애 전파가 문제가 될 때.

그 사이 단계가 모듈러 모놀리스(Modular Monolith) 입니다. 단일 배포 단위는 유지하되, 내부를 도메인 경계로 명확히 분리해 두는 구조입니다. 과거 모놀리스가 경계 의식이 약했다면, 모듈러 모놀리스는 경계를 코드로 강제합니다. 그러면 나중에 팀이 커지고 서비스가 분화될 때 MSA로 자연스럽게 분리할 수 있습니다.

이관은 ① 도메인 경계 식별·의존성 정리 → ② 경계를 코드로 강제(인터페이스 명확화) → ③ 필요 시 서비스 분리 순입니다. 도구로는 Java의 Spring Modulith, .NET의 ABP, Node의 NestJS 등이 있습니다. 중요한 건, 모듈러 모놀리스가 최종 아키텍처일 수도 있다는 점입니다 — MSA의 복잡성을 감수할 만큼 성장했을 때만 넘어가면 됩니다.

Spring Modulith — 경계를 코드로 강제하기​

"경계를 코드로 강제한다"를 자바에서 구현한 것이 Spring Modulith입니다. 별도의 무거운 프레임워크가 아니라 관례 + 검증 도구에 가깝습니다. 메인 어플리케이션 패키지 바로 아래의 최상위 패키지 하나를 어플리케이션 모듈 하나로 보고, 다른 모듈의 내부(internal) 패키지에는 접근하지 못하게 합니다 — 모듈은 공개된 타입으로만 서로를 호출합니다.

com.shop
├── ShopApplication.java
├── order/ ← 주문 모듈
│ ├── Order.java (공개 API — 다른 모듈이 참조 가능)
│ └── internal/ (내부 구현 — 다른 모듈 접근 불가)
├── payment/ ← 결제 모듈
└── member/ ← 회원 모듈

핵심은 이 경계가 문서가 아니라 테스트로 강제된다는 점입니다. 아래 한 줄이 모듈 규칙을 검증해, 어떤 모듈이 다른 모듈의 internal을 참조하면 빌드가 실패합니다.

class ModularityTests {
@Test
void 모듈_경계_검증() {
// 모듈 간 규칙 위반(다른 모듈 internal 참조 등)이 있으면 실패
ApplicationModules.of(ShopApplication.class).verify();
}
}

모듈 간 통신도 직접 호출 대신 이벤트로 느슨하게 묶습니다. 주문 모듈이 결제 모듈을 직접 호출하지 않고 이벤트만 발행하면, 나중에 서비스를 분리할 때 그 이벤트를 그대로 메시지 큐로 이관하기 쉽습니다.

// 주문 모듈: 결제 모듈을 직접 호출하지 않고 이벤트만 발행
events.publishEvent(new OrderCompleted(orderId));

// 결제 모듈: 이벤트를 구독 (트랜잭션 커밋 후 실행)
@ApplicationModuleListener
void on(OrderCompleted event) { /* 결제 처리 */ }

@ApplicationModuleListener는 트랜잭션 커밋 이후 비동기로 실행되고, 이벤트 발행 기록(Event Publication Registry) 이 아웃박스처럼 남아 실패 시 재시도를 돕습니다. 덤으로 Documenter로 모듈 구조 다이어그램을 자동 생성할 수 있어, 앞의 C4 다이어그램을 손으로 그리는 대신 코드에서 뽑아낼 수 있습니다.

Spring Modulith의 가치는 "모듈을 나눴다"가 아니라 "경계를 어기면 빌드가 실패한다"에 있습니다 — 그래야 시간이 지나도 경계가 유지됩니다. 모놀리스는 능력이 부족해 사용하는 게 아니라 의도적으로 선택하는 구조이고, MSA는 조직이 성숙했을 때 감당할 수 있는 구조입니다.

MSA를 지탱하는 세 요소​

MSA로 가면 반드시 따라오는 구성 요소가 있습니다.

① API 게이트웨이 — 단일 진입점​

게이트웨이가 없으면 클라이언트가 모든 API 주소를 알아야 하고, 모든 API가 외부에 노출되며, 인증 같은 공통 기능을 API마다 반영해야 합니다. 게이트웨이는 호텔 프런트 데스크처럼 단일 진입점이 되어 라우팅 · 인증/인가 · 프로토콜 변환 · 레이트 리미팅 · 관측성을 담당합니다.

다만 게이트웨이는 요청을 넘겨주는 곳이지, 비즈니스 로직을 처리하는 곳이 아닙니다. 비즈니스 DB에 직접 접근하거나 트랜잭션을 처리하면 그 자체가 거대한 모놀리스가 되어 병목이 됩니다. 공통 관심사만 맡고 비즈니스 로직은 각 서비스로 넘깁니다. 또 게이트웨이는 단일 장애 지점이라 반드시 이중화합니다. 구현체로는 Spring Cloud Gateway, Nginx, Kong 등이 있습니다.

② 서킷 브레이커 — 연쇄 장애 차단​

추천 API 하나가 과부하로 멈췄는데 메인 페이지까지 멈추는 일이 있습니다. 게이트웨이가 멈춘 API의 응답을 기다리다 자기 스레드가 고갈되기 때문입니다. 서킷 브레이커는 두꺼비집(누전 차단기)처럼, 특정 서비스가 실패하기 시작하면 회로를 끊어 즉시 실패(fail fast) 시킵니다.

상태는 세 가지입니다.

  • Closed — 정상. 요청을 그대로 전달합니다.
  • Open — 에러율이 임계치(예: 50%)를 넘으면 차단해 즉시 에러를 반환합니다.
  • Half-Open — 일정 시간 뒤 요청 하나만 보내, 성공하면 Closed로 복귀합니다.

서킷은 호출하는 쪽에 배치합니다(멈춘 서비스에는 물어볼 수 없으니까요). 열렸을 때 500을 던지기보다 미리 만든 정적 결과 같은 폴백으로 우아한 성능 저하(graceful degradation) 를 꾀하는 게 좋습니다. 구현체는 Java의 Resilience4j(구 Hystrix) 등.

주의할 안티패턴이 있습니다.

  • 재시도 폭풍 — 게이트웨이 3회 + 서비스 3회면 한 클릭에 9번 호출. 재시도는 한 계층에서만.
  • 타임아웃 불일치 — 외부(호출하는 쪽, 예: 게이트웨이)이 1초, 안쪽(호출받는 쪽, 예: 서비스·DB)이 5초면, 외부가 포기한 뒤에도 안쪽은 계속 일해 좀비 프로세스가 됩니다. 타임아웃은 안쪽이 더 짧아야 합니다(타임아웃 예산 참고).
  • 과도하게 넓은 서킷 — 하나의 서킷을 모두가 공유하면 한 의존성 장애가 전체를 차단합니다. 서킷은 의존성 단위로 잘게 나눕니다.

관련해서 장애가 확산되지 않게 막는 전체 구조는 내결함성과 고가용성에서 설명했습니다.

③ 서비스 디스커버리 — 동적 IP 관리​

클라우드에선 오토스케일링으로 서버 IP가 계속 바뀝니다. IP를 하드코딩할 순 없으니, 전화번호부(114) 처럼 "서비스 이름을 주면 살아 있는 IP를 돌려주는" 중간 관리자를 배치합니다. 대표적으로 Eureka가 있고, 동작은 ① 등록(서비스가 기동 시 자기 IP 신고) → ② 하트비트(매초 생존 신고, 끊기면 제거) → ③ 패치(게이트웨이가 30초마다 목록을 받아 로컬 캐싱)입니다.

방식은 두 가지입니다.

  • 클라이언트 사이드 — Eureka. 클라이언트가 직접 주소를 알아와 로드밸런싱합니다.
  • 서버 사이드 — 쿠버네티스. 도메인 이름만 보내면 CoreDNS·Service가 매핑합니다.

요즘은 쿠버네티스로 인프라 레벨에서 처리하는 추세입니다.

정리​

  • 기술은 비용 — MSA의 장점(독립 배포·확장·장애 격리)은 복잡성과 맞바꾼 것.
  • 모놀리스로 시작 → 모듈러 모놀리스(경계를 코드로) → 필요할 때 MSA. 경계만 잘 잡아두면 분리가 쉽습니다.
  • MSA엔 게이트웨이·서킷 브레이커·서비스 디스커버리가 따라옵니다 — 각각 단일 진입점·연쇄 장애 차단·동적 IP 관리.
  • 게이트웨이엔 비즈니스 로직을 넣지 말고, 서킷은 호출자에 의존성 단위로, 타임아웃은 안쪽을 더 짧게.
정답은 MSA가 아니라, 지금 우리 팀과 서비스에 맞는 구조입니다 — 모놀리스의 한계를 직접 부딪쳐 봐야 MSA가 왜 그렇게 설계됐는지 이해합니다.