이 문서는 대규모 모놀리스를 한 번에 재작성하지 않고 경계를 세운 뒤 작은 서비스부터 분리하는 방법을 공부하기 위해 만들었다. 학습 목표는 코드 모듈화, 데이터 소유권, 호출 전환, 관찰과 롤백을 하나의 마이그레이션 흐름으로 설명하는 것이다. 완료 기준은 작은 Spring Boot 모놀리스에서 한 모듈을 독립 서비스로 추출하고, 기존 경로와 신규 경로의 결...
이 문서는 대규모 모놀리스를 한 번에 재작성하지 않고 경계를 세운 뒤 작은 서비스부터 분리하는 방법을 공부하기 위해 만들었다. 학습 목표는 코드 모듈화, 데이터 소유권, 호출 전환, 관찰과 롤백을 하나의 마이그레이션 흐름으로 설명하는 것이다. 완료 기준은 작은 Spring Boot 모놀리스에서 한 모듈을 독립 서비스로 추출하고, 기존 경로와 신규 경로의 결과 비교 및 롤백을 직접 수행하는 것이다.
설계 문서를 읽는 것만으로 대규모 모놀리스를 직접 전환한 경험이 생기지는 않는다. 이 문서의 목적은 실무 경험을 꾸미는 것이 아니라 작은 환경에서 같은 실패 유형을 재현해 판단 근거를 만드는 것이다.
설명할 때는 다음 세 층을 구분한다.
이 경계를 지키면 과장 없이도 전환 원리를 깊게 설명할 수 있다.
모놀리스가 크다는 사실만으로 서비스를 분리할 필요는 없다. 분리하면 독립 배포와 개별 확장이 가능해지지만 네트워크 실패, 데이터 정합성, 관찰 복잡도, 운영 비용이 생긴다.
전환 후보는 다음 문제가 실제로 존재할 때 찾는다.
명확한 효과가 없다면 잘 나눈 모듈러 모놀리스가 더 단순한 해답일 수 있다.
코드가 뒤엉킨 상태에서 프로세스만 나누면 분산 모놀리스가 된다. 배포 단위는 나뉘었지만 변경할 때 여러 서비스를 함께 고쳐야 하고 장애는 네트워크를 따라 전파된다.
먼저 업무 기능을 모듈로 나눈다.
com.example.bank
account
api
application
domain
infrastructure
transfer
api
application
domain
infrastructure
notification
api
application
infrastructure모듈은 다른 모듈의 내부 repository나 entity를 직접 참조하지 않는다. 공개 인터페이스나 도메인 이벤트를 통해서만 협력한다.
Spring Modulith는 Spring Boot 애플리케이션의 기능 모듈을 모델링하고 의존 구조를 검증하는 도구를 제공한다.
class ModularityTest {
@Test
void verifiesModuleBoundaries() {
ApplicationModules.of(BankingApplication.class).verify();
}
}이 테스트가 통과한다고 좋은 도메인 경계가 자동으로 만들어지는 것은 아니다. 금지된 의존을 빠르게 발견하고 경계 가설을 코드로 검증할 수 있다는 점이 중요하다.
첫 추출 대상은 가장 중요한 코어가 아니라 경계가 명확하고 실패 영향이 낮은 영역이 좋다. 알림, 조회 전용 모델, 문서 생성처럼 데이터 소유권이 비교적 분명한 기능이 후보가 될 수 있다.
후보를 평가할 때 다음 축을 점수화한다.
핵심 원장처럼 강한 트랜잭션 경계가 필요한 영역을 첫 대상으로 선택하면 데이터 분리 문제가 한꺼번에 나타난다.
Strangler Fig 패턴은 기존 시스템 앞이나 내부에 라우팅 경계를 두고 기능을 하나씩 신규 구현으로 옮긴다. 빅뱅 재작성과 달리 기존 기능 개발을 멈추지 않고 작은 단위로 검증할 수 있다.
Client -> Router -> Legacy Module
-> New Service초기에는 모든 요청을 기존 경로로 보낸다. 신규 서비스가 준비되면 일부 요청을 복제해 결과만 비교하는 shadow mode를 사용한다. 결과가 안정되면 낮은 비율부터 실제 응답 경로를 전환한다.
전환 단위는 사용자 비율만이 아니다.
롤백은 배포가 아니라 라우팅 설정 변경으로 가능해야 한다.
새 서비스의 API가 기존 내부 인터페이스와 다를 때 모든 호출자를 한 번에 바꾸지 않는다. 기존 인터페이스를 유지하는 adapter 또는 facade를 두고 새 서비스 호출과 모델 변환을 맡긴다.
@Component
class CustomerProfileGateway {
private final CustomerServiceClient client;
LegacyCustomerProfile load(CustomerId id) {
CustomerResponse response = client.getCustomer(id.value());
return LegacyCustomerProfile.from(response);
}
}이 계층은 영구 아키텍처가 아니라 마이그레이션 장치다. 제거 조건과 담당자를 정하지 않으면 새로운 레거시가 된다.
코드를 분리하면서 같은 테이블을 양쪽이 직접 수정하면 독립 서비스가 아니다. 최종적으로 한 데이터 집합에는 하나의 쓰기 소유자가 있어야 한다.
점진 전환 중에는 다음 단계를 고려한다.
애플리케이션에서 두 DB를 순서대로 쓰는 dual write는 중간 실패 시 어긋난다. Transactional Outbox와 멱등한 컨슈머로 변경 전달을 재시도 가능하게 만든다.
신규 경로가 같은 HTTP 상태만 반환한다고 동등한 것이 아니다. 업무 결과와 부수 효과를 비교해야 한다.
비교 대상은 다음과 같다.
시간 값과 무작위 식별자는 정규화한 뒤 비교한다. 금액, 상태, 거래 식별자처럼 정합성에 중요한 값은 허용 오차 없이 비교한다.
레거시와 신규 경로의 메트릭을 같은 이름으로 섞지 않는다. 경로별 성공률, 지연, 결과 불일치율을 분리한다.
전환 전에 롤백 조건을 수치로 정한다.
롤백해도 신규 서비스가 이미 만든 데이터와 이벤트가 남을 수 있다. 트래픽 라우팅 복귀와 데이터 수습 절차를 함께 준비한다.
긴 전환에서는 현재 임시 구조가 왜 존재하는지 시간이 지나며 잊히기 쉽다. 각 추출 단계마다 대상 경계, 성공 지표, 롤백 조건, 임시 의존, 제거 조건을 짧은 결정 로그로 남긴다. 특히 공유 DB 읽기와 Anti-Corruption Layer는 종료 조건이 없는 순간 영구 구조가 된다.
완료 조건은 신규 서비스가 배포됐다는 사실이 아니다. 기존 쓰기 경로가 차단됐고, 호출자가 신규 계약으로 이동했으며, 임시 동기화와 라우팅 규칙을 제거했을 때 한 경계의 전환이 끝난다. 전환 후에도 목표 지표가 개선되지 않았다면 다음 서비스 추출을 멈추고 가설을 다시 평가한다.
완료 전까지 사용자 가치가 없고 기존 기능 변화와 계속 충돌한다. 작은 기능을 추출하고 매 단계 운영 지표로 검증한다.
컴파일 경계와 데이터 소유권이 없어 변경 영향이 계속 퍼진다. 모듈 의존 테스트와 쓰기 소유권 규칙을 함께 둔다.
실패 범위가 크고 비교할 기준선도 사라진다. shadow, canary, 단계별 전환을 사용한다.
서비스 수 증가는 가치가 아니다. 배포 시간, 장애 범위, 변경 리드타임, 확장 비용 중 무엇이 개선됐는지 측정한다.
계좌, 이체, 알림이 한 Spring Boot 애플리케이션에 있는 작은 모놀리스를 만든다. 첫 추출 대상으로 알림 모듈을 선택한다.
Spring Modulith 검증 테스트를 추가한다. 알림 모듈이 이체 repository를 직접 참조하는 의존을 의도적으로 만들고 테스트 실패를 확인한다. 그 의존을 도메인 이벤트로 바꾼다.
알림 모듈을 독립 Spring Boot 서비스로 옮긴다. 모놀리스는 같은 DB 트랜잭션에서 Outbox 항목을 저장한다. 별도 publisher가 이벤트를 Kafka로 보내고 알림 서비스가 멱등하게 처리한다.
각 실험에서 유실, 중복, 복구 시간, 사용자 요청 지연을 기록한다.
계좌와 이체 중 어느 영역을 다음으로 분리할지 앞의 평가 축으로 점수화한다. 실제로 추출하지 않더라도 공유 테이블과 트랜잭션 경계를 그려 난이도를 설명한다.
대규모 모놀리스를 바로 서비스로 쪼개기보다 내부 모듈 경계와 데이터 소유권부터 검증합니다. 변경 빈도와 결합도, 독립 확장 이득을 기준으로 첫 추출 대상을 고르고, Strangler Fig 방식으로 shadow와 canary를 거쳐 호출을 옮깁니다. 기존 호출자는 Anti-Corruption Layer로 보호하고, DB dual write 대신 Outbox와 멱등한 소비로 데이터를 동기화합니다. 경로별 오류율과 결과 불일치율을 보고 롤백하며, 서비스 수가 아니라 배포 리드타임과 장애 범위가 실제로 개선됐는지 평가합니다.
직접 대규모 전환을 주도하지 않았다면 그 사실을 분명히 말한다. 대신 운영 레거시를 작은 변경으로 개선한 경험과 로컬 추출 실습의 검증 결과를 각각 근거로 제시한다.