Modular Monolithic

Modular Monolith Architecture는 단일 배포 단위 내에서 비즈니스 도메인 기반으로 명확히 분리된 모듈 구조를 갖는 설계 방식이다.
각 모듈은 높은 응집도와 낮은 결합도를 유지하며 독립적으로 개발·테스트되고, 명확한 인터페이스를 통해 통신한다. 초기에는 모놀리식의 단순함을 유지하면서도, 필요 시 마이크로서비스로의 전환을 고려한 진화 가능성과 확장성을 동시에 확보할 수 있어 ’ 골디락스 아키텍처 ’ 로도 불린다.

등장 배경 및 발전 과정

Modular Monolith는 단순히 아키텍처 스타일이 아니라,
지나친 분산과 복잡성에 대한 반작용이자, 실용성과 확장성을 동시에 추구하는 전략적 구조로 등장했다.

등장 배경

항목설명
전통적 모놀리식 문제- 코드/도메인/기능이 뒤섞이며, 규모 확장 시 “Big Ball of Mud” 로 변질됨.
- 기능 추가 시 전체 영향 발생, 테스트·배포 범위 광범위
마이크로서비스 한계- 초기 설계 부담, 서비스 분리 및 데이터 분산으로 인한 일관성/성능 문제.
- DevOps 성숙도 없으면 운영 복잡성 증가
기술보단 조직 문제- 마이크로서비스 전환은 종종 팀 간 독립성 확보 (Conway’s Law) 목적에서 비롯되며, 기술적 필요보다 조직적 동기에서 유래하는 경우 많음

하이브리드 모델로서 Modular Monolith 의 등장

항목설명
전략적 절충안운영의 단순성과 개발의 모듈화를 동시에 충족하는 절충 모델로 등장
기술적 기반DDD(Bounded Context) 기반의 내부 모듈 분리 → 계층, 경계, 책임 명확화
Google & ThoughtWorks 동향Google 의 논문 (2023) 에서 마이크로서비스의 한계를 명시하고, 모듈러 모놀리스 트렌드를 강조함 (Service Weaver, “Towards Modern Development of Cloud Applications”)
Eric Evans 의 DDD원래 모놀리스를 위한 설계였음. 이를 Modular Monolith 가 잘 계승
Shopify, StackOverflow 사례MSA 전환 전 Modular Monolith 유지로 운영 안정성과 생산성 확보

발전 과정

시기주요 흐름
2010 년 이전전통적인 레이어드 모놀리스 → 비대화, 유지보수 문제
2010~2015 년MSA 열풍 → Netflix, Amazon 기반 아키텍처 확산
2015~2020 년운영 복잡성, Data Consistency 문제 대두 → MSA 회의론
2020 년 이후Modular Monolith 재조명 → 실무 중심 대기업도 채택 (Uber Monorepo 유지, Google Service Weaver 도입 등)

목적 및 필요성

Modular Monolith 는 단일 배포 구조의 단순함을 유지하면서, 내부적으로는 명확한 모듈 경계를 정의하여 유지보수성과 확장성을 동시에 확보할 수 있는 아키텍처 전략이다. 복잡한 비즈니스 요구, 다수의 협업 팀, 점진적 MSA 전환 필요성, 기술 부채 관리 등의 문제를 해결하기 위한 현실적이고 검증된 접근 방식이다.

목적 (Goals)

목적설명
모듈성 확보와 단일 배포의 균형마이크로서비스 수준의 모듈성과 경계를 유지하면서도, 단일 프로세스/배포 단위로 운영의 단순함 유지
복잡성 완화와 구조적 명확성 확보거대한 코드베이스를 도메인 또는 기능 단위로 나누어 시스템 이해도와 변경 용이성 향상
협업 효율 향상모듈 단위로 팀을 구성하여 병렬 개발과 명확한 책임 분리가 가능하게 함
점진적 MSA 이행 준비도메인 경계를 먼저 정의하고 테스트하여, 필요 시 해당 모듈을 서비스 단위로 추출 가능
비용 효율성과 빠른 개발 사이의 균형고가용성 및 확장성 확보는 유지하면서, 마이크로서비스의 복잡도나 인프라 비용은 최소화

필요성 (Necessity)

필요성 항목상세 설명
복잡성 관리대규모 시스템을 도메인 모듈로 나누어 코드 변경 범위와 리스크를 최소화함
운영 단순화단일 프로세스/컨테이너 배포로 운영 환경 구성 및 배포 파이프라인 복잡도 감소
개발 확장성팀 간 병렬 개발이 가능해지며, 코드 충돌 및 병합 이슈 감소
테스트 용이성모듈 단위의 테스트, 통합 테스트 전략 적용으로 품질 확보가 쉬움
기술 부채 캡슐화모듈 경계에 따라 기술 부채를 격리하고 점진적으로 제거 가능
경계 실험과 도메인 검증실제 MSA 로 이행 전 도메인 경계를 안전하게 실험하고 검증 가능
확장성과 성능 유지In-process 통신 기반으로 네트워크 오버헤드 없이 수평 확장 기반 마련 가능
실제 사례 검증Shopify, GitHub 등 글로벌 서비스들도 장기적으로 Modular Monolith 전략 유지 또는 채택

핵심 개념

Modular Monolith 는 단일 배포 구조의 장점을 유지하면서도, 내부적으로는 도메인 기반으로 모듈을 명확히 분리하여 높은 응집도와 낮은 결합도를 갖는 아키텍처 스타일이다. 각 모듈은 자체 계층 구조를 가지며, 명확한 경계와 책임을 가지고 내부 인터페이스 또는 이벤트를 통해 통신한다.
Modular Monolith는 향후 마이크로서비스로 전환 가능한 구조적 유연성을 제공한다.

Modular Monolith 구조 핵심 요약

항목설명
단일 실행 단위모든 모듈은 하나의 배포 단위로 구성되며, 단일 프로세스 또는 컨테이너에서 실행
도메인 중심 모듈화DDD 의 Bounded Context 기준으로 모듈을 분리하여 책임을 명확히 분산
계층화된 모듈 내부 구조각 모듈은 내부적으로 Domain, Application, Infrastructure, Interface 계층 구조를 가짐
모듈 간 느슨한 결합직접 호출을 피하고, 인터페이스/이벤트 기반의 통신을 통해 의존성 최소화
높은 응집도 유지각 모듈은 하나의 책임에 집중하며 기능을 응집시킴
내부 인터페이스 기반 통신Internal API, Service Interface, Domain Event 등으로 인터페이스화
데이터 저장소 전략단일 DB 를 사용하되, 스키마 또는 테이블 소유권 기준으로 논리적 분리 수행
운영 및 테스트 용이성모듈 단위 개발/테스트 가능, 코드 공유 간소화, CI/CD 파이프라인 단일화
점진적 마이크로서비스 전환 가능도메인 단위로 분리된 구조 덕분에 MSA 로 전환 시 독립 서비스로 쉽게 이관 가능

아키텍처 레벨 구성도

flowchart TB
    subgraph Monolith App
      direction TB
      A1[회원 모듈] --> A1D[Domain]
      A1 --> A1A[Application]
      A1 --> A1I[Infrastructure]
      A1 --> A1IF[Interface/API]

      A2[결제 모듈] --> A2D
      A2 --> A2A
      A2 --> A2I
      A2 --> A2IF

      A3[상품 모듈] --> A3D
      A3 --> A3A
      A3 --> A3I
      A3 --> A3IF
    end

    click A1IF "Internal API or Domain Event"
    click A2IF "Internal API or Domain Event"

실무 구현 전략

전략 항목적용 방안
도메인 중심 설계Event Storming → Bounded Context → 모듈 매핑
모듈 간 통신 방식- 동기: 메서드 호출 or Internal API - 비동기: 이벤트 퍼블리싱, Outbox 기반
데이터 관리 전략- 모듈별 논리적 DB 구분 (스키마 분리) - DB 접근 계층 제한 (Repository 통한 접근만 허용)
의존성 관리DI 활용, Interface + Implementation 구조, 순환 의존 방지
테스트 및 배포 전략- 모듈별 단위 테스트 + 통합 테스트 구성 - CI 파이프라인에서 모듈별 테스트 수행 - 단일 Docker 이미지 구성 유지
확장 아키텍처 적용 가능CQRS, Event Sourcing, Outbox/Inbox, API Gateway 연계 등으로 확장 가능

연관 아키텍처 패턴 적용

패턴설명
CQRSCommand/Query 분리로 책임 구분, 이벤트 기반 읽기 모델 구성
Event Sourcing상태를 이벤트로 저장, 리플레이 기반 복원
Outbox/Inbox트랜잭션과 함께 이벤트 기록 → 비동기 발행 보장
Internal API + Event Bus모듈 간 동기·비동기 통신 혼용 가능 구조 제공

주요 기능 및 역할

Modular Monolith 의 주요 기능은 도메인 기반 모듈 캡슐화, 명확한 인터페이스 설계, 느슨한 결합 구조를 통해 복잡성을 줄이고 유지보수를 용이하게 하는 데 있다. 각 모듈은 독립적인 데이터와 로직을 소유하며, 내부적으로는 함수 호출이나 이벤트 기반 통신을 통해 협력한다. 모든 모듈은 단일 프로세스로 배포되지만, 각기 독립적으로 테스트·확장·전환이 가능한 구조로 구성되어 있으며, 이는 향후 마이크로서비스로의 점진적 진화를 위한 기반이 된다.

기능 분류기능 설명역할 및 실무 적용
모듈 캡슐화각 모듈은 도메인 단위로 코드/로직/데이터를 자체 관리하며 외부에 내부 구현을 노출하지 않음기능별 책임과 변경 범위 제한, 충돌 최소화
명확한 인터페이스각 모듈은 공개된 API, 인터페이스 또는 이벤트를 통해서만 상호작용인터페이스를 통한 테스트 가능, 계약 기반 통신
느슨한 결합모듈 간 직접 참조를 줄이고, 인터페이스나 이벤트로만 연결변경 시 파급효과 최소화, 향후 서비스 분리 용이
도메인 경계 및 조직화DDD 기반 Bounded Context 로 모듈 정의, 공통 언어 기반 설계팀 간 소유권 분리, 협업 명확화
동기/비동기 통신 지원내부 메서드 호출 또는 이벤트 기반 비동기 통신 모두 지원이벤트 기반 확장을 통해 마이크로서비스 전환 대비
데이터 관리모듈별 논리적 DB 분리 또는 명시적 스키마 분리, 트랜잭션 경계 유지데이터 격리, 접근 통제, 일관성 유지
공통 인프라 제공인증, 로깅, 트레이싱, 설정 등 공유 서비스는 중앙화된 기반으로 제공개발 표준화, 중복 방지, 유지보수 간소화
운영 단순화모든 모듈은 단일 프로세스/컨테이너로 배포DevOps 효율성, 테스트 및 릴리즈 간편
테스트 가능성각 모듈 단위로 유닛/통합 테스트 가능, API/이벤트 기반 테스트 구성품질 보장, CI 파이프라인 적용 용이
진화 및 전환 기반아키텍처적으로 서비스 추출에 유리한 구조 제공점진적 MSA 이행 전략 실현 가능

특징

Modular Monolith는 다음과 같은 특징을 가진 아키텍처다:

구조적 특징

항목설명
단일 실행/배포 단위모든 모듈이 하나의 애플리케이션으로 빌드되어 단일 프로세스에서 실행됨
모듈화된 아키텍처도메인 중심으로 모듈을 분리하며, 모듈 내 계층 구조 (Domain, App, Infra, Interface) 를 유지
명확한 경계 유지네임스페이스 분리, 인터페이스 기반 접근, 캡슐화로 모듈 간 경계 명확화
낮은 모듈 간 결합도인터페이스 (계약), DI, 이벤트 등을 활용한 간접 참조 방식 채택

개발 및 테스트 특징

항목설명
통합 코드베이스모든 모듈이 하나의 코드 저장소 (Monorepo 또는 단일 Git Repo) 에 존재
테스트 전략 분리 가능모듈 단위의 유닛 테스트, 경계 간 통합 테스트 적용 가능
개발 병렬화 가능각 모듈은 독립 개발 가능하며, 기능/도메인 단위 병렬 작업에 적합
재사용성과 일관성공통 유틸리티, 보일러플레이트, 표준화된 계층 구조 활용 가능

운영 및 배포 특징

항목설명
배포 단순화단일 아티팩트로 배포되므로 CI/CD 가 단순화됨
성능 효율성모듈 간 인 - 프로세스 호출로 네트워크 지연 없음 (MSA 대비 성능 우위)
모니터링 통합로깅, 트레이싱, 메트릭을 애플리케이션 레벨에서 일관되게 관리 가능
낮은 인프라 복잡도하나의 서비스로 운영되므로 인프라 구성/배포 수단 간소화

Traditional Monolith vs. Modular Monolith vs. MSA 비교

Modular Monolith 는 전통적인 Monolith 의 단순함과 MSA 의 유연성 사이의 균형점이며, MSA 로의 진화적 전환을 위한 전략적 출발점이다. 각 아키텍처는 조직의 규모, 팀 구성, 운영 역량에 따라 선택과 전략이 달라져야 한다.

비교 항목Traditional MonolithModular MonolithMicroservices Architecture (MSA)
배포 단위단일 애플리케이션단일 애플리케이션서비스별 개별 배포 (다수)
실행 환경하나의 프로세스하나의 프로세스각 서비스가 독립된 프로세스로 실행
모듈 경계불명확, 기술 기반 계층 중심도메인 기반 명확한 경계 설정서비스 단위로 완전한 논리적/물리적 분리
통신 방식내부 함수 호출내부 인터페이스 또는 이벤트 버스REST, gRPC, 메시지 브로커 기반 통신
데이터베이스 구조단일 DB 공유단일 DB or 논리적 스키마 분리서비스별 독립 DB, Polyglot Persistence 허용
배포 복잡도매우 낮음낮음~중간 (전체 빌드, 단일 배포)높음 (CI/CD, 서비스 디스커버리 등 필요)
초기 개발 효율성매우 높음높음 (모듈화 관리 필요)낮음 (설정, 인프라 구성 필요)
유지보수성 및 확장성낮음 (규모 커질수록 복잡성 급증)중간~높음 (모듈 단위 테스트/확장 가능)매우 높음 (서비스 단위로 수평 확장 가능)
테스트 전략통합 테스트 중심, 격리 어려움모듈 단위 테스트 가능, 통합 테스트 용이각 서비스 단위 격리 테스트, 계약 기반 통합 테스트
조직 구조 대응기능 기반 팀 구성도메인 또는 모듈 중심 팀 구성서비스별 cross-functional 팀 구성 필요
MSA 전환 용이성구조적 분리가 없어 매우 어려움명확한 경계 기반으로 부분 전환 용이해당 없음 (이미 MSA)

공통점

항목설명
사용자 입장에서는REST API 형태로 동작 (내부 구현과 관계 없이)
도메인 로직 존재3 가지 모두 비즈니스 로직은 존재, 다만 위치와 경계 방식이 다름
유닛 테스트 가능모두 단위 테스트 가능, 다만 격리 수준이 다름
확장 가능성 존재기술적으로는 모두 확장 가능하지만 접근 방식이 다름

주요 차이점

구분핵심 차이
경계 관리Monolith 는 모듈 경계가 약함 / Modular 은 논리적 경계 / MSA 는 물리적 경계까지 존재
배포 단위Monolith 는 전체 배포 / MSA 는 서비스별 독립 배포
결합도Monolith 는 강결합 / Modular 은 느슨한 모듈간 결합 / MSA 는 완전 분산 결합
기술 스택Monolith 는 단일 언어·런타임 기반 / MSA 는 서비스별 Polyglot 가능
운영 부담Monolith 는 운영 단순 / MSA 는 모니터링, 트레이싱, 보안 등 운영이 복잡

전략적 요약

아키텍처 스타일장점단점이상적 사용 시점
Traditional Monolith단순한 개발과 배포경계 불명확, 유지보수 어려움초기 프로토타이핑, 작은 팀
Modular MonolithMSA 전환 고려한 구조화, 유지보수 유리초기 설계 복잡도 있음점진적 성장, 도메인별 팀 운영 조직
MSA완전한 분산, 독립 배포, 유연한 확장높은 운영/설정 복잡도대규모 조직, 고성능 확장 필요 시점

핵심 원칙

Modular Monolith 아키텍처는 명확한 경계 정의, 책임 분리, 의존성 통제, 인터페이스 중심 설계를 기반으로 높은 응집도와 낮은 결합도를 유지하는 것이 핵심이다. 각 모듈은 자신의 책임과 데이터를 소유하며 외부와는 명시적인 계약 (API 또는 이벤트) 으로만 통신하고, 확장성과 변경 용이성을 위해 설계 시 OCP, DIP, ISP 등 SOLID 원칙을 내재화해야 한다.

분류핵심 원칙설명 및 실무 적용 예시
책임 분리 (SRP)모듈은 하나의 도메인 기능만을 담당하며 내부 응집도를 유지Order, Inventory 모듈이 독립된 책임 소유
경계 명확성 (Boundary Clarity)DDD 의 Bounded Context 기준으로 모듈 경계를 설정하고 외부 접근 차단다른 모듈의 클래스/데이터 직접 참조 금지
의존성 역전 (Dependency Inversion)하위 계층의 구체 구현이 상위 계층에 영향을 주지 않도록 인터페이스 기반 추상화 적용OrderServicePaymentPort 에 의존, 실제 구현은 infrastructure 모듈에서
개방 - 폐쇄 원칙 (OCP)기존 코드 변경 없이 기능 확장이 가능하도록 인터페이스 기반 설계새로운 결제 수단 추가 시 기존 모듈 수정 없이 확장 가능
데이터 격리 (Data Ownership)각 모듈은 자신의 데이터를 소유하고 직접 관리하며 외부 모듈의 데이터에 접근하지 않음Order 모듈만이 Order 테이블을 직접 읽고 쓸 수 있음
공통 기능 모듈화 (Shared Concerns Isolation)인증, 로깅, 메시징 등은 별도의 공통 모듈로 분리하고 재사용logging-module, auth-module 등 재사용 가능한 패키지 제공
인터페이스 분리 (ISP)필요한 인터페이스만 제공하고, 과도한 의존을 방지PaymentCommandPaymentQuery 인터페이스를 분리해서 제공
단방향 의존성 (Unidirectional Dependency)계층 간 또는 모듈 간 의존성은 단방향으로만 흐르며 순환 참조 금지Application → Domain → Infrastructure 방향만 허용
graph TD
  SRP["① 책임 분리"]
  Bounded["② 경계 명확성"]
  DIP["③ 의존성 역전"]
  OCP["④ 확장/폐쇄 원칙"]
  Data["⑤ 데이터 격리"]
  Cross["⑥ 공통 기능 모듈화"]
  ISP["⑦ 인터페이스 분리"]
  Flow["⑧ 단방향 의존"]

  SRP --> DIP
  Bounded --> Data
  DIP --> ISP
  Cross --> OCP
  OCP --> Flow

Modular Monolith 핵심 원칙 위반 시 발생하는 주요 안티패턴

