Oracle에서 PostgreSQL로: 폐쇄망 대규모 이관의 개요와 전략

다음: 접속 확립

개요

레거시 Oracle이 시스템 오브 레코드(system of record)로 자리 잡은 조직에서, 애플리케이션이 소비하는 데이터를 자체 운영 PostgreSQL로 옮기는 작업은 단순한 INSERT ... SELECT가 아니다. 특히 대규모 규제 환경의 폐쇄망(air-gapped) 이관은 네트워크 통제, 최소권한 계정, DLP(Data Loss Prevention), 오래된 DB 버전이 겹쳐 있어 “정답 SQL 한 줄”로 끝나지 않는다.

이 시리즈는 그런 환경에서 실제로 밟은 순서를 조직·제품·스키마 정보를 지운 채 일반화한 것이다. 소스는 언제나 records, documents, chunks, embeddings 같은 일반 테이블명으로 표기하며, 개별 단계는 뒤따르는 문서로 위키링크된다. 이 문서는 전체 지도를 먼저 그려, 각 단계에서 무엇을 신뢰하고 무엇을 의심해야 하는지 감을 잡게 하는 것이 목적이다.

왜 옮기나

이관은 목적이 분명해야 중간에 흔들리지 않는다. 통상 세 가지 동기가 겹친다.

동기내용이관 후 이득
소유권·비용외부 벤더 라이선스 DB → 자체 운영 오픈소스 스택라이선스 비용 제거, 통제권 확보
기능pgvector 확장으로 임베딩·유사도 검색을 DB 안에서 처리RAG/검색 파이프라인을 DB 레벨로 통합
스택 정합애플리케이션이 SQLAlchemy + asyncpg로 표준화드라이버·ORM·마이그레이션 도구 일원화

특히 pgvector는 이관을 정당화하는 결정적 이유가 되기도 한다. 임베딩을 별도 벡터 스토어에 두지 않고 embeddings 테이블에 vector 컬럼으로 저장하면, 원본 documents·chunks와 조인 한 번으로 검색이 끝난다. 이 이득을 얻으려면 대상 PostgreSQL에 확장이 실제로 설치·활성화(CREATE EXTENSION vector)되어 있어야 한다. 설치가 누락되면 임베딩 경로가 오류 없이 조용히 무력화되는 함정이 있다(아래 참조).

핵심 개념: 전체 파이프라인

이관은 선형 파이프라인으로 보되, 각 단계가 독립적으로 재실행 가능해야 한다.

단계목표대표 위험상세 문서
1. 접속 확립드라이버·클라이언트·인코딩 결정구서버 미지원, 인증 실패02.접속 확립 (thin·thick·libaio·NLS)
2. 추출대용량 테이블 안전 스캔ORA-01555, CLOB 성능03.데이터 추출 (청킹·keyset·ORA-01555)
3. 정제제어문자·인코딩·암호문 처리NUL 문자, 깨진 한글04.데이터 정제 (NUL 문자·인코딩·암호화 컬럼)
4. 적재멱등·고속 적재자연키 위반, 처리량 병목05.적재 (멱등 upsert·COPY·자연키)
5. 변경 동기화·검증증분 반영·정합성놓친 DELETE, watermark 무력화06.변경 동기화와 검증
6. 보안·프라이버시마스킹·대량추출 통제PII 유출, DLP 차단07.보안·프라이버시 (마스킹·DLP)

“1→6”은 논리적 순서일 뿐, 실무에서는 5번(변경 동기화)이 자리 잡히기 전까지 1~4를 여러 번 반복한다. 그래서 모든 단계가 재실행 안전해야 한다는 원칙이 파이프라인 전체를 관통한다.

세 가지 원칙

