들어가며
결론부터 말하면, 저는 새 서비스를 거의 항상 모놀리스로 시작합니다. 그리고 쪼갤 때는 "도메인이 다르다"가 아니라 "안 쪼개면 안 돌아간다"는 이유가 있을 때만 쪼갭니다.
PHP를 26년 가까이 했고, 주력 분야가 MSA 기반 백엔드 API입니다. AWS·Azure·GCP에서 서비스를 쪼개고 엮는 일을 꽤 오래 해왔죠. 그런 사람이 요즘 혼자서 CodeIgniter 4로 SaaS를 여러 개 만들면서, 정작 대부분을 모놀리스로 시작했습니다.
MSA를 잘 알수록 오히려 쉽게 쪼개지 않게 됩니다. 쪼개는 순간 생기는 청구서를 알기 때문입니다. 이 글은 제가 실제 프로젝트에서 내린 분리/유지 결정을 근거로, 쪼갤지 말지를 가르는 기준을 정리한 것입니다.
쪼개는 비용부터 계산하기
서비스 하나를 떼어내면 코드만 늘어나는 게 아니라, 운영해야 할 것이 통째로 하나 더 생깁니다.
- 배포 파이프라인과 CI 실행 시간
- 환경 설정, 시크릿, 로그, 모니터링
- 서비스 간 인증 전파와 계약(API 스펙) 관리
- 분산 트랜잭션과 데이터 정합성
- 장애 시 "어느 쪽이 문제인가"를 찾는 시간
CI 비용은 제가 직접 격은 일입니다. 개발 초창기에 GitHub Actions 무료 플랜의 월 2,000분을 넘겨서 CI가 멈췄고, 결국 Ubuntu 서버에 Docker로 셀프호스티드 러너 3대를 띄워 직접 운영하게 됐습니다. 저장소가 하나 늘 때마다 테스트·빌드·배포 분이 그대로 곱해진다는 걸 몸으로 배운 셈입니다.
팀이 여럿이면 이 비용을 나눠 지지만, 1인 개발자나 소규모 팀에게는 서비스 개수가 곧 운영 부담의 개수입니다. 그래서 저는 분리를 "설계 취향"이 아니라 "비용을 치를 만한 이유가 있는가"의 문제로 봅니다.
기준 1: 실행 모델이 다를 때는 쪼갠다
가장 확실한 분리 신호는 런타임의 실행 모델이 다를 때입니다. PHP-FPM은 요청이 들어오면 처리하고 끝나는 구조라, 연결을 오래 붙잡고 있어야 하는 일에는 맞지 않습니다.
웹 채팅 상담 솔루션. 처음에는 Node.js 풀스택으로 구상했습니다. 그런데 따져보니 실시간이 필요한 건 메시지 전송뿐이고, 운영자 어드민·상담원 배정·권한·테넌트 분리 같은 백오피스는 전형적인 요청-응답 업무였습니다. 그래서 백오피스는 CI4로, Node.js는 WebSocket 전송 계층만 맡는 하이브리드로 바꿨습니다.
여기서 중요한 건 Node 쪽에 제약을 건 것입니다. 채팅엔진은 DB에 직접 접근하지 않고 비즈니스 로직도 갖지 않는 순수 전송 계층으로 두고, CI4와는 JWT·Redis Pub/Sub·내부 API로만 통신하게 했습니다. 로직이 한 곳에 모여 있으니 쪼갠어도 사실상 "모놀리스 + 전송 어댑터"에 가깝습니다.
실시간 인터콤. 외부 사용자의 음성을 리눅스 서버 스피커로 내보내는 구조를 검토할 때도 같은 판단이었습니다. CI4는 인증과 방 토큰(JWT) 발급만 맡고, 시그널링과 미디어 중계(SFU, TURN)는 별도 컴포넌트로 뚝습니다. 이건 취향이 아니라 PHP로는 물리적으로 안 되는 일이라서입니다.
도메인 경계가 아니라 실행 모델 경계에서 쪼개면, 쪼개는 수도 최소화되고 이유도 분명해집니다.
기준 2: 쓰는 쪽이 둘 이상일 때는 쪼갠다
두 번째 신호는 한 기능을 여러 제품이 동시에 쓸 때입니다. 소비자가 하나뿐인 기능을 떼어내면 네트워크 호출만 늘지만, 여러 제품이 쓰는 기능을 각 제품에 복사하면 고칠 때마다 N곳을 고쳐야 합니다.
AITessera(토큰 기반 인증 API). 제가 만드는 SaaS는 대부분 인증이 필요합니다. 그래서 인증은 처음부터 독립 제품으로 분리했고, RSA/ECDSA 토큰 서명과 Redis 기반 슬라이딩 윈도우 레이트 리미트(인증 10회/분, 일반 120회/분)를 한 곳에서 관리합니다. 보안 정책을 바꾸면 모든 제품에 한 번에 반영됩니다.
ci4-board(공통 인프라). 반대로 JWT·OAuth 소셜 로그인(Google/Kakao/Naver)·API 버저닝·Swagger·i18n을 담은 보일러플레이트는 서비스가 아니라 코드베이스로 공유합니다. 149개 통합 테스트와 함께 각 제품의 출발점이 될 뿐, 런타임에 별도로 떠 있지 않습니다.
둘의 차이는 상태입니다. 토큰 서명 키나 레이트 리미트 카운터처럼 여러 제품이 같은 상태를 공유해야 하면 서비스, 로직만 같고 데이터는 제품별로 따로면 라이브러리나 보일러플레이트로 충분합니다.
기준 3: 스케일 단위가 다르면, 경계만 먼저 긋는다
특정 기능만 부하가 튀 것 같다는 예감은 분리 사유가 되지만, 예감만으로 당장 쪼개지는 않습니다. 대신 나중에 떼기 쉽게 클래스 경계를 먼저 긋습니다.
PhotoTrail(구글 포토 GPS 동선 시각화). 사용자가 고른 사진의 원본을 내려받아 EXIF에서 좌표·시간을 뽑는 부분은 사용자가 늘면 가장 먼저 무거워질 곳입니다. 그래도 지금은 요청당 10장으로 캡을 걸고 컨트롤러 요청 안에서 처리합니다. 큐 워커는 없습니다.
대신 서비스를 이렇게 나눠두었습니다.
app/Services/
├── GooglePhotosAuthService.php // OAuth 토큰 발급/갱신
├── PhotoPickerService.php // 세션 생성/폴링/목록
├── PhotoIngestService.php // 다운로드 + EXIF 추출
└── RouteVisualizationService.php // 날짜별 동선 데이터
핵심은 PhotoIngestService가 HTTP 요청 컨텍스트에 의존하지 않도록 하는 것입니다. 입력은 mediaItemId 배열과 액세스 토큰, 출력은 좌표·시간 배열입니다. 이렇게 두면 나중에 이 클래스만 SQS + 워커 컨테이너나 Cloud Run으로 옮기고, 나머지 CI4 API는 그대로 둡 수 있습니다.
분리 시점은 추측이 아니라 지표로 정합니다. 응답 시간이 타임아웃에 가까워지거나, 이 작업 때문에 다른 API까지 느려지기 시작하면 그때 떼어냅니다.
쪼개지 않은 결정들
위 세 기준에 걸리지 않으면 기존 모놀리스를 그대로 둡니다. 실제로 내린 결정들입니다.
AILicet(라이선스 매니저) CI3→CI4 전환. 오래된 코드를 옮길 때 "이참에 MSA로"라는 유혹이 가장 큽니다. 하지만 프레임워크 전환과 아키텍처 전환을 동시에 하면 문제가 생겼을 때 원인을 가르기 어렵습니다. 그래서 포팅은 모놀리스 그대로, 한 번에 하나만 바꿉니다.
AICassa의 증빙 파싱 파이프라인. 다른 아이템(세무사무소 비정형 증빙 자동화)에서도 재활용할 수 있는 자산이지만, 아직 실제로 쓰는 제품은 하나입니다. 소비자가 둘이 되기 전까지는 코드 재사용으로 충분합니다.
어드민 분리. 협업 프로젝트에서 어드민 DB 분리와 API 그룹 분할을 논의한 적이 있습니다. 이런 요구는 보통 권한·보안·배포 주기의 문제라서, 별도 서비스보다 라우트 그룹·필터·DB 커넥션 분리로 먼저 풀어보는 게 맞다고 봅니다. CI4는 한 앱 안에서 여러 DB 그룹과 라우트별 필터 체인을 가질 수 있습니다.
공통점은, 문제를 서비스 분리로 풀기 전에 같은 프로세스 안의 논리적 분리로 풀 수 있는지를 먼저 확인했다는 점입니다.
CI4로 모듈러 모놀리스 준비하기
안 쪼갠다고 아무렇게나 짜지는 않습니다. 나중에 쪼갤 때 다시 짜지 않도록, 모놀리스 안에서 경계를 미리 지켜둡니다. 제 CLAUDE.md에 넣어두는 규칙들입니다.
- 얇은 컨트롤러, 두꺼운 서비스. 비즈니스 로직은
Services에, DB 접근은Repositories에 둡니다. 컨트롤러는 요청을 DTO로 바꿔 서비스에 넘기기만 합니다. - 서비스는 HTTP를 모른다.
$this->request나 세션에 의존하지 않으면, 같은 클래스를 CLI 워커나 별도 서비스에서 그대로 돌릴 수 있습니다. - 모듈 간 호출은 서비스 인터페이스로만. 다른 모듈의 테이블을 직접 조인하지 않습니다. 테이블마다 주인 모듈을 정해두면 나중에 DB를 나눌 때 끊을 곳이 보입니다.
- API 버저닝과 통일 응답 포맷. 처음부터
/api/v1과 고정된 응답 엔벨로프를 쓰면, 뒷단이 모놀리스에서 서비스로 바뀌어도 클라이언트는 모릅니다. - 테넌트 격리는 베이스 모델에서. 웹채팅 솔루션에서는
TenantScopedModel을 두어 모든 조회에 테넌트 조건이 자동으로 붙게 했습니다. 격리를 인프라가 아니라 코드 규칙으로 해결한 예입니다. - 통합 테스트는 API 계약 기준. 내부 구현이 아니라 엔드포인트 입출력을 테스트해두면, 분리 후에도 같은 테스트로 회귀를 확인할 수 있습니다.
이 정도만 지켜도, 나중의 분리는 "재작성"이 아니라 "이사"가 됩니다.
판단 체크리스트
아래 질문 중 "분리" 쪽에 하나도 해당하지 않으면, 모놀리스를 유지하고 경계만 정리합니다.
|
질문 |
분리 신호 |
유지 신호 |
내 사례 |
|---|---|---|---|
|
실행 모델이 다른가? |
상시 연결, 스트리밍, 긴 백그라운드 작업 |
요청-응답으로 충분 |
웹채팅 엔진, 인터콤 |
|
쓰는 제품이 둘 이상인가? |
상태까지 공유해야 함 |
로직만 같음(라이브러리로) |
AITessera / ci4-board |
|
부하가 한쪽에 몰리는가? |
지표로 확인됨 |
아직 예감뿐 |
PhotoTrail 수집 단계 |
|
배포 주기·담당자가 다른가? |
팀이 다르고 릴리즈가 서로 막힘 |
같은 사람이 같이 배포 |
1인 개발 제품 대부분 |
|
다른 전환과 겹치는가? |
— |
프레임워크·DB 전환 중 |
AILicet CI3→CI4 |
|
논리적 분리로 풀리는가? |
프로세스 격리가 꼭 필요 |
라우트 그룹·필터·DB 그룹으로 해결 |
어드민 분리 검토 |
네 번째 줄은 조직 요인입니다. MSA가 원래 풀려는 문제가 "팀끼리 서로 발목 잡는 배포"라는 점을 떠올리면, 혼자나 소수가 개발하는 곳에서는 이 줄이 거의 항상 "유지"로 나옵니다.
마치며
MSA는 목표가 아니라 특정 문제에 대한 처방입니다. 처방이 필요한 증상은 대개 세 가지로, 실행 모델이 다르거나, 여러 제품이 상태를 공유하거나, 측정된 부하가 한쪽에 몰릴 때입니다.
그 외의 경우에는 모놀리스를 유지하되, 서비스 레이어와 테이블 소유권으로 경계를 지켜둡니다. 그러면 쪼갤 때가 왔을 때 클래스 하나를 옮기는 일로 끝납니다.
"나중에 커질 테니까 지금 쪼갠다"는 말을 들으면 한 번만 되물어 보시길 권합니다. 그 "나중"이 오기 전까지, 우리는 매일 분리 비용을 내고 있는 것은 아닌지요.
댓글 0개