위반 원칙발생 안티패턴설명
단일 책임 원칙 (SRP)God Module / Blob하나의 모듈이 너무 많은 책임을 가짐
경계 명확성 위반Data Bleeding다른 모듈의 내부 데이터에 무단 접근
의존성 역전 위반 (DIP)Concrete Dependency Trap추상화 없이 직접 구현체에 의존
인터페이스 분리 원칙 위반 (ISP)Fat Interface불필요한 인터페이스까지 의존
데이터 격리 위반Shared Database / Schema Coupling여러 모듈이 동일한 테이블/스키마를 공유
단방향 의존성 위반Circular Dependency순환 참조가 발생해 의존성 방향이 붕괴
공통 모듈 오남용Core Overload / Utility Abuse공통 모듈이 도메인 로직까지 침범
확장 - 폐쇄 원칙 위반 (OCP)Scattered Conditional Logic기능 추가 시 코드 곳곳에 조건문 확산
대표 안티패턴 상세 분석
항목핵심 요약
God Module단일 책임 원칙 위반으로 하나의 모듈이 모든 것을 담당
Data Bleeding모듈 경계를 무시하고 내부 구현에 직접 접근
Concrete Dependency Trap추상화 없이 직접 구현체 의존으로 변경 전파 발생
Shared DB CouplingDB 스키마 공유로 변경 시 전체 영향
Circular Dependency구조 순환으로 아키텍처 안정성 붕괴
Utility Abuse공통 모듈이 도메인 로직까지 흡수해 중심이 무너짐
God Module (Blob)
Data Bleeding
Concrete Dependency Trap
Shared Database / Schema Coupling
Circular Dependency
Core Overload / Utility Abuse

주요 원리 (Core Principles)

원리설명
도메인 기반 모듈화비즈니스 도메인별로 모듈을 구분하여 응집도 향상 및 변경 파급 최소화
인터페이스 기반 통신모듈 간 상호작용은 명시적으로 정의된 인터페이스 또는 API 를 통해 수행
단방향 의존성상위 → 하위 모듈만 참조하도록 설계 (의존성 역전 원칙 포함)
모듈 캡슐화모듈 내부 구현은 외부에 노출되지 않도록 캡슐화, 공개 API 만 사용 허용
이벤트 기반 통신도메인 이벤트를 통해 비동기 메시징 적용 가능 (Internal Event)
데이터 격리각 모듈은 전용 스키마 또는 테이블을 가지며, 외부 모듈 데이터에 직접 접근 금지
공통 기능 분리인증, 로깅, 모니터링 등은 Cross-cutting 모듈로 구성하여 재사용 가능성 확보
단일 배포 단위 유지시스템 전체가 하나의 배포 단위로 관리되어 CI/CD 단순화

작동 원리 (Operational Mechanisms)

요청 흐름

sequenceDiagram
  participant Client
  participant API Gateway
  participant Module A
  participant Module B
  participant DB

  Client->>API Gateway: HTTP 요청
  API Gateway->>Module A: 요청 라우팅
  Module A->>Module A: 비즈니스 로직 실행
  Module A->>Module B: API 호출 또는 인터페이스 기반 호출
  Module B->>DB: 데이터 처리
  DB-->>Module B: 응답
  Module B-->>Module A: 결과 반환
  Module A-->>API Gateway: 응답 반환
  API Gateway-->>Client: HTTP 응답

모듈 간 통신 방식

방식설명사용 목적
함수 호출 / API 호출모듈 간 직접 호출 (공개 API 또는 인터페이스)동기적 처리 필요 시
도메인 이벤트모듈 간 비동기 이벤트 전달결합도 감소, 확장성 향상
Outbox 패턴내부 트랜잭션에서 외부 이벤트 메시지를 신뢰성 있게 전송외부 시스템 연계 대비
공유 커널 (Shared Kernel)공통된 도메인 모델/계약 정의메시지 스키마 또는 인터페이스 공유 시
계층 간 메시징 구조 vs. 함수 호출 비교

계층 간 통신은 함수 호출 또는 이벤트/메시징 방식으로 이루어질 수 있다.

항목함수 호출 (Synchronous Call)메시징 구조 (Asynchronous Messaging)
통신 방식직접 호출 (service.method())이벤트 발행 후 구독자 처리
결합도높음 (타 모듈의 명세에 의존)낮음 (이벤트 계약만 공유)
응답 보장즉시 반환됨 (Blocking)결과 비동기적, 실패 시 재시도 필요
에러 처리예외로 명확히 처리 가능에러 전파 어려움, DLQ 등 필요
성능빠름, 디버깅 용이느림, 로그 기반 추적 필요
적용 위치Presentation ↔ ApplicationApplication ↔ DomainDomain → ApplicationApplication → 외부 연동 Cross-cutting concerns (e.g., Logging, Audit)
예시orderService.placeOrder()eventPublisher.publish(OrderPlacedEvent)

Outbox 패턴과 이벤트 흐름

sequenceDiagram
  participant Module A
  participant Outbox Table
  participant Message Dispatcher
  participant Message Broker
  participant Module B

  Module A->>Outbox Table: 트랜잭션 내 메시지 기록
  Message Dispatcher->>Outbox Table: 새 메시지 폴링
  Message Dispatcher->>Message Broker: 메시지 발행
  Message Broker->>Module B: 메시지 전달
  Module B->>Module B: 이벤트 핸들링

주요 원리와 매핑되는 작동 구조

원리대응 구조
모듈 캡슐화Interface 패키지 ↔ Implementation 패키지 분리, public only 접근
단방향 의존성아키텍처 테스트 도구로 순환 참조 방지 검증
이벤트 기반 통신EventBus / Kafka / RabbitMQ 와 통합
데이터 격리모듈 전용 DB 스키마 또는 테이블 사용
공통 기능 분리common, shared, core 모듈로 추상화

구조 및 아키텍처

Modular Monolith 아키텍처의 구조 및 구성 요소는 다음과 같은 특징을 갖는다:

flowchart TD
  subgraph Modular Monolith Application
    A[Host App] --> B[User Module]
    A --> C[Order Module]
    A --> D[Payment Module]
    B --> B_API[Public API] --> B_APP[Application Layer] --> B_DOMAIN[Domain Layer] --> B_INFRA[Infrastructure]
    C --> C_API --> C_APP --> C_DOMAIN --> C_INFRA
    D --> D_API --> D_APP --> D_DOMAIN --> D_INFRA
    B_DOMAIN --> Shared[Shared Kernel]
    C_DOMAIN --> Shared
    D_DOMAIN --> Shared
  end

  subgraph Optional Components
    EventBus -.-> B_APP
    EventBus -.-> C_APP
    EventBus -.-> D_APP
  end

계층별 구성 요소 분류

필수 구성 요소
구성 요소설명
Host ApplicationDI 컨테이너, 모듈 로딩, 전체 앱 실행 관리
도메인 기반 모듈각 비즈니스 도메인별로 완결된 기능 집합 (회원, 주문 등)
모듈 인터페이스 (Public API)외부 또는 다른 모듈이 접근 가능한 서비스/이벤트 계약 정의
** 공통 인프라 (Shared Kernel)**로깅, 인증, 보안, 유틸리티 등 공통 컴포넌트 집합
** 단일 데이터베이스 또는 모듈별 테이블 스키마**물리적 공유 가능, 논리적 소유권 기준 설계 필요
** 모듈 내부 계층 구조**Interface → Application → Domain → Infrastructure 계층 구분 유지
선택 구성 요소
구성 요소설명
이벤트 버스 (In-memory / Kafka)모듈 간 비동기 메시징 및 느슨한 결합
Outbox / Inbox 패턴이벤트 일관성, 재처리 보장을 위한 패턴 적용
CQRS / Event Sourcing읽기 - 쓰기 분리 및 상태 추적 목적 설계
API Gateway (Monolith 내부)인증·라우팅·버전 관리 등을 중앙 집중화할 경우 사용
모듈별 UI / 테스트 디렉토리모듈별 독립된 View, Mock 기반 테스트 가능

모듈 내부 계층 구조

계층책임설명
Interface Layer진입점, API/이벤트 인터페이스HTTP 컨트롤러, gRPC 핸들러, 메시지 리스너 등
Application Layer유스케이스, 서비스도메인 로직 조합, 트랜잭션 관리, Command/Query
Domain Layer비즈니스 규칙, 엔티티, 밸류 오브젝트불변 규칙, 도메인 이벤트, 애그리거트 루트 등
Infrastructure Layer외부 자원 접근DB, 메시지 브로커, 외부 API 연동 구현체
Integration Events외부 전달용 이벤트Kafka 또는 내부 EventBus 에 전달되는 직렬화된 이벤트

설계 시 고려사항

측면고려 사항
모듈 경계 관리Bounded Context 기준으로 모듈을 자율성과 응집도를 기준으로 분리
공개 API 관리인터페이스 명시적 선언 + 안정적 버전 관리 (예: API 버전, 메시지 버전)
데이터 설계모듈별 데이터 소유권 명확화 (동일 DB 라도 테이블 네임스페이스 분리 권장)
테스트 전략모듈별 유닛 테스트, 도메인 단위 통합 테스트, Contract Test 고려
MSA 전환성모듈 단위 이벤트 발행 및 Consumer 구성 → 마이크로서비스 분리 시 활용

구현 기법 및 방법

Modular Monolith 의 구현 기법 및 방법은 단순한 코드 구성의 문제가 아니라, 다음의 총체적인 접근을 요구한다:

아키텍처 스타일 및 구조화 전략

구현 기법설명목적
DDD (Domain-Driven Design)도메인 경계를 기준으로 모듈 분리경계 명확화, 도메인 응집도 확보
Clean ArchitectureInterface → App → Domain → Infra관심사 분리, 테스트 용이성 확보
Hexagonal Architecture포트 - 어댑터 구조, 외부 의존성과 내부 도메인 분리도메인 보호, 유연한 외부 연동
Vertical Slice Architecture기능 단위로 UI~DB 까지 수직 분리단일 책임 기능, 배포 및 유지 관리 최적화

모듈 간 통신 전략

모듈 간 통신 전략은 시스템 아키텍처 설계 단계에서 모듈 간 상호작용을 어떻게 설계할지에 대한 전략적 방향성과 결정을 의미한다.
이는 " 동기 vs 비동기 “, " 직접 호출 vs 메시징 “, " 상태 저장 방식 " 등 전체 시스템의 통신 흐름과 아키텍처의 구조적 특징을 정의하는 상위 개념이다.
예를 들어, 이벤트 기반 아키텍처를 채택하거나, Outbox 패턴을 통한 비동기 이벤트 전송을 사용하는 것은 모두 통신 전략에 해당한다.

반면, 모듈 간 통신 방식 패턴은 이러한 전략을 구체적으로 실현하기 위한 설계 및 구현 수준의 기술적 패턴이다.
즉, 어떤 통신 전략을 선택했는지에 따라 그에 적합한 패턴을 적용하게 되며, 이는 코드 레벨에서 실제 모듈 간 연결을 구현하는 방식이다.
예를 들어 Domain Event, Application Messaging, Command Handler, Interface-based 호출 등은 모두 각기 다른 전략을 실현하기 위한 구체적인 패턴이다.

결국, 통신 전략은 " 무엇을 할 것인가?” 에 대한 방향 설정이며, 통신 방식 패턴은 " 어떻게 구현할 것인가?” 에 대한 방법론이다.

구현 기법설명활용 목적
동기 호출모듈 간 직접 인터페이스 호출단순하고 빠른 처리
비동기 이벤트 기반Kafka, In-memory EventBus 활용느슨한 결합, 확장성 확보
Outbox Pattern트랜잭션 내 이벤트 저장 후 별도 전송메시지 유실 방지, 일관성 보장
CQRS명령/쿼리 분리성능 최적화, 복잡성 분리
Event Sourcing이벤트 로그 기반 상태 관리감사 추적, 리플레이 기능 확보
전략 → 패턴 관계 분석

모듈 간 통신 전략은 아키텍처 설계 단계에서 " 어떤 방식의 통신 구조를 채택할지 " 에 대한 전략적 결정을 의미한다.
모듈 간 통신 방식 패턴은 그러한 전략을 실현하기 위한 구체적인 구현 기법 또는 설계 패턴이다. 따라서, 패턴은 전략의 하위 개념으로 분류되며, 전략이 결정되면 이에 맞는 하나 이상의 패턴을 적용할 수 있다.
예를 들어, 비동기 이벤트 기반 전략은 Domain Event, Application Messaging, Outbox Pattern 등의 패턴으로 구현된다.

통신 전략설명추천 구현 패턴/기법주요 기술 스택 예시
동기 호출인터페이스 기반 직접 호출Interface-based Communication
Shared Kernel
Java Interface, Python Class, REST
비동기 이벤트 기반이벤트 발행 후 구독 방식 (Loose Coupling)Domain Event Pattern
Application Messaging
Kafka, RabbitMQ, In-memory Bus
Outbox PatternDB 트랜잭션 + 메시지 전송 분리Transactional Outbox
Polling Publisher
Debezium, Kafka Connect, Redis
CQRS명령 (Command)/조회 (Query) 책임 분리Command Handler Pattern
Query Model
MediatR, Axon, FastAPI CommandBus
Event Sourcing상태 변경을 이벤트 로그로 기록Aggregate Root + Event Replay PatternEventStoreDB, Axon, custom infra
모듈 간 통신 방식 패턴
통신 전략추천 구현 패턴/기법핵심 특징장점한계/주의사항실무 적용 조건
동기 호출Interface-based Communication모듈 간 직접 함수/메서드 호출단순, 직관적, 디버깅 쉬움강결합, 변경 전파 위험변경 가능성 낮고 성능 요구되는 모듈 간
Shared Kernel공통 도메인 코드 공유중복 제거, 코드 재사용경계 침범 위험, 모듈 독립성 저하공통 VO, Enum 등에 한정
비동기 이벤트 기반Domain Event Pattern비동기 이벤트 퍼블리싱/구독느슨한 결합, 확장성 우수멱등성, 이벤트 유실 처리 필요사후 처리 또는 리액티브 시스템
Application Messaging명령/이벤트 메시지 전송명시적 명령 구조화, CQRS 적합오케스트레이션 시 복잡도 증가Command Handler 구조 필요
Outbox PatternTransactional OutboxDB 와 메시지를 트랜잭션으로 묶음데이터 일관성 보장, 메시지 유실 방지Polling 지연, 복잡한 구성강한 일관성이 필요한 업무 로직
Polling PublisherDB 테이블 → 큐로 메시지 발행구현 단순, 신뢰도 확보실시간성 부족, 중복 처리 필요Kafka, Debezium 연계 환경
CQRSCommand Handler Pattern명령 처리 전용 클래스 구조책임 분리, 테스트 용이설계 복잡도 증가복잡한 도메인, R/W 분리 요구
Read Model쿼리 전용 모델 제공읽기 성능 최적화데이터 동기화 관리 필요읽기 집중, 성능 요구 환경
Event SourcingAggregate + Replay Pattern상태 = 이벤트의 합감사 추적, 완전한 변경 이력구현 난이도 높음, 설계 숙련 필요규제 준수, 금융/헬스케어 도메인
Snapshotting이벤트 집계 지점 저장성능 향상스냅샷 관리 필요이벤트 수 많고 복잡한 경우

코드베이스 구조화 및 의존성 제어

구현 기법설명목적
폴더/패키지 네임스페이스 분리도메인별 디렉토리 및 계층화 구조 유지관리 편의성, 정리된 코드베이스
명시적 인터페이스 노출공개 API, 이벤트, 서비스 인터페이스만 허용모듈 간 경계 명확화
의존성 역전 (DI)추상화 레이어를 통해 의존성 주입상위 계층이 하위 계층에 직접 의존하지 않도록 유지

빌드/운영 전략

구현 기법설명
모듈별 테스트 분리유닛, 통합, 계약 테스트를 각 모듈 단위로 구성
빌드 도구 활용Gradle 멀티모듈, npm workspaces 등으로 모듈 간 의존성 명확화
Strangler Fig 패턴기존 모놀리스를 점진적으로 모듈화하면서 리팩토링

장점

분류항목설명
설계높은 응집도 및 캡슐화도메인 중심의 모듈화로 내부 구현이 은닉되며, 책임이 명확히 분리되어 유지보수가 용이함
단일 트랜잭션 경계모든 모듈이 동일한 DB 또는 트랜잭션 컨텍스트를 공유하여 ACID 트랜잭션 유지 가능
개발개발 단순성단일 코드베이스와 일관된 개발 환경 제공, 통합 IDE 및 디버깅 환경 활용 가능
테스트 용이성명확한 모듈 경계로 유닛/통합/계약 테스트 구성 용이, 테스트 자동화에 적합
팀 생산성 향상팀 단위 모듈 책임 할당 가능, 병렬 개발이 가능하며 충돌 최소화
배포/운영배포 단순화단일 아티팩트로 배포, 배포 전략/롤백/버전 관리 단순
운영비용 효율성단일 배포 및 단일 인스턴스 운영으로 인프라 복잡성 및 비용 절감
높은 성능모듈 간 인메모리 함수 호출로 통신 비용 최소화, 직렬화/네트워크 오버헤드 없음
확장성모듈 기반 확장성모듈 단위로 책임 분리되어 기능 확장 시 해당 모듈만 수정 가능
마이크로서비스 전환 용이명확한 모듈 경계와 인터페이스를 기반으로 모듈 단위 서비스 분리 가능 (MSA 전환 준비 구조)
유지보수코드 변경 영향 최소화모듈 간 결합도 최소화로 변경 시 영향 범위가 제한됨
코드 탐색성 향상모듈별 네임스페이스, 폴더 구조가 명확하여 빠른 코드 탐색 가능

단점과 문제점 그리고 해결방안

단점

카테고리항목설명해결책
확장성독립 확장 어려움특정 모듈만 확장하려 해도 전체 애플리케이션 단위 확장 요구됨병목 모듈 추출, 컨테이너 기반 모듈 격리, 수평 확장 전략 적용
배포단일 배포 단위일부 변경에도 전체 시스템 재배포가 필요함CI/CD 자동화, 점진적 MSA 분리, 피처 플래그 활용
기술 다양성기술 스택 고정화단일 런타임으로 다양한 언어/프레임워크 혼용 어려움다중 런타임 컨테이너 기반 설계, 인터페이스 기반 공통 계약 구성
장애 확산단일 장애점하나의 모듈 장애가 전체 시스템 장애로 확산 가능모듈 격리, 장애 탐지 및 복구 자동화, 장애 전파 차단 설계
협업팀 규모 증가 시 충돌동일한 코드베이스에서 여러 팀이 작업 시 충돌, 컨벤션 불일치모듈 책임 분리, Git Flow/Branch 전략, 코드 오너십 명확화
테스트 복잡도인터모듈 통합 테스트 복잡모듈 간 연동 로직이 증가하며 테스트 작성 및 관리 어려움단위 테스트 우선, 계약 테스트/Mock 적용, 테스트 더블 기반 테스트 분리
통신 복잡도인터페이스/이벤트 설계 어려움모듈 간 API/이벤트 설계 미흡 시 유지보수성/이해도 저하명확한 API 정의 규칙, 이벤트 이름 명세화, 통신 프로토콜 표준화 적용

