한 번 옮긴 뒤에도, 데이터는 계속 바뀐다
Oracle의 데이터를 DocumentDB로 옮기는 일은 첫 적재만으로 끝나지 않는다. 원본에 새 데이터가 생기거나 기존 값이 바뀌면, 대상 문서에도 그 변화가 이어져야 한다.
이 프로젝트는 초기 대량 마이그레이션과 주기적인 증분 동기화를 함께 제공한다. Oracle에서 행을 읽고, YAML에 정의한 규칙으로 문서 구조를 만든 뒤, DocumentDB에 저장한다. 새 테이블을 추가할 때도 공통 처리 코드를 복제하기보다 매핑 정의를 확장하는 구조다.
| 구분 | 초기 마이그레이션 | 증분 동기화 |
|---|---|---|
| 읽는 범위 | 기존 데이터 전체 | 수정 시각 기준의 변경 구간 |
| 실행 방식 | PK 해시로 분할해 병렬 적재 | 스케줄러가 기본 60초 간격으로 실행 |
| 연결 지점 | 시작 시 확보한 DB 기준 시각 | 저장된 워터마크를 기준으로 다음 구간 조회 |
2026년 7월 27일 작성된 프로젝트 문서를 바탕으로 구조와 동작을 정리했다. 소스코드 실행 검증이나 운영 성능 측정 결과는 포함하지 않으며, 보완할 사항은 별도의 검토 항목으로 표시했다.
실행 방법은 여러 개, 핵심 로직은 한 곳
CLI와 스케줄러는 실행 시점과 대상을 결정하고, 실제 ETL 처리는 services/sync.py의 run() 함수로 모은다. 향후 API를 붙이더라도 같은 처리 함수를 재사용하는 것이 문서의 핵심 설계 방향이다.
| 계층 / 파일 | 맡은 역할 |
|---|---|
| config.py | Pydantic Settings로 접속 정보·배치 크기·주기 검증 |
| mapping/schema.py · loader.py | YAML 구조와 식별자 검증, 폴더별 작업 로드·캐시 |
| mapping/transform.py | 컬럼명 변경, 타입 변환, 중첩 문서 구성 |
| services/query.py · sync.py | 조회 조건 생성과 추출·변환·적재 실행 |
| services/watermark.py | 대상 DB의 _sync_state에 동기화 지점 저장 |
| services/migration.py · doctor.py | 초기 병렬 적재와 테이블·컬럼·인덱스 사전 점검 |
어디까지 반영했는지 기억하는 워터마크
워터마크는 마지막으로 성공한 동기화의 기준 시각이다. 매번 전체 테이블을 읽는 대신, 이전 기준점부터 이번 실행의 상한까지를 조회한다. 상한은 애플리케이션 시계 대신 Oracle의 SYSDATE로 확보한다.
SELECT ... FROM T
WHERE UPDATED_AT > :wm_low
AND UPDATED_AT <= :wm_high
ORDER BY UPDATED_AT
-- wm_low = 저장된 워터마크 - overlap (기본 2초)
-- wm_high = 이번 조회 시작 시 Oracle SYSDATE- 작업 정의와 이전 상태 읽기YAML을 로드하고 마지막 워터마크를 확인한다.
- 조회 구간 확정하기DB 시계로 상한을 고정하고, 하한은 설정한 시간만큼 되감는다.
- 배치 단위로 읽고 쓰기Oracle 행을 스트리밍으로 읽어 문서로 변환하고 bulk_write로 적재한다.
- 모두 성공한 뒤 기준점 이동하기모든 배치가 성공했을 때만 워터마크를 이번 상한으로 갱신한다.
중간 실패 시 워터마크를 유지하므로 다음 실행에서 같은 데이터를 다시 만날 수 있다. 따라서 안정적인 매칭 키를 가진 upsert가 재처리 설계의 전제가 된다.
검토 포인트: 2초 되감기는 경계 부근의 커밋 지연을 완화하는 설정이다. 의도적인 중복 조회가 발생하며, 이 범위를 넘는 지연까지 누락 없이 처리한다고 단정할 수는 없다. 수정 시각의 갱신 규칙과 실제 트랜잭션 지연을 확인해야 한다.
테이블별 차이는 YAML에 모으기
테이블, 기본키, 수정 시각 컬럼, 대상 컬렉션과 필드 변환 규칙을 YAML에 정의한다. 지원되는 매핑 범위 안에서는 테이블별 Python 코드를 늘리지 않고 설정으로 변경을 관리할 수 있다.
job_name: customer
source:
table: SCOTT.CUSTOMER
primary_key: [CUST_ID]
updated_column: UPDATED_AT
target:
collection: customers
write_mode: replace
fields:
CUST_ID: { to: _id, type: string }
CUST_NM: { to: name, type: string }
BALANCE: { to: balance, type: decimal }
ZIP_CD: { to: address.zipCode, type: string }CUST_ID는 문서의 _id로, CUST_NM은 name으로 바뀐다. address.zipCode처럼 점 표기를 사용하면 중첩 문서로 구성한다. 금액은 Decimal128으로 변환하는 규칙을 제공한다.
| 매핑 기능 | 사용 방식 |
|---|---|
| 타입 변환 | string / int / float / bool, decimal, date / datetime, json, raw |
| 소스 모드 | 단순 table 또는 조인·집계 등을 담는 sql |
| 그룹 구분 | 폴더명으로 작업을 구분해 main-data.customer 같은 이름 사용 |
| 자동 매핑 | fields를 생략하면 SELECT *와 원본 컬럼명을 사용 |
| 로드 제외 | 이름이 _로 시작하는 파일·폴더는 공용 조각 또는 초안으로 제외 |
예시의 테이블과 고객 필드는 구조 설명용이다. 실제 스키마에 맞춰 매핑 결과를 preview로 확인한 뒤 적재한다.
replace와 merge, 무엇을 보존할 것인가
쓰기 방식은 대상 문서의 필드를 누가 관리하는지에 따라 선택한다. 원본 테이블이 문서 전체의 기준이라면 replace, 다른 처리 과정이 채운 필드를 함께 유지해야 한다면 merge를 검토한다.
| 쓰기 방식 | 문서의 동작 | 확인할 점 |
|---|---|---|
| replace | ReplaceOne + upsert로 문서 전체 교체 | 대상에만 존재하는 필드도 교체 범위에 포함 |
| merge | UpdateOne의 $set + upsert로 지정 필드 반영 | 매핑에서 빠진 기존 필드는 남을 수 있음 |
| PK 없는 작업 | InsertOne으로 삽입 | 동일 구간 재실행 시 중복 삽입 가능 |
여러 문서는 bulk_write(..., ordered=False)로 묶어 쓴다. 문서에서는 부분 오류를 BulkWriteError로 수집하고 성공·실패 건수를 결과에 남기도록 설명한다. 일부 쓰기가 성공해도 전체 배치가 성공하지 않았다면 워터마크는 전진하지 않는다는 원칙이 중요하다.
같은 원본 행이 항상 같은 키로 매칭되어야 한다. PK를 _id로 매핑하는 방식을 우선 검토하고, 키가 없는 집계·리포트 작업은 별도의 중복 방지 또는 재적재 정책을 정해야 한다.
대량 적재에서 증분 처리로 이어가기
초기 적재는 시작 시점의 DB 시각을 먼저 확보하고, 기본키 해시로 데이터를 나누어 워커별로 읽는다. 문서의 분할 조건은 MOD(ORA_HASH(pk), N) = i다. 각 워커가 서로 다른 행 집합을 맡으며 OFFSET 방식의 페이지 순회를 사용하지 않는다.
- 시작 기준 시각 확보초기 적재가 진행되는 동안의 변경을 후속 증분 조회로 이어받을 기준점을 만든다.
- 샤드별 병렬 적재PK 해시로 나눈 데이터를 공통 변환·쓰기 흐름으로 저장한다.
- 적재 후 인덱스 구성문서는 인덱스 유지 비용을 줄이기 위해 적재 후 생성을 설명한다. 필수 키·제약조건의 적용 시점은 실제 구성에서 확인한다.
- 전체 성공 후 증분 동기화 시작실패한 샤드가 있으면 새 워터마크를 설정하지 않는다. 성공한 경우 시작 기준 시각부터 변경분을 이어서 반영한다.
검토 포인트: 해시 분할 자체가 조회 성능이나 일관된 스냅샷을 보장하지는 않는다. 실제 실행 계획과 원본 DB 부하, 초기 적재 중 변경·삭제가 발생했을 때의 결과를 확인할 필요가 있다.
실행 전 확인부터 운영 관찰까지
바로 전체 데이터를 옮기기보다 연결과 스키마를 점검하고, 소량의 변환 결과를 확인한 뒤 초기 적재와 스케줄러를 순서대로 실행한다.
py -m app.cli check
py -m app.cli jobs
py -m app.cli doctor customer
py -m app.cli preview customer -n 3
# 초기 전체 적재 → 주기적 증분 동기화
py migrate.py customer -w 4
py -m app.cli scheduler
# 상태 확인
py -m app.cli state customer문서의 py 명령 표기를 유지했다. 실행 환경에 따라 가상환경의 Python 실행 파일로 바꾸고, 작업 이름은 jobs 출력에서 확인한다.
| 주요 설정 | 문서 기본값 | 설계 의도 |
|---|---|---|
| batch_size | 2,000행 | 한 번에 읽고 쓰는 배치 크기 |
| oracle_arraysize | 5,000 | Oracle fetch 관련 설정 |
| watermark_overlap_seconds | 2초 | 조회 하한을 되감아 경계 지연 완화 |
| scheduler_interval_seconds | 60초 | 증분 처리 실행 간격 |
Oracle 연결은 풀로 재사용하며 결과를 배치 스트리밍으로 읽는다. DocumentDB 연결은 문서상 retryWrites=false와 TLS CA 설정을 사용한다. 접속 정보는 환경 설정으로 분리한다.
운영 결과에 남길 값
SyncResult에는 읽은 행 수, upsert·수정·매칭 건수, 배치 수, 실패 수, 조회 구간, 소요 시간과 오류 목록이 포함된다. 처리 시간뿐 아니라 워터마크가 계속 전진하는지와 실패 후 재처리가 정상 종료되는지를 함께 살펴볼 수 있다.
동일 프로세스 안에서는 작업별 threading.Lock으로 중복 실행을 막고, 스케줄러에는 max_instances=1, coalesce=True를 적용하는 구성이다. 여러 프로세스나 서버에서 실행할 경우에는 이 잠금 범위와 별도로 조정 방식을 검토해야 한다.
운영 전에 더 확인할 네 가지
다음은 문서의 설계를 바탕으로 정리한 추가 검토 항목이다.
- 삭제는 별도의 반영 규칙이 필요하다물리 삭제된 행은 수정 시각 조회로 읽을 수 없다. 소프트 삭제를 쓰더라도 DEL_YN = 'N'으로 필터링하면 삭제 상태로 바뀐 행이 제외되어 대상 문서가 남을 수 있다. 삭제 상태 전달 또는 대상 정리 절차를 함께 설계한다.
- 변경 조회 인덱스와 실행 계획을 확인한다문서는 UPDATED_AT 인덱스를 필수 점검 항목으로 둔다. 상수 조건과 사용자 정의 SQL이 더해졌을 때도 실제 조회 비용이 적절한지 확인한다.
- 중복 재처리와 늦은 커밋을 함께 시험한다배치 중간 실패, 같은 키의 재등장, overlap을 넘는 지연 상황에서 대상 데이터와 워터마크를 대조한다.
- 성능은 실제 데이터로 측정한다워커 수와 배치 크기를 바꿔 소요 시간·메모리·원본 DB 부하를 비교한다. 문서의 소량 실행 예시는 운영 처리량을 나타내는 벤치마크가 아니다.
다음 확장: API에서도 같은 로직 호출하기
문서는 FastAPI에서 공통 sync.run()을 호출하는 방안을 향후 확장으로 제안한다. 동기 드라이버를 고려한 실행 방식과 스케줄러 수명주기 연동이 포함되며, 구현 완료된 기능으로 소개하지는 않는다.
옮기는 로직과, 다시 시작하는 기준을 함께 설계하기.
이 ETL의 중심은 YAML 매핑, 공통 처리 함수, 그리고 성공한 쓰기를 기준으로 움직이는 워터마크다. 변경 조회와 재처리의 경계를 명확히 해야 초기 이관 이후에도 데이터 흐름을 이어갈 수 있다.
「SBSI 신규 ETL jobs — Oracle → AWS DocumentDB ETL 프로젝트 구조 및 개발 로직 문서」, made by kyuhuck, 2026.07.27.
프로젝트 문서의 구조·매핑·실행 절차를 재구성했으며, 운영상 보완 의견은 ‘검토 포인트’와 이 절에서 구분했다.