production DB 백업(.sql.gz)을 로컬 MySQL에 복원하는 PowerShell 스크립트가
ERROR 1062 Duplicate entry '' for key 'linkserver_linkserver.name'로 죽었다.
dump 파일도 멀쩡하고 스크립트 로직도 멀쩡한데, 왜?
범인은 콘솔 인코딩이었고, 잡는 데 한참 헤맨 이유는 "내 환경에선 재현이 안 돼서" 였다.
[Console]::OutputEncoding 을 상속한다. 한글 Windows 콘솔은 기본 CP949다.utf8mb4로 읽으니 한글이 통째로 깨져 빈 문자열 '' 이 됐고, UNIQUE 제약에서 '' 중복 → 1062.기존 DB 초기화: DROP & CREATE DATABASE `smartsilvercenter` ... 초기화 완료
ERROR 1062 (23000) at line 1753: Duplicate entry '' for key 'linkserver_linkserver.name'
DB 초기화는 성공했는데, 데이터를 부어 넣는 도중 죽었다. 키워드 두 개:
Duplicate entry '' — 중복된 값이 NULL이 아니라 빈 문자열이다.key 'linkserver_linkserver.name' — name 컬럼에 UNIQUE 제약이 걸려 있다.💡 MySQL의 UNIQUE 인덱스는 NULL은 여러 개 허용하지만 빈 문자열
''은 하나만 허용한다.
즉 여러 행의name이 전부''로 들어가려 해서 충돌한 것.
.sql.gz 를 풀어 해당 테이블의 INSERT를 직접 확인:
INSERT INTO `linkserver_linkserver` VALUES
(1,'인천시',...),(2,'강화군',...),(4,'미추홀',...);
이름이 셋 다 다르고, 빈 값도 없다. 그러니 이 파일을 그대로 복원하면 1062가 날 이유가 없다.
→ 빈 문자열은 "복원 과정 어딘가"에서 생겨났다는 뜻.
복원 스크립트는 dump를 한 줄씩 읽어 mysql의 stdin 파이프로 흘려보낸다. 경계를 그려보면:
dump(.sql.gz, UTF-8 bytes)
└─[StreamReader: UTF-8 decode]→ 메모리의 올바른 "인천시"
└─[StreamWriter: ??? encode]→ mysql stdin (bytes)
└─[mysql: utf8mb4 decode]→ DB
StreamWriter 가 무슨 인코딩으로 내보내는가? 이게 핵심 경계였다.
여기서 한참 돌았다. 가설은 처음부터 맞았다("stdin 인코딩이 깨진다"). 그런데:
문제는 검증을 내 환경에서만 했다는 것. 알고 보니:
| 환경 | $proc.StandardInput 인코딩 |
|---|---|
| 내 개발 환경(콘솔이 UTF-8) | CP65001 (UTF-8) → 안 깨짐 |
| 사용자 콘솔(한글 Windows) | CP949 → 깨짐 |
결정타는 사용자 환경에서 딱 한 줄 측정한 값:
$p=[Diagnostics.Process]::Start((New-Object Diagnostics.ProcessStartInfo -Property @{
FileName='cmd';RedirectStandardInput=$true;UseShellExecute=$false}))
"stdin=$($p.StandardInput.Encoding.WebName) CP$($p.StandardInput.Encoding.CodePage)"
# → stdin=ks_c_5601-1987 CP949
CP949. 범인 확정.
콘솔을 CP949로 강제(chcp 949)하니 그제서야 원래 코드가 1062로 죽었다. 비로소 진짜 재현.
수정: 콘솔 설정에 의존하지 말고 stdin을 UTF-8로 직접 고정한다.
$proc = [System.Diagnostics.Process]::Start($psi)
# .NET Framework/PS5.1 엔 ProcessStartInfo.StandardInputEncoding 속성이 없으므로
# BaseStream 위에 UTF-8(BOM 없음) StreamWriter 를 직접 씌운다.
$stdin = New-Object System.IO.StreamWriter(
$proc.StandardInput.BaseStream,
(New-Object System.Text.UTF8Encoding($false)))
CP949 콘솔에서 검증:
| 버전 | writer CP | 결과 |
|---|---|---|
| 원래 코드 | 949 | exit 1, ERROR 1062 (재현됨) |
| 수정 코드 | 65001 | exit 0, 한글 정상 저장 ✅ |
"텍스트가 깨졌다" = 어떤 경계의 encode 인코딩 ≠ 다음 경계의 decode 인코딩.
깨진 결과를 보지 말고 경계의 목록을 그려라: 파일 읽기 / 파이프 / 네트워크 / DB 연결 / 터미널 출력.
재현 불가는 결론이 아니라 단서다. "A 성공 + B 실패" = 두 환경의 차이에 범인이 있다.
→ 추측한 원인을 내 환경에서 검증하지 말고, 실패하는 환경의 조건을 재현하라.
''(빈 문자열) ← 값이 통째로 사라짐 = 인코딩 절단의 지문.at line 1753 ← dump 파일 줄이 아니라 mysql 입력 스트림 줄. 숫자는 항상 "어느 좌표계?"를 물어라.같은 곳을 3번 두드리면 멈추고 "내가 검증하는 방법이 틀린 건 아닐까"를 의심하라.
| 함정 | 증상 | 해결 |
|---|---|---|
경로의 대괄호 [ ] | Test-Path/Resolve-Path/cd 가 와일드카드로 오해 → 파일 못 찾음 | -LiteralPath |
| 외부 프로세스 stdin 인코딩 | 콘솔 CP949 상속 → 한글 깨짐 | BaseStream 위 UTF-8 StreamWriter |
.ps1 파일 BOM 없음 | 한글 .ps1을 ANSI로 오독 → 줄 깨짐 | UTF-8 with BOM 저장 |
2>&1 로 native exe stderr | exit 0인데 ErrorRecord로 감싸져 throw | 리다이렉트 말고 $LASTEXITCODE 로 판단 |
| PS 5.1 = .NET Framework | .NET Core 전용 API 없음 | API 도입 버전 확인 후 우회 |
근본 해법: 시스템/터미널을 UTF-8로 통일하거나 PowerShell 7+로 이주.
방어적으로는 스크립트 상단에
$OutputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false).
같은 글자가 여러 바이트 표현을 가질 수 있다. 한글 한:
한 = 코드포인트 1개 (U+D55C)ㅎ+ㅏ+ㄴ = 자모 3개 (U+1112 U+1161 U+11AB)눈엔 똑같지만 "한"(NFC) == "한"(NFD) 은 false, 길이도 1 vs 3.
⚠️ macOS 함정: macOS 파일시스템은 한글 파일명을 NFD로 저장한다.
"Mac에서 만든 한글 파일명을 git에 올렸더니 Windows에서 깨지거나 중복으로 보인다"가 여기서 온다.
| 형식 | 의미 | 용도 |
|---|---|---|
| NFC | 합쳐서 하나 | 웹/DB 저장 표준 ✅ |
| NFD | 분해 | macOS 내부 |
| NFKC/NFKD | 호환 분해(①→1, ㈜→(주)) | 검색 인덱싱 |
원칙: 외부 입력(사용자 입력/업로드 파일명/API)은 저장 전 NFC로 정규화.
import unicodedata
clean = unicodedata.normalize("NFC", user_input)
오늘의
''중복은 바로 collation의 영역 — 두 값을 "같다"고 판정해 UNIQUE를 위반시켰다.
MySQL 필수 상식:
utf8 은 가짜다. 글자당 최대 3바이트 → 이모지·일부 한자 저장 불가. 항상 utf8mb4.
collation 접미사가 동작을 결정한다:
| collation | 특징 |
|---|---|
utf8mb4_general_ci | _ci=대소문자 무시, 빠르지만 부정확(구식) |
utf8mb4_unicode_ci | 유니코드 정렬, 더 정확 |
utf8mb4_0900_ai_ci | MySQL 8.0+ 기본·권장 ✅ (_ai=악센트 무시) |
..._bin | 바이트 그대로, 전부 구분 |
악명 높은 사고 — collation 불일치 JOIN:
SELECT * FROM A JOIN B ON A.name = B.name;
-- ERROR 1267: Illegal mix of collations
서버/DB/테이블/컬럼 + 클라이언트 연결까지 전부 동일 charset·collation으로 통일해야 한다.
(오늘 버그도 DB는 utf8mb4였지만 연결로 보낸 바이트가 어긋나 깨졌다 — "DB만 맞아선 부족하다"는 산 증거.)
정규화(NFC)는 "같은 글자를 같은 바이트로" 만드는 입구 작업이고,
collation은 "그 바이트들을 어떻게 같다고 볼지" 정하는 DB 규칙이다.
둘 다 통일하지 않으면, 분명 같아 보이는 값이 검색·UNIQUE·JOIN에서 제멋대로 갈라진다.
그리고 오늘 가장 크게 남은 한 줄:
데이터가 경계를 넘을 때마다 인코딩 변환이 일어나고,
"내 환경에선 됨"은 환경 차이라는 단서이며, 버그는 추측이 아니라 재현으로 잡는다.