Modular Monolith 는 구조적으로 명확하고 유지보수성이 높은 반면, 확장성과 기술 선택, 장애 대응, 팀 협업 측면에서 제약이 존재한다. 특히 단일 배포 단위의 한계와 팀 간 충돌은 규모가 커질수록 더욱 두드러진다. 이를 해결하기 위해선 컨테이너 기반 격리, 점진적 모듈 추출 전략, 테스트 전략 고도화, 명확한 팀 책임 구분 등이 필요하다.

문제점

카테고리항목원인영향탐지 및 진단예방 방법해결 방법 및 기법
경계 불분명모듈 경계 침범도메인 경계 설계 미흡, 모듈 간 직접 참조코드 스파게티화, 유지보수 어려움정적 분석, 코드 리뷰, 아키텍처 테스트DDD 기반 명확한 컨텍스트 정의, API 기반 통신 규약 설계아키텍처 테스트 도입, 경계 리팩토링
의존성 문제순환 의존성잘못된 의존성 방향 설계컴파일 실패, 빌드 오류, 변경 영향 확산의존성 시각화 도구, 그래프 분석단방향 의존성 원칙, 인터페이스 기반 의존 분리리팩토링, 추상화 레이어 도입
데이터 설계 실패공유 DB 또는 공유 모델 남용모듈 간 직접 DB 접근, 공유 모델을 통한 결합도 상승데이터 일관성 저하, 스키마 변경 시 전체 영향데이터 액세스 추적, SQL 로그 분석API 기반 데이터 접근, 모듈별 DB 뷰 또는 스키마 분리Outbox 패턴, CQRS, DB 마이그레이션 도구 활용
배포 안정성동시 배포 충돌하나의 배포 단위에서 다수 모듈의 동시 수정전체 롤백 발생, 기능 장애 가능Canary, Blue-Green 배포 전략 모니터링모듈별 배포 시뮬레이션, 피처 플래그 및 롤백 기법 구성점진적 기능 활성화, 독립 테스트 후 배포 구조 도입
통신 문제인터페이스 남용세분화되지 않은 API, 잘못된 이벤트 구성통신 프로토콜 혼선, 모듈 간 재사용 어려움인터페이스 호출 빈도 및 실패율 추적계약 기반 인터페이스 설계, 이벤트 명세화이벤트 기반 통신 전환, 인터페이스 리팩토링
성능 문제병목 모듈 집중특정 모듈 과부하, 동기 호출 집중전체 응답 지연, 시스템 병목APM 분석, 응답 시간 프로파일링병목 모듈 별도 분리 또는 캐싱 처리비동기 큐, 메시지 브로커, 서킷 브레이커 도입
복잡성 관리 실패설계 구조 복잡화추상화 남용, 인터페이스 중복, 패턴 무분별 적용개발 속도 저하, 신규 인력 진입 장벽코드 복잡도 분석 도구 활용설계 가이드라인 수립, 정기적인 아키텍처 리뷰단순화 리팩토링, 설계 패턴 통제

Modular Monolith 의 문제점은 실제 구현·운영 단계에서의 경계 침범, 의존성 순환, 공유 자원 오남용, 통신 설계 미흡, 복잡성 증가 등으로 인해 발생한다. 대부분은 명확한 아키텍처 규칙 부족 또는 설계 가이드라인 부재로 인해 발생하며, 이로 인해 시스템 유지보수성과 확장성에 큰 영향을 미친다. 예방을 위해선 설계 단계의 원칙 준수, 아키텍처 테스트 자동화, API/이벤트 명세화, 성능 및 복잡도 지속 진단 도구를 체계적으로 운영하는 것이 핵심이다.

도전 과제

카테고리도전 과제원인/배경영향탐지 및 진단 방법예방 전략 및 해결 기법
모듈 경계 관리모듈 경계 설정·유지도메인 모델 불명확, 요구사항 변화, 팀 간 설계 불일치기능 중복, 결합도 상승, 코드 혼란상호 호출 분석, 코드 리뷰, 이벤트 스토밍Bounded Context 설계, 이벤트 기반 통신, 아키텍처 테스트 자동화
팀 협업 및 조직모듈 간 계약 조율 어려움공유 코드베이스, 계약 변경에 대한 동기화 부족인터페이스 침범, 병합 충돌, 빌드 실패계약 테스트 실패율, 리뷰 주기 분석모듈별 책임 명확화, 계약 기반 CI/CD, 브랜치 전략 고도화
운영 및 배포단일 배포 장애, 모듈 독립성 부족모든 모듈이 하나의 배포 단위로 묶여 있음일부 모듈 오류 시 전체 장애, 롤백 어려움배포 실패 이력, Canary 테스트모듈 리로드, 피처 플래그, Blue-Green 배포, 모듈 분리 가능 설계
통신 및 데이터 일관성교차 모듈 트랜잭션 처리 어려움분산된 기능이 하나의 트랜잭션으로 묶여야 하는 요구데이터 불일치, 실패 전파, 장애 확산트랜잭션 실패율, 데이터 유실 로그 분석Outbox Pattern, SAGA, 명확한 데이터 소유권 정의
테스트 및 품질테스트 자동화·계약 검증 부족인터페이스 기반 테스트 미흡, 통합 테스트 비중 과도테스트 불안정, 배포 실패테스트 커버리지 리포트, 빌드 실패 로그Contract Test (Pact), 격리 테스트, 의존성 주입 테스트 구조화
스케일링 및 성능모듈 간 부하 편중특정 기능 호출 집중, 캐시 미사용, 일괄 처리 부재병목 발생, 시스템 전체 성능 저하APM(예: Datadog), 호출 히트맵 분석CQRS, 캐시 계층화, 읽기 모델 분리, 모듈 독립적 스케일링
모니터링 및 관측성모듈별 메트릭 부재중앙 집중 로깅에 의존, 모듈 단위 관측 설계 누락문제 원인 진단 어려움, 모듈 단위 SLA 미확보분산 추적, 모듈 로그 비율 분석OpenTelemetry, 커스텀 모듈 메트릭, 통합 대시보드 구성
  1. 모듈 경계 관리
    모듈 경계를 명확히 설계하지 않으면 기능 중복과 모듈 간 결합도가 증가한다. 특히 요구사항이 변화하거나 팀 간 협업이 미흡하면 경계가 흐려질 수 있으므로, DDD 기반의 Bounded Context 설정과 정기적인 아키텍처 리팩토링이 필수다.

  2. 팀 협업 및 계약 조율
    공유 코드베이스 환경에서 모듈 간 인터페이스 계약을 동기화하지 않으면 빌드 실패, 코드 충돌이 잦아진다. 모듈별 팀 책임 분배와 **계약 기반 자동화 (CI/CD)**가 중요하며, 브랜치 전략 개선명확한 API 명세가 필요하다.

  3. 운영 및 배포
    단일 배포 구조에서는 하나의 모듈만 장애가 발생해도 전체 시스템이 영향을 받는다. 따라서 피처 플래그, 모듈 리로드, Blue-Green 배포 전략으로 위험을 최소화하고, 장기적으로는 모듈 독립 배포를 고려해야 한다.

  4. 통신 및 데이터 일관성
    교차 모듈 간 트랜잭션은 데이터 일관성 문제를 유발한다. Outbox PatternSAGA는 데이터 일관성을 보장하면서도 모듈 간 느슨한 결합을 유지할 수 있게 해준다.

  5. 테스트 및 품질
    통합 테스트에만 의존하면 결함 위치 파악이 어렵고, 릴리즈 주기가 길어진다. Contract Testing, 의존성 분리 테스트, 테스트 자동화 도구를 통해 각 모듈을 독립적으로 검증하는 구조가 필요하다.

  6. 스케일링 및 성능
    기능 단위로 요청이 집중되면 특정 모듈이 병목이 된다. CQRS 적용, 읽기/쓰기 분리, 캐시 계층화 등의 성능 최적화가 필요하며, 모듈별 독립 스케일링 구조가 성능 향상에 효과적이다.

  7. 모니터링 및 관측성
    모듈 단위로 문제를 진단하기 어렵다면 전체 장애 대응 속도가 느려진다. OpenTelemetry, 분산 추적, 커스텀 모듈 메트릭으로 모듈 레벨의 관측성을 확보해야 운영 안정성을 유지할 수 있다.

Modular Monolith 도전 과제별 실무 대응 사례

도전 과제실무 사례 (기업)대응 전략 / 실제 적용 방법
모듈 경계 설정 및 유지ShopifyShopify 는 도메인 중심 모듈 구조를 유지하면서도 " 패키지 레벨 의존성 그래프 " 를 통해 모듈 간 호출을 자동으로 추적하고, 위반 시 컴파일 에러 발생 구조로 설계함.
Google (Service Weaver)Google 은 각 모듈을 API surface 로 노출하고 인터페이스만 접근 가능하도록 설계. 내부 구현 접근은 Linter 및 Build 시스템에서 금지함.
팀 협업 / 인터페이스 계약StackOverflow각 모듈별 Ownership 을 명확히 하고, Git 단위 PR Template 에 API 변경 시 Impact 분석 필수로 지정. API 문서는 자동화되어 있고, 변경 시 계약 검증 테스트가 병행됨.
운영 및 배포 전략Shopify모놀리스를 유지하지만 모듈마다 독립적인 Feature Flag 를 적용해 모듈 단위의 롤아웃 및 롤백을 가능하게 함. 전체 앱 재시작 없이 모듈 업데이트가 가능함.
트랜잭션과 데이터 일관성GoogleGoogle 의 내부 시스템은 Outbox 패턴을 강화해 사용하며, 모든 모듈 간 커뮤니케이션은 " 이벤트 로그 기반 전파 " 로 처리. DB 트랜잭션 내에 이벤트 삽입 후 전송함.
테스트 및 품질 관리StackOverflow모듈별 테스트 디렉토리를 유지하고, Interface Mock 과 Pact 를 활용한 계약 기반 테스트를 채택. GitHub Action 기반으로 모듈 단위 테스트 자동 실행.
스케일링 및 성능 대응ShopifyCQRS 적용으로 읽기 부하가 높은 모듈은 읽기 전용 캐시로 처리하며, 일부 고부하 모듈은 독립 실행 가능한 Process 로 전환하여 모놀리스 내에서 자체 스케일링을 허용함.
모니터링 및 관측성GoogleOpenTelemetry 기반 Trace ID 를 모듈 간 전파하고, GCP Stackdriver 로 통합 모듈 메트릭을 수집함. 모듈별 latency, error rate, throughput 을 실시간 대시보드로 노출함.

Modular Monolith 아키텍처 검증 체크리스트

Modular Monolith 는 단순한 구조가 아닌, " 모듈화된 아키텍처 원칙 " + " 단일 배포 유닛 “ 을 유지하면서도 MSA 수준의 독립성, 확장성, 테스트성, 관측성을 확보하는 것이 핵심이다.

실무 기업들은 다음과 같은 전략으로 도전 과제를 해결하고 있다:

이 구조와 체크리스트는 “MSA 로 가기 위한 안전한 중간 단계 " 로서 Modular Monolith 를 검증하고 발전시키는 기준이 될 수 있다.

카테고리체크 항목체크 여부 (✓/✗)비고 (설명/도구 등)
🧱 모듈 경계 점검각 도메인은 별도 디렉토리/네임스페이스로 구성되는가
모듈 내부는 Interface → Application → Domain → Infra 계층 구조를 따르는가Clean Architecture 기준
모듈 간 직접 의존 없이 인터페이스 또는 이벤트로 통신하는가REST, EventBus, Kafka 등
호출 의존성을 시각화하거나 분석하는 도구가 적용되어 있는가ArchUnit, Graphviz, jDepend 등
👥 팀 협업 및 계약 전략모듈별 팀 소유권이 명확히 설정되어 있는가코드오너 설정, Git 관리 정책 등
API 변경 시 문서, 테스트, 영향 분석이 자동화되는가GitHub Actions, Swagger, Pact
인터페이스 (API) 는 명시적으로 관리되고 있는가Interface, OpenAPI 등
Contract Testing 도구를 사용하는가Pact, Spring Cloud Contract 등
🚀 운영 및 배포 구조Feature Toggle 또는 설정 기반 Enable/Disable 이 가능한가LaunchDarkly, Spring Config 등
장애 발생 시 일부 모듈만 격리 가능한 구조인가Circuit Breaker, 모듈 격리 처리 등
모듈 수준 Health Check 가 구현되어 있는가Actuator, /health endpoint 등
🔄 트랜잭션 및 데이터 일관성Bounded Context 기준으로 데이터 접근이 구분되는가각 모듈 별 DB schema 또는 view
Cross-module 트랜잭션은 Outbox/Event 기반 처리되는가Transaction + Outbox Table
메시지에 version 및 idempotency key 가 포함되는가Kafka header, message body 등
🧪 테스트 및 품질 전략모듈마다 독립된 유닛/통합 테스트 구조가 있는가tests/order/test_*.py
계약 기반 테스트 (Contract Test) 가 존재하는가Pact, Interface Test 등
테스트는 CI 파이프라인에서 자동으로 실행되는가GitHub Actions, GitLab CI 등
📈 성능 및 스케일링 전략읽기/쓰기 분리 전략 (CQRS 등) 이 구현되어 있는가ReadModel, Projection 등
고부하 모듈은 캐시 또는 독립 서비스로 확장 가능한가Redis, 분리된 실행 컨테이너 등
모듈별 성능 지표가 수집되고 알림 설정이 있는가Prometheus, Grafana, Alertmanager
🔭 모니터링 및 관측성모듈별 로그, 메트릭, 트레이스를 수집하는가OpenTelemetry, Loki, Prometheus
Trace ID 가 모듈 간에 전파되는가Trace Context, B3, W3C Header 등
APM 또는 관측 도구가 도입되어 있는가Jaeger, Zipkin, New Relic 등

분류 기준에 따른 종류 및 유형

분류 기준유형설명적용 예시
모듈 구조 방식계층형 모듈 (Layered)기술 계층 (UI, 서비스, 데이터 접근) 중심으로 모듈을 분리MVC, N-Tier 기반 구조
도메인형 모듈 (Domain-Based)도메인 또는 경계 컨텍스트 중심으로 기능을 그룹화DDD 기반 User, Order, Payment 모듈
기능형 모듈 (Feature-Based)CRUD 단위 기능별로 모듈 구성게시판, 알림 기능 단위 분리
Vertical Slice기능 흐름 전체를 모듈 단위로 구성입력 → 검증 → 처리 → 응답 구조 모듈화
통신 방식동기 호출 (In-process)내부 메서드, 인터페이스 호출 방식의 직접 통신Service → Repository 직접 호출
비동기 호출 (Event-driven)도메인 이벤트, 메시지 큐 기반 통신으로 느슨한 결합 제공Kafka, RabbitMQ 사용 구조
데이터 관리 방식공유 스키마모든 모듈이 하나의 DB 및 스키마를 공유단일 PostgreSQL 스키마
분리 스키마모듈별 테이블, DB 스키마를 분리하여 소유권 명확화모듈별 스키마 및 접근 계층 구분
모듈별 DB 인스턴스 분리아예 모듈별 DB 인스턴스를 분리하여 MSA 전환 대비각 모듈별 MySQL/PostgreSQL 분리
배포 전략단일 배포전체 애플리케이션을 하나의 JAR/WAR 로 빌드 및 배포Spring Boot Monolith 구조
선택적 모듈 배포일부 모듈만 선택적으로 교체 가능한 구조 (플러그인/피처 단위 빌드)모듈별 Docker 이미지 분리 가능
저장소 관리 방식모노레포 (Monorepo)모든 모듈을 단일 저장소에서 관리GitHub 단일 리포지토리
멀티레포 (Manyrepo)모듈별로 별도 저장소 구성 → 분리/독립 배포에 유리GitHub 조직 내 모듈별 리포지토리
아키텍처 스타일DDD 기반도메인 주도 설계를 통해 경계 및 책임 분리 강조Context 별 Aggregate 설계 구조
CQRS 기반읽기/쓰기 모델 분리, Event 기반 처리 강화Query/Command Handler 분리 구조
도메인 범위 구성단일 도메인 집중형하나 또는 두 개의 비즈니스 도메인에 집중ERP 의 인사관리 단위 구성
다중 도메인 분리형여러 도메인을 명확히 구분하고 통신 및 데이터 분리 적용전자상거래의 주문/결제/상품 모듈화
운영 확장성고정형 모듈런타임 독립성 없이 내부 의존성이 강한 구조정적 구성 기반 모듈 설계 구조
확장형 모듈추후 독립 실행 가능성을 고려해 모듈 인터페이스와 의존성 분리 설계 적용MSA 이행 대비 구조화 모듈
  1. 모듈 구조 방식:
    기술 계층 (Layer), 도메인 (Domain), 기능 (Feature), Vertical Slice 방식으로 나뉘며, 도메인/기능 중심 분리는 비즈니스 변화에 유리하고, 계층형은 기술적 통합 관리에 유리함.

  2. 통신 방식:
    동기 호출 구조는 단순성과 성능에 유리하나 결합도가 높고, 비동기 방식은 유연한 확장성과 오류 복원력, 낮은 결합도를 제공함. 이벤트 기반 구조가 MSA 전환을 유도하기에 적합함.

  3. 데이터 관리 방식:
    공유 스키마는 빠른 구축에 유리하나 변경 전파 리스크가 크며, 분리 스키마 및 DB 인스턴스 분리는 독립성과 유지보수성 확보에 유리. MSA 전환을 고려한다면 필수 고려 사항.

  4. 배포 전략:
    단일 배포 구조는 배포 자동화가 간편하나, 모듈 영향도가 크며 전체 재배포가 필요하다. 선택적 모듈 배포 또는 모듈별 Docker 이미지 분리가 이상적 운영 구조.

  5. 저장소 관리 방식:
    Monorepo 는 변경 추적과 협업이 쉬우나 충돌이 많고, Manyrepo 는 독립 배포와 이력 관리에 유리하며 서비스 분리 전환에도 강점을 가진다.

  6. 아키텍처 스타일:
    DDD, CQRS 등은 모듈 간 경계 명확화와 확장성, 이벤트 처리 기반 설계에 필수. 특히 CQRS 기반 구조는 읽기와 쓰기 흐름 분리로 시스템 안정성 확보에 효과적.

  7. 도메인 범위 구성:
    단일 도메인 구조는 집중적인 기능 최적화가 가능하나 확장성 부족, 다중 도메인 구조는 시스템 복잡도는 올라가지만 팀 간 분리 및 병렬 개발에 유리함.

  8. 운영 확장성:
    고정형은 단순하지만 유연성이 부족하고, 확장형은 향후 마이크로서비스 전환 및 클라우드 네이티브 대응 측면에서 필수 구조.

실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점

