식별자 채번: 같은 번호가 두 번 찍히는 사고가 가장 자주 나는 곳
시퀀스·식별자 채번 방식의 차이가 만드는 중복 채번 사고와 안전한 전환법을 정리합니다.
앞 글에서 데이터가 따라야 할 규칙 — 제약조건과 DEFAULT — 을 다뤘습니다. 그 규칙 가운데, 가장 단순해 보이면서 이관 후 가장 자주 사고가 나는 영역이 식별자 채번입니다.
장면은 거의 정해져 있습니다. 데이터 이관이 끝나고, 건수도 맞고, 무결성 검증도 통과합니다. 오픈 직후 첫 INSERT가 들어오는 순간 PK 충돌 에러가 발생합니다. 원인은 단 하나 — 시퀀스의 NEXTVAL이 현재 PK의 최대값보다 작은 상태로 이관되었기 때문입니다. 시퀀스 동기화가 누락된 것입니다.
채번 사고가 빈번한 이유는 채번 메커니즘 자체가 단순해 보이기 때문입니다. "다음 번호를 받는다"는 한 줄의 동작 뒤에, 세 DB는 서로 다른 객체와 서로 다른 동시성 모델, 서로 다른 갭 정책을 가지고 있습니다. 이 차이를 모르고 옮기면, 같은 번호가 두 번 찍히거나, 번호가 비정상적으로 건너뛰거나, 다중 노드에서 순서가 뒤집힙니다.
이 글은 시퀀스/IDENTITY/AUTO_INCREMENT처럼 DB가 발급하는 채번 방식을 중심에 두지만, UUID/GUID 같은 분산 친화적 채번과 애플리케이션이 직접 MAX(id)+1을 계산하는 패턴까지 포함해 다룹니다. 채번은 시퀀스만의 일이 아니고, 이관은 채번 방식 자체를 재검토할 기회이기도 합니다. 방향은 Oracle → 오픈소스와 오픈소스 → Oracle 모두를 고려합니다.
세 DB의 채번 모델 — 객체가 있는가, 속성만 있는가
| 항목 | Oracle | PostgreSQL | MySQL |
|---|---|---|---|
| 독립 시퀀스 객체 | SEQUENCE | SEQUENCE | (없음; MariaDB 10.3+ 별도) |
| 컬럼 채번 속성 | IDENTITY (12c+) | IDENTITY (10+), SERIAL (legacy) | AUTO_INCREMENT |
| 호출 방식 | seq.NEXTVAL / seq.CURRVAL | nextval('seq') / currval('seq') / lastval() | INSERT 후 LAST_INSERT_ID() |
| 캐시 기본값 | CACHE 20 | CACHE 1 | innodb_autoinc_lock_mode에 따름 |
| 다중 컬럼/테이블 공유 | 시퀀스를 어디서든 참조 가능 | 시퀀스를 어디서든 참조 가능 | AUTO_INCREMENT는 테이블당 1컬럼 한정 |
| 트랜잭션 격리 | 비격리 (ROLLBACK해도 갭 발생) | 비격리 (ROLLBACK해도 갭 발생) | 비격리 (lock_mode에 따라 갭 패턴 다름) |
| UUID 네이티브 지원 | SYS_GUID() (RAW(16)) | uuid 타입, gen_random_uuid() | UUID(), UUID_TO_BIN() (8.0+) |
표가 보여주는 핵심은 두 가지입니다. 첫째, 독립된 SEQUENCE 객체의 존재 여부가 DB마다 다릅니다. Oracle과 PostgreSQL은 시퀀스를 별도 객체로 노출하므로 여러 테이블이 같은 시퀀스를 공유하거나, INSERT 외 컨텍스트에서 NEXTVAL을 호출하는 패턴이 가능합니다. MySQL은 AUTO_INCREMENT가 컬럼 속성이라 시퀀스를 객체로 다루지 못합니다(MariaDB 10.3+에서 SEQUENCE 객체가 도입되었지만 표준 MySQL은 여전히 부재). 둘째, 시퀀스는 어느 DB에서든 트랜잭션 격리 대상이 아닙니다. ROLLBACK해도 발급된 번호는 되돌아오지 않습니다. 갭은 정상이고, 갭이 없기를 기대하는 설계는 어디서든 위험합니다.
시퀀스 객체의 위상 — Oracle/PostgreSQL이 가지고 있는 자유, MySQL이 갖지 못하는 자유
Oracle과 PostgreSQL의 SEQUENCE는 독립 객체입니다. 여러 테이블이 하나의 시퀀스를 공유할 수 있고, 시퀀스를 SQL 컨텍스트(SELECT, 함수 인자, INSERT … VALUES 등) 어디에서나 참조할 수 있습니다. 글로벌 ID 발급기, 비-PK 컬럼의 채번, 코드 생성 등 응용 범위가 넓습니다.
MySQL의 AUTO_INCREMENT는 다릅니다. 컬럼에 묶인 속성이며 객체로 노출되지 않습니다. 한 테이블에는 하나의 AUTO_INCREMENT 컬럼만 둘 수 있으며, 해당 컬럼은 인덱스에 포함되어 MySQL이 다음 값을 찾을 수 있어야 합니다. InnoDB에서는 인덱스 설계가 AUTO_INCREMENT 동작과 밀접하게 연결됩니다. 여러 테이블이 같은 채번 공간을 공유하는 패턴은 native 기능으로는 구현되지 않습니다. AUTO_INCREMENT 값을 단독으로 미리 발급받아 다른 컬럼에 채워 넣는 패턴도 자연스럽지 않습니다.
Oracle/PostgreSQL → MySQL 방향에서 가장 큰 함정이 여기에 있습니다. Oracle에서 여러 테이블이 GLOBAL_ID_SEQ 같은 공용 시퀀스를 참조하던 경우, MySQL에서는 직접 대응이 어렵습니다. 보통은 채번 전용 테이블을 만들어 INSERT ... ON DUPLICATE KEY UPDATE로 우회하거나 별도 채번 서비스를 두는데, 동시성과 성능 모두 native 시퀀스에 미치지 못합니다. 이관 전에 "이 시퀀스가 정말 공유되어야 하는가, 아니면 테이블별 채번으로 분리 가능한가"를 먼저 결정해야 합니다.
MySQL → Oracle/PostgreSQL 방향에서는 반대로 자유의 폭이 넓어집니다. AUTO_INCREMENT가 SEQUENCE 객체로 분리되면서 채번 정책을 더 정교하게 설계할 수 있게 됩니다. 다만 이관 시점에는 1:1 대응(테이블당 시퀀스 1개)으로 가져가고, 통합·재설계는 이관 후 별도 작업으로 분리하는 것이 안전합니다.
시퀀스 동작 옵션 — CACHE/ORDER, CYCLE, 범위와 증감
시퀀스의 동작은 여러 옵션의 조합으로 결정됩니다. 어느 DB든 표준에 가까운 옵션 집합을 가지지만, 기본값과 세부 동작이 모두 다릅니다.
CACHE, ORDER, 그리고 동시성 정책 — 갭과 순서의 트레이드오프
Oracle은 기본 CACHE 20입니다. 인스턴스가 시퀀스 카운터에서 20개를 미리 가져와 메모리에 캐시하고 세션에 발급합니다. 인스턴스가 비정상 종료되면 캐시된 미사용 번호가 모두 손실되므로 갭이 발생합니다. RAC 환경에서는 노드별로 다른 캐시 범위를 가지므로 시간 순서와 번호 순서가 일치하지 않을 수 있습니다. 이를 막으려면 ORDER를 명시해야 하지만, 그러면 노드 간 동기화 비용이 추가되어 성능이 떨어집니다. NOCACHE는 갭은 줄이지만 매 NEXTVAL마다 데이터 사전 갱신이 발생해 고부하 환경에 부적합합니다.
PostgreSQL은 기본 CACHE 1이라 세션이 미리 확보하는 번호 범위가 1개뿐입니다. CACHE 값을 키우면 세션별로 번호를 묶어 확보하고, 세션 종료 시 미사용 번호가 손실될 수 있습니다. 즉 PostgreSQL의 시퀀스 캐시는 세션 단위이며, 다중 세션 환경에서는 시간 순서와 번호 순서가 자연스럽게 어긋납니다.
MySQL InnoDB는 innodb_autoinc_lock_mode 파라미터로 동작이 결정됩니다. 8.0의 기본값은 2(interleaved) 로, 락을 최소화해 동시성이 가장 높지만 다중 행 INSERT에서 번호가 비연속적으로 발급될 수 있습니다. 1(consecutive)은 단일 행 INSERT에는 가벼운 락만 쓰고 다중 행 INSERT에서는 미리 범위를 잡는 절충 모드로, 5.7까지의 기본값이었습니다. 0(traditional)은 INSERT 문 전체에 테이블 락을 걸어 연속 번호를 보장하지만 동시성이 떨어집니다. 복제 환경에서는 statement-based replication과 lock_mode 2의 조합이 안전하지 않아 row-based replication이 사실상 필수입니다.
CYCLE, MINVALUE/MAXVALUE, INCREMENT BY
Oracle 시퀀스는 MINVALUE, MAXVALUE, INCREMENT BY, CYCLE/NOCYCLE을 모두 지원합니다. 기본은 NOCYCLE이고, MAXVALUE에 도달하면 ORA-08004 에러가 발생합니다. CYCLE을 명시하면 MINVALUE로 되돌아가 다시 시작합니다. INCREMENT BY는 음수도 허용되어 역방향 시퀀스를 만들 수 있습니다. MAXVALUE를 명시하지 않으면 28자리 숫자까지 갑니다.
PostgreSQL도 동일한 옵션을 제공합니다. 기본은 NO CYCLE, 시퀀스의 데이터 타입(smallint/integer/bigint)에 따라 MAXVALUE 상한이 다르고, bigint 시퀀스는 2^63 - 1까지 갑니다. INCREMENT 음수도 가능합니다. CYCLE 동작은 Oracle과 유사합니다.
MySQL AUTO_INCREMENT는 이 옵션 체계 자체가 단순합니다. CYCLE 개념이 없습니다. 시작값은 AUTO_INCREMENT = N으로 지정 가능하고, 증가 단계는 auto_increment_increment 시스템 변수로 인스턴스 수준에서 조정합니다(주로 다중 마스터 복제에서 사용). 데이터 타입의 최대값(INT SIGNED는 약 21억, INT UNSIGNED는 약 42억, BIGINT SIGNED는 약 2^63 - 1, BIGINT UNSIGNED는 약 2^64 - 1)에 도달하면 더 이상 INSERT가 되지 않습니다. AUTO_INCREMENT는 음수를 발급하지 않으므로, UNSIGNED 타입으로 잡으면 SIGNED 대비 두 배의 범위를 얻을 수 있습니다. 마이그레이션 시 채번 한계를 검토할 때 SIGNED/UNSIGNED 선택이 함께 고려되어야 합니다. 또한 음수 INCREMENT도 지원하지 않습니다.
Oracle → MySQL 방향에서 CYCLE 설정에 의존하던 시퀀스가 있다면 native 대체가 없어 애플리케이션 로직이나 채번 테이블로 우회해야 합니다. INCREMENT가 양수 외 값이거나, 여러 시퀀스가 서로 다른 시작값·증가폭으로 운영되던 경우도 마찬가지입니다.
MySQL → Oracle/PostgreSQL 방향에서는 옵션 체계가 풍부해지면서 신규 설계 기회가 생기지만, 이관 시점에는 단순한 1씩 증가 시퀀스로 시작하고 옵션 활용은 안정화 이후로 미루는 것이 안전합니다.
채번 구현 방식 — 트리거, IDENTITY, AUTO_INCREMENT
같은 "PK에 채번된 번호를 넣는다"는 동작도 구현 방식이 여러 가지입니다.
Oracle 11g까지의 전형적인 패턴은 시퀀스 + BEFORE INSERT 트리거였습니다. 트리거가 NEXTVAL을 호출해 :NEW.id에 채워 넣는 방식입니다. 12c부터 GENERATED ALWAYS AS IDENTITY / GENERATED BY DEFAULT AS IDENTITY / GENERATED BY DEFAULT ON NULL AS IDENTITY 컬럼이 추가되어, 트리거 없이도 자동 채번이 가능해졌습니다. Oracle IDENTITY도 내부적으로는 시스템 생성 시퀀스를 사용하지만, 일반 SEQUENCE처럼 애플리케이션이 직접 참조하는 설계와는 관리 방식이 다릅니다. 자동 생성된 시퀀스는 데이터 사전(USER_SEQUENCES)에서 조회는 가능하지만, 일반적인 운영 흐름에서는 컬럼 정의를 통해 다뤄집니다.
PostgreSQL의 legacy 방식은 SERIAL/BIGSERIAL입니다. SERIAL은 실제 데이터 타입이 아니라 시퀀스 생성과 DEFAULT 연결, OWNED BY 관계까지 자동화하는 문법 관용구입니다. 따라서 메타데이터 분석 시 컬럼 속성과 시퀀스 객체를 따로 추적해야 합니다. 10부터 도입된 SQL 표준 IDENTITY는 컬럼 정의 안에 채번 의도가 명시되어 이관 도구가 해석하기 쉽고, 권한 관리(시퀀스 별도 GRANT 불필요), 소유권, pg_dump 동작에서도 SERIAL보다 깔끔합니다. 신규 설계에서는 IDENTITY가 권장됩니다.
세 IDENTITY 변형(GENERATED ALWAYS, GENERATED BY DEFAULT, Oracle만 가지는 GENERATED BY DEFAULT ON NULL)의 차이는 단순한 옵션 선택이 아니라 이관 적재 단계에서 즉시 영향을 미칩니다. 타겟 컬럼을 GENERATED ALWAYS AS IDENTITY로 정의하면 이관 데이터의 기존 PK 값을 명시 INSERT 할 수 없습니다. Oracle은 ORA-32795: cannot insert into a generated always identity column 에러로 즉시 실패하고, PostgreSQL은 INSERT에 OVERRIDING SYSTEM VALUE 절을 명시하지 않으면 거절합니다. 따라서 적재 단계에서는 GENERATED BY DEFAULT AS IDENTITY로 두어 명시 값 INSERT를 허용한 뒤, 적재가 끝나면 운영 정책에 따라 ALWAYS로 전환할지(애플리케이션의 직접 채번을 차단하고 싶은 경우) 그대로 둘지 결정하는 것이 안전합니다. PostgreSQL은 ALTER TABLE ... ALTER COLUMN ... SET GENERATED { ALWAYS | BY DEFAULT }로 사후 전환이 가능합니다. 이 함정은 에러 메시지가 명확해 즉시 진단되지만, 적재 스크립트를 짜기 시작한 뒤에 발견되면 정의 변경과 재적재의 비용이 발생하므로 사전에 결정해 두는 것이 효율적입니다.
MySQL은 일관되게 AUTO_INCREMENT입니다. 8.0부터는 InnoDB Persistent Auto-Increment Counter가 표준 동작으로, 카운터 값이 redo log에 영구 기록되어 재시작해도 마지막 발급 위치가 유지됩니다. 다만 5.7 이하 레거시 환경에서 이관하는 경우라면 카운터가 메모리에만 존재하던 시절의 함정 — 서버 재시작 후 첫 INSERT 시점에 InnoDB가 SELECT MAX(ai_col) FROM tbl FOR UPDATE에 해당하는 동작으로 최대값을 다시 계산하며, 재시작 직전에 가장 큰 ID 행을 DELETE 했다면 재시작 후 그 ID가 재사용될 수 있다는 점 — 까지 함께 검토해야 합니다.
Oracle → 오픈소스 방향에서 가장 중요한 결정은 트리거 기반 채번을 그대로 옮길지, IDENTITY로 정리할지입니다. 트리거 안에 다른 로직(감사, 가공)이 같이 들어 있었다면 그 로직을 별도 트리거로 분리해야 합니다. 이관은 이 정리 작업의 자연스러운 시점입니다. MySQL로 가는 경우는 AUTO_INCREMENT로 흡수되며, 8.0 이상이 표준이므로 영구 카운터를 그대로 활용할 수 있습니다.
오픈소스 → Oracle 방향에서는 IDENTITY/AUTO_INCREMENT가 Oracle 12c+ IDENTITY로 자연스럽게 매핑됩니다. 11g 이하로 가는 드문 경우에만 시퀀스 + 트리거 패턴으로 풀어야 합니다.
이관 직후 시퀀스 동기화 — 가장 흔한 사고
서두에서 언급한 PK 충돌 사고의 원인이 여기 있습니다. 데이터를 이관해도 시퀀스의 NEXTVAL 위치는 자동으로 따라가지 않습니다. Oracle에서 PK 최대값이 100,000인 상태에서 시퀀스를 새로 만들면 NEXTVAL은 기본 시작값(START WITH 1)에서 출발합니다. 오픈 직후 첫 INSERT는 PK=1을 시도하고 즉시 충돌합니다.
이관 직후 반드시 수행해야 하는 절차는 다음과 같습니다.
첫째, 모든 채번 객체의 현재값을 데이터의 max(PK)에 맞춰 재설정합니다.
- Oracle:
ALTER SEQUENCE ... RESTART START WITH N구문은 12.2(12c R2)부터 지원됩니다. 12.1 이하에서는 시퀀스를 DROP/CREATE 하거나, INCREMENT BY를 임시로 큰 값으로 바꿔 NEXTVAL을 한 번 호출한 뒤 다시 원래 INCREMENT로 되돌리는 우회가 필요합니다. 19c 이상 환경이라면 RESTART 구문을 그대로 쓰면 됩니다. - PostgreSQL:
setval('seq_name', N)또는setval('seq_name', N, true/false)(true면 N 다음부터, false면 N부터 발급). - MySQL:
ALTER TABLE ... AUTO_INCREMENT = N. 단 InnoDB는 N이 현재 max보다 작으면 max+1로 자동 보정합니다.
둘째, 여유분을 두고 재설정합니다. max(PK)에 정확히 맞추면 마지막 발급 번호와 충돌할 수 있고, 이관 후 첫 INSERT 시점에 다른 세션에서 미처 적재되지 않은 데이터가 들어올 가능성도 있습니다. 통상 max(PK) + 일정 마진(예: 1000)을 시작값으로 잡습니다. 갭은 어차피 정상이라는 사실을 받아들이면 이 마진의 비용은 무시할 수 있습니다.
셋째, 이 작업을 일괄 스크립트화합니다. 시스템 카탈로그에서 시퀀스 목록과 각 시퀀스가 채번하는 컬럼의 max값을 조회해 RESTART/setval 문을 자동 생성하는 스크립트를 만들어 둡니다. 시퀀스가 수십~수백 개라면 수작업은 누락의 원인이 됩니다.
넷째, 재설정 후 검증입니다. 각 채번 컬럼에 대해 "NEXTVAL > max(PK)"가 성립하는지 일괄 검증합니다.
함정이 하나 더 있습니다. PostgreSQL의 SERIAL/IDENTITY 컬럼은 시퀀스 이름이 자동 생성되어 숨겨져 있어 동기화 대상에서 누락되기 쉽습니다. pg_get_serial_sequence(table, column) 함수로 컬럼별 연결된 시퀀스를 찾아 처리해야 합니다. MySQL AUTO_INCREMENT는 테이블 속성이라 누락 위험이 비교적 적지만, 분할 적재로 일부 테이블이 빈 채로 남는 경우 시작값을 명시적으로 설정해야 합니다.
시퀀스가 답이 아닐 때 — UUID/GUID와 MAX(id)+1
채번이 시퀀스나 IDENTITY로만 이루어지는 것은 아닙니다. 이관 시점에 반드시 한 번은 짚어야 하는 두 패턴이 있습니다.
UUID / GUID — 분산 친화적 채번
분산 환경, 멀티 리전, 오프라인 클라이언트가 식별자를 미리 생성해야 하는 시스템에서는 UUID가 채번의 대안이 됩니다. DB 왕복 없이 클라이언트나 애플리케이션 계층에서 생성 가능하고, 다중 마스터·다중 리전에서도 충돌 위험이 사실상 0입니다.
| DB | 생성 함수 | 저장 권장 타입 |
|---|---|---|
| Oracle | SYS_GUID() | RAW(16) |
| PostgreSQL | gen_random_uuid() (13+ core 내장; 12 이하는 pgcrypto 확장 필요) | uuid 네이티브 타입 |
| MySQL | UUID() (v1 시간 기반) | BINARY(16) + UUID_TO_BIN() (8.0+) |
PostgreSQL은 13 이전에는 gen_random_uuid()가 pgcrypto 확장으로만 제공되었습니다. 별개의 확장인 uuid-ossp는 uuid_generate_v4(), uuid_generate_v1() 등 다른 이름의 함수를 제공하므로, 함수명 기준으로는 두 확장이 동일하지 않다는 점을 이관 도구 매핑 시 주의해야 합니다.
다만 UUID는 채번의 모든 문제를 해결하지 않습니다. 세 가지 측면을 함께 검토해야 합니다.
첫째, 저장 크기입니다. UUID는 16바이트로 4바이트 INTEGER, 8바이트 BIGINT보다 큽니다. PK이고 Secondary Index가 많은 테이블에서는 인덱스 크기가 비례해 커집니다. 특히 MySQL InnoDB는 Secondary Index 리프에 PK가 복사되므로 영향이 직접적입니다(저장구조 편에서 다룬 대로).
둘째, PK 값 패턴입니다. UUID v4는 무작위 값이라 InnoDB 클러스터드 인덱스에서 INSERT마다 페이지 분할이 빈발해 쓰기 성능이 떨어집니다. 이를 완화하는 접근은 두 갈래로 나뉘는데, 자주 혼동되지만 메커니즘이 다른 별개의 기법입니다. 하나는 UUID v7이나 ULID처럼 시간 순서를 처음부터 식별자 구조에 반영한 새로운 표준을 사용하는 방향입니다. 다른 하나는 MySQL UUID_TO_BIN(uuid, 1)처럼 기존 UUID v1의 비트 배치(time_low / time_mid / time_hi 영역)를 재배열해 정렬 가능한 바이너리 표현으로 저장하는 변환 옵션을 사용하는 방향입니다. 전자는 식별자 자체가 단조 증가하도록 새로 설계된 것이고, 후자는 이미 발급된 v1 UUID를 정렬 친화적으로 저장하기 위한 MySQL 고유 기법입니다. 어느 쪽을 선택하는지에 따라 채번 코드와 저장 표현이 달라지므로, 이관 시점에 명시적으로 결정해야 합니다.
셋째, 저장 표현의 일관성입니다. 같은 UUID가 어떤 시스템은 36자 하이픈 포함 문자열로, 어떤 시스템은 16바이트 바이너리로 저장됩니다. 이관 시 표현이 달라지면 JOIN과 비교가 깨질 수 있어, 양쪽 표현을 통일하는 변환 정책이 사전에 정해져야 합니다.
MAX(id)+1 패턴 — 이관 시 반드시 제거해야 할 시한폭탄
레거시 시스템에서 흔히 발견되는 패턴이 있습니다. 애플리케이션이 INSERT 전에 SELECT MAX(id)+1 FROM t로 다음 ID를 직접 계산해 INSERT 문에 넣는 방식입니다.
이 패턴이 만들어진 이유는 보통 단순합니다. 시퀀스 객체에 대한 권한이 없었거나, 시퀀스 사용법이 익숙하지 않았거나, 처음에 한두 명만 쓰던 시스템이었거나. 문제는 이 패턴이 동시성 환경에서 본질적으로 안전하지 않다는 점입니다. 두 세션이 동시에 같은 max+1을 받으면 PK 충돌이 발생하고, FOR UPDATE 락으로 직렬화하면 채번 자체가 병목이 됩니다.
이관은 이 패턴을 정리할 가장 자연스러운 시점입니다. 어차피 SQL과 애플리케이션 코드를 손봐야 하는 시점이고, 타겟 DB의 시퀀스/IDENTITY/AUTO_INCREMENT로 교체하는 것이 작업 흐름상 부담이 적습니다.
검토 절차는 단순합니다. 첫째, 애플리케이션 코드와 프로시저, 패키지에서 MAX(... )+1, MAX(... ) 패턴을 전수 추출합니다. 둘째, 각 패턴이 PK 채번용인지, 일회성 최대값 조회인지, 비-PK 컬럼의 채번인지 분류합니다. 셋째, PK 채번용은 모두 시퀀스/IDENTITY로 교체합니다. 비-PK 채번은 별도 시퀀스 도입 또는 UUID 전환을 검토합니다. 이 정리를 이관 후로 미루면, 이관으로 늘어난 동시 사용자 수가 그대로 채번 충돌 빈도 증가로 돌아옵니다.
결론
식별자 채번 이관은 가장 단순해 보이지만, 채번 방식의 선택(시퀀스/IDENTITY/AUTO_INCREMENT/UUID/직접 계산), 객체의 위상, 동시성 정책, 옵션의 동작, 그리고 이관 직후 동기화라는 다섯 결정이 모두 들어 있는 영역입니다. 그리고 그 결정 중 하나라도 빠뜨리면, 오픈 직후 첫 INSERT에서 사고가 납니다.
방향에 관계없이 다섯 가지 질문으로 수렴됩니다.
첫째, 소스 시스템의 채번 방식은 무엇인가 — 시퀀스, IDENTITY, AUTO_INCREMENT, UUID, 또는 MAX(id)+1. 그리고 그 선택이 여전히 유효한가, 이관 시점에 재검토할 가치가 있는가.
둘째, 시퀀스가 단일 테이블 채번용인가, 다중 테이블 공유용인가, 비-PK 컨텍스트에서 직접 호출되는가. 특히 MySQL 방향이라면 공유·직접 호출 패턴을 어떻게 우회할 것인가.
셋째, CACHE/ORDER/lock_mode/CYCLE/INCREMENT 같은 옵션의 차이가 갭과 순서, 범위 도달 동작에 어떤 영향을 주는가. 시간 순서가 비즈니스적으로 중요한 컬럼은 어디에 있는가.
넷째, MySQL은 8.0의 영구 AUTO_INCREMENT 카운터를 전제로 운영 모델을 설계했는가. 5.7 이하 레거시 환경에서 이관하는 경우라면 재시작 후 max 재계산 동작과 ID 재사용 위험까지 검토했는가. 그리고 IDENTITY 컬럼을 GENERATED ALWAYS로 정의해 적재가 거절되는 함정은 사전에 차단했는가.
다섯째, 이관 직후 시퀀스 동기화 스크립트는 준비되어 있는가. 모든 채번 컬럼에 대해 "NEXTVAL > max(PK) + 마진"이 검증되었는가. Oracle 12.1 이하라면 RESTART 우회 절차가 마련되어 있는가.
이 질문에 답이 나오기 전에 데이터 이관을 마치고 오픈 일정을 잡으면, 오픈 직후 첫 INSERT가 시스템 전체의 첫 사고가 됩니다. 그리고 그 사고는 보통 운영 시간대에 발생합니다.
채번이 결정되면 그 번호가 어디에 어떻게 저장되고 검색되는지로 넘어갑니다. PK의 값 패턴 — 단조 증가하는가, 무작위인가, 길이는 얼마인가 — 은 인덱스 구조와 쓰기 성능에 직결됩니다. UUID v4를 PK로 선택한 결과가 InnoDB 클러스터드 인덱스에서 어떻게 드러나는지, 시퀀스 기반 단조 증가 PK가 페이지 분할을 어떻게 줄이는지는 다음 인덱스 편에서 다시 살펴봅니다.
