모놀리식으로 만든 PG를 다시 MSA로 나눈 이유
2024년에는 모놀리스가 맞았고 2025년에는 MSA가 필요했다. 같은 시스템에 다른 선택을 내린 기준과 분리 순서를 적었다.
2024년 PG 전환을 시작할 때는 모놀리스가 맞았다. 도메인 경계가 불분명했고 운영 기준도 정리되지 않은 상태에서 서비스를 나누면 복잡성만 늘어날 가능성이 컸다. 하나의 Spring Boot 애플리케이션 안에서 모듈 경계를 먼저 확인했다.
2025년에는 같은 시스템을 다시 나누기로 했다. 처음 선택이 틀렸기 때문이 아니라, 운영하면서 독립적으로 바뀌어야 할 경계가 확인됐기 때문이다.
모놀리스가 옳았던 시점
초기 전환에서는 기존 ASP 동작을 안전하게 옮기는 일이 우선이었다. 하나의 트랜잭션 안에서 결제와 정산 상태를 비교하기 쉬웠고, 배포와 모니터링 체계를 한 번에 만들 수 있었다.
Payment, Merchant, Settlement, Admin을 모듈로 나눴지만 물리적으로는 같은 애플리케이션과 데이터베이스를 사용했다. 이 구조 덕분에 숨겨진 규칙과 결합을 찾는 데 집중할 수 있었다.
모놀리스 안에서 먼저 경계를 시험했다
패키지만 나누고 서로의 repository와 table을 자유롭게 사용하면 이름만 모듈인 구조가 됐다. 다른 모듈이 필요로 하는 기능은 공개된 application service를 통해 호출하고 내부 구현 타입은 밖으로 내보내지 않게 했다.
payment
├─ api
├─ application
├─ domain
└─ infrastructure
settlement
├─ api
├─ application
├─ domain
└─ infrastructure
모듈 사이 의존 방향을 확인하고, 정산 코드가 결제 테이블을 직접 수정하는 지점을 줄였다. 같은 프로세스 안이라 호출 비용은 작았지만 서비스가 분리됐다고 가정하고 입력과 출력을 명시했다.
이 단계에서 경계를 자주 바꿀 수 있었다. 네트워크와 별도 배포가 없는 상태에서 책임을 옮겨 보며 어떤 데이터가 어느 영역에 있어야 자연스러운지 확인했다. 물리적 분리 전에 잘못된 경계를 고치는 비용이 훨씬 작았다.
분리가 필요하다는 신호
고도화가 이어지면서 결제와 정산의 변경 주기와 장애 영향이 달라졌다. 결제 승인은 즉시 응답과 외부 연동이 중요했고, 정산은 재처리와 대량 계산이 중요했다. 한 영역의 배포와 부하가 다른 영역까지 묶는 비용이 커졌다.
서비스 수를 늘리는 것이 목표는 아니었다. 다음 질문에 독립적인 답이 생긴 영역만 분리 대상으로 봤다.
- 자기 데이터를 스스로 소유할 수 있는가
- 실패와 복구 절차가 다른 영역과 구분되는가
- 독립 배포가 실제 운영 비용을 줄이는가
- 네트워크 실패를 감당할 만큼 경계가 명확한가
분리 판단을 정량 점수가 아니라 증거 목록으로 남겼다
‘규모가 커졌으니 MSA가 필요하다’는 설명으로는 부족했다. 배포 충돌, 장애 영향, 부하 특성, 데이터 소유권, 팀의 운영 능력을 항목별로 적고 실제 분리로 줄어드는 비용이 있는지 확인했다.
| 신호 | 분리 전에 답한 질문 |
|---|---|
| 변경 주기 | 독립 배포가 실제 대기와 충돌을 줄이는가 |
| 장애 영향 | 한 영역의 실패를 다른 영역에서 차단할 수 있는가 |
| 부하 특성 | 서로 다른 확장 정책이 필요한가 |
| 데이터 | 각 영역이 자기 원본 데이터를 소유할 수 있는가 |
| 운영 | 배포·관측·복구를 서비스별로 감당할 수 있는가 |
한두 항목만으로 서비스를 만들지 않았다. 독립 배포가 가능해도 데이터 소유권이 불분명하면 분산된 조인과 양방향 동기화가 늘어났다. 운영할 근거가 모인 경계부터 분리했다.
데이터 소유권을 테이블이 아니라 사실로 나눴다
Payment는 승인과 취소라는 결제 사실을 소유하고, Settlement는 어떤 결제가 언제 얼마의 정산 대상이 되는지를 소유하게 했다. 두 서비스가 같은 ‘금액’이라는 컬럼을 갖더라도 그 의미와 변경 주체가 달랐다.
| 사실 | 소유 서비스 | 다른 서비스의 사용 방식 |
|---|---|---|
| 승인·취소 상태 | Payment | 이벤트로 변경 사실 수신 |
| 정산 대상·계산 결과 | Settlement | 명시적 조회 계약으로 제공 |
| 가맹점 계약 조건 | Merchant | 필요한 시점의 값 또는 스냅샷 사용 |
다른 서비스의 데이터베이스를 읽으면 빠르게 구현할 수 있었지만 독립성은 사라졌다. 스키마 변경이 곧 다른 서비스의 장애가 되고, 어느 쪽이 잘못된 값을 고쳐야 하는지 모호해졌다. 직접 조회를 없애는 작업을 서비스 분리보다 먼저 진행했다.
동기 호출과 이벤트의 역할을 나눴다
모든 통신을 Kafka로 바꾸지 않았다. 사용자가 즉시 결과를 받아야 하고 상대의 현재 답이 필요한 조회는 동기 계약을 사용했다. 이미 일어난 사실을 여러 영역이 각자 처리해야 하는 경우에는 이벤트를 사용했다.
Command / Query
→ 지금 상대의 답이 있어야 다음 행동을 결정
Domain Event
→ 이미 일어난 사실을 알림
→ 소비자는 자기 트랜잭션으로 처리
이벤트 이름은 상대 서비스가 실행할 명령이 아니라 발생한 사실로 지었다. 정산 서비스를 직접 지시하는 메시지보다 ‘결제가 승인됐다’는 사건을 발행했다. 소비자가 늘어나도 결제 서비스가 각 후속 작업을 알 필요가 없게 했다.
동기 호출에는 Timeout과 실패 전파 기준을 뒀다. 상대가 느릴 때 요청 스레드와 연결 풀이 함께 고갈되지 않게 했다. 호출 실패를 숨기고 기본값으로 처리하면 금액 상태가 틀릴 수 있어 결과를 확정할 수 없는 경우를 따로 표현했다.
이벤트 계약은 내부 객체와 분리했다
결제 엔티티를 그대로 직렬화하면 내부 필드 변경이 메시지 계약 변경이 됐다. 이벤트에 소비자가 알아야 할 식별자, 사건 시각, 금액, 버전을 명시하고 내부 모델과 매핑했다.
{
"eventId": "stable-event-id",
"eventType": "PaymentApproved",
"paymentId": "business-key",
"occurredAt": "event-time",
"amount": "approved-amount",
"version": 1
}
새 필드를 추가할 때 이전 소비자가 무시해도 되는지 확인했다. 의미를 바꾸는 변경은 같은 필드 이름에 덮어쓰지 않고 버전이나 새 사건으로 분리했다. 생산자와 소비자가 동시에 배포된다는 가정을 버렸다.
분산된 실패를 정상 흐름으로 설계했다
모놀리스에서는 메서드 호출 실패가 같은 스택과 트랜잭션 안에 있었다. 서비스가 나뉜 뒤에는 요청이 처리됐지만 응답만 유실되거나, 이벤트가 늦게 도착하거나, 같은 메시지가 다시 전달될 수 있었다.
| 실패 | 필요한 설계 |
|---|---|
| 응답 유실 | 멱등 키와 결과 조회 |
| 이벤트 지연 | 처리 최신성 지표와 대사 |
| 중복 전달 | 소비자 멱등성 |
| 순서 역전 | 파티션 키와 상태 전이 검증 |
| 부분 장애 | 격리, 재처리, 운영 가시성 |
이 복구 장치가 준비되지 않은 경계는 분리하지 않았다. 네트워크 호출을 추가하는 일은 쉬웠지만 실패 상태를 운영하는 일은 별도 기능이 필요했다.
결제와 정산부터 나눴다
Payment Service
└─ Payment DB
└─ Kafka event
└─ Settlement Service
└─ Settlement DB
결제 서비스는 승인과 취소 상태를 소유하고, 정산 서비스는 정산 대상과 계산 결과를 소유하게 했다. 서로의 테이블을 직접 읽던 흐름은 이벤트와 명시적인 조회 계약으로 바꿨다.
이 순간부터 하나의 데이터베이스 트랜잭션으로 처리하던 문제가 메시지 중복, 순서, 재처리 문제로 바뀌었다. MSA는 복잡성을 없애지 않았다. 각 영역이 책임져야 할 복잡성을 분리했다.
분리 순서에 운영 준비를 포함했다
코드를 새 저장소로 옮기는 것으로 서비스 분리가 끝나지 않았다. 서비스별 배포, 로그와 메트릭, 알람, 재처리, 권한, 장애 대응 담당이 준비돼야 했다. 운영자가 결제 ID 하나로 두 서비스를 따라갈 수 있게 공통 식별자를 전달했다.
준비 순서
1. 모듈 경계와 직접 DB 참조 제거
2. 계약과 데이터 소유권 확정
3. 이벤트 발행·소비의 복구 경로 준비
4. 관측과 운영 도구 준비
5. 일부 흐름 전환
6. 안정화 뒤 물리적 의존 제거
새 서비스의 대시보드가 없는 상태에서 트래픽부터 넘기지 않았다. 실패 메시지가 쌓였을 때 누가 어떻게 확인하고 다시 처리할지도 전환 전에 연습했다.
한 번에 모두 나누지 않았다
가맹점과 관리자 기능까지 동시에 분리하지 않았다. 결제와 정산 경계에서 이벤트 발행, 소비, 모니터링, 복구 방식을 먼저 검증했다. 운영 절차가 준비되지 않은 서비스 분리는 미뤘다.
관리자 기능은 여러 도메인의 정보를 조합해 보여주는 특성이 있었다. 이를 독립 도메인처럼 먼저 분리하면 다시 모든 서비스에 강하게 의존할 가능성이 컸다. 조회 모델과 권한 경계를 더 정리할 때까지 함께 뒀다.
MSA 전환 뒤에도 서비스 안의 모듈 경계는 유지했다. 네트워크 경계가 생겼다고 내부 구조가 자동으로 좋아지는 것은 아니었다. 작은 모놀리스가 여러 개 생기지 않게 같은 설계 기준을 적용했다.
선택의 기준은 시점에 따라 달랐다
2024년에는 모놀리스가 변화의 속도를 높였고, 2025년에는 일부 영역의 독립성이 더 중요해졌다. 아키텍처 이름을 먼저 고른 것이 아니라 당시 가장 큰 위험을 줄이는 구조를 골랐다.
이 경험 뒤 모놀리스와 MSA를 정답처럼 비교하지 않게 됐다. 경계가 아직 보이지 않을 때는 함께 두고, 데이터 소유권과 실패 책임이 확인된 뒤 나누는 편이 안전했다.