카테고리항목설명권장 사항
설계도메인 경계 정의모듈 간 경계가 흐려지지 않도록 도메인 중심으로 구조 설계DDD 기반 이벤트 스토밍, 명확한 Bounded Context 매핑 및 문서화
인터페이스 표준화모듈 간 계약이 불분명하면 통합·확장 시 문제 발생명세 기반 API 설계 (OpenAPI), 인터페이스 버전 관리 정책 수립
설계 표준 운영팀 간 설계/코드 기준이 달라지면 유지보수 어려움공통 코딩 컨벤션, 모듈 표준 템플릿 및 폴더 구조 정의
구현의존성 관리순환 의존 및 결합도 증가 위험DI, 정적 분석 도구 (ArchUnit), 아키텍처 린터 도입
공통 코드 오남용 방지공통 유틸 또는 레이어가 경계 침범 유도 가능공통 코드는 명확한 경계 (Shared Module 또는 Core Layer) 로 관리
데이터 격리DB 공유 시 모듈 간 결합 발생모듈 단위 스키마 분리, 레포지토리 캡슐화, API 기반 데이터 접근 전략
품질테스트 전략모듈 경계 테스트 미비 시 장애 발견 지연계약 기반 테스트, 모듈 단위 유닛 테스트 + 통합 테스트 병행
경계 위반 감지무분별한 의존 또는 직접 참조아키텍처 테스트 도구 (e.g. ArchUnit, NetArchTest), 인터페이스만 통한 접근
운영CI/CD 전략전체 배포와 모듈 계약의 상호 영향도 존재Contract-first 테스트, 버전 고정 및 점진적 릴리즈 전략 적용
배포 안정화전체 시스템 중단 또는 장애 대응 미흡Blue-Green, Canary 등 무중단 배포 전략 구성
환경 일관성 유지환경 차이로 발생하는 버그 대응 어려움컨테이너 기반 환경 일치화, IaC 기반 환경 자동화
모니터링/관찰성 강화모듈 수준의 문제 파악 어려움모듈 단위 로깅/메트릭/트레이싱 구성, APM 및 OpenTelemetry 통합
협업/문화모듈 기반 팀 운영중앙 집중형 조직은 경계와 책임의 모호성 유발Conway’s Law 고려한 팀 - 모듈 정렬, 문서 기반 협업
문서화 및 시각화설계 지식이 암묵적으로 남아 있을 경우 온보딩/유지보수에 문제 발생API 문서 자동화, 도메인/모듈 다이어그램 관리 (C4 Model 등)

설계 측면

구분내용
경계 설정DDD 기반 Bounded Context 명확히 구분, interfaceimplementation 분리
의존성 관리DI 적용, 계층 간 단방향 의존 (예: Presentation → Application → Domain)
인터페이스각 모듈은 내부 인터페이스를 통해서만 상호작용, 외부 노출 최소화

품질 측면

테스트 전략
구분전략
유닛 테스트각 모듈 내부 서비스/도메인에 대해 단위 테스트 작성 (Mock 활용)
모듈 테스트각 모듈 인터페이스를 외부처럼 호출하여 모듈 단위 통합 테스트 수행
계약 기반 테스트다른 모듈과의 인터페이스 연동을 Contract Test 로 명시 (MSA 전환 대비)
테스트 격리테스트용 모듈 구성 또는 테스트 전용 ApplicationContext 구성 (Spring) 사용

운영 측면

항목설명도구/기술 예시
CI/CD모듈 단위 단위 테스트 → 전체 빌드 배포GitHub Actions, GitLab CI
Observability로그, 메트릭, 트레이싱 통합OpenTelemetry, Grafana, ELK Stack
Static Architecture Check경계 위반, 순환 참조 검사ArchUnit, PyArchitecture
Contract Testing모듈 간 계약 위반 방지Pact, Dredd, OpenAPI Mocking
Boundary Enforcement인터페이스 외부 접근 방지린트 규칙, 모듈 바인딩 제한

최적화하기 위한 고려사항 및 주의할 점

카테고리항목설명권장사항
성능 최적화모듈 간 통신 최적화모듈 간 RPC, API 호출에 따른 지연 및 오버헤드 최소화인메모리 호출, 비동기 처리, 이벤트 집계, 배치 처리 활용
캐싱 전략반복 조회되는 데이터의 성능 향상 및 부하 감소모듈별 Redis, CQRS+Read Cache 구성
병목 분석 및 모니터링자원 과다 사용 또는 느린 처리 흐름의 모듈 식별APM, 로깅/모니터링 도구로 성능 추적 및 튜닝
데이터 접근 최적화쿼리 병목, 불필요한 조인, 과도한 DB 호출 등인덱스, 쿼리 튜닝, 연결 풀 분리, 캐시 계층 도입
확장성 전략모듈 분리 전략모놀리식 구조 내에서도 모듈을 독립적으로 확장 가능해야 함느슨한 결합 구조 유지, 점진적 MSA 분리 전략
무상태 설계확장을 위한 기본 전제 조건으로 세션/상태 외부 저장세션 저장소 외부화 (예: Redis), 무상태 API 설계
모듈 용량/복잡도 관리과도하게 커진 모듈은 오히려 단일 장애점으로 작용 가능기능 단위 수직 슬라이스로 구조 분할
유지보수성코드 품질 관리시간이 지남에 따라 레거시 코드가 쌓이고 유지보수 비용 증가정기적 리팩토링, 정적 분석 도구, 코드 리뷰 자동화
문서화 및 설계 기록의사결정의 흐름과 API 변화 추적이 어려울 수 있음ADR 작성, Swagger/OpenAPI 기반 문서 자동화
테스트 전략통합 테스트 난이도 상승, 모듈 간 계약 위반 리스크단위 + 통합 + Contract Testing 병행
보안 및 복원력인증·인가 체계통합 시스템에서 공통 인증 모듈은 전체 보안에 영향을 줌JWT, OAuth2, RBAC, 보안 로깅
데이터 보호민감 정보 유출, 저장·전송 중 보안 리스크TLS, DB 암호화, Vault 기반 키 관리
장애 격리 및 복구하나의 모듈 오류가 전체 시스템 장애로 이어질 수 있음Circuit Breaker, 타임아웃, 롤백 전략
백업 및 재해복구 계획운영 DB 와 아키텍처 요소에 대한 복구 시나리오 미비주기적 백업 + DR 환경 구성 + 복구 시뮬레이션
구조적 품질인터페이스 경량화모듈 간 API 복잡도는 유지보수성과 테스트 복잡도를 증가시킴작고 명확한 계약, DTO 분리, 버전 관리 적용
데이터 일관성트랜잭션 경계가 모듈별로 나뉘어 데이터 불일치 발생 가능이벤트 기반 최종 일관성, 보상 트랜잭션 (Saga)
과도한 모듈화모듈 수 증가 시 관리 비용 및 복잡성 증가 가능도메인 중심 적정 분리, 공통 유틸리티 모듈화 기준 수립

실무 사용 예시

카테고리사용 예시사용 목적사용 기술/개념주요 효과
전자상거래 플랫폼Shopify 등상품/주문/결제 모듈화, 성능 확장성 확보Ruby on Rails, MySQL, Redis, Event Bus대규모 트래픽 처리, 기능 분리, 유지보수 향상
기업 포털사내 인트라넷 등사람/조직/게시판 등 역할별 모듈화Spring Boot, Thymeleaf, LDAP역할 기반 관리, 병렬 개발, 보안 구분
헬스케어 시스템진료 플랫폼, 병원 시스템환자/의료진/처방 모듈 분리 및 보안 강화Node.js, MongoDB, Kafka, Kubernetes모듈별 접근 제어, 데이터 보안, 감사 추적
금융 서비스온라인 은행, 보험 플랫폼계좌/결제/리스크 모듈화 및 트랜잭션 안정성 확보Spring Boot, PostgreSQL, Kafka, Docker규제 준수, 데이터 정합성, 이벤트 기반 비동기 처리
교육 플랫폼온라인 러닝 시스템강의/학생/평가/인증 모듈 구성Django, PostgreSQL, Redis기능 독립성 확보, 학습 데이터 확장성
콘텐츠 플랫폼CMS 등콘텐츠/사용자/워크플로우 모듈화.NET Core, MediatR, Elasticsearch, RabbitMQ검색 최적화, 모듈 기반 변경 유연성
전환 준비 구조성장 기반 모놀리스마이크로서비스 전환 전 단계적 분리 구조Clean Architecture, DDD, CQRS + Outbox Pattern모듈 경계 유지, 점진적 전환 가능성 확보

활용 사례

사례 1: E- 커머스 플랫폼

시스템 구성:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
┌─────────────────────────────────────┐
│           API Gateway               │
├─────────────────────────────────────┤
│  ┌─────────┐  ┌─────────┐  ┌─────── │
│  │Product  │  │Order    │  │Payment │
│  │Module   │  │Module   │  │Module  │
│  └─────────┘  └─────────┘  └─────── │
│  ┌─────────┐  ┌─────────┐  ┌─────── │
│  │User     │  │Inventory│  │Shipping│
│  │Module   │  │Module   │  │Module  │
│  └─────────┘  └─────────┘  └─────── │
├─────────────────────────────────────┤
│           Event Bus                 │
├─────────────────────────────────────┤
│         Shared Kernel               │
└─────────────────────────────────────┘
graph TB
    subgraph "E-Commerce Modular Monolith"
        A[API Gateway] --> B[Product Module]
        A --> C[Order Module]
        A --> D[Payment Module]
        A --> E[User Module]
        A --> F[Inventory Module]
        A --> G[Shipping Module]
        
        H[Event Bus] --> B
        H --> C
        H --> D
        H --> E
        H --> F
        H --> G
        
        I[Shared Kernel] --> B
        I --> C
        I --> D
        I --> E
        I --> F
        I --> G
        
        B --> J[(Product DB)]
        C --> K[(Order DB)]
        D --> L[(Payment DB)]
        E --> M[(User DB)]
        F --> N[(Inventory DB)]
        G --> O[(Shipping DB)]
    end

활용 사례 Workflow:

sequenceDiagram
    participant Customer
    participant API Gateway
    participant Product Module
    participant Order Module
    participant Payment Module
    participant Event Bus
    participant Inventory Module
    participant Shipping Module
    
    Customer->>API Gateway: Browse Products
    API Gateway->>Product Module: Get Product List
    Product Module-->>API Gateway: Product Data
    API Gateway-->>Customer: Product Catalog
    
    Customer->>API Gateway: Create Order
    API Gateway->>Order Module: Process Order
    Order Module->>Event Bus: Publish OrderCreated Event
    Event Bus->>Inventory Module: Update Stock
    Event Bus->>Payment Module: Process Payment
    
    Payment Module->>Event Bus: Publish PaymentProcessed Event
    Event Bus->>Shipping Module: Prepare Shipment
    Event Bus->>Order Module: Update Order Status
    
    Order Module-->>API Gateway: Order Confirmation
    API Gateway-->>Customer: Order Success

역할 및 차이점:

구현 예시:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# 모듈러 모놀리틱 E-커머스 플랫폼 구현 예시

from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
import uuid
import asyncio
from collections import defaultdict

# =============================================================================
# 공유 커널 (Shared Kernel) - 공통 기능
# =============================================================================

class DomainEvent:
    """도메인 이벤트 기본 클래스"""
    def __init__(self, event_type: str, data: Dict[str, Any]):
        self.event_id = str(uuid.uuid4())
        self.event_type = event_type
        self.data = data
        self.timestamp = datetime.now()

class EventBus:
    """이벤트 버스 - 모듈 간 비동기 통신"""
    def __init__(self):
        self._subscribers: Dict[str, List[callable]] = defaultdict(list)
    
    def subscribe(self, event_type: str, handler: callable):
        """이벤트 구독"""
        self._subscribers[event_type].append(handler)
    
    async def publish(self, event: DomainEvent):
        """이벤트 발행"""
        handlers = self._subscribers.get(event.event_type, [])
        for handler in handlers:
            try:
                if asyncio.iscoroutinefunction(handler):
                    await handler(event)
                else:
                    handler(event)
            except Exception as e:
                print(f"Error handling event {event.event_type}: {e}")

class ModuleInterface(ABC):
    """모듈 인터페이스 - 모든 모듈이 구현해야 하는 기본 인터페이스"""
    
    @abstractmethod
    def get_name(self) -> str:
        pass
    
    @abstractmethod
    async def initialize(self, event_bus: EventBus):
        pass

# =============================================================================
# 상품 모듈 (Product Module)
# =============================================================================

@dataclass
class Product:
    """상품 엔티티"""
    id: str
    name: str
    description: str
    price: float
    category: str
    stock: int = 0
    created_at: datetime = field(default_factory=datetime.now)

class ProductRepository:
    """상품 리포지토리"""
    def __init__(self):
        self._products: Dict[str, Product] = {}
    
    def save(self, product: Product) -> Product:
        self._products[product.id] = product
        return product
    
    def find_by_id(self, product_id: str) -> Optional[Product]:
        return self._products.get(product_id)
    
    def find_all(self) -> List[Product]:
        return list(self._products.values())
    
    def find_by_category(self, category: str) -> List[Product]:
        return [p for p in self._products.values() if p.category == category]

class ProductService:
    """상품 도메인 서비스"""
    def __init__(self, repository: ProductRepository, event_bus: EventBus):
        self.repository = repository
        self.event_bus = event_bus
    
    async def create_product(self, name: str, description: str, price: float, 
                           category: str, stock: int) -> Product:
        """상품 생성"""
        product = Product(
            id=str(uuid.uuid4()),
            name=name,
            description=description,
            price=price,
            category=category,
            stock=stock
        )
        
        saved_product = self.repository.save(product)
        
        # 상품 생성 이벤트 발행
        event = DomainEvent("ProductCreated", {
            "product_id": saved_product.id,
            "name": saved_product.name,
            "price": saved_product.price,
            "stock": saved_product.stock
        })
        await self.event_bus.publish(event)
        
        return saved_product
    
    def get_product(self, product_id: str) -> Optional[Product]:
        """상품 조회"""
        return self.repository.find_by_id(product_id)
    
    def get_products_by_category(self, category: str) -> List[Product]:
        """카테고리별 상품 조회"""
        return self.repository.find_by_category(category)

class ProductModule(ModuleInterface):
    """상품 모듈"""
    def __init__(self):
        self.repository = ProductRepository()
        self.service = None
        self.event_bus = None
    
    def get_name(self) -> str:
        return "ProductModule"
    
    async def initialize(self, event_bus: EventBus):
        self.event_bus = event_bus
        self.service = ProductService(self.repository, event_bus)
        
        # 재고 업데이트 이벤트 구독
        event_bus.subscribe("StockUpdated", self._handle_stock_update)
    
    async def _handle_stock_update(self, event: DomainEvent):
        """재고 업데이트 이벤트 처리"""
        product_id = event.data.get("product_id")
        new_stock = event.data.get("new_stock")
        
        product = self.repository.find_by_id(product_id)
        if product:
            product.stock = new_stock
            self.repository.save(product)

# =============================================================================
# 주문 모듈 (Order Module)
# =============================================================================

class OrderStatus(Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    PROCESSING = "processing"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

@dataclass
class OrderItem:
    """주문 항목"""
    product_id: str
    product_name: str
    quantity: int
    unit_price: float
    
    @property
    def total_price(self) -> float:
        return self.quantity * self.unit_price

@dataclass
class Order:
    """주문 엔티티"""
    id: str
    customer_id: str
    items: List[OrderItem]
    status: OrderStatus = OrderStatus.PENDING
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)
    
    @property
    def total_amount(self) -> float:
        return sum(item.total_price for item in self.items)

class OrderRepository:
    """주문 리포지토리"""
    def __init__(self):
        self._orders: Dict[str, Order] = {}
    
    def save(self, order: Order) -> Order:
        order.updated_at = datetime.now()
        self._orders[order.id] = order
        return order
    
    def find_by_id(self, order_id: str) -> Optional[Order]:
        return self._orders.get(order_id)
    
    def find_by_customer(self, customer_id: str) -> List[Order]:
        return [o for o in self._orders.values() if o.customer_id == customer_id]

class OrderService:
    """주문 도메인 서비스"""
    def __init__(self, repository: OrderRepository, event_bus: EventBus):
        self.repository = repository
        self.event_bus = event_bus
    
    async def create_order(self, customer_id: str, items: List[Dict]) -> Order:
        """주문 생성"""
        order_items = []
        for item_data in items:
            order_item = OrderItem(
                product_id=item_data["product_id"],
                product_name=item_data["product_name"],
                quantity=item_data["quantity"],
                unit_price=item_data["unit_price"]
            )
            order_items.append(order_item)
        
        order = Order(
            id=str(uuid.uuid4()),
            customer_id=customer_id,
            items=order_items
        )
        
        saved_order = self.repository.save(order)
        
        # 주문 생성 이벤트 발행
        event = DomainEvent("OrderCreated", {
            "order_id": saved_order.id,
            "customer_id": saved_order.customer_id,
            "total_amount": saved_order.total_amount,
            "items": [{"product_id": item.product_id, "quantity": item.quantity} 
                     for item in saved_order.items]
        })
        await self.event_bus.publish(event)
        
        return saved_order
    
    async def update_order_status(self, order_id: str, status: OrderStatus):
        """주문 상태 업데이트"""
        order = self.repository.find_by_id(order_id)
        if order:
            old_status = order.status
            order.status = status
            self.repository.save(order)
            
            # 주문 상태 변경 이벤트 발행
            event = DomainEvent("OrderStatusChanged", {
                "order_id": order_id,
                "old_status": old_status.value,
                "new_status": status.value,
                "customer_id": order.customer_id
            })
            await self.event_bus.publish(event)

class OrderModule(ModuleInterface):
    """주문 모듈"""
    def __init__(self):
        self.repository = OrderRepository()
        self.service = None
        self.event_bus = None
    
    def get_name(self) -> str:
        return "OrderModule"
    
    async def initialize(self, event_bus: EventBus):
        self.event_bus = event_bus
        self.service = OrderService(self.repository, event_bus)
        
        # 결제 완료 이벤트 구독
        event_bus.subscribe("PaymentProcessed", self._handle_payment_processed)
    
    async def _handle_payment_processed(self, event: DomainEvent):
        """결제 완료 이벤트 처리"""
        order_id = event.data.get("order_id")
        if order_id:
            await self.service.update_order_status(order_id, OrderStatus.CONFIRMED)

# =============================================================================
# 결제 모듈 (Payment Module)
# =============================================================================

class PaymentStatus(Enum):
    PENDING = "pending"
    PROCESSING = "processing"
    SUCCESS = "success"
    FAILED = "failed"
    REFUNDED = "refunded"

@dataclass
class Payment:
    """결제 엔티티"""
    id: str
    order_id: str
    customer_id: str
    amount: float
    status: PaymentStatus = PaymentStatus.PENDING
    payment_method: str = "credit_card"
    created_at: datetime = field(default_factory=datetime.now)
    processed_at: Optional[datetime] = None

class PaymentRepository:
    """결제 리포지토리"""
    def __init__(self):
        self._payments: Dict[str, Payment] = {}
    
    def save(self, payment: Payment) -> Payment:
        self._payments[payment.id] = payment
        return payment
    
    def find_by_id(self, payment_id: str) -> Optional[Payment]:
        return self._payments.get(payment_id)
    
    def find_by_order_id(self, order_id: str) -> Optional[Payment]:
        for payment in self._payments.values():
            if payment.order_id == order_id:
                return payment
        return None

