단일 아키텍처에서 마이크로서비스로의 발전은 점진적 마이그레이션 전략에 대한 중요한 필요성을 창출했습니다. 조직은 Oracle 또는 SQL Server 레거시 시스템을 운영하는 대규모의 경우, 전 세계를 중단하는 완전한 마이그레이션을 감당할 여유가 없습니다. 이 질문은 기업들이 역사적 데이터 무결성을 희생하지 않고 현대화해야 했던 실제 시나리오에서 등장했습니다.
핵심적인 도전 과제는 여러 도메인에 걸친 단일 ACID 트랜잭션과 마이크로서비스의 분산 특성 간의 불일치입니다. 데이터베이스를 분해할 때, 업데이트가 레거시 시스템과 새로운 서비스에서 동시에 발생하는 스플릿 브레인 시나리오에 직면하게 됩니다. 두 시스템을 운영 가능하게 유지하면서 네트워크 경계에서 참조 무결성을 유지하는 것은 단순한 데이터베이스 복제로 해결할 수 없는 분산 합의 문제를 만듭니다.
신뢰할 수 있는 이벤트 출판을 보장하기 위해 **Change Data Capture (CDC)**와 함께 Outbox Pattern을 사용할 이벤트 기반 아키텍처를 구현합니다. 레거시 데이터베이스 트랜잭션 로그에서 행 수준의 변경 사항을 캡처하고 Apache Kafka로 스트리밍 이벤트를 보낼 Debezium 커넥터를 배포합니다. 동시에 분산 트랜잭션을 처리하기 위해 마이크로서비스 계층에서 Saga Pattern을 구현하여 각 서비스의 운영 자율성을 유지하면서 최종 일관성을 보장합니다.
포춘 500 대 전자 상거래 플랫폼은 10년 된 Oracle 단일체에서 PostgreSQL 기반의 마이크로서비스로 주문 관리 시스템을 이주해야 했습니다. 재고, 가격 및 주문 이행 모듈은 12개 주요 테이블에 걸쳐 외래 키 제약과 긴밀하게 연결되어 있었습니다. 휴일 시즌 동안 시스템은 데이터 손실이나 다운타임에 대해 제로 관용으로 분당 50,000건의 거래를 처리했습니다.
솔루션 A: 이중 기록 전략
엔지니어링 팀은 처음에 레거시 애플리케이션 코드를 수정하여 Oracle와 새로운 PostgreSQL 서비스 모두에 동시에 기록하도록 고려했습니다. 이 접근 방식은 기록을 동기화하고 일관성을 유지하여 단순함을 약속했습니다. 그러나 이는 치명적인 결합 위험을 초래했습니다. 새로운 서비스가 지연이나 실패를 경험하면 전체 레거시 시스템이 충돌했습니다. 또한 XA 프로토콜을 통한 분산 트랜잭션 구현은 성능 저하를 초래하고, 최대 부하 시 응답 시간을 400% 증가시켰습니다.
솔루션 B: 데이터베이스 트리거 및 뷰
또 다른 옵션은 Oracle에서 행 수정 시 REST 엔드포인트를 직접 호출하는 데이터베이스 트리거를 생성하는 것이었습니다. 이는 애플리케이션 변경이 필요하지 않기 때문에 매력적으로 보였습니다. 그러나 이는 데이터베이스 인프라와 네트워크 토폴로지 간의 긴밀한 결합을 초래하여 시스템을 취약하게 만들었습니다. 마이크로서비스 엔드포인트가 도달할 수 없게 되면 트리거가 실패하고 전체 레거시 트랜잭션이 롤백되어 제로 다운타임 요구를 위반하게 됩니다. 또한 트리거가 특정 열 구조에 의존했을 때 스키마 마이그레이션 관리가 거의 불가능해졌습니다.
솔루션 C: 이벤트 소싱이 포함된 변경 데이터 캡처
선택된 아키텍처는 Debezium을 사용하여 Oracle의 리도 로그를 모니터링하고, 각각의 삽입, 업데이트 및 삭제를 불변 이벤트로 캡처하여 Apache Kafka에 게시했습니다. 마이크로서비스는 Kafka Streams를 통해 이러한 이벤트를 소비하고, Outbox Pattern을 사용하여 이를 PostgreSQL에 변환 및 지속시켜 정확히 한 번의 의미론을 보장했습니다. Confluent가 관리하는 Schema Registry는 Avro 스키마를 사용하여 역호환성 및 전방호환성을 보장했습니다. 이는 레거시 시스템을 마이그레이션 복잡성에서 분리했습니다. Oracle은 새로운 아키텍처에 대해 무관심한 상태이며, 서비스는 자신의 속도로 이벤트를 소비했습니다.
선택한 솔루션 및 이유
팀은 Single Responsibility Principle을 존중하고 결함 격리를 제공한 솔루션 C를 선택했습니다. 이중 기록과는 달리 레거시 시스템 성능은 마이크로서비스 지연의 영향을 받지 않았습니다. 트리거와 비교할 때 Debezium은 비동기적으로 작동하며 트랜잭션을 차단하지 않았습니다. 이벤트 로그는 불변 감사 추적을 제공했고, Kafka의 보존 정책을 통해 마이크로서비스가 스키마 진화 중에 재처리가 필요할 경우 역사적 데이터를 재생할 수 있었습니다.
결과
8개월의 마이그레이션 후, 플랫폼은 99.97% 가동 시간을 기록하며 200TB의 트랜잭션 데이터를 성공적으로 이전했습니다. 이 시스템은 지난해보다 40% 낮은 지연으로 블랙 프라이데이 트래픽을 처리했습니다. 새로운 서비스에서 가격 계산 버그가 발견되었을 때 팀은 레거시 Oracle 시스템에 손을 대지 않고 Kafka에서 3일치 이벤트를 재생하여 230만 개의 기록을 수정했습니다. CDC 파이프라인은 이제 Apache Flink를 사용한 실시간 분석의 기반으로 작동합니다.
모놀리스를 변경하면서 스키마 진화는 어떻게 처리합니까?
후보자들은 종종 마이그레이션 중에 스키마를 동결할 것을 제안하는데, 이는 민첩한 비즈니스에는 비현실적입니다. 올바른 접근 방식은 Confluent Schema Registry를 구현하고 Avro 스키마를 사용하여 전방 및 역호환성 모드를 적용하는 것입니다. Oracle 테이블이 변경될 때 Debezium 커넥터는 업데이트된 스키마와 함께 이벤트를 게시하지만, 레지스트리는 호환성 규칙을 강제합니다. 서비스는 Apache Avro의 해상도 규칙을 사용하여 Schema-on-Read 패턴을 구현해야 하며, 알 수 없는 필드를 무시하고 누락된 필드에 대해 기본값을 사용해야 합니다. 또한, 읽기 모델이 소스 스키마와 독립적으로 발전할 수 있도록 하는 CQRS 패턴을 배포하여 Kafka Connect 변환기를 사용하여 중첩 구조를 소비 엔드포인트에 도달하기 전에 평탄화할 수 있습니다.
전환 기간 동안 두 시스템이 동일한 엔티티를 동시에 업데이트하면 어떻게 됩니까?
이것은 단순한 타임스탬프만으로 해결할 수 없는 분할된 두뇌 시나리오를 생성합니다. 아키텍트들은 결정론적 충돌 해결을 위해 Vector Clocks 또는 **CRDTs (Conflict-free Replicated Data Types)**를 구현해야 합니다. 마이크로서비스 이벤트를 소비하고 Kafka Connect JDBC Sink를 사용하여 Oracle에 다시 쓰는 Bi-Directional Sync 구성 요소를 배포하되, 하이브리드 논리 클락에 기반한 엄격한 Last-Write-Wins (LWW) 의미론을 적용해야 합니다.
더 중요한 것은, 마이그레이션 동안 집합체 루트당 단일 쓰기 소유권을 부여하여 Domain-Driven Design 경계를 구현해야 합니다. 레거시 또는 마이크로서비스 중 하나만 소유해야 하며, 두 시스템 모두 아닌 경우가 되어야 합니다. Oracle의 Database Flags를 사용하여 마이그레이션 상태를 표시하고, API Gateway를 통해 쓰기 트래픽을 해당 경로로 라우팅해야 합니다. Strangler Fig Pattern을 사용하는 것이 좋습니다.
레거시 데이터베이스와 새로운 마이크로서비스 모두에 걸쳐 비즈니스 작업이 발생할 경우 트랜잭션 무결성을 보장하는 패턴을 설명하세요.
대부분의 후보자들은 이질적인 시스템 간에 **Two-Phase Commit (2PC)**를 사용하여 분산 트랜잭션을 제안하는데, 이는 부서간 결합과 가용성 문제를 야기합니다. 올바른 솔루션은 Saga Pattern과 Compensating Transactions를 사용하는 것입니다. 사용자의 조치가 Oracle(레거시)와 PostgreSQL(신규) 모두를 업데이트해야 할 경우, 이를 Camunda 또는 Temporal을 기반으로 하는 Saga Orchestrator를 통해 조정합니다. 이 과정은 지역 트랜잭션을 순차적으로 실행합니다: 먼저 Oracle을 업데이트하고, 도메인 이벤트를 게시한 후, 마이크로서비스 작업을 실행합니다. 어떤 단계가 실패하면 보상 트랜잭션을 실행합니다. 마이크로서비스 커밋이 실패하면, 레거시 시스템이 수신하여 Oracle 변경을 되돌리는 롤백 이벤트를 트리거합니다. 이렇게 하면 네트워크 경계를 가로막지 않고도 최종 일관성을 유지할 수 있습니다.