Character Set: 마이그레이션의 첫 번째 관문
Character Set 전환에서 발생할 수 있는 깨짐, 잘림, 변형 문제와 실무 검증 기준을 정리합니다.
마이그레이션 프로젝트에서 Character Set 변환을 소홀히 하면, 그 대가는 혹독합니다.
이관이 끝나고 시스템이 안정화되었다고 판단한 시점, 이런 민원이 들어옵니다. "특정 고객 이름이 검색되지 않습니다." 확인해보면 해당 고객의 이름에 포함된 한자가 물음표로 바뀌어 있습니다. 데이터는 존재하지만 원래의 문자가 아닙니다.
기술팀은 이관 도구의 문제를 의심합니다. 그런데 이관 로그에는 에러가 없습니다. 건수도 맞습니다. 이관 직후 샘플링 검증도 통과했습니다. 문제는 전체 데이터 중 극히 일부, 멀티바이트 문자가 포함된 레코드에서만 발생하고 있었고, 샘플링에 걸리지 않았을 뿐입니다.
이것이 Character Set 문제의 전형적인 발견 패턴입니다. 이관 시점이 아니라 운영 중에, 에러가 아니라 민원으로 발견됩니다. 그리고 발견 시점에는 이미 Source 시스템이 폐기되었거나, 원본과의 비교가 불가능한 상태인 경우가 대부분입니다.
데이터 타입을 잘못 선택해도 수정할 수 있습니다. 인덱스를 빼먹어도 나중에 추가할 수 있습니다. 하지만 Character Set을 잘못 선택하면, 데이터 자체를 믿을 수 없게 됩니다. 더 심각한 것은, 이 문제가 오픈 직후에 바로 드러나지 않는 경우가 더 많다는 점입니다.
그런데 이 문제의 원인을 추적하려면, Character Set이 실제로 무엇을 결정하는지부터 알아야 합니다. 데이터 타입이나 인덱스와 달리, Character Set은 이관 과정에서 검토 대상으로 인식되지 않는 경우가 많습니다. 그래서 문제가 발생해도 원인을 Character Set으로 연결하지 못하는 경우가 많습니다.
Character Set은 마이그레이션의 첫 번째 관문입니다. 여기서 어긋나면, 그 이후 모든 작업이 흔들립니다.
Character Set은 무엇을 결정하는가
Character Set은 흔히 "한글이 되느냐 안 되느냐" 정도로만 이해됩니다. 하지만 실제로 Character Set이 결정하는 것은 그보다 훨씬 근본적인 문제입니다. 그것은 바로 문자를 어떤 비트 패턴으로 저장하고, 그 비트를 어떤 문자로 해석할 것인가에 대한 약속입니다.
데이터베이스는 문자를 "글자"로 저장하지 않습니다. 내부적으로는 모두 숫자, 즉 비트의 조합으로 저장합니다. Character Set은 이 비트 조합이 어떤 문자를 의미하는지를 정의하는 사전과도 같습니다. 같은 비트라도 어떤 Character Set으로 해석하느냐에 따라 전혀 다른 문자로 보일 수 있습니다.
이 때문에 Character Set이 달라지는 순간, 데이터의 의미 자체가 달라질 수 있습니다. 기존 시스템에서는 정상적인 문자였던 데이터가 새로운 시스템에서는 존재하지 않는 문자로 취급되거나, 전혀 다른 문자로 해석될 수 있습니다. 이 경우 데이터는 손상되지만, 시스템은 이를 오류로 인식하지 못하는 경우도 많습니다.
이기종 마이그레이션에서 이것이 문제가 되는 이유는, 데이터베이스마다 Character Set의 구현 방식이 다르기 때문입니다. 실무에서 가장 자주 마주치는 Character Set은 크게 세 부류로 나뉩니다.
| 부류 | 주요 명칭 | 특징 | 마이그레이션 시 주의점 |
|---|---|---|---|
| 레거시 계열 | EUC-KR, KO16MSWIN949 | 한글 위주의 좁은 표현 범위 | 특수 한자, 고어(古語) 표현 불가 |
| 표준 유니코드 | UTF-8, AL32UTF8 | 전 세계 언어 수용 (글로벌 표준) | DB 제품별 바이트(Byte) 처리 방식 확인 필수 |
| 특수 변종 | MySQL utf8, utf8mb4 | 이름은 같으나 이모지 지원 여부 다름 | utf8mb4를 써야 이모지 저장 가능 |
레거시 계열인 EUC-KR, KO16MSWIN949는 일반적인 한글 2,350자와 자주 쓰이는 한자는 문제없이 표현할 수 있지만, Unicode 전체를 담지 못합니다. 특정 한자, 확장 문자, 이모지, 특수 기호는 아예 저장할 수 없거나 물음표(?)로 깨져버립니다. 국내 시스템에서 수십 년간 사용되어 왔기 때문에, 이기종 마이그레이션의 Source로 가장 자주 만나는 Character Set이기도 합니다.
유니코드 계열인 UTF-8, AL32UTF8, UTF8 은 전 세계 거의 모든 언어와 문자를 하나의 체계로 담을 수 있는 글로벌 표준이기 때문에, 이기종 마이그레이션에서는 사실상 표준처럼 사용됩니다. 하지만 "Unicode라서 안전하다"는 생각은 위험합니다. Oracle의 AL32UTF8과 PostgreSQL의 UTF8은 이름만 보면 같은 UTF-8이지만, Oracle 구버전의 UTF8(AL24UTF8 기반)은 4바이트 문자를 저장할 수 없습니다. 이름이 같아도 동작이 다릅니다.
가장 주의해야 할 부류는 MySQL의 utf8과 utf8mb4입니다. utf8은 최대 3바이트까지만 지원하고, utf8mb4는 4바이트까지 지원합니다. 이 차이 때문에 이모지(😊, 🎉)나 일부 확장 한자가 포함된 데이터를 utf8에 저장하려고 하면 오류가 발생하거나 데이터가 잘립니다. 이름만으로는 이 차이를 짐작할 수 없기 때문에 주의가 필요합니다.
이기종 마이그레이션에서 Character Set 문제가 더 까다로운 이유
데이터베이스마다 기본 Character Set이 다르고, 같은 이름의 Character Set이라도 내부 구현 방식이 다르기 때문에 이기종 마이그레이션에서는 상황이 훨씬 복잡해집니다.
캐릭터셋(Character Set) 변경 시 문자 데이터에서 발생하는 문제는 크게 '깨짐(Corruption)'과 '잘림(Truncation)' 그리고 '변형(Silent Corruption)' 세 가지로 요약됩니다.
| 유형 | 현상 | 발견 시점 | 치명도 |
|---|---|---|---|
| 깨짐 (Corruption) | 기호, 외계어 출력 (모지바케) | 즉시 혹은 조회 시 | 높음 (복구 어려움) |
| 잘림 (Truncation) | 데이터 끝부분 유실 | 이관/입력 시 (Error 발생) | 보통 (원인 파악 용이) |
| 변형 (Silent Corruption) | 엉뚱한 문자로 대체 저장 | 운영 중 뒤늦게 발견 | 매우 높음 (데이터 불신) |
깨짐(Corruption) 현상은 기존 데이터의 비트 패턴을 새로운 Character Set이 잘못 해석하면서 발생합니다. 흔히 '모지바케(Mojibake)'라고 불리는 현상으로, 의미 없는 기호나 외계어처럼 보이는 문자가 화면에 나타납니다. 이 경우 데이터는 이미 손상된 상태이며, 원본을 다시 확인하지 않는 한 복구가 어렵습니다
잘림(Truncation) 현상은 Character Set 변경으로 인해 한 문자가 차지하는 바이트 수가 늘어나면서, 기존 컬럼 길이를 초과하는 경우 발생합니다. EUC-KR에서 한글이 2바이트였는데 UTF-8에서 3바이트가 되면, 같은 컬럼 길이에 담을 수 있는 글자 수가 줄어듭니다. 이 문제는 데이터 이관 시 에러가 바로 드러나기 때문에 원인 인지가 쉬운 편입니다
변형(Silent Corruption) 현상은 데이터가 에러 없이 저장되고 조회되지만 원래 의도한 문자와는 다르게 암묵적으로 바뀌어 있는 경우를 말합니다. 운영 중 오랜 시간이 지난 뒤에야 발견되는 경우가 많고, 그때는 이미 원본 데이터와 비교하기 어려운 상황이 되어버립니다. Character Set 문제가 치명적인 이유는 바로 이 변형 때문입니다. 시스템은 정상처럼 보이지만, 데이터의 신뢰성은 서서히 무너집니다.
Collation이 만드는 또 다른 함정
Character Set이 데이터의 '형태'를 정의한다면 Collation은 데이터의 '질서'를 결정합니다. 즉, 문자열을 어떻게 비교하고 정렬할지 결정합니다.
같은 Character Set을 사용해도 Collation이 다르면 검색 결과가 특히 정렬이 달라질 수 있습니다.
- 정렬의 변화:
ORDER BY시 한글, 영문, 특수문자의 우선순위가 바뀜 - 검색의 혼선: 대소문자 구분 여부(Case-Sensitive)에 따라 검색 결과가 달라짐
- 가공의 오류:
GROUP BY나LIKE검색결과가 기존 시스템과 불일치
Oracle에서는 이러한 동작을 NLS_SORT, NLS_COMP와 같은 파라미터로 제어합니다. Open Source 데이터베이스에서는 이를 Collation 설정으로 관리합니다. 이 차이를 이해하지 못하면, 기존 시스템과 동일한 결과를 재현하기 어렵습니다.
Character Set 변환, 실무에서는 어떻게 접근해야 하는가
실무에서 중요한 것은 "UTF-8으로 간다"는 결론이 아니라, 그 과정에서 무엇을 확인하고 책임질 것인가입니다. Character Set 변환은 단순히 대상 데이터베이스의 기본 설정을 따르는 것으로 끝나지 않습니다. 데이터베이스 하나만 바꾼다고 해결되지 않기 때문입니다.
Character Set, 문자 집합. 기술적으로는 단순해 보이는 개념이지만 실무에서는 시스템 전체를 관통하는 문제입니다.
데이터베이스뿐 아니라 OS, 웹 서버, 프레임워크, 드라이버까지 모든 레이어의 설정이 일치해야 하는데, 이 부분은 시스템을 운영하고 있는 담당자가 가장 잘 압니다. 그래서 마이그레이션 프로젝트를 시작하면 담당자와 함께 전체 시스템을 그려보고, 각 레이어의 설정을 확인하는 작업을 빠뜨리지 않아야 합니다.
그래서 실제 프로젝트에서는 Character Set 변경 시 다음의 지침을 따르고 있습니다
첫째, 신규 시스템은
UTF-8로 간다. 현재,UTF-8이 아닌 다른 선택지를 고려할 이유가 없다. DATABASE, OS, APPLICATION 모두UTF-8을 기준으로 구성한다둘째, 레거시 시스템을 바꾼다면, 실제 데이터를 철저하게 분석한다. 테스트 데이터가 아닌 레거시 시스템 운영 DB에서 문자형 데이터를 최대한 많이 스캔한다. 특수문자, 확장 한자(일본 한자), 이모지 등등이 포함되어 있는 데이터가 신규 시스템에서 표현 가능한지 검증한다
셋째, 데이터가 흘러가는 전체 경로를 문서화 한다. 데이터가 사용자로부터 DB를 거쳐 다시 사용자로 돌아가는 모든 경로를 도식화 하고 각 단계마다의 Character Set을 함께 기술하여 한눈에 볼 수 있도록 한다
넷째, End-to-End 테스트를 진행한다. 애플리케이션 화면에서 직접 입력하고 다시 화면에 출력되는 것을 확인한다. 특히 SQL 툴에서 확인하지 않고 화면 테스트를 우선시 한다
이 과정은 번거롭고 시간이 많이 드는 작업입니다. 하지만 이 단계를 건너뛰면, 마이그레이션 이후 훨씬 더 큰 비용을 치르게 됩니다.
Character Set 변환, 어떻게 검증할 것인가
"검증은 다 했습니다." 프로젝트 막바지, 가장 자주 듣는 말입니다. 그리고 가장 믿기 어려운 말이기도 합니다. 특히 Character Set이 변경되는 마이그레이션에서 "검증 완료"라는 말은, 때로는 위험한 착각일 수 있습니다. 데이터 10억 건 중에서 1,000,000건을 샘플링해서 확인했다면, 그것은 0.1%를 본 것입니다. 나머지 99.9%는 어떻게 보장할 것인가? "샘플에서 문제없으니 나머지도 괜찮겠지"라는 기대는, 운이 좋을 때만 맞습니다. 그리고 그 운이 오픈 후에도 계속되리라는 보장은 없습니다.
검증의 핵심은 두 가지입니다. "데이터가 잘리지 않았는가(Length)"와 "데이터가 변질되지 않았는가(Validity)". 이 둘을 확인하지 않으면, 마이그레이션이 끝난 후에도 불안은 계속됩니다.
Oracle 환경: DMU가 있다는 행운
Oracle에서 Oracle로, 또는 Oracle을 거쳐가는 마이그레이션이라면 운이 좋다고 해도 과언이 아닙니다. Oracle은 Database Migration Utility(DMU)라는 강력한 도구를 제공합니다.
DMU의 핵심은 Character Set Scanner입니다. 이 도구는 데이터베이스 전체를 스캔하면서, Character Set 변환 시 발생할 수 있는 문제를 사전에 찾아냅니다. 어떤 테이블의 어떤 컬럼에, 변환 불가능한 문자가 몇 건 있는지 정확히 보고합니다. 그리고 명확한 결과 레포트를 제공합니다.
Lossy Conversion: 변환 시 데이터 손실이 발생하는 경우 Truncation: 컬럼 길이 부족으로 잘릴 위험이 있는 경우 Exceptional: 변환 불가능한 문자가 포함된 경우
DMU의 존재 여부는 Character Set 변환의 가시성(Visibility) 차이를 만듭니다. Source가 Oracle이든, Target이 Oracle이든, 어느 한쪽이라도 Oracle이라면 DMU를 활용하는 방법이 가장 명확합니다. 데이터 이관 전에 실행해서 문제를 찾고, 이관 후에 다시 실행해서 결과를 비교하면 캐릭터셋 변환의 문제를 상당부분 예방할 수 있습니다.
문제는 비Oracle 환경
문제는 Oracle이 아닌 환경입니다. PostgreSQL, MySQL, MariaDB... 이런 오픈소스 데이터베이스에는 DMU 같은 도구가 없습니다. 그래서 많은 프로젝트가 "샘플링"이라는 방법을 선택합니다. 각 테이블에서 일부 데이터를 뽑아서, 육안으로 확인하거나 간단한 스크립트로 체크하는 방식입니다.
이 방법의 문제는, 문제가 없는 데이터만 계속 확인하게 될 가능성이 높다는 것입니다. 검증 서두에 언급했듯이 샘플링은 장애의 비중을 줄일 뿐, 장애 자체를 막지 못합니다.
또한 많은 사람들이 간과하는 것 중 하나가 Source 데이터의 무결성입니다. "UTF-8로 바꿨더니 데이터가 깨졌어요. 원래 EUC-KR에서는 정상이었는데..." 정말 그럴까요? 같은 상황을 반복적으로 추적해본 결과는 다릅니다. "UTF-8에서 깨진 데이터는, 애초에 EUC-KR에서도 제대로 저장되지 않았을 가능성이 높다".
예를 들어 Source 데이터가 ????, ���� 또는 □□□□ 와 같다면, Target 데이터도 동일하게 ????, ���� 또는 □□□□로 저장됩니다. 문제는, 이런 데이터가 존재하는지 아무도 모른 채 지나갔다는 점입니다. 시스템은 에러를 안 냈습니다. 데이터는 저장됐습니다. SQL 툴로 조회해도 뭔가 나옵니다.
이 상태로 몇 년을 운영하고 나서 UTF-8로 마이그레이션을 했을 때 누군가 이렇게 말합니다: "이거 UTF-8로 바꿔서 깨진 거 아니에요?" 아닙니다. 이미 깨져 있었습니다. 단지 이제서야 발견됐을 뿐입니다. 그렇다면 검증의 기준점을 추가해야 합니다. "Source와 Target이 같은가?"가 아니라 "Source가 애초에 정상인가?"도 같이 봐야 합니다.
그렇다면 어떻게 해야할까요? 답은 간단합니다. 전체를 보면 됩니다. "전체 문자가 몇 개인데 어떻게 다 보냐"는 질문이 나올 것입니다. 하지만 사람이 눈으로 볼 필요는 없습니다. 명확한 기준선이 존재한다면 기준선 밖에 있는 데이터를 기계가 찾게 하면 됩니다. 그 기준이 바로 ASCII 코드입니다. ASCII는 0x00부터 0x7F까지, 128개의 문자를 정의합니다. 영문 대소문자, 숫자, 기본 특수문자가 여기에 포함됩니다.
이 범위를 벗어나는 문자는 모두 멀티바이트 문자입니다. Character Set 변환에서 문제가 생기는 곳은 바로 이 멀티바이트 영역입니다. ASCII 범위의 문자는 거의 모든 Character Set에서 동일하게 표현됩니다. 그래서 Character Set 검증 초기에 전수 스캔 프로그램을 작성합니다. PostgreSQL이라면 정규표현식으로 ASCII 외 문자를 찾아내고, MySQL이라면 LENGTH와 CHAR_LENGTH의 차이로 멀티바이트를 검출합니다. 이 프로그램을 배치로 실행해 멀티바이트 문자를 모두 추출하고, 결과를 CSV로 저장합니다. 그리고 발견된 특수문자들을 Target 시스템에 직접 입력해 저장 가능 여부를 확인하면 검증이 완료됩니다.
Character Set, 메타데이터 변환의 첫 번째 관문이자 가장 기초적인 관문
서버의 부하와 많은 시간은 리스크입니다. 하지만 한 번의 부하로 오픈 후 닥쳐올 많은 장애를 예방할 수 있다면 결코 손해 보는 일은 아닐 것입니다. Character Set 변경이 필요한 마이그레이션 프로젝트에서 "검증 완료"라는 말을 사용하기 전에 세 가지 질문이 필요합니다.
✓ 샘플링 조사였습니까, 전수 조사였습니까? ✓ Source 데이터가 정상인 것은 어떻게 확인했습니까? ✓ 특수 문자를 Target DB에 직접 저장하고 조회해 보았습니까?
이들 질문에 대한 답변이 명확하지 않다면, 아직 검증이 끝나지 않은 것입니다.
Character Set은 눈에 보이지 않습니다. 평소에는 괜찮지만 특정 상황, 특정 데이터에서만 문제가 생깁니다. 그래서 더 위험하고, 그래서 더 철저해야 합니다. Character Set이 흔들리면 이 위에 쌓는 모든 것이 흔들립니다.
같은 컬럼 정의가 캐릭터셋에 따라 다른 크기로 저장된다는 사실은 다음 글의 출발점이기도 합니다. 다음 글에서는 VARCHAR — 길이라는 숫자가 새 환경에서 어떻게 다시 정의되어야 하는지 — 를 살펴봅니다.