class PaymentService:
    """결제 도메인 서비스"""
    def __init__(self, repository: PaymentRepository, event_bus: EventBus):
        self.repository = repository
        self.event_bus = event_bus
    
    async def process_payment(self, order_id: str, customer_id: str, 
                            amount: float, payment_method: str = "credit_card") -> Payment:
        """결제 처리"""
        payment = Payment(
            id=str(uuid.uuid4()),
            order_id=order_id,
            customer_id=customer_id,
            amount=amount,
            payment_method=payment_method,
            status=PaymentStatus.PROCESSING
        )
        
        # 실제 결제 처리 로직 (외부 결제 게이트웨이 연동)
        # 여기서는 단순화하여 항상 성공으로 처리
        await asyncio.sleep(0.1)  # 결제 처리 시뮬레이션
        
        payment.status = PaymentStatus.SUCCESS
        payment.processed_at = datetime.now()
        
        saved_payment = self.repository.save(payment)
        
        # 결제 완료 이벤트 발행
        event = DomainEvent("PaymentProcessed", {
            "payment_id": saved_payment.id,
            "order_id": saved_payment.order_id,
            "customer_id": saved_payment.customer_id,
            "amount": saved_payment.amount,
            "status": saved_payment.status.value
        })
        await self.event_bus.publish(event)
        
        return saved_payment

class PaymentModule(ModuleInterface):
    """결제 모듈"""
    def __init__(self):
        self.repository = PaymentRepository()
        self.service = None
        self.event_bus = None
    
    def get_name(self) -> str:
        return "PaymentModule"
    
    async def initialize(self, event_bus: EventBus):
        self.event_bus = event_bus
        self.service = PaymentService(self.repository, event_bus)
        
        # 주문 생성 이벤트 구독
        event_bus.subscribe("OrderCreated", self._handle_order_created)
    
    async def _handle_order_created(self, event: DomainEvent):
        """주문 생성 이벤트 처리 - 자동 결제 처리"""
        order_id = event.data.get("order_id")
        customer_id = event.data.get("customer_id")
        total_amount = event.data.get("total_amount")
        
        if order_id and customer_id and total_amount:
            await self.service.process_payment(order_id, customer_id, total_amount)

# =============================================================================
# 애플리케이션 호스트 (Application Host)
# =============================================================================

class ECommerceApplication:
    """E-커머스 애플리케이션 호스트"""
    def __init__(self):
        self.event_bus = EventBus()
        self.modules: Dict[str, ModuleInterface] = {}
        
        # 모듈 등록
        self.register_module(ProductModule())
        self.register_module(OrderModule())
        self.register_module(PaymentModule())
    
    def register_module(self, module: ModuleInterface):
        """모듈 등록"""
        self.modules[module.get_name()] = module
    
    async def initialize(self):
        """애플리케이션 초기화"""
        print("🚀 E-커머스 모듈러 모놀리틱 애플리케이션 초기화 중…")
        
        for name, module in self.modules.items():
            await module.initialize(self.event_bus)
            print(f"✅ {name} 초기화 완료")
        
        print("🎉 애플리케이션 초기화 완료!")
    
    def get_module(self, module_name: str) -> Optional[ModuleInterface]:
        """모듈 조회"""
        return self.modules.get(module_name)

# =============================================================================
# 사용 예시 및 테스트
# =============================================================================

async def main():
    """애플리케이션 실행 예시"""
    app = ECommerceApplication()
    await app.initialize()
    
    # 모듈별 서비스 접근
    product_module = app.get_module("ProductModule")
    order_module = app.get_module("OrderModule")
    payment_module = app.get_module("PaymentModule")
    
    print("\n📦 상품 생성 예시:")
    # 상품 생성
    laptop = await product_module.service.create_product(
        name="Gaming Laptop",
        description="고성능 게이밍 노트북",
        price=1500000.0,
        category="Electronics",
        stock=10
    )
    print(f"생성된 상품: {laptop.name} - {laptop.price:,}원")
    
    mouse = await product_module.service.create_product(
        name="Gaming Mouse",
        description="게이밍 마우스",
        price=80000.0,
        category="Electronics",
        stock=50
    )
    print(f"생성된 상품: {mouse.name} - {mouse.price:,}원")
    
    print("\n🛒 주문 생성 예시:")
    # 주문 생성
    order = await order_module.service.create_order(
        customer_id="customer_123",
        items=[
            {
                "product_id": laptop.id,
                "product_name": laptop.name,
                "quantity": 1,
                "unit_price": laptop.price
            },
            {
                "product_id": mouse.id,
                "product_name": mouse.name,
                "quantity": 2,
                "unit_price": mouse.price
            }
        ]
    )
    print(f"생성된 주문 ID: {order.id}")
    print(f"주문 총액: {order.total_amount:,}원")
    print(f"주문 상태: {order.status.value}")
    
    # 잠시 대기 (이벤트 처리 시간)
    await asyncio.sleep(0.2)
    
    # 결제 확인
    payment = payment_module.repository.find_by_order_id(order.id)
    if payment:
        print(f"\n💳 결제 처리 완료:")
        print(f"결제 ID: {payment.id}")
        print(f"결제 상태: {payment.status.value}")
        print(f"결제 금액: {payment.amount:,}원")
    
    # 주문 상태 확인
    updated_order = order_module.repository.find_by_id(order.id)
    print(f"\n📋 최종 주문 상태: {updated_order.status.value}")

if __name__ == "__main__":
    asyncio.run(main())

사례 2: 이커머스 플랫폼

시스템 구성

graph TD
    A[Order Module] -->|API| B[Payment Module]
    B -->|API| C[Inventory Module]
    D[Shared DB] -- Order Table --> A
    D -- Payment Table --> B
    D -- Inventory Table --> C

Workflow

  1. 사용자가 주문 생성
  2. 주문 모듈이 결제 모듈에 결제 요청
  3. 결제 모듈이 재고 모듈에 재고 확인 요청
  4. 결제 및 재고 확인 후 주문 완료

역할

차이점

구현 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 모듈별로 클래스로 분리 (간단 예시)
class OrderModule:
    def create_order(self, item_id, user_id):
        # 결제 모듈 호출
        payment_result = PaymentModule().process_payment(user_id, 10000)
        if payment_result:
            # 재고 모듈 호출
            inventory_result = InventoryModule().check_stock(item_id)
            if inventory_result:
                return "Order created"
        return "Order failed"

class PaymentModule:
    def process_payment(self, user_id, amount):
        return True  # 결제 성공 가정

class InventoryModule:
    def check_stock(self, item_id):
        return True  # 재고 있음 가정

# 사용 예시
order = OrderModule()
print(order.create_order("item1", "user1"))

사례 3: 전자상거래 플랫폼 (Shopify 사례 기반)

시나리오: 대규모 전자상거래 플랫폼 (Shopify 사례 기반)

시스템 구성:

graph TB
    subgraph "Global Load Balancer"
        LB[Load Balancer]
    end
    
    subgraph "Shopify Modular Monolith"
        direction TB
        
        subgraph "API Gateway"
            API[Rails API Gateway]
        end
        
        subgraph "Core Modules"
            direction LR
            OM[Orders Module]
            IM[Inventory Module]
            PM[Payments Module]
            CM[Customers Module]
            SM[Shipping Module]
            AM[Analytics Module]
        end
        
        subgraph "Shared Infrastructure"
            CACHE[(Redis/Memcached Cache)]
            EVENT[Event Bus]
            QUEUE[Background Jobs]
        end
    end
    
    subgraph "Data Layer - Pods Architecture"
        direction LR
        POD1[(MySQL Pod 1<br/>Shops 1-1000)]
        POD2[(MySQL Pod 2<br/>Shops 1001-2000)]
        POD3[(MySQL Pod 3<br/>Shops 2001-3000)]
        PODN[(MySQL Pod N<br/>Shops N…)]
    end
    
    subgraph "External Services"
        PAYMENT_GW[Payment Gateways]
        SHIPPING_API[Shipping APIs]
        ANALYTICS_SV[Analytics Services]
    end
    
    subgraph "Monitoring & Observability"
        LOGS[Centralized Logging]
        METRICS[Metrics & Monitoring]
        TRACE[Distributed Tracing]
    end
    
    CLIENT[Clients] --> LB
    LB --> API
    
    API --> OM
    API --> IM
    API --> PM
    API --> CM
    API --> SM
    API --> AM
    
    OM --> EVENT
    IM --> EVENT
    PM --> EVENT
    CM --> EVENT
    SM --> EVENT
    
    EVENT --> QUEUE
    
    OM --> CACHE
    IM --> CACHE
    PM --> CACHE
    CM --> CACHE
    SM --> CACHE
    
    OM -.->|Shop-based routing| POD1
    OM -.->|Shop-based routing| POD2
    OM -.->|Shop-based routing| POD3
    IM -.->|Shop-based routing| PODN
    
    PM --> PAYMENT_GW
    SM --> SHIPPING_API
    AM --> ANALYTICS_SV
    
    API --> LOGS
    API --> METRICS
    API --> TRACE
    
    classDef moduleStyle fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef dataStyle fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
    classDef extStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
    classDef infraStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    
    class OM,IM,PM,CM,SM,AM moduleStyle
    class POD1,POD2,POD3,PODN,CACHE dataStyle
    class PAYMENT_GW,SHIPPING_API,ANALYTICS_SV extStyle
    class EVENT,QUEUE,LOGS,METRICS,TRACE infraStyle

Workflow:

  1. 클라이언트 요청이 글로벌 로드 밸런서를 통해 Rails API 게이트웨이로 라우팅
  2. API 게이트웨이가 요청을 적절한 모듈로 분배 (주문, 재고, 결제 등)
  3. 각 모듈이 자체 비즈니스 로직을 실행하고 필요시 이벤트 발행
  4. 모듈 간 통신은 이벤트 버스를 통해 비동기적으로 처리
  5. 데이터는 shop_id 기반으로 샤딩된 MySQL 포드에 저장
  6. 캐시 계층 (Redis/Memcached) 을 통해 성능 최적화
  7. 백그라운드 작업은 큐 시스템을 통해 비동기 처리

역할:

유무에 따른 차이점:

구현 예시:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
// 모듈러 모놀리스 구현 예시 (Node.js/TypeScript)

// 1. 도메인 이벤트 정의
interface DomainEvent {
  eventId: string;
  eventType: string;
  aggregateId: string;
  payload: any;
  occurredAt: Date;
}

// 2. Event Bus 구현 (모듈 간 통신)
class EventBus {
  private handlers: Map<string, Array<(event: DomainEvent) => Promise<void>>> = new Map();

  // 이벤트 핸들러 등록
  subscribe(eventType: string, handler: (event: DomainEvent) => Promise<void>): void {
    if (!this.handlers.has(eventType)) {
      this.handlers.set(eventType, []);
    }
    this.handlers.get(eventType)!.push(handler);
  }

  // 이벤트 발행 (비동기 처리)
  async publish(event: DomainEvent): Promise<void> {
    const handlers = this.handlers.get(event.eventType) || [];
    
    // Outbox 패턴 구현: 이벤트를 먼저 데이터베이스에 저장
    await this.saveToOutbox(event);
    
    // 핸들러들을 비동기로 실행
    await Promise.all(handlers.map(handler => 
      handler(event).catch(error => 
        console.error(`Event handler failed for ${event.eventType}:`, error)
      )
    ));
  }

  // Outbox 패턴: 이벤트를 데이터베이스에 먼저 저장
  private async saveToOutbox(event: DomainEvent): Promise<void> {
    // 실제 구현에서는 데이터베이스에 저장
    console.log(`Saving event to outbox: ${event.eventType}`);
  }
}

// 3. 주문 모듈 (Orders Module) 구현
namespace OrdersModule {
  
  // 주문 집계 루트
  export class Order {
    constructor(
      public readonly id: string,
      public readonly customerId: string,
      public readonly items: OrderItem[],
      public status: OrderStatus = OrderStatus.Pending
    ) {}

    // 주문 확인 비즈니스 로직
    confirm(): DomainEvent {
      if (this.status !== OrderStatus.Pending) {
        throw new Error('Only pending orders can be confirmed');
      }
      
      this.status = OrderStatus.Confirmed;
      
      return {
        eventId: crypto.randomUUID(),
        eventType: 'OrderConfirmed',
        aggregateId: this.id,
        payload: {
          orderId: this.id,
          customerId: this.customerId,
          items: this.items,
          totalAmount: this.calculateTotal()
        },
        occurredAt: new Date()
      };
    }

    private calculateTotal(): number {
      return this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    }
  }

  export interface OrderItem {
    productId: string;
    quantity: number;
    price: number;
  }

  export enum OrderStatus {
    Pending = 'pending',
    Confirmed = 'confirmed',
    Shipped = 'shipped',
    Delivered = 'delivered'
  }

  // 주문 모듈의 공개 API
  export class OrdersAPI {
    constructor(
      private eventBus: EventBus,
      private orderRepository: OrderRepository
    ) {}

    // 주문 생성 API
    async createOrder(customerId: string, items: OrderItem[]): Promise<string> {
      const orderId = crypto.randomUUID();
      const order = new Order(orderId, customerId, items);
      
      await this.orderRepository.save(order);
      
      const event: DomainEvent = {
        eventId: crypto.randomUUID(),
        eventType: 'OrderCreated',
        aggregateId: orderId,
        payload: { orderId, customerId, items },
        occurredAt: new Date()
      };
      
      await this.eventBus.publish(event);
      return orderId;
    }

    // 주문 확인 API
    async confirmOrder(orderId: string): Promise<void> {
      const order = await this.orderRepository.findById(orderId);
      if (!order) {
        throw new Error('Order not found');
      }

      const event = order.confirm();
      await this.orderRepository.save(order);
      await this.eventBus.publish(event);
    }
  }

  // 리포지토리 인터페이스 (데이터 액세스 추상화)
  export interface OrderRepository {
    save(order: Order): Promise<void>;
    findById(id: string): Promise<Order | null>;
  }
}

// 4. 재고 모듈 (Inventory Module) 구현
namespace InventoryModule {
  
  export class InventoryItem {
    constructor(
      public readonly productId: string,
      public availableQuantity: number,
      public reservedQuantity: number = 0
    ) {}

    // 재고 예약 비즈니스 로직
    reserve(quantity: number): void {
      if (this.availableQuantity < quantity) {
        throw new Error('Insufficient inventory');
      }
      
      this.availableQuantity -= quantity;
      this.reservedQuantity += quantity;
    }
  }

  // 재고 모듈의 공개 API
  export class InventoryAPI {
    constructor(
      private eventBus: EventBus,
      private inventoryRepository: InventoryRepository
    ) {
      // 주문 생성 이벤트 구독
      this.eventBus.subscribe('OrderCreated', this.handleOrderCreated.bind(this));
    }

    // 주문 생성 이벤트 핸들러 (모듈 간 통신)
    private async handleOrderCreated(event: DomainEvent): Promise<void> {
      const { orderId, items } = event.payload;
      
      console.log(`Processing inventory reservation for order: ${orderId}`);
      
      try {
        // 모든 아이템에 대해 재고 예약
        for (const item of items) {
          await this.reserveInventory(item.productId, item.quantity);
        }
        
        // 재고 예약 완료 이벤트 발행
        const reservedEvent: DomainEvent = {
          eventId: crypto.randomUUID(),
          eventType: 'InventoryReserved',
          aggregateId: orderId,
          payload: { orderId, items },
          occurredAt: new Date()
        };
        
        await this.eventBus.publish(reservedEvent);
        
      } catch (error) {
        // 재고 부족 시 보상 이벤트 발행
        const failedEvent: DomainEvent = {
          eventId: crypto.randomUUID(),
          eventType: 'InventoryReservationFailed',
          aggregateId: orderId,
          payload: { orderId, error: error.message },
          occurredAt: new Date()
        };
        
        await this.eventBus.publish(failedEvent);
      }
    }

    // 재고 예약 API
    async reserveInventory(productId: string, quantity: number): Promise<void> {
      const inventory = await this.inventoryRepository.findByProductId(productId);
      if (!inventory) {
        throw new Error(`Product ${productId} not found in inventory`);
      }
      
      inventory.reserve(quantity);
      await this.inventoryRepository.save(inventory);
    }
  }

  export interface InventoryRepository {
    save(inventory: InventoryItem): Promise<void>;
    findByProductId(productId: string): Promise<InventoryItem | null>;
  }
}

// 5. 애플리케이션 부트스트랩 (모든 모듈 조합)
class ModularMonolithApp {
  private eventBus: EventBus;
  private ordersAPI: OrdersModule.OrdersAPI;
  private inventoryAPI: InventoryModule.InventoryAPI;

  constructor() {
    this.eventBus = new EventBus();
    
    // 각 모듈 초기화 (의존성 주입)
    this.ordersAPI = new OrdersModule.OrdersAPI(
      this.eventBus,
      new MockOrderRepository() // 실제 구현에서는 실제 리포지토리 사용
    );
    
    this.inventoryAPI = new InventoryModule.InventoryAPI(
      this.eventBus,
      new MockInventoryRepository() // 실제 구현에서는 실제 리포지토리 사용
    );
  }

  // 애플리케이션 시작
  async start(): Promise<void> {
    console.log('Modular Monolith Application started');
    
    // 예시: 주문 생성 플로우
    try {
      const orderId = await this.ordersAPI.createOrder('customer-123', [
        { productId: 'product-1', quantity: 2, price: 10.00 },
        { productId: 'product-2', quantity: 1, price: 25.00 }
      ]);
      
      console.log(`Order created: ${orderId}`);
      
      // 잠시 후 주문 확인
      setTimeout(async () => {
        await this.ordersAPI.confirmOrder(orderId);
        console.log(`Order confirmed: ${orderId}`);
      }, 1000);
      
    } catch (error) {
      console.error('Order processing failed:', error);
    }
  }
}

// 6. Mock Repositories (실제 구현에서는 실제 데이터베이스 연동)
class MockOrderRepository implements OrdersModule.OrderRepository {
  private orders = new Map<string, OrdersModule.Order>();

  async save(order: OrdersModule.Order): Promise<void> {
    this.orders.set(order.id, order);
  }

  async findById(id: string): Promise<OrdersModule.Order | null> {
    return this.orders.get(id) || null;
  }
}

class MockInventoryRepository implements InventoryModule.InventoryRepository {
  private inventory = new Map<string, InventoryModule.InventoryItem>([
    ['product-1', new InventoryModule.InventoryItem('product-1', 100)],
    ['product-2', new InventoryModule.InventoryItem('product-2', 50)]
  ]);

  async save(inventory: InventoryModule.InventoryItem): Promise<void> {
    this.inventory.set(inventory.productId, inventory);
  }

  async findByProductId(productId: string): Promise<InventoryModule.InventoryItem | null> {
    return this.inventory.get(productId) || null;
  }
}

// 애플리케이션 실행
const app = new ModularMonolithApp();
app.start();

