요약
1. PostgreSQL에서 MongoDB로 데이터 마이그레이션이 필요한 상황이 생겼다.
2. Kafka CDC를 활용해서 구현을 하려다가 MongoDB Relational Migrator라는 도구를 찾았다.
3. 해당하는 도구를 통해 실시간 혹은 Snapshot 기반의 데이터 마이그레이션 방법을 소개한다.

공용 모듈을 기반으로 여러 서버를 개발하고 있었는데, 점점 다양한 요구 사항이 많이 들어오다보니, PostgreSQL의 jsonb column을 많이 사용하게 됐다. Column을 다양하게 관리하자니, Migration에서 Lock이 걸려서 무중단 업데이트가 어려워질 수도 있었고, 그렇다고 jsonb column을 그대로 사용하자니 Select 성능에 문제가 생기기 시작했다.
이러한 문제를 해소하고자 Schema 규약에서 조금 자유롭고, json 데이터를 더 효율적으로 관리할 수 있는 MongoDB로 넘어가고자 했다. DB를 변경하는건 좋지만 많은 데이터를 어떻게 마이그레이션을 해야할지 고민이 생겼다.
처음으로 생각난 방법은 Kafka CDC를 활용해서 무중단 마이그레이션을 생각했다.
이미 Confluent Kafka를 사용하고 있었고, Sink Connector와 Source Connector만 설정을 하면 비지니스 로직 구현 없이 간단하게 가능할 것이라고 생각했다. 하지만 테이블 별로 설정이 필요했고, 프로젝트마다 설정이 필요해서 이론상 Connector만 수천개가 필요한 상황이 생겼다.
또한 테이블별로 데이터를 어떻게 옮겨야할지에 대한 설정도 필요해서 비용적인 측면에서 어려움이 있다 판단해 작업하지 않았다.

Kafka CDC말고 다른 방법을 찾던 도중 찾게 됐다. MongoDB Relational Migrator라는 도구였다.
해당 도구는 기존 RDB (MySQL, PostgreSQL, Oracle, Cassandra등)에서 MongoDB로 편리하게 마이그레이션할 수 있도록 기능을 제공해준다. 해당하는 도구를 통해 데이터 마이그레이션을 진행해보니, Snapshot 방식과 CDC 방식 모두 제공을 했다. CDC 방식으로 진행하게 되면, 기존 데이터를 Snapshot 방식으로 먼저 마이그레이션을 하고, snapshot 방식으로 마이그레이션한 시점부터 CDC로 진행하게 된다. 따라서 Snapshot 방식으로 마이그레이션이 진행된 이후에 CDC로 마이그레이션이 동작하는지 확인하고 Application을 업데이트 해야된다.
PostgreSQL CDC(Logical Replication)를 사용하려면 WAL(Write-Ahead Logging)기반 설정이 필요하다.
wal_level=logical, max_replication_slots, max_wal_senders 설정이 요구된다. rds.logical_replication)
또한 Relational Migrator를 사용하면서 편리했던 부분중 하나가 컬럼 이름을 CamelCase로 변경할건지, 기존 테이블 형식을 RDB 형식으로 진행할건지, 아니면 Mongo 형식으로 진행할건지 선택할 수 있었다.
RDB 형태로 진행하면 테이블에서 Collection으로 그대로 마이그레이션을 진행하고, Mongo 방식으로 진행하면 embedded 방식으로 진행해준다. embedded 방식으로 진행할까 고민을 했지만, 그렇게 진행할 경우 비지니스 로직을 전부다 바꿔야하는 상황과 embedded로 가서 손해보는 경우도 존재해서 기본 방식으로 진행했다.
기본적으로 Controller-Service-Repository 패턴이였고, Repository도 BaseRepository를 기반으로 interface가 있는 상태로 구현된 상태였다. 이에 맞춰서 기존 PG BaseRepository를 Mongo BaseRepository interface를 맞춰서 코드도 비교적 편리하게 마이그레이션 했다. 다만 몇몇 케이스에서 문제가 발생했다.
PG는 PK를 기반으로 upsert에 where절을 넣지 않더라도 DB 레벨에서 판단을 해줬지만, Mongo에서는 지원하지 않았기 때문에 upsert에 대한 로직을 변경해야했다.
기존에 PG Table에 PK로 Column 이름을 id로 설정했는데, Mongo에서는 기본적으로 (virtual 옵션을 키면) Types.ObjectId가 들어가기 때문에 문제가 있었다. 해당하는 문제를 해결하기 위해 override를 통해서 해결하거나, 컬럼 이름 자체를 변경해서 해결했다. 혹은 단순히 독립성을 가지기 위한 ID라면 삭제하고 ObjectId를 사용했다.
PG/Mongo 모두 Transaction의 큰 개념은 같으나 세부적인 동작 방식이 달랐다. 예를 들면 Mongo Transaction을 사용하려면 기본적으로 Standalone로는 동작하지 않고 Replica Set 형태로 존재해야하며, Deadlock 방지를 위해, 추후에 접근한 Transaction은 기본적으로 오류를 발생시키고 retry를 하라고 권장한다. (오류 메세지가 Please retry your operation or multi-document transaction)
또한 기존에는 Transaction 관리를 @Transactional 데코레이터를 직접 만들어서 범위 관리를 했는데, Mongo에서는 기본적으로 구현하려면 코드가 깔끔? 하지 않아서 문제가 있었다. (이는 추후 다른 게시물로 다룰 예정)
이러한 문제들 때문에 @Transactional을 동일하게 만들고 변경해서 테스트해보니 Conflict 오류가 단일 테스트에서는 발생하지 않고 스트레스 테스트 단계에서 많이 발생하게 됐다. 이를 해결하기 위해 Transaction 범위를 줄이거나 동일한 Document를 수정하는 비지니스 로직이 있다면 한번에 변경할 수 있도록 수정하는 작업을 진행했다.
결과적으로 마이그레이션을 진행한 이후 크리티컬한 이슈가 발생하거나 하지는 않았다. 데이터 마이그레이션도 생각보다 깔끔하게 진행이 됐고, 코드적으로도 큰 문제는 발생하지 않았다. 다만 아무래도 PG보다는 Mongo가 성능적인면은 부족한게 사실이라 API들에 대한 Latency가 조금씩 증가했다. (기존에는 P95가 80ms면 100ms정도로 평균적으로 10~20%씩 증가했다.)
다만 데이터 마이그레이션을 진행할 때 PG Table 설계를 그대로 가져왔기 때문에 어쩔 수 없이 손해를 보는 부분이 있었고, 추후에 Mongo에 맞춰서 개발했을 때는 따로 성능적인 문제가 발생하지 않았다.