마치며: 데이터가 아니라 의미를 옮기는 일
캐릭터셋부터 통계까지, 이기종 마이그레이션 시리즈 전체를 관통하는 핵심을 정리합니다.
13편을 마쳤습니다. 캐릭터셋부터 통계 정보까지, 데이터베이스 안에 살고 있는 모든 종류의 메타데이터를 살펴봤습니다. 이 글은 13편을 가로지르는 통합 정리이자, 시리즈를 처음 시작할 때 던졌던 질문 — "데이터만 잘 옮기면 되는 것 아닌가요?" — 에 대한 마지막 응답입니다.
13편을 관통하는 한 가지 사실이 있다면, 모든 편이 같은 말을 다른 각도에서 반복했다는 점입니다. DDL은 복사되지만, 그 DDL이 데이터를 통제하던 방식은 복사되지 않습니다. 같은 컬럼 정의가 다른 의미를 만들고, 같은 객체 이름이 다른 모델로 동작하며, 같은 SQL이 다른 계획으로 실행됩니다. 이 사실을 어디서 어떻게 마주치는지가 13편의 내용이었습니다.
13편이 채운 메타데이터의 지도
서론(00)에서 메타데이터를 네 축으로 정의했습니다. 각 편이 그 네 축에 어떻게 매핑되는지, 그리고 4축 분류 바깥에 추가된 한 축(옵티마이저 입력)이 어디에 위치하는지를 정리하면 다음과 같습니다.
| 메타데이터 축 | 다룬 편 | 핵심 메시지 |
|---|---|---|
| 저장 형태 | 01 캐릭터셋 · 02 VARCHAR · 03 CHAR/NVARCHAR · 04 LOB · 05 날짜형 · 06 숫자형 · 08 저장구조 | 같은 값이 같은 모양으로 저장되지 않는다. 도구의 기본값이 결정을 대신하지 못한다 |
| 흐름의 규칙 | 09 제약조건·DEFAULT · 10 시퀀스·식별자 채번 | 규칙은 정의가 아니라 실행 상태로 옮겨진다. 이관 직후 동기화가 마지막 매듭 |
| 접근성 | 07 사용자·스키마·권한 · 11 인덱스 | 같은 단어가 다른 구조를 가리키고, 같은 인덱스가 다르게 해석된다 |
| 내부 로직 | 12 프로시저·트리거·뷰·MV·Scheduler | 가장 깊이 박힌 코드가 가장 늦게 옮겨지고 가장 자주 누락된다 |
| (확장) 옵티마이저 입력 | 13 통계 정보 | 같은 SQL이 다르게 해석되는 이유는 통계 격차에 있다 |
13편(통계)은 4축에 정확히 들어가지 않습니다. 통계는 객체 자체가 아니라 옵티마이저의 입력이라는 별도 위상을 가지기 때문입니다. 인덱스 편(11)에서 옵티마이저가 다르게 생각하는 이유를 비용 모델 차이로 설명했지만, 그 비용 모델의 입력값이 통계입니다. 4축 모두를 정확히 옮겨도, 통계가 비어 있거나 부정확하면 옵티마이저는 잘못된 입력으로 계획을 만듭니다. 그래서 시리즈 마지막에 별도의 축으로 위치를 부여했습니다.
시리즈를 가로지르는 세 가지 공통 메시지
13편이 다룬 영역은 서로 다르지만, 글을 가로지르는 세 가지 메시지는 같습니다.
첫째, 도구의 기본값은 우리의 결정이 아닙니다.
자동 변환 도구는 합리적인 기본값을 적용할 뿐, 우리 시스템의 의도를 알지 못합니다. 숫자형 편(06)에서 본 NUMBER → NUMERIC(38,10)의 표현 왜곡, 캐릭터셋 편(01)의 묵음 변형(Silent Corruption), LOB 편(04)의 단순 타입 치환, 시퀀스 편(10)의 동기화 누락 — 이 모든 사고가 "도구가 결정했고 우리는 검토하지 않았다"의 결과입니다. Oracle의 모호함(precision 없는 NUMBER, USER ≡ SCHEMA의 일체성)은 오픈소스로 넘어오는 순간 누군가가 명시적으로 결정해야 하는 공백이 됩니다. 그 공백을 도구가 채우면, 스키마의 결정이 우리의 결정이 아니게 됩니다.
둘째, 1:1 매핑은 환상입니다.
같은 단어가 다른 구조를 가리킵니다. Oracle USER ≡ SCHEMA, PostgreSQL의 ROLE / DATABASE / SCHEMA 분리, MySQL의 DATABASE ≡ SCHEMA — 모두 "사용자"라는 한 개념을 다르게 쪼개고 묶은 결과입니다. 같은 옵션이 다른 기본값을 갖습니다. 시퀀스 CACHE 기본값 20 대 1, MySQL innodb_autoinc_lock_mode 5.7의 1 대 8.0의 2. 같은 객체가 다른 모델로 동작합니다. 트리거 모델, 머티리얼라이즈드 뷰의 refresh 정책, FK 검증 시점, NULL의 인덱스 저장 여부. 같은 함수가 다른 시점에 평가됩니다. PostgreSQL now()의 트랜잭션 시작 시점 대 Oracle SYSDATE의 호출 시점. 매핑은 항상 의도의 번역이고, 번역에는 정보 손실과 재해석이 따릅니다.
셋째, 이관은 정리의 기회입니다.
트리거 기반 채번을 IDENTITY로 정리할 시점, 흩어진 USER별 GRANT를 ROLE로 통합할 시점, MAX(id)+1 동시성 사고를 시퀀스로 교체할 시점, NOT NULL을 통과시키려고 넣어둔 더미값을 진짜 NULL로 정리할 시점, PK 없는 테이블에 PK를 정의할 시점, DB 안의 비즈니스 로직 중 "정말 DB 안에 있어야 하는가"를 다시 묻는 시점. 이관은 어차피 코드를 손대는 시점이므로, 옮기는 비용과 정리하는 비용의 차이가 가장 작은 시점입니다. 이 시점에 손대지 않은 레거시는 새 환경에서도 그대로 살아남고, 새 환경의 특성과 충돌해 더 큰 문제가 됩니다.
양방향성 — 한 방향 가이드는 절반의 진실
13편 모두가 양방향, 즉 Oracle → 오픈소스와 오픈소스 → Oracle 모두를 다뤘습니다. 이는 단순한 균형 잡기가 아니라 실무의 현실을 반영합니다.
오픈소스로의 이관은 흔히 일방향으로 인식되지만, 멀티 DB 운영 시대에는 양방향이 동시에 일어납니다. 한 시스템은 Oracle에서 PostgreSQL로, 다른 시스템은 클라우드 매니지드 PostgreSQL에서 Oracle로, 또 다른 시스템은 MySQL에서 PostgreSQL로 옮깁니다. 같은 조직 안에서, 같은 시점에. 이런 환경에서 한 방향만 다루는 가이드는 절반의 진실입니다.
방향이 달라지면 사고의 패턴도 달라집니다.
- Oracle → 오픈소스: Oracle의 풍부한 메타데이터(패키지, 자율 트랜잭션, MV Fast Refresh, Bitmap Index, Plan Baseline, SecureFile DEDUPLICATION)가 단순 모델로 흡수됩니다. 무엇이 사라지고, 그 의도를 어떻게 우회할지가 핵심 결정입니다. 사라진 것을 그대로 두고 옮기면 운영 중에 의미가 빠진 코드가 발견됩니다.
- 오픈소스 → Oracle: 단순한 모델이 Oracle의 풍부한 옵션을 만납니다. 무엇을 새로 설계할 기회로 삼을지가 핵심 결정입니다. 1:1로만 옮기면 Oracle 환경의 이점을 활용하지 못한 채 단순 모델이 그대로 굳어집니다.
방향에 따라 결정의 책임 위치가 다릅니다. 전자는 "잃지 않는 책임", 후자는 "새로 설계할 권한"입니다.
메타데이터의 본질 — 의미는 정의가 아니라 동작에 있다
13편을 관통하는 또 하나의 인식이 있습니다. 메타데이터의 의미는 DDL 정의가 아니라 데이터·SQL·운영 절차의 결합 안에 있습니다.
DDL은 정적입니다. VARCHAR2(100)이라는 정의는 두 환경에서 같은 글자로 보이지만, 의미는 캐릭터셋·길이 단위·NLS 설정의 결합 안에서 결정됩니다. FOREIGN KEY라는 정의는 ENABLE 상태인지, VALIDATE인지, DEFERRABLE인지, ON DELETE 액션이 무엇인지의 결합 안에서 의미를 갖습니다. CREATE INDEX라는 정의는 옵티마이저가 그 인덱스를 선택하는지, 통계가 정확한지, 비용 모델이 적절한지의 결합 안에서만 살아 있습니다.
따라서 메타데이터 이관은 DDL을 옮기는 작업이 아니라 데이터·SQL·운영 절차의 결합 전체를 옮기는 작업입니다. 그리고 이 작업의 표준 절차는 어느 편에서나 같았습니다 — 측정 → 결정 → 검증.
- 측정: 정의가 아니라 실제 데이터를 본다. 컬럼이 실제로 어떤 길이로 쓰이는지, 시퀀스의 현재값이 무엇인지, 인덱스가 실제로 선택되는지, LOB 컬럼이 정말 LOB이어야 하는지.
- 결정: 측정 결과를 근거로 결정한다. 도구의 기본값에 맡기지 않는다. 결정의 근거를 문서화한다.
- 검증: 결정이 실제로 의도대로 동작하는지 확인한다. 데이터·SQL·운영 절차 모두에서.
이 세 단계는 어떤 편에서든 같은 형태로 반복되었습니다. 메타데이터 변환의 본질은 이 반복 안에 있습니다.
통합 체크리스트 — 단계별로 무엇을 점검할 것인가
13편의 결론에 흩어져 있던 질문들을 이관 단계별로 묶었습니다. 각 항목 옆 괄호는 자세한 논의가 있는 편입니다.
실사 단계 — 무엇을 측정하는가
- 문자형 컬럼의 실제 최대 길이·문자 분포 (01·02·03)
- LOB 컬럼이 실제로 얼마나 크고 어떻게 쓰이는지 (04)
- 시간 컬럼이 타임존에 의존하는지 (05)
- 숫자 컬럼의 실제 최댓값·최솟값·소수 사용 (06)
- USER가 실제로 어떤 역할(접속·스키마·권한 묶음·사용 중단)인지 (07)
- TABLESPACE 분리가 어떤 의도(공간 통제·백업 단위·수명주기)였는지 (08)
- CHECK·FK가 실제로 ENABLE / VALIDATE 상태였는지 (09)
- 채번 방식(시퀀스·IDENTITY·
MAX(id)+1·UUID)이 무엇인지 (10) - PK 없는 테이블과 비효율 인덱스가 어디인지 (11)
- 내부 로직 안에 숨은 의존성(
DBMS_xxx·UTL_HTTP·DB Link·딕셔너리 참조)은 어디인지 (12)
설계 단계 — 무엇을 결정하는가
- 캐릭터셋과 collation, OS·애플리케이션까지의 통일 (01)
- 길이 정의 단위(BYTE vs CHAR)와 재산정 결과 (02·03)
- LOB 타입 결정(VARCHAR2·Extended VARCHAR2·CLOB·SecureFile 옵션) (04)
- 날짜형 타입과 타임존 처리 정책 (05)
- 숫자형 타입(정수 범위·UNSIGNED·NUMERIC 정밀도) (06)
- USER 분류와 매핑 단위(단일 DB / 다중 SCHEMA vs 다중 DATABASE) (07)
- TABLESPACE·파티션·압축·LOB 저장 정책 (08)
- DEFAULT·CHECK·FK·DEFERRABLE 동작과 UNIQUE+NULL 경계 (09)
- 채번 방식의 일관 결정(시퀀스·IDENTITY·UUID),
MAX+1정리 (10) - PK 전략, 인덱스 분리·통합, 특수 인덱스 대체 (11)
- 내부 로직의 DB 잔류 여부와 패키지·자율 트랜잭션·예외 모델 재설계 (12)
- 통계 자동 수집·확장 통계·시스템 통계 정책 (13)
적재 단계 — 무엇을 동기화하는가
- 시퀀스의 NEXTVAL을
max(PK) + 마진으로 일괄 재설정 (10) - IDENTITY 컬럼을
GENERATED BY DEFAULT로 두고 적재 (10·12) - 통계를 일괄 수집(
DBMS_STATS.GATHER_SCHEMA_STATS/ANALYZE/ANALYZE TABLE) (13) - 확장 통계·히스토그램 명시적 재생성 (13)
- PROCEDURE / FUNCTION / TRIGGER / PACKAGE의 의존성 순서 재컴파일 (
utlrp.sql격) (12)
검증 단계 — 무엇을 확인하는가
- 멀티바이트 문자의 전수 검증, Source 무결성까지 (01)
- 길이 초과·잘림·중복 충돌 (02·03)
- LOB의 JDBC / ORM / Connection Pool 동작 (04)
- 타임존이 다른 환경에서의 날짜 비교 (05)
- 집계·반올림·정산 결과의 합계 일치 (06)
- 권한·search_path·딕셔너리 의존 운영 스크립트 (07)
- I/O 분산·파티션 정합성·LOB 저장량 (08)
- 빈 문자열 ↔ NULL 변환으로 인한 의도하지 않은 중복 (09·11)
- 모든 채번 컬럼에서 NEXTVAL > max(PK) (10)
- 인덱스가 실제로 선택되는지 (실행 계획 확인) (11)
- 예외 분기·
SELECT INTO동작 단위 테스트 (12) - 옵티마이저 비용 파라미터가 환경에 맞는지 (13)
운영 단계 — 무엇을 모니터링하는가
- 운영 자동화 스크립트(딕셔너리 의존)의 작동 여부 (07·12)
- 인덱스 사용 모니터링 (
DBA_INDEX_USAGE/pg_stat_user_indexes/sys.schema_unused_indexes) (11) - Plan regression 모니터링 (
pg_stat_statements, Performance Schema) (13) - LOB·압축 테이블의 디스크 사용량 변화 (08)
- 자동 통계 수집의 작동 여부와 변경 적은 테이블의 통계 신선도 (13)
마치며
데이터는 옮길 수 있습니다. 하지만 의미까지 함께 옮길 수 있을지는 준비에 달려 있습니다. 서론(00)의 마지막 문장이었고, 13편을 마치는 자리에도 같은 문장이 답으로 남습니다.
13편은 그 준비의 지도였습니다. 어느 층에서 무엇을 점검해야 하는지, 어느 방향으로 옮길 때 어떤 결정이 따라오는지, 도구가 채워주지 않는 공백이 어디인지를 한 편 한 편 짚었습니다. 모든 메타데이터의 모든 함정을 다루지는 못했지만, 어떤 시각으로 메타데이터를 봐야 하는지는 충분히 전달되었을 것이라 믿습니다.
마지막으로 한 가지를 덧붙입니다. 이 시리즈는 "이관은 어렵다"를 강조하는 데 목적이 있지 않습니다. 이관이 어렵다는 사실을 직시한 사람만이 준비를 시작할 수 있고, 준비된 이관만이 의미까지 옮길 수 있다는 것을 말하고 싶었습니다. 모호함을 명확함으로 바꾸는 작업, 도구의 기본값을 우리의 결정으로 되찾는 작업, DDL을 의미로 채우는 작업 — 이 작업의 가치는 이관이 끝난 시스템의 첫 사용자가 "달라진 게 없네요"라고 말할 때 비로소 증명됩니다.
그 한 문장을 듣기 위한 시리즈였습니다.