사례 4: 온라인 쇼핑몰

시나리오: 온라인 쇼핑몰에서 상품, 주문, 결제, 배송 도메인을 각각 별도 모듈로 관리하면서, 단일 Application 으로 배포하는 구조

시스템 구성:

graph TB
  subgraph Modular Monolith Application
    A[API 게이트웨이]
    B[상품 모듈]
    C[주문 모듈]
    D[결제 모듈]
    E[배송 모듈]
    F["공통 인프라(인증, 로깅)"]
    G[단일 DB]
    A --> B
    A --> C
    A --> D
    A --> E
    B --- F
    C --- F
    D --- F
    E --- F
    B --> G
    C --> G
    D --> G
    E --> G
  end

Workflow:

역할:

유무에 따른 차이점:

구현 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 상품 모듈 (/modules/products/service.py)
class ProductService:
    def get_product(self, product_id: int):
        # 상품 데이터 조회 및 검증
        pass

# 주문 모듈 (/modules/orders/service.py)
class OrderService:
    def __init__(self, product_service):
        self.product_service = product_service

    def create_order(self, order_data: dict):
        # 1. 상품 모듈의 get_product 호출로 유효성 검증
        product = self.product_service.get_product(order_data["product_id"])
        # 2. 주문 생성 로직
        pass

# 메인 어플리케이션 레이어 (main.py)
from fastapi import FastAPI
from modules.products.service import ProductService
from modules.orders.service import OrderService

app = FastAPI()
product_service = ProductService()
order_service = OrderService(product_service)

@app.post("/order")
def create_order(order_data: dict):
    return order_service.create_order(order_data)

Git 기반 예제 프로젝트 구조

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
modular-monolith/
├── src/
│   ├── api/                     # Entry point (Web/API)
│   │   └── app.ts
│   ├── modules/
│   │   ├── order/
│   │   │   ├── dto/
│   │   │   ├── domain/
│   │   │   └── infra/
│   │   ├── inventory/
│   │   │   ├── dto/
│   │   │   ├── domain/
│   │   │   └── infra/
│   │   └── shared/
│   │       └── contracts/      # 모듈 간 이벤트/DTO 계약
│   ├── common/
│   │   ├── di-container.ts
│   │   └── middleware/
│   └── infrastructure/
│       ├── db/
│       └── event-bus/
└── package.json

구조 설명:

구현 예시 (TypeScript + Node.js)

이벤트 계약 (shared/contracts/order.events.ts)
1
2
3
4
export interface OrderCreated {
  orderId: string;
  items: Array<{ productId: string; qty: number; }>;
}
Order 모듈 - 도메인 로직 및 이벤트 발행
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// modules/order/domain/orderService.ts
import { OrderCreated } from '../../shared/contracts/order.events';
import { EventBus } from '../../infrastructure/event-bus';

export class OrderService {
  constructor(private eventBus: EventBus) {}

  async createOrder(orderId: string, items: unknown[]) {
    // 비즈니스 로직, DB 저장…
    const event: OrderCreated = { orderId, items: items as any };
    this.eventBus.publish('order.created', event);
  }
}
Inventory 모듈 - 이벤트 처리 핸들러
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// modules/inventory/infra/inventorySubscriber.ts
import { OrderCreated } from '../../shared/contracts/order.events';
import { EventBus } from '../../infrastructure/event-bus';

export class InventorySubscriber {
  constructor(eventBus: EventBus) {
    eventBus.subscribe('order.created', this.handleOrderCreated.bind(this));
  }

  async handleOrderCreated(payload: OrderCreated) {
    // 재고 확인 및 차감
    console.log(`Reduce stock for order ${payload.orderId}`);
  }
}
이벤트 버스 구성
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// infrastructure/event-bus/eventBus.ts
type Handler = (payload: any) => Promise<void> | void;

export class EventBus {
  private handlers: Record<string, Handler[]> = {};

  publish(topic: string, payload: any) {
    (this.handlers[topic] || []).forEach(h => h(payload));
  }

  subscribe(topic: string, handler: Handler) {
    this.handlers[topic] = this.handlers[topic] || [];
    this.handlers[topic].push(handler);
  }
}
DI 및 앱 초기화
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// api/app.ts
import express from 'express';
import { OrderService } from '../modules/order/domain/orderService';
import { InventorySubscriber } from '../modules/inventory/infra/inventorySubscriber';
import { EventBus } from '../infrastructure/event-bus/eventBus';

const app = express();
const bus = new EventBus();

// DI
const orderSvc = new OrderService(bus);
new InventorySubscriber(bus);

app.post('/order', async (req, res) => {
  const { orderId, items } = req.body;
  await orderSvc.createOrder(orderId, items);
  res.sendStatus(201);
});

app.listen(3000, () => console.log('Server started'));

전통 모놀리식 → Modular Monolith 로 전환 로드맵

단계목표상세 설명
1 단계기능 그룹 정의도메인 기능별로 책임 분리 (예: 주문, 결제, 상품)
2 단계폴더/패키지 구조 정리모듈 폴더화, 유틸 공통화, 내부/외부 API 구분
3 단계Layered 구조 도입Presentation, Application, Domain, Infra 구조화
4 단계인터페이스 경계 정의모듈 간 직접 참조 방지, 의존 역전 원칙 적용
5 단계모듈화 린트 적용정적 분석 도입, 경계 침범 자동 감지 도구 배포
6 단계테스트 기반 리팩토링기존 기능은 테스트 후 전환, TDD 병행
7 단계In-Process → 이벤트 도입느슨한 결합 확보 (예: 이벤트 버스 패턴 도입)
8 단계성능/관측 체계 통합성능 분석 및 장애 진단 체계 마련

Modular Monolith 아키텍처 안에서 Kafka 를 이용한 모듈 간 이벤트 통신

Kafka 를 Modular Monolith 내부에서 모듈 간 통신에 도입하면 다음 효과를 얻을 수 있다:

개요 및 설계 목적

항목설명
아키텍처 유형Modular Monolith (단일 배포 + 모듈 분리)
통신 방식Kafka 기반 비동기 메시징 (Domain Event → Topic 발행 → 모듈 수신)
도입 목적모듈 간 직접 호출을 제거하여 느슨한 결합 구현, 마이크로서비스 전환 유연성 확보
전환 확장성MSA 전환 시 Kafka 구독 구조 그대로 활용 가능 (모듈 → 서비스 이관 시 무중단 전환)
기능 적용 예시주문 완료 후 결제/재고/알림 모듈에서 독립 처리 (OrderPlaced → 여러 Consumer)

아키텍처 구성

flowchart TD
  subgraph Modular Monolith App
    direction TB
    A1[Order Module] -->|OrderPlaced Event| KafkaProducer
    A2[Payment Module]
    A3[Inventory Module]
    A4[Notification Module]
  end

  KafkaProducer -->|Publish to Topic: order.placed| KafkaBroker[(Kafka Broker)]

  KafkaBroker --> C1["KafkaConsumer (PaymentListener)"]
  KafkaBroker --> C2["KafkaConsumer (InventoryListener)"]
  KafkaBroker --> C3["KafkaConsumer (NotificationListener)"]

  C1 --> A2
  C2 --> A3
  C3 --> A4

설계 구성 요소 및 역할

구성 요소설명
KafkaProducerDomainEvent 를 Kafka Topic 에 발행 (발행자 모듈 내부에 위치)
KafkaBroker이벤트를 중계하는 Kafka 클러스터 (토픽 기반 라우팅)
KafkaConsumer특정 토픽을 구독하여 모듈에 이벤트를 전달 (모듈별 리스너 존재)
DomainEvent모듈 내부 상태 변화에 따라 생성되는 이벤트 (OrderPlaced, UserRegistered)
EventDispatcher이벤트를 Kafka 로 발행하는 통합 인터페이스
EventHandlerConsumer 쪽에서 실제 비즈니스 로직 처리 담당
Event Mapper (선택)Domain → Integration Event 로 변환 필요 시 사용 (버전/스키마 고려)

코드 설계 예시

도메인 이벤트 정의
1
2
3
4
5
6
# events.py
class OrderPlaced:
    def __init__(self, order_id, user_id, items):
        self.order_id = order_id
        self.user_id = user_id
        self.items = items
Kafka Producer
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# kafka_producer.py
from kafka import KafkaProducer
import json

producer = KafkaProducer(
    bootstrap_servers="localhost:9092",
    value_serializer=lambda v: json.dumps(v).encode("utf-8")
)

def publish_event(topic, event):
    producer.send(topic, value=vars(event))
도메인 서비스 내부 발행
1
2
3
4
5
6
7
8
# order_service.py
from events import OrderPlaced
from kafka_producer import publish_event

def place_order(order_data):
    # 주문 처리 로직 …
    event = OrderPlaced(order_id=order_data.id, user_id=order_data.user_id, items=order_data.items)
    publish_event("order.placed", event)
Kafka Consumer & 리스너
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# inventory_listener.py
from kafka import KafkaConsumer
import json

consumer = KafkaConsumer(
    "order.placed",
    bootstrap_servers="localhost:9092",
    value_deserializer=lambda m: json.loads(m.decode("utf-8")),
    group_id="inventory-service"
)

for message in consumer:
    data = message.value
    print(f"재고 차감 시작: 주문 ID = {data['order_id']}")
    # 재고 처리 로직 실행

실무 설계 고려사항

항목전략
이벤트 중복 처리이벤트 ID 를 Redis 나 DB 에 저장하여 중복 소비 방지
메시지 순서 보장파티션 키 사용 (order_id) 또는 토픽 설계 시 순서 고려
Consumer Group 전략모듈별 고유 Consumer Group 할당 → 병렬 소비 가능
에러 처리DLQ(Dead Letter Queue) 설정 또는 에러 리포팅 로직 구성
Schema 관리JSON → Avro/Protobuf + Schema Registry 도입 가능
테스트Mock Kafka 사용 (예: kafka-python, testcontainers-kafka)
MSA 전환 대비현재 모듈의 Kafka Consumer/Producer 코드 → 독립 서비스에서 그대로 사용 가능하게 설계
구성 자동화Docker Compose 또는 Kubernetes 로 Kafka, Zookeeper 구성 관리

구조적 정리

모듈 간 이벤트 통신 시나리오
단계설명
1주문 모듈에서 주문 완료 처리 후 OrderPlaced 이벤트 생성
2이벤트를 Kafka Topic (order.placed) 에 발행
3재고/결제/알림 등 모듈에서 해당 Topic 을 구독하여 이벤트 수신
4각각 독립적으로 후속 비즈니스 처리 수행

모놀리식 내 이벤트 로그 기반 장애 추적 및 모니터링 구조

이벤트 기반 구조를 채택한 모놀리식 시스템에서는 모듈 간 통신 흐름과 처리 결과를 추적할 수 있어야 한다.
이벤트 로그 + 트레이싱 + 로깅을 기반으로 장애 발생 원인을 빠르게 파악할 수 있도록 해야 한다.

아키텍처 구성 개요
flowchart TD
  A[Module A] -->|Event: OrderCreated| B[Internal EventBus]
  B --> C["Module B (Inventory)"]
  B --> D["Module C (Notification)"]
  C --> E["Event Logger (Append)"]
  D --> E
  E --> F[Monitoring/Tracing Dashboard]
구성 요소 및 설명
구성 요소설명도구/기술 예시
EventBus내부 이벤트 라우팅 (동기/비동기)Spring ApplicationEventPublisher, 자체 버스
Event Logger이벤트 흐름, 결과 상태 기록Custom Logging Layer, Kafka (내부 메시지 저장), Logstash
Trace ID모듈 간 이벤트 추적을 위한 고유 식별자UUID, ULID, Correlation ID
Structured Logging로그 포맷 표준화, JSON 기반 처리loguru (Python), slf4j (Java), winston (Node.js)
Dashboard이벤트 흐름 및 상태 시각화ELK Stack, Grafana + Loki, Prometheus, Zipkin
예시: 이벤트 로그 포맷
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "event_name": "OrderCreated",
  "event_id": "evt-123456",
  "timestamp": "2025-08-01T12:30:00Z",
  "producer": "order-module",
  "consumers": [
    {"module": "inventory-module", "status": "SUCCESS"},
    {"module": "notification-module", "status": "FAIL", "error": "SMTP timeout"}
  ],
  "trace_id": "trace-aaa-bbb-ccc"
}
장애 추적 전략
항목전략
이벤트 실패 추적리스너 실패 시 로그에 에러 스택 기록 + 알림 연동 (예: Slack, PagerDuty)
트랜잭션 외 이벤트 로깅이벤트가 트랜잭션 외부에서 실행되므로, DB commit 이후 성공 여부 별도 기록
재시도 전략실패한 이벤트는 Retry Queue 또는 Dead Letter Queue 에 적재하여 후속 처리
트레이싱 연동Trace ID 를 전달하여 로그, 메트릭, 트레이스를 연계 추적 (OpenTelemetry 기반 가능)

Eventual Consistency 보장 전략 및 데이터 동기화 설계

모듈 간 독립성과 성능을 확보하기 위해, 즉각적인 강한 일관성 대신 지연 일관성 (Eventual Consistency) 을 선택할 수 있다. 특히 모듈 간 이벤트 기반 통신을 활용할 때, 일관성 유지 전략이 필수이다.

전제 조건
설계 구성
구성 요소설명
도메인 이벤트상태 변경 후 발행 (OrderPlacedEvent, PaymentReceivedEvent)
이벤트 핸들러수신 모듈에서 비동기 처리하여 상태 반영
보상 트랜잭션 (Compensating Transaction)이벤트 실패 시 원상 복구 처리 수행
이벤트 저장소 (Outbox Pattern)DB 트랜잭션과 함께 이벤트 저장 후 별도 프로세스가 발행
Outbox Pattern + Polling Publisher 구조
sequenceDiagram
  participant DB
  participant Service
  participant Outbox
  participant EventPublisher

  Service->>DB: Insert(Order)
  Service->>Outbox: Insert(Event Row)
  Note right of Outbox: {"status": "PENDING"}
  EventPublisher->>Outbox: Poll + Mark "SENT"
  EventPublisher->>Kafka: publish(OrderPlacedEvent)

Eventual Consistency 전략
전략/패턴설명
Outbox Pattern트랜잭션 내 이벤트를 함께 저장, 외부에서 비동기 발행
Inbox Pattern중복 이벤트 방지를 위한 수신 측 처리 로그 저장
Idempotency중복 이벤트에 대한 무해성 보장 (예: 키 기반 업데이트)
Timeout + Retry실패 시 백오프 + 재시도 전략 적용
Compensating Action실패한 경우 보상 작업 실행 (예: 결제 취소 등)
Snapshot / Projection읽기 모델을 주기적으로 동기화 (CQRS 활용 시)

In-memory EventBus ↔ Kafka 전환 전략

In-memory EventBus

애플리케이션 내부에서 이벤트를 동기 또는 비동기 방식으로 퍼블리시/구독하게 해주는 메모리 기반 이벤트 전달 시스템이다.

주요 목적 및 장점
목적설명
느슨한 결합발행자와 구독자가 서로 몰라도 통신 가능
비동기 처리이벤트 기반 후속 처리를 메인 로직에서 분리
테스트 용이성외부 시스템 없이 테스트 가능
성능 향상메모리 기반으로 처리 속도 매우 빠름
구조 간소화Kafka 등 브로커 없이도 이벤트 시스템 구현 가능
기본 구조
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# EventBus 인터페이스
class EventBus:
    def __init__(self):
        self.subscribers = {}

    def subscribe(self, event_type, handler):
        self.subscribers.setdefault(event_type, []).append(handler)

    def publish(self, event):
        for handler in self.subscribers.get(type(event), []):
            handler(event)

# 예제 이벤트
class OrderCreatedEvent:
    def __init__(self, order_id):
        self.order_id = order_id

# 핸들러 등록 및 실행
bus = EventBus()
bus.subscribe(OrderCreatedEvent, lambda e: print(f"Send Email for Order {e.order_id}"))
bus.publish(OrderCreatedEvent(order_id=123))
아키텍처
flowchart TD
  A[Order Module] -->|emit OrderCreated| EB[In-memory EventBus]
  EB --> B[Inventory Handler]
  EB --> C[Notification Handler]
  EB --> D[Audit Logger]
한계 및 주의점
항목설명
멀티 프로세스 환경다른 프로세스/서버로 이벤트 전달 불가
신뢰성 부족메시지 손실 가능, 재시도/보장 없음
확장성수평 확장 시 메시지 전달 어려움
오류 처리실패 시 DLQ/재시도 로직 구현 필요
정렬/중복 처리 없음멱등성/순서 보장 안 됨 (수동 구현 필요)
In-memory Vs External Broker (Kafka 등) 비교
항목In-memory EventBusKafka, RabbitMQ 등
성능매우 빠름 (μs)비교적 느림 (ms 단위)
운영 복잡도매우 낮음설치, 구성, 운영 필요
장애 허용성낮음 (메모리 기반)높음 (내결함성 구성 가능)
신뢰성낮음 (영속성 없음)높음 (디스크 기반, 재처리 가능)
적용 대상내부 처리, 단일 인스턴스분산 시스템, 외부 통신 필요 시

전환 필요 배경

항목설명
개발 초기Kafka 도입 없이 경량화된 In-memory EventBus 활용 (예: Python Signal, Spring Event, Node EventEmitter)
운영 확장 시점다중 인스턴스 환경, 서비스 간 통신 필요 발생 → Kafka 같은 분산 이벤트 브로커 필요
점진적 전환 필요전면 교체 대신 추상 인터페이스 기반 이중 지원 필요

구조적 전환 설계

flowchart TD
    A[도메인 서비스] -->|이벤트 발행| B["EventPublisher (Interface)"]
    B -->|InMemory| C[InMemoryEventBus]
    B -->|Kafka| D[KafkaEventBus]
    D -->|Topic 발행| E[Kafka Broker]
    E -->|Consume| F[모듈별 Kafka Consumer]
    C -->|Direct Call| G[모듈별 리스너 함수]

설계 패턴 적용

구성 요소전략 및 구현 방식
EventPublisher (인터페이스)publish(event: DomainEvent) 정의. 구현체 교체 가능성 확보
InMemoryEventBus애플리케이션 내 메서드 직접 호출 기반
KafkaEventBusKafka Topic 으로 메시지 전송
Configurable Bus설정 값 기반으로 런타임 시점 Bus 선택 가능하도록 DI 적용
Failover HookKafka 실패 시 fallback to InMemory (옵션)

구현 예시

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# domain_event.py
class DomainEvent:
    pass

class UserCreatedEvent(DomainEvent):
    def __init__(self, user_id: str):
        self.user_id = user_id

# event_publisher.py
class EventPublisher:
    def publish(self, event: DomainEvent):
        raise NotImplementedError()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# in_memory_bus.py
class InMemoryEventBus(EventPublisher):
    def __init__(self):
        self.subscribers = {}

    def subscribe(self, event_type, handler):
        self.subscribers.setdefault(event_type, []).append(handler)

    def publish(self, event: DomainEvent):
        for handler in self.subscribers.get(type(event), []):
            handler(event)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# kafka_bus.py
