MSA에서 모놀리스로 회귀한 회고
2026년 4월 4일, Lumie는 18개 서비스로 나뉜 백엔드 구조를 되돌리고 모듈러 모놀리스로 통합하기로 결정했습니다. 이는 장애 대응이 아니라, 마이크로서비스의 비용은 내고 있지만 그에 맞는 이점을 얻지 못한다는 판단에 따른 아키텍처 롤백이었습니다.
현재 저장소는 이 결정의 결과입니다. 하나의 Spring Boot 백엔드 안에서 modules/* 아래에 명시적인 모듈 경계를 두고, 공유 계약은 libs/internal-api에 둔 단일 런타임 구조입니다.
배경
기존 백엔드는 tenant, auth, academy, exam, billing, report 같은 서비스로 나뉘어 있었습니다. 대부분의 서비스 간 호출은 gRPC를 사용했고, 비동기 메시징이 실제로 필요한 작업은 일부에 그쳤습니다.
하지만 이 분리는 일반적인 마이크로서비스의 독립성을 만들지 못했습니다.
| 기대했던 마이크로서비스 이점 | Lumie의 실제 상태 |
|---|---|
| 독립 배포 | 프로토콜과 라우트 변경은 여전히 조정된 릴리스를 요구했습니다 |
| 독립 확장 | 별도 확장 단위를 정당화할 만큼 부하가 치우친 도메인이 없었습니다 |
| 데이터 소유권 | 서비스들이 같은 Postgres 인스턴스를 공유했고 실제 데이터 경계가 없었습니다 |
| 팀 자율성 | 팀 규모가 18개 서비스를 독립 소유하는 구조와 맞지 않았습니다 |
반면 분산 시스템의 비용은 그대로 발생했습니다.
| 비용 | 구체적인 형태 |
|---|---|
| 네트워크 호출 | 같은 프로세스 안의 도메인 호출이 gRPC 호출과 proto 유지보수로 바뀌었습니다 |
| 운영 | 서비스별 ArgoCD app, Helm values, namespace, Service, route가 생겼습니다 |
| 중복 | application class, JPA 설정, 보안 설정, 예외 처리, Docker/actuator 설정이 반복됐습니다 |
| 로컬 개발 | 현실적인 흐름을 실행하려면 여러 서비스 또는 클러스터형 로컬 환경이 필요했습니다 |
| 테스트 | 통합 테스트가 다른 서비스를 mock 하거나 띄워야 했고, 계약 변경이 프로세스 경계를 넘었습니다 |
결정 계기
계기는 평범한 아키텍처 질문이었습니다. 새 도메인 하나를 별도 서비스로 분리할지 검토하면서, 그 서비스가 추가 ArgoCD app, 추가 route, 추가 build, 추가 cross-process contract를 정당화할 수 있는지 따져보게 됐습니다.
그 표를 기존 18개 서비스에도 그대로 적용하자 결론이 분명해졌습니다. 네 가지 주요 이점은 사실상 꺼져 있었고, 주요 비용은 모두 켜져 있었습니다.
이전 접근이 실패한 이유
핵심 실수는 서비스 개수를 아키텍처 진전으로 본 것입니다. 코드는 분산되어 있었지만, 데이터 경계와 소유권 경계는 분산되어 있지 않았습니다.
가장 중요한 신호는 공유 데이터베이스였습니다. 18 개 서비스가 강한 데이터 경계 없이 같은 데이터베이스에 쓰면, 런타임 분리는 분산된 모놀리스가 됩니다. 그 위에 gRPC와 배포 분리를 얹으면 결합은 줄지 않고 장애 형태만 늘어납니다.
두 번째 신호는 반복 보일러플레이트였습니다. 여러 벌의 persistence 설정, security 설정, exception handler, application entrypoint, deployment template은 유용한 추상화가 아니라 경계가 잘못 잡혔다는 증거였습니다.
해결
복구 경로는 무질서한 단일 애플리케이션이 아니라 모듈러 모놀리스였습니다.
백엔드는 다음 규칙으로 수렴했습니다.
- 하나의 application process와 하나의 deployable artifact를 둡니다.
- 모듈 내부에는
domain,application,adapter계층을 유지합니다. - 모듈 간 호출은 HTTP나 gRPC가 아니라
libs/internal-api계약을 통합니다. - 큐 의미가 실제로 필요한 use case에만 비동기 인프라를 유지합니다.
- 분산 처리가 필요하지 않은 use case는 일반적인 로컬 트랜잭션으로 일관성을 처리합니다.
현재 규칙에도 이 교훈이 반영되어 있습니다. 새 코드는 다른 모놀리스 모듈을 HTTP/gRPC로 호출하지 않아야 하며, 다른 모듈의 domain entity를 직접 import하지 않고 internal API 계약을 사용해야 합니다.
현재 상태
현재 코드베이스 에는 더 이상 18개 서비스 토폴로지가 없습니다. 따라서 이 문서는 라이브 토폴로지 가이드가 아니라 결정 기록입니다.
현재 동작은 다음 문서를 기준으로 확인해야 합니다.
- 현재 모듈 책임은 backend module reference 문서
- 모놀리스 형태는
lumie-backend/README.md - 허용된 모듈 간 계약은
libs/internal-api
잘된 점
- 선호가 아니라 명시적인 비용/이점 근거로 결정을 내렸습니다.
- "마이크로서비스는 때때로 유효하다"와 "이 시스템이 그 조건을 만족한다"를 분리했습니다.
- 롤백 후에도 모듈 경계를 유지해, 나중에 증거가 바뀌면 추출할 수 있는 여지를 남겼습니다.
- 마이크로서비스를 제거한다는 이유만으로 RabbitMQ를 제거하지 않고, 비동기가 정당한 곳에는 남겼습니다.
아쉬웠던 점
- 원래 분리는 팀, 데이터, 확장 경계가 정당화되기 전에 시작됐습니다.
- 공유 Postgres 접근 때문에 서비스 분리는 강한 데이터 소유권 기반을 갖지 못했습니다.
- 로컬 개발 경험이 매일 아키텍처 비용을 드러낼 만큼 무거워졌습니다.
- "독립 배포"는 운영 사실이라기보다 머릿속 모델에 가까웠습니다.
다시 검토할 조건
마이크로서비스는 다음 중 하나가 실제로 발생할 때만 다시 검토합니다.
| 조건 | 필요한 증거 |
|---|---|
| 독립 확장 | 특정 도메인이 모놀리스 안에서 깔끔하게 처리하기 어려운 지속적이고 비대칭적인 부하를 가집니다 |
| 독립 소유권 | 한 팀이 도메인을 end-to-end로 소유하고 별도 릴리스 주기가 필요합니다 |
| 데이터 자율성 | 공유 테이블 결합 없이 도메인이 자기 데이터 경계를 소유할 수 있습니다 |
| 외부 계약 안정성 | 추출이 내부 호출자들의 lockstep 변경을 강제하지 않습니다 |
그 전까지는 모놀리스 안의 모듈 경계를 개선하는 것이 더 안전한 기본값입니다.
교훈
- 데이터 경계가 없는 마이크로서비스 경계는 대개 분산된 모놀리스 경계입니다.
- 서비스마다 복사된 보일러플레이트는 모듈성의 증거가 아니라 경고 신호입니다.
- 로컬 개발 비용은 아키텍처 피드백입니다. 정상 경로가 너무 이른 시점부터 클러스터 수준 장비를 요구하면 분리를 의심해야 합니다.
- 잘못된 복잡도를 제거하고 올바른 내부 경계를 남긴다면, 롤백도 아키텍처 진전이 될 수 있습니다.