숫자를 옮기지 말고 재설계하라: VARCHAR 변환 전략
VARCHAR 길이와 문자 단위 차이를 이해하고, 실제 데이터 기준으로 타입을 재설계하는 방법을 정리합니다.
이기종 마이그레이션 프로젝트를 되짚어보면, 전체의 60% 이상이 오픈을 불과 2주 앞두고 메타데이터를 전면 재정의해야 했습니다. 특정 테이블 몇 개가 아닌 메타데이터 전체를 다시 구성해야 할 만큼 상황은 시급했습니다. '툴로 자동 변환했으니 문제없겠지'라고 생각했다가, 실제 데이터를 넣어보니 에러가 발생했습니다.
가장 많은 원인은 복잡한 로직이 아니라, 가장 기본이라 생각했던 문자형 데이터 타입이었습니다.
데이터 타입은 그 문자를 어떤 형태로 정의하고 다룰 것인가에 대한 약속입니다. 같은 데이터라도 어떤 타입으로 정의하느냐에 따라 저장 공간이 달라지고, 인덱스 전략이 달라지고, SQL 처리 방식이 달라집니다. 그리고 이 모든 차이는 메타데이터 변환 과정에서 반드시 이해하고 결정해야 할 사항입니다.
문자형 데이터 타입, 같은 이름, 그러나 다른 세계
문자형 데이터 타입 변환은 단순히 타입의 이름을 바꾸는 것이 아니라, 데이터의 성질과 시스템 성능, 서비스 무결성을 결정짓는 고도의 설계 작업입니다. 데이터베이스마다 문자형을 정의하는 철학이 달라서, 같은 이름의 타입이라도 내부 구현과 허용 길이, 심지어 저장 방식까지 다르기 때문입니다.
먼저, 전체 타입 매핑을 확인하겠습니다. 대부분의 자동 변환 툴이 아래 기준으로 변환하지만, 각 타입마다 숨은 함정이 있습니다.
| 분류 | Oracle | MySQL | PostgreSQL |
|---|---|---|---|
| 고정 길이 문자 | CHAR(n) (1~2000 BYTE) | CHAR(n) (0~255) | CHAR(n) |
| 가변 길이 문자 | VARCHAR2(n) (1~4000 BYTE) | VARCHAR(n) (0~65,535) | VARCHAR(n) |
| 유니코드 고정 | NCHAR(n) (1~2000 BYTE) | - | CHAR(n) |
| 유니코드 가변 | NVARCHAR2(n) | VARCHAR(n) | VARCHAR(n) |
| 중형 텍스트 | VARCHAR2(4000) <br>초과는 CLOB | TEXT (최대 65,535 BYTE) <br>MEDIUMTEXT (최대 16MB) | TEXT (최대 1GB) |
| 대용량 텍스트 | CLOB (최대 4GB) | LONGTEXT (최대 4GB) | TEXT (최대 1GB) |
| 레거시 대용량 | LONG (최대 2GB) *(Deprecated)* | - | - |
| 유니코드 대용량 | NCLOB (최대 4GB) | LONGTEXT | TEXT |
위에 언급한 대로 데이터베이스마다 문자형 타입을 정의하는 철학이 다르기 때문에 이 기계적 매핑만으로는 부족합니다. 매핑되는 데이터 타입만 보고 안심하면, 그 뒤에 숨은 차이를 놓치게 됩니다.
VARCHAR와 CHAR: 길이라는 함정
VARCHAR(100) "문자형 데이터 100개를 저장할 수 있다"는 것까지는 명확합니다. 하지만 무엇의 100개인가? 여기서부터 데이터베이스마다 이야기가 달라집니다.
Oracle은 BYTE 단위와 CHAR 단위를 명시적으로 구분합니다. 단위를 명시하지 않으면 데이터베이스의 NLS_LENGTH_SEMANTICS 파라미터 설정에 따라 결정됩니다. 많은 레거시 시스템이 BYTE 단위를 기본값으로 사용하고 있습니다. 즉, VARCHAR2(100) 는 VARCHAR2(100 BYTE)와 같다고 표현할 수 있습니다. 하지만 PostgreSQL과 MySQL은 이런 구분이 없습니다. 이들 오픈소스 데이터베이스에서 VARCHAR(100)은 항상 100글자를 의미합니다.
여기에 Character Set 변환이 겹치면 계산이 복잡해집니다. Character Set이 EUC-KR에서 UTF-8로 바뀌면, 한글 한 글자가 차지하는 바이트가 2바이트에서 3바이트로 늘어납니다. 영문과 숫자는 여전히 1바이트지만, 한글은 1.5배로 커집니다.
Target 데이터베이스가 MySQL 또는 PostgreSQL이라면 문자 단위로 길이를 계산하므로 한글 7자인 '주식회사바토스'는 VARCHAR(20)에 문제없이 저장됩니다. 하지만 Oracle에서 Oracle로 이관한다면, Character Set만 바뀌어도 저장 가능한 문자 개수가 달라집니다. EUC-KR 환경의 VARCHAR2(20 BYTE)에 한글 10자가 들어가던 컬럼이, UTF-8로 바뀌면 6자만 들어갑니다.
이관에서 VARCHAR 길이는 데이터 타입 뒤의 숫자를 옮기는 문제가 아닙니다. 데이터베이스가 바뀌면 길이의 정의가 달라지고, Character Set이 바뀌면 바이트가 바뀌고, 저장 가능 범위가 함께 움직입니다. 문자형 컬럼의 길이는 "스키마에 적혀 있던 값"이 아니라, 새 환경(캐릭터셋/DB의 길이 Semantics)에서 데이터가 안전하게 보존되도록 다시 정의해야 하는 메타데이터입니다.
발생 가능한 현상: 언제, 어떻게 드러나는가
길이 정의의 차이와 Character Set 변환이 만나면, 크게 두 가지 유형의 문제가 발생합니다. 이 문제들은 발견되는 시점과 위험도가 완전히 다릅니다.
첫번째는 데이터 입력 중 길이 초과 에러가 발생하면서 입력이 취소되는 현상입니다. 그렇지만 문제가 즉시 드러나기 때문에 컬럼 길이를 재산정하거나, 데이터를 정제하거나, 이관 전략을 바로 수정할 수 있습니다. 무엇보다 오픈 전에 문제를 발견했다는 점이 중요합니다.
두번째는 에러없이 진행되었지만 데이터 잘림 또는 입력 가능한 문자 수 제한이 걸리는 경우입니다. 이관 작업은 에러 없이 완료되고, 건수도 일치하고, 로그에도 문제가 보이지 않습니다. 하지만 두 가지 문제가 숨어 있습니다.
일부 이관 도구는 설정에 따라 길이를 초과하는 데이터를 자동으로 잘라서 저장합니다. 또는 특정 옵션을 켜면 "truncate on overflow" 모드로 동작하기도 합니다. 이관 작업은 성공적으로 완료되지만, 데이터는 이미 손상된 상태입니다. 이 잘림은 단순히 글자 몇 개가 사라진 것으로 끝나지 않습니다. 서로 다른 문자열이 잘림 때문에 같은 값이 되어버려 중복/충돌이 발생할 수 있고, JOIN의 결과와 GROUP BY 집계 결과에 영향을 끼칠 수 있습니다.
이 문제가 위험한 이유는 발견 시점이 늦다는 것입니다. 며칠 또는 몇 달이 지난 후 장애가 발생하고, 그때에서야 DBA는 데이터를 확인하고 원본을 찾기 시작합니다. 하지만 이미 Source 시스템은 폐기되었을 수도 있습니다.
다른 하나는 미래의 데이터가 막히는 경우입니다. 기존 데이터는 모두 컬럼 길이 안에 들어왔기 때문에 문제 없었지만 오픈 후 신규 데이터를 입력할 때 주로 발생합니다. 애플리케이션은 기존 EUC-KR 환경을 기준으로 검증 로직을 작성했습니다. "이름은 최대 50자까지 입력 가능"이라는 룰이 있고, 프론트엔드 화면에서 50자를 넘으면 입력을 막습니다. 백엔드 코드에도 같은 검증 로직이 있습니다.
만약 사용자가 한글 40자를 입력한다면 프론트엔드 검증을 통과합니다. 백엔드 검증도 통과합니다. 하지만 한글 40자는 UTF-8에서 120바이트이고, 컬럼은 100바이트만 허용하기 때문에 데이터베이스에서 에러가 발생하게 됩니다.
해당 원인을 찾기 위해서는 데이터베이스뿐만 아니라 애플리케이션의 로그까지 확인해야 하고, 원인 규명과 책임 소재를 가리는 데 많은 시간이 소요됩니다.
해결 전략: 숫자를 옮기지 말고 재설계하라
#### 재설계의 3단계: 측정 → 계산 → 적용
이기종 마이그레이션 프로젝트에서 VARCHAR와 CHAR 타입의 길이를 다루는 일은 단순한 숫자 복사(Copy & Paste)가 아닙니다. 그것은 Source 시스템의 데이터를 담았던 컬럼의 크기를 측정하고, Target 시스템의 재질(Semantics)과 환경(Character Set)에 맞춰 새로운 길이를 산출해내는 '재설계' 과정입니다.
재설계 원칙은 명확합니다. Source 데이터의 실제 크기를 측정하고, Target의 Semantics와 변경될 Character Set을 반영해 필요한 공간을 계산하며, 그 결과가 새 스키마에서 온전히 보존되는지 검증하는 것. 이 3단계가 완벽히 수행될 때 비로소 재설계가 완료됩니다.
#### 실제 데이터 길이를 측정하라
많은 프로젝트가 시스템 카탈로그에서 확인한 스키마 정의서만 믿고 이관을 시작합니다. 하지만 이기종 마이그레이션에서 스키마 정의는 '과거의 약속'일 뿐, '현재의 실체'가 아닙니다. VARCHAR2(1000)으로 정의되어 있어도, 실제로는 최대 100바이트만 사용하고 있을 수 있고, 반대로 VARCHAR2(100)인데 실제로는 98바이트까지 빼곡하게 차있을 수도 있습니다.
그래서 첫 단계는 해당 컬럼에 들어 있는 데이터를 전수 조사하여 최대 바이트 길이, 최대 문자 길이, 평균 길이를 확인하는 것입니다. 테이블이 수백 개라면 값을 분석할 수 있는 자동화 스크립트를 통해 이를 분석하고, 그 결과를 문서화 합니다.
단순 길이뿐 아니라 데이터 분포를 파악하는 것도 중요합니다. 평균 길이는 짧은데 최대치가 유난히 큰 컬럼이 있다면, 실무자와 함께 "왜 그런 값이 들어왔는지"를 확인해야 합니다. 필요하다면 컬럼 길이를 재정의하거나 데이터 정제 전략을 세워야 합니다. 더 위험한 신호는 컬럼 정의 길이의 90% 이상을 사용하는 데이터가 다수 발견될 때입니다. 이때는 실제 데이터를 샘플링하여 문장이 온전히 끝나는지, 아니면 이미 Source 시스템에서부터 잘린(Truncated) 상태로 저장되었는지 확인해야 합니다.
#### Character Set 변환을 고려한 길이 재산정
데이터의 실제 크기를 측정했다면, 다음은 Target에 맞는 새로운 길이를 계산해야 합니다. 이 과정은 단순히 "한글이니까 1.5배", "안전하게 2배"를 곱하는 기계적인 산술이 아니라, Target의 Semantics(단위)와 Character Set, 그리고 DB별 제약을 모두 고려한 계산이 필요합니다.
가장 먼저 정리해야 할 건 Semantics 입니다. Oracle은 문자형 길이를 BYTE와 CHAR를 명시적으로 구분합니다. 어떤 시스템은 "100 BYTE"를 의도하고 만들었고, 어떤 시스템은 "100글자"를 의도하고 만들었습니다. 그런데 스키마에는 그 의도가 드러나지 않습니다. 그래서 계산 단계에서는 이 차이를 표면으로 끌어올려야 합니다.
이제 이관 방향에 따라 계산의 초점을 고려해 이관 전략을 수립해야 합니다. Oracle에서 MySQL/PostgreSQL 같은 오픈소스 DB로 가는 경우, 길이 계산 관점에서는 상대적으로 마음이 편할 수 있습니다. 오픈소스 계열은 보통 VARCHAR(100) 을 "100글자"로 해석하는 방향이어서, Oracle 쪽이 BYTE Semantics였다면 Target이 더 넉넉해지는 경우가 많습니다. 즉, "길이가 모자라서 못 들어간다"보다는 "생각보다 크게 잡혀서 넘어간다"가 더 흔합니다. 그래서 이 방향에서는 캐릭터셋 변환 자체가 길이 실패를 바로 만들기보다는, 운영 규칙(입력 검증, 인덱스/조인 키의 정규화, 공백 처리 등) 쪽에서 문제를 드러내는 경우가 많습니다.
하지만 MySQL은 예외입니다. MySQL은 행(row) 단위 저장 구조 때문에, 한 행에 담을 수 있는 데이터 크기에 65,535 바이트라는 강한 제약이 존재합니다. Oracle에서 관대하게 잡아둔 여러 개의 VARCHAR2(2000), VARCHAR2(4000) 같은 컬럼을 그대로 가져오는 순간, DDL 단계부터 막히거나(생성 실패), 설계 자체를 다시 해야 하는 상황이 생깁니다. 따라서 Oracle → MySQL 이관은 다른 DB보다 더 정밀한 계산이 수행되어야 합니다. 측정 결과를 기반으로 정말 필요한 만큼만, 그리고 전체 컬럼의 길이를 합산해서 정확히 산정해야 합니다.
반대로 MySQL/PostgreSQL 같은 오픈소스 DB에서 Oracle로 들어오는 경우, "문자 수"가 BYTE semantics로 해석되는 순간 컬럼 정의가 갑자기 좁아질 수 있습니다. 특히 Character Set이 바뀌는 상황이라면 그 영향은 더 커집니다. Source에서는 문제 없이 들어가던 값이 Target Oracle에서는 "바이트가 초과"되어 적재 실패를 일으키거나, 더 위험하게는 이관 도구 설정에 따라 에러 없이 들어갈 수 있습니다. Oracle VARCHAR2 컬럼에서 처리할 수 있는 범위보다 큰 문자열이 들어오는 경우 Oracle VARCHAR2는 최대 4000바이트까지만 지원하므로, 초과 시에는 CLOB 타입으로 재설계가 필요할 수 있습니다. 이 방향의 계산은 결국 한 문장으로 수렴합니다. "Source의 n글자를, Oracle에서는 몇 바이트로 수용해야 하는가."
계산 결과는 반드시 표로 정리합니다. 모든 문자형 컬럼에 대해 Source 타입, 실제 최대 길이(바이트/문자), Target 타입, 산정 근거를 문서화합니다. 이 문서가 이관 작업의 기준이 되고, 나중에 "왜 이렇게 정했는가"에 대한 답이 됩니다.
#### 적용: 계산을 실체로 전환
측정과 계산이 끝났으면 이제 규칙을 스키마로 구성할 차례입니다. 적용은 새로운 규칙을 더 고민하는 시간이 아니라, 정의한 규칙을 실체로 전환하는 시간입니다.
적용 단계의 핵심 도구는 자동화입니다. DDL을 수작업으로 작성하면, 테이블 수가 늘어날수록 실수는 확률이 아니라 필연이 됩니다. 누군가는 길이를 잘못 입력하고, 누군가는 BYTE/CHAR 단위를 빼먹고, 누군가는 같은 컬럼인데 타입을 다르게 옮깁니다. 이런 차이는 데이터 이관이 끝난 뒤에 발견되면 되돌리기 어렵고, 무엇보다 "왜 달라졌는지"를 설명하기도 힘들어집니다.
측정과 계산을 거쳐 만들어진 내용을 입력으로 받아, 테이블/컬럼 정의를 Target 문법에 맞춘 DDL로 기계적으로 생성하는 스크립트를 만들고, 그 스크립트를 수행해 전체 스키마를 일괄 생성하도록 합니다. 중요한 건 DDL 생성 자체가 아니라, 생성 과정이 재현 가능해야 한다는 점입니다. 같은 문서를 넣으면 언제든 같은 DDL이 나와야 하고, 문서가 바뀌면 변경된 부분만 정확히 반영되어야 합니다. 그래야 설계 변경이 "누군가의 손"이 아니라 "문서의 diff"로 추적됩니다.
적용은 "DDL을 실행한다"가 아니라, 스키마를 구축하는 파이프라인을 만든다에 가깝습니다. 이 파이프라인이 있으면, Target 환경이 새로 만들어지거나 스키마를 다시 구성해야 할 때도 같은 절차로 반복할 수 있습니다.
VARCHAR 타입의 길이는 복사하는 값이 아니다
VARCHAR의 길이 재산정, 이것만으로도 측정-계산-적용의 체계적 프로세스가 필요합니다. "툴이 자동으로 해주겠지"라는 생각으로 시작한 프로젝트가 오픈 2주 전에 메타데이터를 전면 재구성하게 되는 이유가 여기 있습니다. 길이는 복사하는 값이 아니라, 새 환경에 맞춰 다시 설계해야 하는 메타데이터입니다.
VARCHAR가 가변 길이의 함정이었다면, 다음 글에서는 CHAR와 NVARCHAR — 고정 길이가 만드는 공백과 인코딩 계약의 흔적 — 를 살펴봅니다.