from kafka import KafkaProducer
import json

class KafkaEventBus(EventPublisher):
    def __init__(self):
        self.producer = KafkaProducer(
            bootstrap_servers='localhost:9092',
            value_serializer=lambda v: json.dumps(v).encode('utf-8')
        )

    def publish(self, event: DomainEvent):
        self.producer.send(type(event).__name__, vars(event))

Domain Event Vs Integration Event 설계 구분

항목Domain EventIntegration Event
정의하나의 도메인 내부에서 발생한 의미 있는 상태 변화시스템 간 통합을 위해 발행되는 이벤트 (MSA/모듈 간 통신용)
관심사도메인 규칙과 상태 전이서비스 간 데이터 전달 및 이벤트 동기화
스코프내부 모듈/서비스 한정외부 시스템과의 경계 포함
의도비즈니스 불변 조건 (Event Storming 기반) 표현통합 목적의 메시지 전달 및 확장성 확보

차이점

구분Domain EventIntegration Event
발행 주체내부 도메인 서비스 (ex. 주문, 사용자 등)외부 시스템 통합 또는 다른 바운디드 컨텍스트 대상
의도비즈니스 상태 변화 반영 (OrderPlaced, UserSignedUp)다른 시스템과 상태 동기화 또는 알림 (InventoryReserved)
보존 여부일반적으로 일회성 처리 (Replay 목적 아님)메시지 브로커에서 보존 (Kafka 에 저장, 재처리 가능)
전파 범위모놀리식 내부 모듈 또는 같은 바운디드 컨텍스트마이크로서비스, 외부 시스템 (Pub/Sub)
형식코드 클래스 기반 이벤트 객체JSON, Avro 등 스키마 정의된 메시지
예시UserCreated, OrderCanceledSendWelcomeEmail, NotifyWarehouse

발생 및 흐름

flowchart LR
  A[도메인 서비스] --> B[Domain Event 발생]
  B --> C[도메인 이벤트 핸들러 처리]
  C --> D["Outbox 기록 (Optional)"]
  D --> E[Integration Event 발행]
  E --> F[외부 서비스 수신]

설계 가이드

항목권장 전략
DomainEvent ↔ IntegrationEvent 매핑OrderPlacedOrderCreatedIntegrationEvent
IntegrationEvent 버전 관리Kafka 메시지에 version 필드 포함, 스키마 관리 도구 사용 (e.g. Confluent Schema Registry)
테스트 전략단위 테스트는 DomainEvent, 통합 테스트는 IntegrationEvent 기준 분리
이벤트 메시지 디자인명확한 context 명시 (ex. user-service.user.created)

실제 예시 (Python)

도메인 이벤트 정의

1
2
3
4
5
# 도메인 레이어
class OrderCreatedEvent:
    def __init__(self, order_id, user_id):
        self.order_id = order_id
        self.user_id = user_id

Integration Event 정의 (Kafka 등 외부 전파)

1
2
3
4
5
6
# 인티그레이션 계층
class OrderCreatedIntegrationEvent:
    def __init__(self, order_id, user_id, timestamp):
        self.order_id = order_id
        self.user_id = user_id
        self.timestamp = timestamp

변환 예시

1
2
3
4
5
6
7
8
# Application 계층
def handle_domain_event(domain_event: OrderCreatedEvent):
    integration_event = OrderCreatedIntegrationEvent(
        domain_event.order_id,
        domain_event.user_id,
        timestamp=datetime.utcnow()
    )
    kafka_producer.send("order.created", integration_event)

Kafka 기반 Domain Event → Integration Event 변환 구조

구조 개요
flowchart TD
    A[도메인 서비스] --> B["Domain Event 생성 (예: OrderPlaced)"]
    B --> C[Domain Event Dispatcher]
    C --> D[Integration Event Mapper]
    D --> E[Kafka Integration Event Publisher]
    E --> F[Kafka Broker]
    F --> G[외부 시스템 또는 마이크로서비스]
흐름 설명
단계구성 요소설명
1Domain Event비즈니스 트랜잭션 내에서 발생 (OrderPlaced, PaymentFailed)
2Domain Event Dispatcher등록된 핸들러에게 이벤트를 브로드캐스트
3MapperDomain → Integration 변환 (예: OrderPlaced → OrderCreatedIntegrationEvent)
4Kafka PublisherKafka 토픽으로 직렬화된 메시지 발행
5Consumer (MSA)외부 시스템/모듈에서 메시지를 구독 및 처리
예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Domain Event
class OrderPlaced:
    def __init__(self, order_id, user_id):
        self.order_id = order_id
        self.user_id = user_id

# Integration Event
class OrderCreatedIntegrationEvent:
    def __init__(self, order_id, user_id):
        self.type = "OrderCreated"
        self.version = 1
        self.order_id = order_id
        self.user_id = user_id

# Mapper
def map_domain_to_integration(event: OrderPlaced):
    return OrderCreatedIntegrationEvent(event.order_id, event.user_id)

# Publisher
def publish(event):
    integration_event = map_domain_to_integration(event)
    kafka_producer.send("order.created", value=vars(integration_event))

Domain Event Dispatcher 설계 패턴

DomainEventDispatcher 는 하나의 이벤트가 여러 핸들러로 전달될 수 있도록 이벤트 등록 및 전달 책임을 가진 중앙 집중형 이벤트 디스패처이다.

구조

flowchart TD
    A[도메인 서비스] --> B[DomainEventDispatcher]
    B --> C1[EmailEventHandler]
    B --> C2[LoggingEventHandler]
    B --> C3[KafkaIntegrationPublisher]

Dispatcher 설계 (Python)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class DomainEventDispatcher:
    def __init__(self):
        self._handlers = {}

    def register(self, event_type, handler):
        self._handlers.setdefault(event_type, []).append(handler)

    def dispatch(self, event):
        for handler in self._handlers.get(type(event), []):
            handler(event)
1
2
3
4
# 예시 사용
dispatcher = DomainEventDispatcher()
dispatcher.register(OrderPlaced, send_email)
dispatcher.register(OrderPlaced, publish_to_kafka)

장점

Outbox 패턴 상세 구현 및 트랜잭션 연결

데이터베이스 트랜잭션과 함께 이벤트를 Outbox 테이블에 저장하고, 이후 별도 프로세스 (이벤트 릴레이어) 가 이벤트를 메시징 시스템 (Kafka 등) 으로 전송하는 패턴이다.

sequenceDiagram
  participant Domain
  participant DB
  participant Outbox
  participant Processor
  participant Kafka

  Domain->>DB: save(order)
  Domain->>Outbox: save(OrderCreatedEvent)
  Processor->>Outbox: poll & mark as SENT
  Processor->>Kafka: publish event

Outbox 패턴의 장단점

구분장점단점
✅ 장점- 트랜잭션 내 메시지 저장으로 데이터와 이벤트 일관성 확보
- 메시지 손실 방지, 장애 복구 가능
- 외부 시스템과의 느슨한 결합
- Outbox Processor 구현 필요
- 중복 발행 방지 위한 멱등성 처리 복잡성
- 지연 전파 (latency) 발생 가능
❌ 단점- DB 에 쓰기 부하 증가
Processor 의 장애/지연 시 발행 지연
- DB 와 메시징 브로커 상태 추적/관리가 필요함

Outbox 테이블 스키마 예시

컬럼설명
idUUID or ULID
event_type이벤트 이름 (OrderCreated)
aggregate_id관련 엔터티 ID
payloadJSON 직렬화된 이벤트 내용
statusPENDING, SENT, FAILED
created_at타임스탬프

기반 구현 예시

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class OutboxEvent(Base):
    __tablename__ = "outbox"
    id = Column(UUID, primary_key=True)
    event_type = Column(String)
    payload = Column(JSON)
    status = Column(String, default="PENDING")
    created_at = Column(DateTime, default=datetime.utcnow)

def save_order(order_data):
    db.insert("orders", order_data)
    event = OutboxEvent(event_type="OrderCreated", payload=order_data)
    db.insert("outbox", event)

def publish_outbox():
    events = db.query("outbox").filter_by(status="PENDING")
    for event in events:
        kafka.send(event.event_type, event.payload)
        db.update("outbox", event.id, {"status": "SENT"})

고려 사항

Event Store 기반 Replay 가능한 Hybrid EventBus 설계

Hybrid EventBusIn-memory EventBus외부 메시지 브로커 (Kafka, RabbitMQ, NATS 등) 를 결합하여 이벤트를 내부 모듈 간 빠르게 처리하면서, 동시에 시스템 외부로도 이벤트를 전파할 수 있도록 설계한 이벤트 처리 아키텍처 패턴이다.

목적

  1. 이벤트 소실 방지
  2. 장애 복구 및 재처리
  3. 이벤트 소싱 기반 설계 확장성

구조

flowchart TD
    A[도메인 서비스] --> B[Domain Event Dispatcher]
    B --> C[Hybrid EventBus]
    C --> D1[Kafka Producer]
    C --> D2["Event Store (DB)"]
    D2 -->|Query| E[Replay Engine]
    E --> C[재발행]

구성 요소 설명

구성 요소역할
Hybrid EventBusKafka 발행 + Event Store 저장 병행 수행
Event StoreDB 또는 파일 기반 이벤트 저장소 (ex. PostgreSQL, EventStoreDB)
Replay Engine특정 시간 이후 이벤트를 다시 발행할 수 있도록 처리
Dispatcher + BusDispatcher 는 핸들러 관리, Bus 는 발행 방식 관리

구현 전략

예시 저장 구조

1
2
3
4
5
6
CREATE TABLE event_store (
    id UUID PRIMARY KEY,
    event_type TEXT,
    payload JSONB,
    occurred_at TIMESTAMP
);

Replay 코드

1
2
3
4
5
def replay_events(from_time):
    rows = db.query("SELECT * FROM event_store WHERE occurred_at >= %s", (from_time,))
    for row in rows:
        event = deserialize(row['payload'], row['event_type'])
        hybrid_event_bus.publish(event)

Outbox Pattern vs. Hybrid EventBus 구조 비교

구분Outbox PatternHybrid EventBus 구조
이벤트 처리 방식트랜잭션 DB 내 outbox 테이블 에 이벤트 기록 → 별도 프로세스가 이를 Kafka 등으로 전송이벤트 발생 시 Kafka 로 직접 발행 + 이벤트 저장소 (DB) 에 동시에 저장
일관성 보장 수준Strong Consistency (Transactional Outbox)
→ DB 트랜잭션과 메시지 쓰기 동시 처리
Eventual Consistency
→ Kafka 발행과 저장이 별도 실행
실행 시점DB commit 이후 Outbox Poller 에 의해 비동기 전송이벤트 발생 시 즉시 Kafka 전송 및 이벤트 저장 병행
Replay 기능기본적으로 없음
(Outbox 테이블에는 replay 기능 없음)
별도 Event Store 에 저장된 이벤트를 기반으로 재전송 가능
운영 구성 요소DB, Outbox 테이블, Poller (Kafka Producer 역할)Event Dispatcher, Event Store, Hybrid Bus, Replay Engine
주요 장점✅ 트랜잭션 경계 내 안전한 메시지 발행
✅ 메시지 손실 없음
✅ 메시지 순서 보장
✅ Kafka 장애 시에도 저장소에 안전 저장
✅ Replay / Audit 가능
✅ 마이크로서비스 전환 대비 유연성
주요 단점❌ Polling 지연 (전송 지연)
❌ Outbox 처리 실패 관리 필요
❌ 단일 DB 락 이슈
❌ 일관성 보장 어려움 (Kafka 발행 실패 시 복구 어려움)
❌ 중복 방지 및 Idempotency 보장 로직 필요
Kafka 의존성Poller 가 Kafka Producer 역할직접 Kafka 연동 혹은 EventBus 추상화 통해 교체 가능
재처리 (Replay)Poller 재시도 필요 (기본 제공 안함)저장소 기반 전체 재처리 또는 조건별 재처리 가능
적합한 상황✅ 일관성이 중요한 주문, 결제 시스템
✅ 트랜잭션 DB 기반 시스템
✅ 이벤트 중심 복합 처리, 분석, 감사 로그가 필요한 시스템
✅ MSA 연계 또는 재처리 유스케이스

아키텍처 흐름 시각화

Outbox Pattern 구조
sequenceDiagram
    participant App
    participant DB
    participant OutboxTable
    participant Kafka
    participant Poller

    App->>DB: 트랜잭션 처리 (Order + Outbox Insert)
    Poller->>OutboxTable: Poll + Fetch 이벤트
    Poller->>Kafka: 이벤트 발행
Hybrid EventBus 구조
sequenceDiagram
    participant Domain
    participant Dispatcher
    participant EventStore
    participant Kafka

    Domain->>Dispatcher: Domain Event 발생
    Dispatcher->>Kafka: Kafka 발행
    Dispatcher->>EventStore: DB 저장

Modular Monolith → Microservices 전환 전략

Modular Monolith 에서 Microservices Architecture 로 전환할 때는 단계적 분리 전략이 필요하다.
단순 모듈 → 마이크로서비스 추출까지는 코드, 빌드, 배포, 인프라 모두에서의 점진적 진화가 필수이다.

flowchart TD
  A[도메인 모듈화 완료] --> B[계층/의존성 정리 및 경계 확인]
  B --> C[데이터 분리 전략 수립]
  C --> D[빌드/테스트 자동화 분리]
  D --> E[배포 파이프라인 분리]
  E --> F["독립 서비스로 추출 (MSA)"]

단계별 상세 전략

단계구분전략
1 단계코드 분리- DDD 기반 Bounded Context 정의
- 인터페이스와 구현체 분리
- 내부 모듈 간 직접 호출 제거
2 단계의존성 정리- DI 기반으로 의존성 역전
- 기술적 계층 대신 도메인 기준 계층화
- 모듈 간 순환 의존 제거
3 단계데이터 분리- 논리적 DB 분리 → 물리적 분리 계획
- 데이터 소유권 (Boundary Ownership) 명시
Transaction 을 Domain 내부에만 한정
4 단계빌드/테스트 분리- 모듈별 테스트 전용 CI 단계 구성
- 테스트 시 DB/환경 격리 (Testcontainers, mocks)
5 단계배포 구조 분리- 모듈 단위 빌드 (Gradle multi-module, pnpm workspace 등)
Docker 이미지 생성 구조 분리
- 서비스별 Helm 차트/Manifest 준비
6 단계독립 서비스 추출- 각 모듈을 독립 서비스로 분리 배포
API Gateway 및 Service Registry 도입
- 모듈 간 통신을 HTTP/gRPC or 메시지 브로커로 전환

이벤트 기반 통합 설계

MSA 전환 없이도 Modular Monolith 내부에서 도메인 이벤트를 활용한 비동기 통합이 가능하다. 이는 향후 서비스 분리 시에도 그대로 외부 이벤트로 확장될 수 있다.

설계 구조
sequenceDiagram
  participant ModuleA
  participant EventBus
  participant ModuleB
  participant ModuleC

  ModuleA->>EventBus: publish(OrderCreatedEvent)
  EventBus->>ModuleB: handle(OrderCreatedEvent)
  EventBus->>ModuleC: handle(OrderCreatedEvent)
이벤트 통합 구성 방식
요소설명구현 예시 (Spring 기준)
도메인 이벤트 정의이벤트 객체 정의 (OrderCreatedEvent)POJO, record, DTO
퍼블리셔도메인 로직 이후 이벤트 발행eventPublisher.publishEvent()
이벤트 버스내부 메시지 버스 또는 Observer 패턴Spring ApplicationEventPublisher
리스너관심 있는 모듈에서 이벤트 수신@EventListener, @TransactionalEventListener
비동기 처리별도 스레드로 처리하거나 큐 기반 처리@Async, 또는 Kafka 등 메시징 연계
적용 시 고려사항
고려 항목설명
트랜잭션 경계이벤트는 트랜잭션 성공 이후 발행해야 함 (TransactionalEventListener)
동기/비동기 처리모놀리스 내부에서는 동기/비동기 선택 가능, MSA 전환 시 비동기로 교체 용이
중복 처리 방지이벤트 핸들러에서 멱등성 (idempotency) 확보 필요
이벤트 저장 여부Event Sourcing 이 아닌 경우, 저장은 필수 아님 (추후 확장을 고려해 로그 남길 수도 있음)
예시 코드
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# event.py
class OrderCreatedEvent:
    def __init__(self, order_id: str, user_id: str):
        self.order_id = order_id
        self.user_id = user_id

# publisher.py
class EventBus:
    def __init__(self):
        self.subscribers = {}

    def subscribe(self, event_type, handler):
        self.subscribers.setdefault(event_type, []).append(handler)

    def publish(self, event):
        for handler in self.subscribers.get(type(event), []):
            handler(event)

# listener.py
def handle_order_created(event: OrderCreatedEvent):
    print(f"[ModuleB] Handling order: {event.order_id}")

# main.py
bus = EventBus()
bus.subscribe(OrderCreatedEvent, handle_order_created)
bus.publish(OrderCreatedEvent(order_id="1234", user_id="5678"))

Event-Driven Modular Monolith → Microservices 로의 점진적 분리 실전 사례

Event-Driven Modular Monolith 는 MSA 전환의 출발점으로 매우 효과적이며, 내부 이벤트 퍼블리싱 구조와 Outbox 를 먼저 구성해두면 이후 각 모듈을 서비스로 독립시키는 데 있어서 위험도 없이 유연한 점진적 분리가 가능하다. 특히, 이벤트 명세 표준화와 멱등성, 모니터링 체계를 미리 준비하는 것이 성공적인 전환의 핵심이다.

flowchart TD
  subgraph Modular Monolith
    A1[Order Module]
    A2[Payment Module]
    A3[Inventory Module]
    A4[Notification Module]
    E1[Internal EventBus]
  end

  E1 --> A2
  E1 --> A3
  E1 --> A4

  subgraph Microservices
    B1[order-service]
    B2[payment-service]
  end

  A1 -.-> B1
  A2 -.-> B2

핵심 개념:

단계별 전환 전략
단계설명기술/전략
1 단계모듈 경계 정의 및 이벤트 발행 구조 구성DomainEvent, EventBus, @EventListener
2 단계Outbox 테이블 도입 (트랜잭션 분리)Outbox 패턴, Kafka Topic 설계
3 단계메시지 브로커 연동Kafka, RabbitMQ, SNS/SQS
4 단계특정 모듈을 독립 서비스로 추출Spring Boot App / FastAPI App 별도 실행
5 단계내부 호출 → API 호출 or 메시지 기반 교체Feign, HTTP, gRPC, Kafka Consumer
6 단계공유 DB → 모듈별 DB 분리Polyglot Persistence 전략
7 단계API Gateway, 서비스 디스커버리 도입Kong, Istio, Consul 등
실전 사례: 전자상거래 시스템
전환 전 구조 (Modular Monolith)
1
2
3
4
5
6
7
8
# domain event
class OrderCreatedEvent:
    def __init__(self, order_id): ...
    
# application layer
def create_order():
    save_order()
    event_bus.publish(OrderCreatedEvent(order_id))
