Modular Monolithic
Modular Monolith Architecture는 단일 배포 단위 내에서 비즈니스 도메인 기반으로 명확히 분리된 모듈 구조를 갖는 설계 방식이다.
각 모듈은 높은 응집도와 낮은 결합도를 유지하며 독립적으로 개발·테스트되고, 명확한 인터페이스를 통해 통신한다. 초기에는 모놀리식의 단순함을 유지하면서도, 필요 시 마이크로서비스로의 전환을 고려한 진화 가능성과 확장성을 동시에 확보할 수 있어 ’ 골디락스 아키텍처 ’ 로도 불린다.
등장 배경 및 발전 과정
Modular Monolith는 단순히 아키텍처 스타일이 아니라,
지나친 분산과 복잡성에 대한 반작용이자, 실용성과 확장성을 동시에 추구하는 전략적 구조로 등장했다.
- 복잡한 도메인을 갖는 단일 애플리케이션을 팀 규모와 복잡도에 맞춰 내부 모듈화
- 분산 전환 전의 안정적 중간단계로 사용
- 잘 설계된 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 연계 등으로 확장 가능 |
연관 아키텍처 패턴 적용
패턴 | 설명 |
---|---|
CQRS | Command/Query 분리로 책임 구분, 이벤트 기반 읽기 모델 구성 |
Event Sourcing | 상태를 이벤트로 저장, 리플레이 기반 복원 |
Outbox/Inbox | 트랜잭션과 함께 이벤트 기록 → 비동기 발행 보장 |
Internal API + Event Bus | 모듈 간 동기·비동기 통신 혼용 가능 구조 제공 |
주요 기능 및 역할
Modular Monolith 의 주요 기능은 도메인 기반 모듈 캡슐화, 명확한 인터페이스 설계, 느슨한 결합 구조를 통해 복잡성을 줄이고 유지보수를 용이하게 하는 데 있다. 각 모듈은 독립적인 데이터와 로직을 소유하며, 내부적으로는 함수 호출이나 이벤트 기반 통신을 통해 협력한다. 모든 모듈은 단일 프로세스로 배포되지만, 각기 독립적으로 테스트·확장·전환이 가능한 구조로 구성되어 있으며, 이는 향후 마이크로서비스로의 점진적 진화를 위한 기반이 된다.
기능 분류 | 기능 설명 | 역할 및 실무 적용 |
---|---|---|
모듈 캡슐화 | 각 모듈은 도메인 단위로 코드/로직/데이터를 자체 관리하며 외부에 내부 구현을 노출하지 않음 | 기능별 책임과 변경 범위 제한, 충돌 최소화 |
명확한 인터페이스 | 각 모듈은 공개된 API, 인터페이스 또는 이벤트를 통해서만 상호작용 | 인터페이스를 통한 테스트 가능, 계약 기반 통신 |
느슨한 결합 | 모듈 간 직접 참조를 줄이고, 인터페이스나 이벤트로만 연결 | 변경 시 파급효과 최소화, 향후 서비스 분리 용이 |
도메인 경계 및 조직화 | DDD 기반 Bounded Context 로 모듈 정의, 공통 언어 기반 설계 | 팀 간 소유권 분리, 협업 명확화 |
동기/비동기 통신 지원 | 내부 메서드 호출 또는 이벤트 기반 비동기 통신 모두 지원 | 이벤트 기반 확장을 통해 마이크로서비스 전환 대비 |
데이터 관리 | 모듈별 논리적 DB 분리 또는 명시적 스키마 분리, 트랜잭션 경계 유지 | 데이터 격리, 접근 통제, 일관성 유지 |
공통 인프라 제공 | 인증, 로깅, 트레이싱, 설정 등 공유 서비스는 중앙화된 기반으로 제공 | 개발 표준화, 중복 방지, 유지보수 간소화 |
운영 단순화 | 모든 모듈은 단일 프로세스/컨테이너로 배포 | DevOps 효율성, 테스트 및 릴리즈 간편 |
테스트 가능성 | 각 모듈 단위로 유닛/통합 테스트 가능, API/이벤트 기반 테스트 구성 | 품질 보장, CI 파이프라인 적용 용이 |
진화 및 전환 기반 | 아키텍처적으로 서비스 추출에 유리한 구조 제공 | 점진적 MSA 이행 전략 실현 가능 |
특징
Modular Monolith는 다음과 같은 특징을 가진 아키텍처다:
- 구조적으로는 모듈화된 내부 계층 + 단일 배포 구조
- 개발적으로는 병렬 개발과 테스트 전략 최적화
- 운영적으로는 배포/모니터링/성능 측면에서 단순성과 효율성 제공
- 또한, MSA 로의 점진적 전환을 고려한 설계 토대를 제공한다.
구조적 특징
항목 | 설명 |
---|---|
단일 실행/배포 단위 | 모든 모듈이 하나의 애플리케이션으로 빌드되어 단일 프로세스에서 실행됨 |
모듈화된 아키텍처 | 도메인 중심으로 모듈을 분리하며, 모듈 내 계층 구조 (Domain , App , Infra , Interface ) 를 유지 |
명확한 경계 유지 | 네임스페이스 분리, 인터페이스 기반 접근, 캡슐화로 모듈 간 경계 명확화 |
낮은 모듈 간 결합도 | 인터페이스 (계약), DI, 이벤트 등을 활용한 간접 참조 방식 채택 |
개발 및 테스트 특징
항목 | 설명 |
---|---|
통합 코드베이스 | 모든 모듈이 하나의 코드 저장소 (Monorepo 또는 단일 Git Repo) 에 존재 |
테스트 전략 분리 가능 | 모듈 단위의 유닛 테스트, 경계 간 통합 테스트 적용 가능 |
개발 병렬화 가능 | 각 모듈은 독립 개발 가능하며, 기능/도메인 단위 병렬 작업에 적합 |
재사용성과 일관성 | 공통 유틸리티, 보일러플레이트, 표준화된 계층 구조 활용 가능 |
운영 및 배포 특징
항목 | 설명 |
---|---|
배포 단순화 | 단일 아티팩트로 배포되므로 CI/CD 가 단순화됨 |
성능 효율성 | 모듈 간 인 - 프로세스 호출로 네트워크 지연 없음 (MSA 대비 성능 우위) |
모니터링 통합 | 로깅, 트레이싱, 메트릭을 애플리케이션 레벨에서 일관되게 관리 가능 |
낮은 인프라 복잡도 | 하나의 서비스로 운영되므로 인프라 구성/배포 수단 간소화 |
Traditional Monolith vs. Modular Monolith vs. MSA 비교
Modular Monolith 는 전통적인 Monolith 의 단순함과 MSA 의 유연성 사이의 균형점이며, MSA 로의 진화적 전환을 위한 전략적 출발점이다. 각 아키텍처는 조직의 규모, 팀 구성, 운영 역량에 따라 선택과 전략이 달라져야 한다.
비교 항목 | Traditional Monolith | Modular Monolith | Microservices 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 Monolith | MSA 전환 고려한 구조화, 유지보수 유리 | 초기 설계 복잡도 있음 | 점진적 성장, 도메인별 팀 운영 조직 |
MSA | 완전한 분산, 독립 배포, 유연한 확장 | 높은 운영/설정 복잡도 | 대규모 조직, 고성능 확장 필요 시점 |
핵심 원칙
Modular Monolith 아키텍처는 명확한 경계 정의, 책임 분리, 의존성 통제, 인터페이스 중심 설계를 기반으로 높은 응집도와 낮은 결합도를 유지하는 것이 핵심이다. 각 모듈은 자신의 책임과 데이터를 소유하며 외부와는 명시적인 계약 (API 또는 이벤트) 으로만 통신하고, 확장성과 변경 용이성을 위해 설계 시 OCP, DIP, ISP 등 SOLID 원칙을 내재화해야 한다.
분류 | 핵심 원칙 | 설명 및 실무 적용 예시 |
---|---|---|
책임 분리 (SRP) | 모듈은 하나의 도메인 기능만을 담당하며 내부 응집도를 유지 | Order , Inventory 모듈이 독립된 책임 소유 |
경계 명확성 (Boundary Clarity) | DDD 의 Bounded Context 기준으로 모듈 경계를 설정하고 외부 접근 차단 | 다른 모듈의 클래스/데이터 직접 참조 금지 |
의존성 역전 (Dependency Inversion) | 하위 계층의 구체 구현이 상위 계층에 영향을 주지 않도록 인터페이스 기반 추상화 적용 | OrderService 가 PaymentPort 에 의존, 실제 구현은 infrastructure 모듈에서 |
개방 - 폐쇄 원칙 (OCP) | 기존 코드 변경 없이 기능 확장이 가능하도록 인터페이스 기반 설계 | 새로운 결제 수단 추가 시 기존 모듈 수정 없이 확장 가능 |
데이터 격리 (Data Ownership) | 각 모듈은 자신의 데이터를 소유하고 직접 관리하며 외부 모듈의 데이터에 접근하지 않음 | Order 모듈만이 Order 테이블을 직접 읽고 쓸 수 있음 |
공통 기능 모듈화 (Shared Concerns Isolation) | 인증, 로깅, 메시징 등은 별도의 공통 모듈로 분리하고 재사용 | logging-module , auth-module 등 재사용 가능한 패키지 제공 |
인터페이스 분리 (ISP) | 필요한 인터페이스만 제공하고, 과도한 의존을 방지 | PaymentCommand 와 PaymentQuery 인터페이스를 분리해서 제공 |
단방향 의존성 (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 Coupling | DB 스키마 공유로 변경 시 전체 영향 |
Circular Dependency | 구조 순환으로 아키텍처 안정성 붕괴 |
Utility Abuse | 공통 모듈이 도메인 로직까지 흡수해 중심이 무너짐 |
God Module (Blob)
- 원칙 위반: 단일 책임 원칙 (SRP), 책임 분리 실패
- 원인: 모든 도메인 기능을 한 파일/클래스/모듈이 담당
- 결과:
- 변경 파급력 극대화
- 테스트 불가능
- 신규 개발 시 타 기능까지 변경 위험
- 해결 전략:
- DDD 기반 Bounded Context 로 분리
- Use Case 별로 Service 클래스를 나누기
- 모듈별 책임 분산
Data Bleeding
- 원칙 위반: 경계 명확성, 데이터 격리 원칙 위반
- 원인:
order_module
이user_module
의 내부 엔티티 직접 조회 - 결과:
- 경계 침범으로 변경 파급
- 의존성 정리 어려움
- 테스트 불가능
- 해결 전략:
- 공개된 API or Event 만 통해 접근
- Anti-Corruption Layer 적용
- DTO/Facade 구조 도입
Concrete Dependency Trap
- 원칙 위반: DIP 위반 (인터페이스 아닌 구현체 직접 의존)
- 원인:
OrderService
가JpaOrderRepository
에 직접 의존 - 결과:
- 테스트 시 대체 불가
- 변경 시 ripple effect
- 해결 전략:
OrderRepository
인터페이스 도입- 의존성 주입 (DI) 기반 연결
- Mocking 용이성 확보
Shared Database / Schema Coupling
- 원칙 위반: 데이터 격리 실패
- 원인: 모든 모듈이
users
,orders
등 테이블을 직접 쿼리 - 결과:
- 스키마 변경 시 전체 모듈 재배포
- 데이터 소유권 불분명
- 해결 전략:
- 논리적 스키마 분리 (
schema per module
) - Repository 계층 외 접근 금지
- Outbox/DTO 기반 데이터 복제
- 논리적 스키마 분리 (
Circular Dependency
- 원칙 위반: 단방향 의존 원칙 위반
- 원인:
Order → Inventory → Payment → Order
의존 순환 - 결과:
- 컴파일/빌드 실패
- 이해 불가능한 구조
- 계층이 무너짐
- 해결 전략:
- 인터페이스 역전 (DIP 적용)
- Application Service 로 의존성 경계 이동
- Dependency Rule 테스트 적용 (ArchUnit 등)
Core Overload / Utility Abuse
- 원칙 위반: 공통 기능 모듈 오남용
- 원인: 모든 도메인의 로직이
core
,utils
,common
디렉토리에 집중 - 결과:
- 공통 모듈이 실질적 도메인 기능을 포함
- 분리도 불가능한 구조
- 해결 전략:
- 공통 모듈은 진짜 " 재사용성 " 중심 기능만 포함 (e.g. 공통 DTO, 인증, 로깅 등)
- 도메인 로직은 도메인 안에만 존재하도록 유지
주요 원리 (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 ↔ Domain | Domain → 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: 이벤트 핸들링
- 이 구조는 내부 트랜잭션과 외부 메시징 시스템을 분리하여 Atomicity + Eventually Consistent 구조 보장
주요 원리와 매핑되는 작동 구조
원리 | 대응 구조 |
---|---|
모듈 캡슐화 | Interface 패키지 ↔ Implementation 패키지 분리, public only 접근 |
단방향 의존성 | 아키텍처 테스트 도구로 순환 참조 방지 검증 |
이벤트 기반 통신 | EventBus / Kafka / RabbitMQ 와 통합 |
데이터 격리 | 모듈 전용 DB 스키마 또는 테이블 사용 |
공통 기능 분리 | common , shared , core 모듈로 추상화 |
구조 및 아키텍처
Modular Monolith 아키텍처의 구조 및 구성 요소는 다음과 같은 특징을 갖는다:
- 필수 요소는 Host, 모듈, 계층 구조, 인터페이스, 공통 인프라로 구성
- 선택 요소는 이벤트 기반 통신, CQRS, Outbox, API Gateway 등으로 유연한 확장 가능
- 모듈 내부는 계층화(Interface → App → Domain → Infra) 구조를 따라 구성
- 실무적으로는 마이크로서비스 전환을 고려한 설계 전략 기반으로 점진적 분리 및 독립화 가능
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 Application | DI 컨테이너, 모듈 로딩, 전체 앱 실행 관리 |
도메인 기반 모듈 | 각 비즈니스 도메인별로 완결된 기능 집합 (회원, 주문 등) |
모듈 인터페이스 (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, 계층 분리, 수직 슬라이스 등 아키텍처 정렬
- 통신 전략: 이벤트 기반, Outbox, CQRS 등의 느슨한 결합 처리
- 운영 전략: 모듈 단위 테스트, Strangler Fig 패턴 통한 점진적 도입
- 기술 기반: 언어/프레임워크별 빌드/의존성 도구를 통한 코드베이스 관리
아키텍처 스타일 및 구조화 전략
구현 기법 | 설명 | 목적 |
---|---|---|
DDD (Domain-Driven Design) | 도메인 경계를 기준으로 모듈 분리 | 경계 명확화, 도메인 응집도 확보 |
Clean Architecture | Interface → 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 Pattern | DB 트랜잭션 + 메시지 전송 분리 | 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 Pattern | EventStoreDB, Axon, custom infra |
모듈 간 통신 방식 패턴
통신 전략 | 추천 구현 패턴/기법 | 핵심 특징 | 장점 | 한계/주의사항 | 실무 적용 조건 |
---|---|---|---|---|---|
동기 호출 | Interface-based Communication | 모듈 간 직접 함수/메서드 호출 | 단순, 직관적, 디버깅 쉬움 | 강결합, 변경 전파 위험 | 변경 가능성 낮고 성능 요구되는 모듈 간 |
Shared Kernel | 공통 도메인 코드 공유 | 중복 제거, 코드 재사용 | 경계 침범 위험, 모듈 독립성 저하 | 공통 VO, Enum 등에 한정 | |
비동기 이벤트 기반 | Domain Event Pattern | 비동기 이벤트 퍼블리싱/구독 | 느슨한 결합, 확장성 우수 | 멱등성, 이벤트 유실 처리 필요 | 사후 처리 또는 리액티브 시스템 |
Application Messaging | 명령/이벤트 메시지 전송 | 명시적 명령 구조화, CQRS 적합 | 오케스트레이션 시 복잡도 증가 | Command Handler 구조 필요 | |
Outbox Pattern | Transactional Outbox | DB 와 메시지를 트랜잭션으로 묶음 | 데이터 일관성 보장, 메시지 유실 방지 | Polling 지연, 복잡한 구성 | 강한 일관성이 필요한 업무 로직 |
Polling Publisher | DB 테이블 → 큐로 메시지 발행 | 구현 단순, 신뢰도 확보 | 실시간성 부족, 중복 처리 필요 | Kafka, Debezium 연계 환경 | |
CQRS | Command Handler Pattern | 명령 처리 전용 클래스 구조 | 책임 분리, 테스트 용이 | 설계 복잡도 증가 | 복잡한 도메인, R/W 분리 요구 |
Read Model | 쿼리 전용 모델 제공 | 읽기 성능 최적화 | 데이터 동기화 관리 필요 | 읽기 집중, 성능 요구 환경 | |
Event Sourcing | Aggregate + 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, 커스텀 모듈 메트릭, 통합 대시보드 구성 |
모듈 경계 관리
모듈 경계를 명확히 설계하지 않으면 기능 중복과 모듈 간 결합도가 증가한다. 특히 요구사항이 변화하거나 팀 간 협업이 미흡하면 경계가 흐려질 수 있으므로, DDD 기반의 Bounded Context 설정과 정기적인 아키텍처 리팩토링이 필수다.팀 협업 및 계약 조율
공유 코드베이스 환경에서 모듈 간 인터페이스 계약을 동기화하지 않으면 빌드 실패, 코드 충돌이 잦아진다. 모듈별 팀 책임 분배와 **계약 기반 자동화 (CI/CD)**가 중요하며, 브랜치 전략 개선과 명확한 API 명세가 필요하다.운영 및 배포
단일 배포 구조에서는 하나의 모듈만 장애가 발생해도 전체 시스템이 영향을 받는다. 따라서 피처 플래그, 모듈 리로드, Blue-Green 배포 전략으로 위험을 최소화하고, 장기적으로는 모듈 독립 배포를 고려해야 한다.통신 및 데이터 일관성
교차 모듈 간 트랜잭션은 데이터 일관성 문제를 유발한다. Outbox Pattern과 SAGA는 데이터 일관성을 보장하면서도 모듈 간 느슨한 결합을 유지할 수 있게 해준다.테스트 및 품질
통합 테스트에만 의존하면 결함 위치 파악이 어렵고, 릴리즈 주기가 길어진다. Contract Testing, 의존성 분리 테스트, 테스트 자동화 도구를 통해 각 모듈을 독립적으로 검증하는 구조가 필요하다.스케일링 및 성능
기능 단위로 요청이 집중되면 특정 모듈이 병목이 된다. CQRS 적용, 읽기/쓰기 분리, 캐시 계층화 등의 성능 최적화가 필요하며, 모듈별 독립 스케일링 구조가 성능 향상에 효과적이다.모니터링 및 관측성
모듈 단위로 문제를 진단하기 어렵다면 전체 장애 대응 속도가 느려진다. OpenTelemetry, 분산 추적, 커스텀 모듈 메트릭으로 모듈 레벨의 관측성을 확보해야 운영 안정성을 유지할 수 있다.
Modular Monolith 도전 과제별 실무 대응 사례
도전 과제 | 실무 사례 (기업) | 대응 전략 / 실제 적용 방법 |
---|---|---|
모듈 경계 설정 및 유지 | Shopify | Shopify 는 도메인 중심 모듈 구조를 유지하면서도 " 패키지 레벨 의존성 그래프 " 를 통해 모듈 간 호출을 자동으로 추적하고, 위반 시 컴파일 에러 발생 구조로 설계함. |
Google (Service Weaver) | Google 은 각 모듈을 API surface 로 노출하고 인터페이스만 접근 가능하도록 설계. 내부 구현 접근은 Linter 및 Build 시스템에서 금지함. | |
팀 협업 / 인터페이스 계약 | StackOverflow | 각 모듈별 Ownership 을 명확히 하고, Git 단위 PR Template 에 API 변경 시 Impact 분석 필수로 지정. API 문서는 자동화되어 있고, 변경 시 계약 검증 테스트가 병행됨. |
운영 및 배포 전략 | Shopify | 모놀리스를 유지하지만 모듈마다 독립적인 Feature Flag 를 적용해 모듈 단위의 롤아웃 및 롤백을 가능하게 함. 전체 앱 재시작 없이 모듈 업데이트가 가능함. |
트랜잭션과 데이터 일관성 | Google 의 내부 시스템은 Outbox 패턴을 강화해 사용하며, 모든 모듈 간 커뮤니케이션은 " 이벤트 로그 기반 전파 " 로 처리. DB 트랜잭션 내에 이벤트 삽입 후 전송함. | |
테스트 및 품질 관리 | StackOverflow | 모듈별 테스트 디렉토리를 유지하고, Interface Mock 과 Pact 를 활용한 계약 기반 테스트를 채택. GitHub Action 기반으로 모듈 단위 테스트 자동 실행. |
스케일링 및 성능 대응 | Shopify | CQRS 적용으로 읽기 부하가 높은 모듈은 읽기 전용 캐시로 처리하며, 일부 고부하 모듈은 독립 실행 가능한 Process 로 전환하여 모놀리스 내에서 자체 스케일링을 허용함. |
모니터링 및 관측성 | OpenTelemetry 기반 Trace ID 를 모듈 간 전파하고, GCP Stackdriver 로 통합 모듈 메트릭을 수집함. 모듈별 latency, error rate, throughput 을 실시간 대시보드로 노출함. |
Modular Monolith 아키텍처 검증 체크리스트
Modular Monolith 는 단순한 구조가 아닌, " 모듈화된 아키텍처 원칙 " + " 단일 배포 유닛 “ 을 유지하면서도 MSA 수준의 독립성, 확장성, 테스트성, 관측성을 확보하는 것이 핵심이다.
실무 기업들은 다음과 같은 전략으로 도전 과제를 해결하고 있다:
- Shopify: 단일 배포 유지, 철저한 모듈 분리 + 피처 플래그 기반 운영
- Google: Bounded Context 경계와 인터페이스 보장 → MSA 전환도 무리 없음
- StackOverflow: 계약 중심 개발문화와 테스트 자동화를 통해 협업 리스크 최소화
이 구조와 체크리스트는 “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 이행 대비 구조화 모듈 |
모듈 구조 방식:
기술 계층 (Layer), 도메인 (Domain), 기능 (Feature), Vertical Slice 방식으로 나뉘며, 도메인/기능 중심 분리는 비즈니스 변화에 유리하고, 계층형은 기술적 통합 관리에 유리함.통신 방식:
동기 호출 구조는 단순성과 성능에 유리하나 결합도가 높고, 비동기 방식은 유연한 확장성과 오류 복원력, 낮은 결합도를 제공함. 이벤트 기반 구조가 MSA 전환을 유도하기에 적합함.데이터 관리 방식:
공유 스키마는 빠른 구축에 유리하나 변경 전파 리스크가 크며, 분리 스키마 및 DB 인스턴스 분리는 독립성과 유지보수성 확보에 유리. MSA 전환을 고려한다면 필수 고려 사항.배포 전략:
단일 배포 구조는 배포 자동화가 간편하나, 모듈 영향도가 크며 전체 재배포가 필요하다. 선택적 모듈 배포 또는 모듈별 Docker 이미지 분리가 이상적 운영 구조.저장소 관리 방식:
Monorepo 는 변경 추적과 협업이 쉬우나 충돌이 많고, Manyrepo 는 독립 배포와 이력 관리에 유리하며 서비스 분리 전환에도 강점을 가진다.아키텍처 스타일:
DDD, CQRS 등은 모듈 간 경계 명확화와 확장성, 이벤트 처리 기반 설계에 필수. 특히 CQRS 기반 구조는 읽기와 쓰기 흐름 분리로 시스템 안정성 확보에 효과적.도메인 범위 구성:
단일 도메인 구조는 집중적인 기능 최적화가 가능하나 확장성 부족, 다중 도메인 구조는 시스템 복잡도는 올라가지만 팀 간 분리 및 병렬 개발에 유리함.운영 확장성:
고정형은 단순하지만 유연성이 부족하고, 확장형은 향후 마이크로서비스 전환 및 클라우드 네이티브 대응 측면에서 필수 구조.
실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점
카테고리 | 항목 | 설명 | 권장 사항 |
---|---|---|---|
설계 | 도메인 경계 정의 | 모듈 간 경계가 흐려지지 않도록 도메인 중심으로 구조 설계 | 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 기반의 경계 설정, 인터페이스 표준화, 모듈 설계 규칙이 핵심이다. 이는 모듈 경계가 오염되지 않고 장기적으로 유지보수 가능한 구조를 만드는 데 중요하다.
구현 측면에서는 의존성 방향을 철저히 관리하고, 공통 코드 또는 데이터 공유로 인한 침범을 방지하는 전략이 필요하다. DI, 레이어 캡슐화, 리포지토리 분리가 실효성이 높다.
품질 측면에서는 테스트 커버리지와 아키텍처 검증이 중요하며, 특히 모듈 경계 테스트와 계약 기반 통합 테스트는 필수이다.
운영 측면에서는 배포 자동화, 환경 일관성 유지, 모듈 단위 모니터링 구조가 실무 안정성 확보의 기반이 된다.
협업과 문화 측면에서는 Conway’s Law 에 따라 팀 구조를 모듈 구조에 정렬시키는 것이 효과적이며, 암묵적 지식을 제거하기 위한 문서화와 시각화가 장기적인 유지보수에 결정적이다.
설계 측면
구분 | 내용 |
---|---|
경계 설정 | DDD 기반 Bounded Context 명확히 구분, interface 와 implementation 분리 |
의존성 관리 | 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) | |
과도한 모듈화 | 모듈 수 증가 시 관리 비용 및 복잡성 증가 가능 | 도메인 중심 적정 분리, 공통 유틸리티 모듈화 기준 수립 |
성능 최적화:
Modular Monolith 는 모듈 간 통신 비용과 데이터 접근 병목이 성능 저하의 주요 원인이 되므로, 인메모리 호출, 캐시 계층화, 쿼리 튜닝, APM 도구 활용을 통해 성능 병목을 사전에 감지하고 완화해야 한다.확장성 전략:
무상태 설계를 전제로 수평 확장이 가능하도록 구조를 잡고, 단일 모듈의 과도한 비대화 (모듈 붕괴) 를 방지하며 기능 중심의 수직 슬라이스로 분할한다. 추후 MSA 로 분리 가능성을 고려해 느슨한 결합과 독립 배포성을 확보한다.유지보수성:
코드 일관성을 위한 자동화된 품질 검사와 정적 분석 도입이 필수이며, ADR 및 API 문서를 통해 설계/변경 이력을 추적 가능해야 한다. 테스트는 단위 + 계약 기반 테스트를 함께 구성하여 경계 위반을 사전에 검출한다.보안 및 복원력:
전체 시스템이 단일 배포이므로 인증/인가 시스템이 취약하면 전체 서비스에 영향이 가므로, 보안 계층은 통합되되 경량화되어야 하며 장애 격리, 데이터 백업, 복구 시나리오까지 사전에 준비해야 안정적 운영이 가능하다.구조적 품질:
복잡한 인터페이스는 경계 침범 가능성을 높이므로 단순하고 명확한 API 계약이 중요하다. 트랜잭션은 서비스 간 경계를 고려한 보상 패턴 기반 일관성을 적용하며, 과도한 모듈화는 오히려 유지비용 증가 요인이 될 수 있어 주의가 필요하다.
실무 사용 예시
카테고리 | 사용 예시 | 사용 목적 | 사용 기술/개념 | 주요 효과 |
---|---|---|---|---|
전자상거래 플랫폼 | 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 | 모듈 경계 유지, 점진적 전환 가능성 확보 |
전자상거래 플랫폼은 높은 트래픽과 빠른 릴리즈 주기를 갖기 때문에, 모듈 단위로 기능을 분리하고 Outbox, CQRS 등으로 유연한 메시징 기반 확장을 구현하여 성능과 안정성을 동시에 확보함.
기업 포털은 내부 조직 구조에 따라 사람, 조직, 게시판, 결재 등의 기능이 나뉘며, 팀 단위 관리가 용이하고 역할 기반 보안 정책을 적용할 수 있는 모듈러 구조가 효과적임.
헬스케어 시스템은 환자 정보 보호, 의료법 규제 준수를 위한 보안 설계가 핵심이며, 모듈별로 접근 제어를 분리하고 감사 추적이 가능한 구조로 운영되어야 함.
금융 서비스 플랫폼은 복잡한 트랜잭션과 이벤트 흐름이 존재하며, 데이터 일관성 및 트랜잭션 보장을 위해 Kafka 기반 메시징 및 Outbox 패턴과 함께 적용하는 사례가 많음.
교육 플랫폼은 기능별로 강의, 학습자, 평가 시스템이 분리되어 있고, 학습 데이터를 모듈 단위로 관리하여 점진적인 개선 및 기능 독립성이 용이함.
콘텐츠 플랫폼은 검색, 게시, 사용자 인터랙션 등 복잡한 흐름을 가지며, 워크플로우 및 상태 관리를 모듈 단위로 분리해 콘텐츠 처리 성능을 높임.
전환 준비 구조는 초기에는 모놀리식으로 개발되지만, 모듈 경계를 명확히 하여 추후 마이크로서비스로의 자연스러운 전환이 가능하도록 설계되며, Clean Architecture 및 CQRS/Outbox 기반 패턴이 자주 활용됨.
활용 사례
사례 1: E- 커머스 플랫폼
시스템 구성:
|
|
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
역할 및 차이점:
- Product Module: 상품 카탈로그 관리, 가격 정책
- Order Module: 주문 생성, 상태 관리, 비즈니스 규칙
- Payment Module: 결제 처리, 환불, 정산
- Event Bus: 모듈 간 비동기 통신 중재
- 차이점: 기존 모놀리틱 대비 모듈별 독립적 개발/테스트
구현 예시:
|
|
사례 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
- 사용자가 주문 생성
- 주문 모듈이 결제 모듈에 결제 요청
- 결제 모듈이 재고 모듈에 재고 확인 요청
- 결제 및 재고 확인 후 주문 완료
역할
- 주문 모듈: 주문 생성 및 관리
- 결제 모듈: 결제 처리
- 재고 모듈: 재고 관리
차이점
- 기존 모놀리식: 모든 기능이 하나의 코드베이스에 혼재
- Modular Monolith: 도메인별로 모듈 분리, 명확한 경계
구현 예시:
|
|
사례 3: 전자상거래 플랫폼 (Shopify 사례 기반)
시나리오: 대규모 전자상거래 플랫폼 (Shopify 사례 기반)
시스템 구성:
- Ruby on Rails 기반 280 만 라인 이상의 코드와 50 만 개의 커밋을 가진 핵심 애플리케이션
- 주문 관리, 재고 관리, 결제 처리, 고객 관리, 배송 관리 모듈
- MySQL/Vitess 데이터베이스 클러스터
- Redis/Memcached 캐싱 계층
- Kafka 메시징 시스템
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:
- 클라이언트 요청이 글로벌 로드 밸런서를 통해 Rails API 게이트웨이로 라우팅
- API 게이트웨이가 요청을 적절한 모듈로 분배 (주문, 재고, 결제 등)
- 각 모듈이 자체 비즈니스 로직을 실행하고 필요시 이벤트 발행
- 모듈 간 통신은 이벤트 버스를 통해 비동기적으로 처리
- 데이터는 shop_id 기반으로 샤딩된 MySQL 포드에 저장
- 캐시 계층 (Redis/Memcached) 을 통해 성능 최적화
- 백그라운드 작업은 큐 시스템을 통해 비동기 처리
역할:
- API Gateway: 모든 외부 요청의 진입점, 인증 및 라우팅 담당
- 모듈들: 각각 특정 비즈니스 도메인 관리 (주문, 재고, 결제 등)
- Event Bus: 모듈 간 느슨한 결합 통신 지원
- Pods Architecture: 데이터베이스 샤딩을 통한 확장성 제공
유무에 따른 차이점:
- 모듈러 모놀리스 적용 전: 기능적으로 구별 가능한 측면들이 모두 얽혀있어 배송료 계산 코드가 체크아웃 코드와 함께 있고, 서로를 호출하는 것을 막는 것이 거의 없음
- 모듈러 모놀리스 적용 후: 모든 코드를 하나의 코드베이스에 유지하되 서로 다른 컴포넌트 간에 경계가 정의되고 존중되도록 보장
구현 예시:
|
|
사례 4: 온라인 쇼핑몰
시나리오: 온라인 쇼핑몰에서 상품, 주문, 결제, 배송 도메인을 각각 별도 모듈로 관리하면서, 단일 Application 으로 배포하는 구조
시스템 구성:
- API 게이트웨이 모듈
- 상품 도메인 모듈
- 주문 도메인 모듈
- 결제 모듈
- 배송 모듈
- 공통 인프라 모듈 (인증/로깅 등)
- 단일 DB
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:
- 사용자가 주문 생성 API 호출
- 주문 모듈에서 상품, 결제, 배송 등 관련 모듈에 필요한 기능 호출
- 각 도메인 내에서 책임/검증 진행
- 최종적으로 단일 DB 에 트랜잭션 처리 후 응답 반환
역할:
- 각 모듈은 자신의 도메인 책임만 수행
- 공통 모듈은 인증, 로그 등 시스템 전반 지원
- 단일 배포로 운영이나 내부 구조는 분리되어 관리
유무에 따른 차이점:
- Modular Monolith(모듈러 모놀리스) 미적용 시 도메인 간 로직이 혼재되어 변화 영향 폭이 커지고, 유지보수가 어려우며, 마이크로서비스 전환이 힘듦
- 적용 시 각 도메인/비즈니스 책임이 분명, 추후 서비스 쪼개기 쉬움
구현 예시:
|
|
- 각 도메인별 모듈의 경계를 서비스 객체로 캡슐화
- 모듈 간 의존성은 생성자 주입 방식으로 관리
- 모든 배포는 하나의 애플리케이션으로, 테스트 및 운영 시 모듈별 책임 유지
Git 기반 예제 프로젝트 구조
|
|
구조 설명:
api/
: 모듈 호출 진입점 (Express, NestJS 등 사용 가능)modules/{name}
: 도메인 단위 모듈shared/contracts
: 모듈 간 계약 (이벤트, DTO) 정의common/di-container.ts
: 의존성 주입 설정infrastructure/event-bus
: 이벤트 버스 구현 (예: in-memory, RabbitMQ 등)
구현 예시 (TypeScript + Node.js)
이벤트 계약 (shared/contracts/order.events.ts)
Order 모듈 - 도메인 로직 및 이벤트 발행
|
|
Inventory 모듈 - 이벤트 처리 핸들러
|
|
이벤트 버스 구성
|
|
DI 및 앱 초기화
|
|
전통 모놀리식 → 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 내부에서 모듈 간 통신에 도입하면 다음 효과를 얻을 수 있다:
- 모듈 간 강한 결합 해소 → 유지보수성 상승
- 비동기 처리 도입 → 응답 속도 개선
- MSA 전환 구조 선제적 확보 → 이벤트 기반 서비스 분리 용이
- 중앙 브로커 기반 처리 → 확장성과 유연한 로깅/모니터링 가능
개요 및 설계 목적
항목 | 설명 |
---|---|
아키텍처 유형 | 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
설계 구성 요소 및 역할
구성 요소 | 설명 |
---|---|
KafkaProducer | DomainEvent 를 Kafka Topic 에 발행 (발행자 모듈 내부에 위치) |
KafkaBroker | 이벤트를 중계하는 Kafka 클러스터 (토픽 기반 라우팅) |
KafkaConsumer | 특정 토픽을 구독하여 모듈에 이벤트를 전달 (모듈별 리스너 존재) |
DomainEvent | 모듈 내부 상태 변화에 따라 생성되는 이벤트 (OrderPlaced , UserRegistered ) |
EventDispatcher | 이벤트를 Kafka 로 발행하는 통합 인터페이스 |
EventHandler | Consumer 쪽에서 실제 비즈니스 로직 처리 담당 |
Event Mapper (선택) | Domain → Integration Event 로 변환 필요 시 사용 (버전/스키마 고려) |
코드 설계 예시
도메인 이벤트 정의
Kafka Producer
도메인 서비스 내부 발행
Kafka Consumer & 리스너
|
|
실무 설계 고려사항
항목 | 전략 |
---|---|
이벤트 중복 처리 | 이벤트 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 |
예시: 이벤트 로그 포맷
|
|
장애 추적 전략
항목 | 전략 |
---|---|
이벤트 실패 추적 | 리스너 실패 시 로그에 에러 스택 기록 + 알림 연동 (예: Slack, PagerDuty) |
트랜잭션 외 이벤트 로깅 | 이벤트가 트랜잭션 외부에서 실행되므로, DB commit 이후 성공 여부 별도 기록 |
재시도 전략 | 실패한 이벤트는 Retry Queue 또는 Dead Letter Queue 에 적재하여 후속 처리 |
트레이싱 연동 | Trace ID 를 전달하여 로그, 메트릭, 트레이스를 연계 추적 (OpenTelemetry 기반 가능) |
Eventual Consistency 보장 전략 및 데이터 동기화 설계
모듈 간 독립성과 성능을 확보하기 위해, 즉각적인 강한 일관성 대신 지연 일관성 (Eventual Consistency) 을 선택할 수 있다. 특히 모듈 간 이벤트 기반 통신을 활용할 때, 일관성 유지 전략이 필수이다.
전제 조건
- 모듈 간 강한 트랜잭션 경계는 지양하고, 이벤트 기반 동기화를 사용
- 각 모듈은 자기 완결적 상태 (self-contained state) 를 가짐
설계 구성
구성 요소 | 설명 |
---|---|
도메인 이벤트 | 상태 변경 후 발행 (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, RabbitMQ 등의 외부 브로커 없이도 이벤트 아키텍처를 경량 구현할 수 있음
주요 목적 및 장점
목적 | 설명 |
---|---|
느슨한 결합 | 발행자와 구독자가 서로 몰라도 통신 가능 |
비동기 처리 | 이벤트 기반 후속 처리를 메인 로직에서 분리 |
테스트 용이성 | 외부 시스템 없이 테스트 가능 |
성능 향상 | 메모리 기반으로 처리 속도 매우 빠름 |
구조 간소화 | Kafka 등 브로커 없이도 이벤트 시스템 구현 가능 |
기본 구조
|
|
- **
publish()
**는 모든 핸들러를 동기 호출함 - 실무에선
threading
,asyncio
,queue
를 활용해 비동기 처리 가능
아키텍처
flowchart TD A[Order Module] -->|emit OrderCreated| EB[In-memory EventBus] EB --> B[Inventory Handler] EB --> C[Notification Handler] EB --> D[Audit Logger]
Order Module
은 EventBus 에 이벤트를 퍼블리시Handler
들은 사전 등록된 이벤트 타입만 처리
한계 및 주의점
항목 | 설명 |
---|---|
멀티 프로세스 환경 | 다른 프로세스/서버로 이벤트 전달 불가 |
신뢰성 부족 | 메시지 손실 가능, 재시도/보장 없음 |
확장성 | 수평 확장 시 메시지 전달 어려움 |
오류 처리 | 실패 시 DLQ/재시도 로직 구현 필요 |
정렬/중복 처리 없음 | 멱등성/순서 보장 안 됨 (수동 구현 필요) |
In-memory Vs External Broker (Kafka 등) 비교
항목 | In-memory EventBus | Kafka, 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 | 애플리케이션 내 메서드 직접 호출 기반 |
KafkaEventBus | Kafka Topic 으로 메시지 전송 |
Configurable Bus | 설정 값 기반으로 런타임 시점 Bus 선택 가능하도록 DI 적용 |
Failover Hook | Kafka 실패 시 fallback to InMemory (옵션) |
구현 예시
|
|
|
|
Domain Event Vs Integration Event 설계 구분
항목 | Domain Event | Integration Event |
---|---|---|
정의 | 하나의 도메인 내부에서 발생한 의미 있는 상태 변화 | 시스템 간 통합을 위해 발행되는 이벤트 (MSA/모듈 간 통신용) |
관심사 | 도메인 규칙과 상태 전이 | 서비스 간 데이터 전달 및 이벤트 동기화 |
스코프 | 내부 모듈/서비스 한정 | 외부 시스템과의 경계 포함 |
의도 | 비즈니스 불변 조건 (Event Storming 기반) 표현 | 통합 목적의 메시지 전달 및 확장성 확보 |
차이점
구분 | Domain Event | Integration Event |
---|---|---|
발행 주체 | 내부 도메인 서비스 (ex. 주문, 사용자 등) | 외부 시스템 통합 또는 다른 바운디드 컨텍스트 대상 |
의도 | 비즈니스 상태 변화 반영 (OrderPlaced , UserSignedUp ) | 다른 시스템과 상태 동기화 또는 알림 (InventoryReserved ) |
보존 여부 | 일반적으로 일회성 처리 (Replay 목적 아님) | 메시지 브로커에서 보존 (Kafka 에 저장, 재처리 가능) |
전파 범위 | 모놀리식 내부 모듈 또는 같은 바운디드 컨텍스트 | 마이크로서비스, 외부 시스템 (Pub/Sub) |
형식 | 코드 클래스 기반 이벤트 객체 | JSON, Avro 등 스키마 정의된 메시지 |
예시 | UserCreated , OrderCanceled | SendWelcomeEmail , NotifyWarehouse |
발생 및 흐름
flowchart LR A[도메인 서비스] --> B[Domain Event 발생] B --> C[도메인 이벤트 핸들러 처리] C --> D["Outbox 기록 (Optional)"] D --> E[Integration Event 발행] E --> F[외부 서비스 수신]
OrderCreated
(Domain Event) 는InventoryHandler
등 도메인 로직을 호출- 그 이후
OrderCreatedIntegrationEvent
가 외부 Kafka 로 퍼블리시
설계 가이드
항목 | 권장 전략 |
---|---|
DomainEvent ↔ IntegrationEvent 매핑 | OrderPlaced → OrderCreatedIntegrationEvent |
IntegrationEvent 버전 관리 | Kafka 메시지에 version 필드 포함, 스키마 관리 도구 사용 (e.g. Confluent Schema Registry) |
테스트 전략 | 단위 테스트는 DomainEvent, 통합 테스트는 IntegrationEvent 기준 분리 |
이벤트 메시지 디자인 | 명확한 context 명시 (ex. user-service.user.created ) |
실제 예시 (Python)
도메인 이벤트 정의
Integration Event 정의 (Kafka 등 외부 전파)
변환 예시
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[외부 시스템 또는 마이크로서비스]
흐름 설명
단계 | 구성 요소 | 설명 |
---|---|---|
1 | Domain Event | 비즈니스 트랜잭션 내에서 발생 (OrderPlaced , PaymentFailed ) |
2 | Domain Event Dispatcher | 등록된 핸들러에게 이벤트를 브로드캐스트 |
3 | Mapper | Domain → Integration 변환 (예: OrderPlaced → OrderCreatedIntegrationEvent ) |
4 | Kafka Publisher | Kafka 토픽으로 직렬화된 메시지 발행 |
5 | Consumer (MSA) | 외부 시스템/모듈에서 메시지를 구독 및 처리 |
예시
|
|
Domain Event Dispatcher 설계 패턴
DomainEventDispatcher
는 하나의 이벤트가 여러 핸들러로 전달될 수 있도록 이벤트 등록 및 전달 책임을 가진 중앙 집중형 이벤트 디스패처이다.
구조
flowchart TD A[도메인 서비스] --> B[DomainEventDispatcher] B --> C1[EmailEventHandler] B --> C2[LoggingEventHandler] B --> C3[KafkaIntegrationPublisher]
Dispatcher 설계 (Python)
장점
- SRP 원칙에 따라 핸들러 로직 분리
- 테스트 용이성 증가
- 마이크로서비스 전환 시 핸들러 분산 가능
Outbox 패턴 상세 구현 및 트랜잭션 연결
데이터베이스 트랜잭션과 함께 이벤트를 Outbox 테이블에 저장하고, 이후 별도 프로세스 (이벤트 릴레이어) 가 이벤트를 메시징 시스템 (Kafka 등) 으로 전송하는 패턴이다.
- 데이터베이스 상태와 이벤트 발행의 일관성 보장 (Exactly-once 보장 또는 At-least-once 기반 처리) 을 목적으로 한다.
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 테이블 스키마 예시
컬럼 | 설명 |
---|---|
id | UUID or ULID |
event_type | 이벤트 이름 (OrderCreated ) |
aggregate_id | 관련 엔터티 ID |
payload | JSON 직렬화된 이벤트 내용 |
status | PENDING , SENT , FAILED |
created_at | 타임스탬프 |
기반 구현 예시
|
|
고려 사항
- 트랜잭션 내 이벤트 저장은 DB 일관성 보장
- 발행 실패 시 상태 보존 및 Retry 필요
- 외부 메시지 브로커 장애 시 이벤트 유실 방지 (DLQ + Retry Queue 구성)
Event Store 기반 Replay 가능한 Hybrid EventBus 설계
Hybrid EventBus 는 In-memory EventBus 와 외부 메시지 브로커 (Kafka, RabbitMQ, NATS 등) 를 결합하여 이벤트를 내부 모듈 간 빠르게 처리하면서, 동시에 시스템 외부로도 이벤트를 전파할 수 있도록 설계한 이벤트 처리 아키텍처 패턴이다.
목적
- 이벤트 소실 방지
- 장애 복구 및 재처리
- 이벤트 소싱 기반 설계 확장성
구조
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 EventBus | Kafka 발행 + Event Store 저장 병행 수행 |
Event Store | DB 또는 파일 기반 이벤트 저장소 (ex. PostgreSQL, EventStoreDB) |
Replay Engine | 특정 시간 이후 이벤트를 다시 발행할 수 있도록 처리 |
Dispatcher + Bus | Dispatcher 는 핸들러 관리, Bus 는 발행 방식 관리 |
구현 전략
HybridEventBus.publish(event)
:- Kafka 에 발행
- JSON 직렬화 후 Event Store 에 저장
ReplayEngine.replay(from_timestamp)
:- 저장소에서 이벤트 불러와 다시 발행
- 이벤트 IDempotency 보장을 위해 처리 상태를 저장 (
processed_event_ids
)
예시 저장 구조
Replay 코드
Outbox Pattern vs. Hybrid EventBus 구조 비교
구분 | Outbox Pattern | Hybrid 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 메시지 브로커로 전환 |
- 모놀리식 코드 구조를 유지하면서도 경계를 명확히 나누는 것이 전환의 출발점이다.
- 위 전략은 Strangler Fig Pattern 과 결합하여 점진적으로 적용하는 것이 가장 안전하다.
이벤트 기반 통합 설계
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 이 아닌 경우, 저장은 필수 아님 (추후 확장을 고려해 로그 남길 수도 있음) |
예시 코드
|
|
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
핵심 개념:
- 처음에는 Internal EventBus 기반의 Event-Driven Modular Monolith
- 점차 각 모듈을 MSA 로 분리하되 이벤트 구조는 재사용
단계별 전환 전략
단계 | 설명 | 기술/전략 |
---|---|---|
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)
order
모듈: 주문 등록, 상태 변경payment
모듈: 결제 승인 처리notification
모듈: 이메일/SMS 발송- 이벤트 구조:
OrderCreated
,PaymentCompleted
전환 단계 요약
단계 | 구체 예시 |
---|---|
모듈 → 서비스 추출 | payment 모듈 → payment-service 로 분리 |
이벤트 발행 전환 | 기존 Internal Event → Kafka 발행 |
Consumer 구성 | payment-service 는 Kafka 에서 OrderCreated 수신 |
DB 분리 | order-service 와 payment-service 각각 독립 DB |
인증 처리 | JWT 기반 토큰 전달, 내부 인증 연동 |
오류 처리 | Retry + DLQ 설정, Kafka consumer 멱등 처리 |
주요 구현 예시
- Python (FastAPI + SQLAlchemy + Kafka 구조)
- CQRS + Outbox 기반 Kafka 발행 구조
구성 요소 | 설명 |
---|---|
OrderCreatedEvent | 도메인 이벤트 정의 |
OutboxEvent 테이블 | 이벤트 저장소 (내부 DB) |
publish_outbox_events | Kafka 발행기 (Batch or Scheduler) |
KafkaConsumer | Microservice 간 이벤트 수신 |
order-service
: 주문 생성 및 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
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 로 주기적으로 실행 가능
payment-service
: Kafka Consumer1 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)
보조: 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 Store | CQRS+Event 기반의 도메인 흐름 구현 | |
운영 및 배포 | 배포 전략 | 단일 배포, CI/CD, Blue-Green, Canary | 모듈 단위이지만 전체를 하나의 패키지로 배포하며 자동화된 배포 전략 적용 |
장애 대응/확장성 | Circuit Breaker, Zero Downtime Deployment | 운영 안정성 확보 및 무중단 서비스 전환 가능 | |
테스트 전략 | 계약 및 경계 테스트 | Contract Testing, Hexagonal Testing | 모듈 간 통신 및 외부 통합 시 계약 검증을 통해 안정성 확보 |
프로세스/조직 | 협업 및 품질 관리 | 설계 표준, 코드 리뷰, Static Analysis Tool | 팀 기반 협업을 위한 표준화와 품질 확보 도구 도입 |
학습/실험 전략 | 도메인 기반 PoC, Event Storming | 개념 검증과 설계 실험을 통한 경계 명확화 및 설계 검증 |
아키텍처 설계:
Modular Monolith 는 도메인 중심으로 명확한 모듈 경계를 설정하고, 내부 아키텍처를 계층적 (Layered), 헥사고날 (Hexagonal) 또는 클린 아키텍처로 구분하여 내부 유연성과 외부로의 확장을 동시에 고려한다. 점진적인 마이그레이션이 가능하도록 설계되어야 한다.설계 원칙:
SOLID 원칙과 Interface Segregation 등을 통해 각 모듈이 독립적으로 유지보수될 수 있도록 하며, 수직 슬라이스 설계는 기능 중심의 응집도 높은 구조를 가능하게 한다. 공통 코드 (Shared Kernel) 는 신중하게 관리되어야 한다.구현 기법:
DDD 기반의 풍부한 도메인 모델링, CQRS 와 Event Sourcing 의 적용을 통해 시스템 상태를 유연하게 관리할 수 있다. IoC 와 DI 는 결합도를 낮추고, 아키텍처 테스트를 통해 설계 위반을 사전에 방지한다.운영 및 배포:
단일 배포 전략이지만, CI/CD, 블루그린 또는 카나리 배포와 결합하여 안정성과 무중단성을 확보한다. 장애 발생 시 빠른 복구를 위한 회로 차단기 (Circuit Breaker) 적용도 중요하다.테스트 전략:
모듈 간 인터페이스 안정성을 위해 Contract Testing 이 필수적이며, 포트 - 어댑터 기반의 헥사고날 테스트는 테스트 격리를 통해 구조 테스트를 가능하게 한다.프로세스/조직:
코드 리뷰, 아키텍처 표준화, 자동화 분석 도구를 통해 품질을 보장하고, 도메인 기반의 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 | 적응 가능한 구조, 변경 수용성 설계 | 시스템의 장기적인 확장성과 변화 대응 확보 |
소프트웨어 설계 원칙:
Modular Monolith 아키텍처의 기반은 도메인 주도 설계 (DDD) 와 SOLID 원칙이다. 특히 Bounded Context 및 모듈 경계 정의는 구조적 유지보수성과 확장을 위해 반드시 필요하며, 레이어드 구조를 통해 책임 분리도 강화된다.아키텍처 스타일 및 구조 패턴:
Monolith, Modular Monolith, Microservices 간 차이를 이해하고, Clean/Hexagonal Architecture 등을 통해 유연하고 테스트 가능한 구조를 설계해야 한다. 수직 슬라이스 및 계층화된 모듈화 전략은 핵심 구성이다.구현 기술 및 통신 방식:
의존성 주입과 In-process 이벤트 기반 통신은 모듈 간 유연한 연결을 가능하게 하며, API 인터페이스 설계는 테스트 및 연동을 안전하게 만든다.데이터 및 트랜잭션 설계:
모듈 간 데이터 설계의 핵심은 충돌 방지와 데이터 일관성 유지다. Outbox 와 Saga 는 메시지 기반 트랜잭션 구현에 필수적이다.테스트 및 품질 보증:
단위 및 통합 테스트를 통해 모듈 경계 위반을 감지하고, 정적 분석 도구를 활용하여 코드 품질을 지속적으로 유지해야 한다.배포 및 운영 전략:
CI/CD, Canary Release, APM 등은 모듈별 빠르고 안정적인 배포와 운영 관리를 위한 기반이며, Kubernetes 등의 오케스트레이션 도구는 스케일업/다운을 자동화하는 데 필수적이다.시스템 진화 및 전환 전략:
Strangler Fig Pattern 은 기존 모놀리스를 점진적으로 MSA 나 고도로 모듈화된 구조로 전환할 때 유용하며, Evolutionary Architecture 는 변경에 강한 구조 설계의 방향성을 제공한다.
용어 정리
카테고리 | 용어 | 설명 |
---|---|---|
아키텍처 스타일 | Monolithic Architecture | 모든 기능이 하나의 코드베이스 및 실행 단위로 구성된 전통적 구조 |
Modular Monolith | 내부적으로 모듈화되어 있지만 단일 배포 단위를 유지하는 구조 | |
Microservices Architecture | 서비스 단위로 분산되어 독립적 개발·배포가 가능한 아키텍처 | |
Event-Driven Architecture | 이벤트 흐름 기반의 비동기 처리 중심 아키텍처 | |
설계 원칙 | Bounded Context | DDD 에서 도메인 경계를 명시적으로 정의하는 논리적 경계 |
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 Testing | API 또는 인터페이스 계약을 기준으로 양측 테스트를 수행하는 방식 |
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 | 모듈/서비스 단위 독립성과 수평 확장을 위해 데이터 분리, 서비스 디스커버리 필수. |
참고 및 출처
- Domain‑Driven Design: Tackling Complexity in the Heart of Software (Eric Evans)
- Building Microservices: Designing Fine‑Grained Systems (Sam Newman)
- Software Architecture: The Hard Parts (Neal Ford, Mark Richards, Pramod Sadalage, Zhamak Dehghani)
- Patterns of Enterprise Application Architecture (Martin Fowler)
- 마틴 파울러의 마이크로서비스 아티클
- 마이크로서비스 패턴 웹사이트
- Thoughtworks의 모듈러 모놀리틱 아티클
- Milan Jovanović의 모듈러 모놀리틱 시리즈
- Shopify의 모듈러 모놀리틱 경험담
- Spring Modulith Documentation