이관 전체를 지배하는 세 원칙은 다음과 같다.

  1. 멱등 재실행(idempotent) — 언제 중단·재시작해도 중복이나 유실 없이 이어져야 한다. 네트워크가 끊기고, 세션이 만료되고, DLP가 경보를 울려 작업이 멈추는 것을 전제로 설계한다. 적재는 ON CONFLICT ... DO UPDATE로, 재개는 이미 적재된 키를 건너뛰는 방식으로 구현한다(05.적재 (멱등 upsert·COPY·자연키)).
  2. 소스를 신뢰하지 말 것 — 제어문자(\x00), 잘못 디코딩된 한글, DB측 암호문, 공백/NULL 자연키 등 “더러운 데이터”를 항상 가정한다. 레거시 시스템에 20년치 데이터가 쌓였다면 스키마가 허용하는 모든 이상 케이스가 실재한다고 봐야 한다(04.데이터 정제 (NUL 문자·인코딩·암호화 컬럼)).
  3. 범위를 좁혀라 — 백업·이력·테스트 테이블(_BAK, _HIS, _TR, _TEST 접미어)을 이관 대상에서 제외하는 것만으로도 적재량이 3~4배 줄기도 한다. 무엇을 옮기지 않을지 먼저 정하는 것이 무엇을 옮길지 정하는 것보다 효과가 크다.

실전: 이관 실행의 골격

파이프라인을 코드로 옮기면 대략 다음과 같은 오케스트레이션이 된다. 각 함수의 구현은 해당 단계 문서에서 다룬다.

def migrate_table(src, dst, table: str, last_key: str | None):
    """단일 테이블을 청킹하며 멱등 적재."""
    while True:
        rows = extract_chunk(src, table, after=last_key, size=50_000)  # 03 추출
        if not rows:
            break
        rows = [clean_row(r) for r in rows]                            # 04 정제
        upsert_rows(dst, table, rows)                                  # 05 적재
        last_key = rows[-1]["natural_key"]
        checkpoint(table, last_key)      # 재개 지점 기록 (멱등 재실행)

체크포인트(checkpoint)는 세 원칙 중 첫 번째를 구현하는 지점이다. 마지막으로 성공한 키를 영속화해 두면, 중단 후 재시작해도 그 지점부터 이어진다. 전체 이관을 하나의 거대한 트랜잭션으로 묶지 않고, 청크 단위로 커밋·체크포인트하는 것이 폐쇄망 환경에서 살아남는 방식이다.

함정·트러블슈팅 (미리보기)

각 함정의 상세 해법은 뒤따르는 문서에 있다. 여기서는 이관 초반에 반드시 만나는 것들을 미리 표로 잡아 둔다.

증상원인상세
최신 드라이버 thin 모드가 접속 거부Oracle 서버가 너무 오래됨(예: 11g)02.접속 확립 (thin·thick·libaio·NLS)
대용량 SELECT가 도중에 실패undo 초과(ORA-01555)03.데이터 추출 (청킹·keyset·ORA-01555)
PostgreSQL 적재가 특정 행에서 중단CLOB의 NUL 문자를 text가 거부04.데이터 정제 (NUL 문자·인코딩·암호화 컬럼)
임베딩 검색이 조용히 비어 있음대상에 pgvector 미설치05.적재 (멱등 upsert·COPY·자연키)
증분 이후 대상 행이 소스보다 많음DELETE가 반영 안 됨06.변경 동기화와 검증
대량 SELECT가 갑자기 차단·경보DLP 정책07.보안·프라이버시 (마스킹·DLP)

정리

이관은 “데이터를 옮기는 일”이 아니라 “믿을 수 없는 소스에서 신뢰 가능한 대상을 재구축하는 일”이다. 세 원칙(멱등 재실행·소스 불신·범위 축소)을 파이프라인 전체에 관철하면, 폐쇄망의 불확실성(끊기는 세션, 막는 DLP, 오래된 서버) 속에서도 안전하게 반복 가능한 절차를 얻는다. 다음 문서부터 각 단계를 실전 코드와 함께 파고든다.