전환 단계 요약
단계구체 예시
모듈 → 서비스 추출payment 모듈 → payment-service 로 분리
이벤트 발행 전환기존 Internal Event → Kafka 발행
Consumer 구성payment-service 는 Kafka 에서 OrderCreated 수신
DB 분리order-servicepayment-service 각각 독립 DB
인증 처리JWT 기반 토큰 전달, 내부 인증 연동
오류 처리Retry + DLQ 설정, Kafka consumer 멱등 처리
주요 구현 예시
구성 요소설명
OrderCreatedEvent도메인 이벤트 정의
OutboxEvent 테이블이벤트 저장소 (내부 DB)
publish_outbox_eventsKafka 발행기 (Batch or Scheduler)
KafkaConsumerMicroservice 간 이벤트 수신
  1. order-service: 주문 생성 및 Outbox 저장

    1
    2
    3
    4
    5
    6
    
    # domain_event.py
    class OrderCreatedEvent:
        def __init__(self, order_id: str):
            self.order_id = order_id
            self.type = "OrderCreated"
            self.payload = {"order_id": order_id}
    
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    # repository/outbox_repository.py
    from sqlalchemy.orm import Session
    from models import OutboxEvent
    
    def save_outbox_event(db: Session, event: OrderCreatedEvent):
        outbox = OutboxEvent(
            event_type=event.type,
            payload=json.dumps(event.payload),
            status="PENDING"
        )
        db.add(outbox)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    # service/order_service.py
    from domain_event import OrderCreatedEvent
    from repository import order_repository, outbox_repository
    
    def create_order(db, order_data):
        order = order_repository.save_order(db, order_data)
        event = OrderCreatedEvent(order.id)
        outbox_repository.save_outbox_event(db, event)
        return order
    
  2. Outbox Processor (Background Task or Celery Beat)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    # task/outbox_processor.py
    from kafka import KafkaProducer
    import json
    from repository import outbox_repository
    from database import SessionLocal
    
    producer = KafkaProducer(bootstrap_servers='localhost:9092')
    
    def publish_outbox_events():
        db = SessionLocal()
        events = outbox_repository.get_pending_events(db)
    
        for event in events:
            producer.send("order.created", event.payload.encode("utf-8"))
            outbox_repository.mark_sent(db, event.id)
    
        db.close()
    
    • 위 코드는 BackgroundScheduler (e.g. APScheduler), Celery periodic task 로 주기적으로 실행 가능
  3. payment-service: Kafka Consumer

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    # consumer/payment_consumer.py
    from kafka import KafkaConsumer
    import json
    
    consumer = KafkaConsumer(
        "order.created",
        bootstrap_servers='localhost:9092',
        group_id='payment-group',
        auto_offset_reset='earliest',
        enable_auto_commit=True
    )
    
    for message in consumer:
        event = json.loads(message.value.decode("utf-8"))
        order_id = event["order_id"]
    
        # 결제 승인 처리 로직
        print(f"Received order.created event for order_id: {order_id}")
        # approve_payment(order_id)
    
  4. 보조: Outbox 테이블 모델 정의 (SQLAlchemy)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    # models.py
    from sqlalchemy import Column, Integer, String, Text, DateTime
    from datetime import datetime
    from database import Base
    
    class OutboxEvent(Base):
        __tablename__ = "outbox_events"
    
        id = Column(Integer, primary_key=True, index=True)
        event_type = Column(String(100))
        payload = Column(Text)
        status = Column(String(20), default="PENDING")
        created_at = Column(DateTime, default=datetime.utcnow)
    

실무 전환 시 고려 사항 및 교훈

고려 항목설명
이벤트 설계의 표준화이벤트 이름, 버전, 스키마 관리가 핵심 (예: Avro, JSON Schema)
멱등성 보장Consumer 는 중복 이벤트에 대해 동일 결과 보장
계약 테스트이벤트 기반 통합의 경우 Event Contract Test 필요
모니터링/트레이싱Trace ID 전달 필수 (OpenTelemetry, Zipkin 연동)
DB 분리 및 데이터 동기화데이터 중복 허용 → Eventually Consistent 설계 적용
모놀리스 부분 잔존 허용점진적으로 하나씩 서비스화, 나머지는 계속 모듈로 유지 가능
팀 조직 리디자인 필요모듈별 → 서비스별 팀 구성 변경 필요 가능성 있음

MSA 전환 시 메시징 구조 확장 전략

Internal Event → External Event 확장 흐름
flowchart TD
  A[Domain Layer] -->|DomainEvent 발행| B(Application Layer)
  B -->|EventBus 전달| C[Internal EventBus]
  C -->|Local handler 실행| D[Other Module]
  C -->|Outbox 저장| E[Outbox Table]
  E --> F[Outbox Processor]
  F --> G[Kafka/RabbitMQ 등 External Broker]
  G --> H[외부 Microservice]
단계별 전략
단계설명기술 적용 예
1 단계도메인 이벤트 발행 (OrderCreatedEvent)도메인 객체 내부에서 상태 변화 후 발행
2 단계Internal EventBus 에서 핸들링Application Layer 에서 다른 모듈 호출 or 저장
3 단계Outbox 테이블에 이벤트 저장status: PENDING 으로 기록
4 단계비동기 처리기로 외부 브로커 발행Kafka, RabbitMQ, AWS SNS 등
5 단계외부 서비스에서 수신 및 후속 처리각 서비스의 Consumer 에서 처리
Internal → External 전환 장점
장점설명
점진적 전환기존 internal 로직을 유지하면서 외부 발행만 추가
테스트 용이성Outbox 기록만으로도 로컬 디버깅 가능
이벤트 재처리 가능성실패 시 재시도, DLQ 적용 용이
계약 기반 설계로 확장성 확보이벤트 스키마를 명확히 관리 가능 (Avro, JSON Schema 등)

주목할 내용

카테고리주제핵심 항목설명
아키텍처 설계모듈 경계 설정Bounded Context, Domain Module도메인 기반으로 기능 단위를 모듈화하여 책임과 경계를 명확히 설정
아키텍처 스타일Modular Monolith, Layered Architecture, Clean Architecture, Hexagonal Architecture내부 구조를 계층적 또는 포트 - 어댑터 방식으로 구분하여 유연성 확보
점진적 마이그레이션 전략Strangler Fig Pattern, Service Extraction모놀리식 구조를 점진적으로 마이크로서비스로 전환하는 전략
설계 원칙경계 유지Interface Segregation, Loose Coupling명확한 API 및 의존성 최소화로 모듈 간 결합도 감소
모듈화 단위 설계Vertical Slice, Shared Kernel기능 단위로 전체 계층을 포함하는 구조 또는 공통 코드의 중앙 관리 방식
객체지향 설계 원칙SOLID, 12-Factor App유연한 변경과 재사용을 위한 설계 기준 적용
구현 기법도메인 중심 설계DDD, Domain Modeling, Aggregate풍부한 도메인 모델을 기반으로 책임을 분산
의존성 제어 및 테스트Dependency Injection, IoC Container, Architecture Testing결합도 제어와 아키텍처 위반 자동 감지로 유지보수성 확보
통신 및 데이터 저장In-Process Communication, CQRS, Event Sourcing, Event StoreCQRS+Event 기반의 도메인 흐름 구현
운영 및 배포배포 전략단일 배포, CI/CD, Blue-Green, Canary모듈 단위이지만 전체를 하나의 패키지로 배포하며 자동화된 배포 전략 적용
장애 대응/확장성Circuit Breaker, Zero Downtime Deployment운영 안정성 확보 및 무중단 서비스 전환 가능
테스트 전략계약 및 경계 테스트Contract Testing, Hexagonal Testing모듈 간 통신 및 외부 통합 시 계약 검증을 통해 안정성 확보
프로세스/조직협업 및 품질 관리설계 표준, 코드 리뷰, Static Analysis Tool팀 기반 협업을 위한 표준화와 품질 확보 도구 도입
학습/실험 전략도메인 기반 PoC, Event Storming개념 검증과 설계 실험을 통한 경계 명확화 및 설계 검증

반드시 학습해야 할 내용

카테고리주제항목설명
1. 소프트웨어 설계 원칙Domain-Driven Design (DDD)전략적/전술적 패턴, Bounded Context, Aggregates비즈니스 도메인 기반의 경계 설계 및 책임 분리 설계
모듈 경계 설계모듈화 원칙, 응집도·결합도, 내부 문서화변경 최소화, 유연한 확장성, 내부 설계 가시화
SOLID, 레이어드 아키텍처단일 책임, 의존성 분리, 계층 분할모듈 내부의 유지보수성과 테스트 가능성 확보
2. 아키텍처 스타일 및 구조 패턴Monolithic vs Microservices비교 분석, 전환 전략시스템 복잡도, 조직 구조, 도메인 특성 기반 선택
Modular Monolith Architecture수직 슬라이스, 계층 설계, 내부 모듈 인터페이스독립적 변경·배포 가능한 단위로 구조화
Hexagonal / Clean Architecture의존성 역전, 포트/어댑터 구조, 계층 간 책임 명확화내부 비즈니스 로직과 외부 요소의 명확한 분리
3. 구현 기술 및 통신 방식의존성 주입 (Dependency Injection)IoC 컨테이너, DI 구현모듈 간 결합도 감소 및 테스트 용이성 증가
이벤트 기반 통신In-process Event Bus, 메시지 브로커비동기 확장과 관심사 분리, 마이크로서비스 전환 기반 마련
API 및 인터페이스 설계REST, RPC, Contract-First모듈 간 계약 기반 설계로 통합 테스트 용이
4. 데이터 및 트랜잭션 설계모듈 간 데이터 관리 설계데이터 스키마 분리, 레포지토리 분리모듈 간 충돌 최소화 및 데이터 일관성 보장
Outbox / Saga 패턴메시지 전송 원자성, 분산 트랜잭션 대응메시지 일관성 및 롤백 지원
5. 테스트 및 품질 보증테스트 전략단위/통합 테스트, ArchUnit / NetArchTest모듈 경계 보장과 자동화 테스트 전략 수립
코드 품질 관리정적 분석, Lint, SonarQube품질 확보와 기술 부채 예방
6. 배포 및 운영 전략CI/CD 및 배포 자동화파이프라인 설계, Blue-Green, Canary안전한 배포와 빠른 롤백을 위한 전략
모듈 단위 모니터링APM, 메트릭, 로그, 트레이싱운영 중 이슈 조기 감지 및 분석 용이
컨테이너 오케스트레이션Docker, Kubernetes, Helm 등운영 환경에서의 일관된 실행 및 확장 지원
7. 시스템 진화 및 전환 전략Strangler Fig Pattern점진적 리팩토링, 서비스 분리 단계화기존 모놀리스를 안정적으로 모듈러 또는 MSA 로 전환
Evolutionary Architecture적응 가능한 구조, 변경 수용성 설계시스템의 장기적인 확장성과 변화 대응 확보
  1. 소프트웨어 설계 원칙:
    Modular Monolith 아키텍처의 기반은 도메인 주도 설계 (DDD) 와 SOLID 원칙이다. 특히 Bounded Context 및 모듈 경계 정의는 구조적 유지보수성과 확장을 위해 반드시 필요하며, 레이어드 구조를 통해 책임 분리도 강화된다.

  2. 아키텍처 스타일 및 구조 패턴:
    Monolith, Modular Monolith, Microservices 간 차이를 이해하고, Clean/Hexagonal Architecture 등을 통해 유연하고 테스트 가능한 구조를 설계해야 한다. 수직 슬라이스 및 계층화된 모듈화 전략은 핵심 구성이다.

  3. 구현 기술 및 통신 방식:
    의존성 주입과 In-process 이벤트 기반 통신은 모듈 간 유연한 연결을 가능하게 하며, API 인터페이스 설계는 테스트 및 연동을 안전하게 만든다.

  4. 데이터 및 트랜잭션 설계:
    모듈 간 데이터 설계의 핵심은 충돌 방지와 데이터 일관성 유지다. Outbox 와 Saga 는 메시지 기반 트랜잭션 구현에 필수적이다.

  5. 테스트 및 품질 보증:
    단위 및 통합 테스트를 통해 모듈 경계 위반을 감지하고, 정적 분석 도구를 활용하여 코드 품질을 지속적으로 유지해야 한다.

  6. 배포 및 운영 전략:
    CI/CD, Canary Release, APM 등은 모듈별 빠르고 안정적인 배포와 운영 관리를 위한 기반이며, Kubernetes 등의 오케스트레이션 도구는 스케일업/다운을 자동화하는 데 필수적이다.

  7. 시스템 진화 및 전환 전략:
    Strangler Fig Pattern 은 기존 모놀리스를 점진적으로 MSA 나 고도로 모듈화된 구조로 전환할 때 유용하며, Evolutionary Architecture 는 변경에 강한 구조 설계의 방향성을 제공한다.


용어 정리

카테고리용어설명
아키텍처 스타일Monolithic Architecture모든 기능이 하나의 코드베이스 및 실행 단위로 구성된 전통적 구조
Modular Monolith내부적으로 모듈화되어 있지만 단일 배포 단위를 유지하는 구조
Microservices Architecture서비스 단위로 분산되어 독립적 개발·배포가 가능한 아키텍처
Event-Driven Architecture이벤트 흐름 기반의 비동기 처리 중심 아키텍처
설계 원칙Bounded ContextDDD 에서 도메인 경계를 명시적으로 정의하는 논리적 경계
Single Responsibility하나의 클래스 또는 모듈은 하나의 책임만 가져야 한다는 원칙 (SRP)
High Cohesion관련된 기능을 하나의 단위로 묶어 응집도를 높이는 설계 원칙
Loose Coupling모듈 간의 의존성을 줄여 유연성과 유지보수성을 확보하는 원칙
Interface Segregation클라이언트별로 최소한의 인터페이스만 제공하도록 분리하는 원칙
구현 기법Dependency Injection (DI)외부에서 객체의 의존성을 주입하여 유연성을 높이는 방식
Event Sourcing상태 변경 이력을 이벤트 시퀀스로 저장하는 방식
Repository Pattern데이터 접근 로직을 캡슐화해 도메인 로직과 분리하는 패턴
Factory Pattern객체 생성 로직을 분리하여 추상화하는 생성 패턴
Vertical Slice기능 단위로 계층을 종단 분리하여 모듈화하는 설계 방식
Strangler Fig Pattern기존 시스템 위에 새로운 구조를 병행하여 점진적으로 교체하는 전략
통신 패턴In-Process Communication네트워크 없이 같은 프로세스 내에서 이루어지는 동기 함수 호출 방식
Request-Response클라이언트 요청에 대해 응답하는 전통적인 동기 통신 방식
Publish-Subscribe이벤트 기반으로 메시지를 발행하고 여러 구독자가 처리하는 방식
Message Queue메시지를 임시 저장하여 비동기적으로 처리하는 방식
데이터 관리Shared Database여러 모듈이 동일한 데이터베이스를 공유하는 방식
Database per Module각 모듈이 별도의 DB 를 가지는 설계 방식
Event Store이벤트를 영구 저장하는 전용 데이터 저장소
Projection이벤트를 기반으로 읽기 전용 뷰를 생성하는 방식
Snapshot특정 시점의 Aggregate 상태를 저장한 스냅샷
운영 전략CI/CD빌드, 테스트, 배포를 자동화하는 통합 파이프라인
Blue-Green Deployment두 개의 환경을 번갈아 사용해 무중단 배포를 구현하는 전략
Canary Deployment일부 트래픽만 새 버전으로 보내는 점진적 배포 방식
Zero Downtime Deployment가용성 저하 없이 새로운 버전으로 전환하는 배포 방식
Circuit Breaker장애 발생 시 서비스 연쇄 중단을 방지하는 보호 패턴
테스트 전략Contract TestingAPI 또는 인터페이스 계약을 기준으로 양측 테스트를 수행하는 방식
Integration Testing모듈 간 상호 작용을 검증하는 테스트 방식
End-to-End Testing (E2E)사용자 관점에서 시스템 전체의 흐름을 검증하는 테스트 방식
TDD (Test Driven Development)테스트를 먼저 작성하고 이후 코드를 구현하는 개발 방식
품질 관리Static Analysis Tool코드 실행 없이 정적 분석을 통해 문법, 스타일, 보안 문제를 탐지하는 도구
Architecture Tests아키텍처 규칙 위반, 계층 침범, 의존성 흐름을 검사하는 테스트

실무 사용 사례별 용어

사용 사례관련 용어 (구현 기술/패턴/원칙/전략)설명
MSA 환경에서의 통신 구성Request-Response, Publish-Subscribe, Message Queue, Circuit Breaker, API Gateway, Contract Testing서비스 간 통신은 REST/gRPC 또는 이벤트 방식. 장애 방지를 위해 회로 차단 패턴 (Circuit Breaker) 사용.
Event-Driven Architecture 구현Event Sourcing, Event Store, Projection, Snapshot, CQRS, Publish-Subscribe이벤트 중심 시스템 구성. 상태 저장은 이벤트 저장소를 통해 수행하며, 읽기 모델 분리 (CQRS) 로 성능 향상.
모놀리식 구조에서의 내부 통신In-Process Communication, Shared Database, Layered Architecture, Tight Coupling메서드 호출로 통신하며, DB 공유. 내부 결합도가 높아 구조적 분리 어려움.
CI/CD 기반 자동화 파이프라인 구성CI/CD, Blue-Green Deployment, Canary Deployment, Zero Downtime Deployment, Static Analysis Tool배포 자동화와 품질 확보를 위해 블루그린/카나리 배포와 정적 분석 도구를 함께 적용.
도메인 중심의 모듈화 설계Bounded Context, Aggregate, Vertical Slice, DDD, Interface Segregation, Repository Pattern각 도메인을 기준으로 모듈을 수직 슬라이스로 분리하고, 외부 노출 계약은 인터페이스 기반으로 관리.
마이크로서비스 전환 전략 적용 시Strangler Fig Pattern, Modular Monolith, Contract Testing, Anti-corruption Layer기존 시스템을 점진적으로 교체하기 위한 구조적 접근 방식. 인터페이스 계약 기반 테스트 필수.
API 통합 시스템 구현 시Public API, Interface Package, API Gateway, Request-Response, Contract Testing외부 소비자와 통합 시 명확한 인터페이스 및 테스트가 중요하며, 게이트웨이를 통한 통합 제어 수행.
복잡한 데이터 변경 및 읽기 분리 처리CQRS, Event Sourcing, Projection, Repository Pattern, Snapshot, Aggregate데이터 저장/조회 경로 분리 및 이벤트 흐름 기반 데이터 일관성 처리 방식 적용.
운영 안정성 확보 목적 시스템 설계Circuit Breaker, Health Check, Blue-Green Deployment, Zero Downtime Deployment, Monitoring장애 방지 및 운영 중단 없이 배포하기 위한 전략적 구성.
고가용성 및 확장 환경 구성Database per Module, Shared Nothing, Stateless, Horizontal Scaling, Service Discovery모듈/서비스 단위 독립성과 수평 확장을 위해 데이터 분리, 서비스 디스커버리 필수.

참고 및 출처