UTF-8을 기본으로 쓰면 운영체제마다 다르던 텍스트 처리의 출발점이 단순해진다. 하지만 이미 CP949로 저장된 파일의 바이트는 그대로다. Python을 바꾸는 작업과 파일 형식을 바꾸는 작업을 구분하고, 입력의 인코딩과 결과를 받을 프로그램의 약속을 각각 정해야 한다.
업그레이드 뒤 실패한 주문 파일, 무엇이 바뀐 걸까
거래처가 매일 보내는 orders.csv를 읽는 작은 프로그램이 있다. 코드에는 open("orders.csv")만 적혀 있고, 파일은 CP949로 만들어진다고 가정하자. 이전 실행 환경의 기본 인코딩도 CP949였다면 한글 품목명이 읽혔을 수 있다. 그런데 Python을 바꾼 뒤 같은 파일에서 UnicodeDecodeError가 발생한다. 파일이 갑자기 손상됐다고 판단하기 전에 생략했던 인코딩의 기본값을 살펴볼 필요가 있다.
2026년 10월 9일 공개된 Python 3.15.0에는 UTF-8을 기본으로 사용하는 변경이 포함됐다. 정확히는 Python UTF-8 모드를 기본 활성화한 것이다. 앞의 주문 파일 상황은 이 변화가 영향을 줄 수 있는 가상 업무 사례다. 모든 Windows 설치나 모든 CSV가 CP949였다는 뜻은 아니다. 3.15.0 릴리스, 3.15 변경 사항
이 글은 10월 26일 확인한 3.15 계열 문서와 PEP 686을 기준으로 한다. 재현은 별도로 확보한 CPython 3.15.0과 3.14.5, macOS arm64에서 자체 제작한 작은 파일로 수행했다. 3.15.0을 최신 패치 버전이라고 권하는 글은 아니다. Windows의 실제 코드 페이지, Excel의 가져오기 화면, 거래처 프로그램과 대용량 변환은 시험하지 않았다.
바뀌는 것은 읽는 규칙이고, 파일의 바이트는 그대로다
문자열은 사람이 읽는 글자이고, 파일에는 바이트가 저장된다. 인코딩은 글자와 바이트 사이의 변환 규칙이다. 같은 사과도 규칙에 따라 다음처럼 저장된다. 아래 값은 예제에서 직접 생성해 확인했다.
| 같은 문자열 | 저장한 인코딩 | 파일 속 바이트의 16진수 표현 |
|---|---|---|
| 사과 | CP949 | bb e7 b0 fa |
| 사과 | UTF-8 | ec 82 ac ea b3 bc |
프로그램의 기본값이 UTF-8로 바뀌어도 첫 번째 행의 바이트가 두 번째 행으로 바뀌지는 않는다. 변환하려면 CP949로 읽어 문자열을 얻고, 그 문자열을 UTF-8로 다시 써야 한다. 파일 이름을 바꾸거나 확장자를 .csv로 붙이는 일은 이 변환을 수행하지 않는다.
자체 제작한 설명도다. 바이트 값은 합성 예제의 실행값이며, 실제 거래처 파일이나 제품 화면을 재현한 그림은 아니다.
3.15의 open()과 TextIOWrapper는 UTF-8을 기본으로 사용한다. UTF-8 모드를 끄면 기본값은 로캘에 따른다. 반면 encoding="cp949"처럼 명시한 파일 경로는 그 인코딩을 사용하며, 바이너리 모드인 "rb"는 텍스트 디코딩을 하지 않는다. 기존 코드가 이미 encoding="utf-8"을 명시했다면 이 기본값 변경 때문에 같은 읽기 규칙이 새로 바뀌는 것은 아니다. 텍스트 인코딩과 I/O 문서
변화는 파일에만 한정되지 않는다. UTF-8 모드는 표준 입출력 등에도 영향을 주지만, 표준 스트림에는 별도 설정이 적용될 수 있다. 따라서 터미널 출력의 인코딩을 보고 모든 파일과 외부 명령의 출력도 같다고 가정하지 않는다. 현재 프로세스의 모드는 sys.flags.utf8_mode로 확인할 수 있다. UTF-8 모드의 적용 범위
입력의 약속과 출력의 약속을 따로 정한다
가상 주문 처리의 목표는 “일단 읽기”가 아니라, 001이라는 주문번호와 사과라는 품목이 다음 프로그램까지 그대로 전달되는 것이다. 아래 표는 파일별로 선택할 기준이다. CSV의 구분자나 열 구조가 같아도 인코딩은 다를 수 있다.
| 확인한 파일 계약 | 읽을 때의 선택 | 결과를 쓸 때의 선택 |
|---|---|---|
| 생산자가 UTF-8이라고 명시 | encoding="utf-8" |
다음 소비자도 UTF-8이면 명시적으로 UTF-8 |
| 생산자가 CP949라고 명시 | encoding="cp949" |
새 소비자가 UTF-8을 요구하면 변환해서 별도 파일 생성 |
| UTF-8이며 파일 맨 앞에 BOM이 올 수 있음 | encoding="utf-8-sig" |
소비자가 BOM을 요구하는지 확인해 UTF-8 또는 UTF-8-SIG 선택 |
| 현재 운영체제의 로캘 인코딩을 쓰기로 정함 | encoding="locale" |
해당 환경과의 결합을 유지하는 선택임을 기록 |
| 생산자가 무엇으로 썼는지 모름 | 파일 생성 옵션·명세·대표 원본 확인 | 확인 전 일괄 변환 보류 |
encoding="locale"은 CP949의 다른 이름이 아니다. 실행 환경의 로캘 인코딩을 따르겠다는 뜻이므로, 한 컴퓨터에서 CP949였다는 사실이 다른 컴퓨터까지 이어지지는 않는다. locale.getencoding()은 UTF-8 모드와 별개로 로캘의 인코딩을 확인하는 API다. PEP 686의 호환성 설명
이 표에서 가장 먼저 확보할 정보는 파일 생산자의 설정이다. 한글 Windows에서 만들어졌다는 추측이나 파일 확장자만으로 인코딩을 확정하지 않는다. 특히 ASCII 문자만 있는 시험 파일은 UTF-8과 CP949 양쪽에서 같게 읽힐 수 있다. 001,apple만 통과한 시험을 001,사과의 증거로 사용할 수 없는 이유다.
오류를 없앴는데 데이터가 사라지는 두 가지 경우
errors="ignore"로 조용히 지나간다
합성 예제에서 CP949로 저장한 사과를 UTF-8로 읽으면 엄격한 기본 처리에서는 오류가 난다. 같은 바이트에 errors="ignore"를 적용하면 빈 문자열이 됐다. 프로그램이 끝났다는 성공 신호와 품목명 보존이 정반대가 된 것이다. ignore는 디코딩 오류 부분을 버리는 정책이지, 맞는 인코딩을 찾아 주는 정책이 아니다. 오류 처리 방식
이 경우 다시 살릴 입력은 삭제된 글자가 들어 있던 원본 바이트다. 잘못 읽은 문자열을 UTF-8로 저장한 파일만 남기면 원래 품목을 복원할 정보가 사라질 수 있다. 원본을 보존하고 올바른 인코딩을 확인한 다음 다시 읽어야 한다.
예외 없이 다른 글자로 읽힌다
반대 방향의 함정도 있다. UTF-8로 저장한 café를 CP1252로 읽는 예제에서는 오류 없이 café가 나왔다. 이것은 CP949의 결과가 아니라 다른 인코딩으로도 디코딩이 성공할 수 있다는 별도 반례다. “오류가 없으면 인코딩을 맞혔다”는 자동 판정은 성립하지 않는다.
여러 인코딩을 차례로 시도하고 처음 성공한 것을 채택하는 방식은 파일 계약이 없는 상태를 숨길 수 있다. 자동 추정기를 쓰더라도 추정 결과와 확정 계약을 구분하고, 한글 품목·식별자·기대하는 열 이름을 원본의 알려진 값과 대조해야 한다. 이 글의 예제는 인코딩 탐지기가 아니다.
UTF-8인데 첫 열 이름만 안 맞는다면 BOM을 본다
BOM은 파일 앞에 붙는 표식이다. Python의 utf-8-sig 코덱은 쓸 때 UTF-8 BOM을 붙이고, 읽을 때 파일 처음의 해당 표식을 건너뛴다. 일반 utf-8로 같은 파일을 읽으면 첫 문자열에 \ufeff가 남을 수 있다. UTF-8-SIG 설명
예제의 첫 열은 주문번호다. BOM이 있는 파일을 일반 UTF-8로 읽어 CSV로 나누면 실제 첫 열은 \ufeff주문번호가 된다. 한글이 잘 보이고 디코딩 오류도 없는데, 코드가 기대하는 열 이름과는 다르다. utf-8-sig로 읽은 결과는 기대한 열 이름과 일치했다.
모든 파일에서 눈에 안 보이는 문자를 무작정 제거할 필요는 없다. UTF-8 입력 계약에 BOM이 포함될 수 있음을 확인한 뒤 그 계약에 맞는 코덱을 사용한다. 출력에 BOM을 붙일지도 다음 프로그램의 요구에 따라 정한다. 이 실험만으로 Excel의 모든 버전과 가져오기 경로에 특정 옵션을 권하지 않는다.
작은 주문 파일을 변환하고 실패를 남기는 코드
다음은 다운로드 예제와 같은 변환 함수다. CP949라고 확인한 입력을 읽고, 예제에서 정한 두 열과 행의 열 수를 확인한 뒤 새로운 UTF-8 파일을 만든다. 전체 내용을 메모리에 읽는 작은 파일용 교육 예제이며, 주문의 실제 유효성이나 중복 여부를 검증하는 시스템은 아니다.
import csv
def convert_orders(source, destination, source_encoding):
with open(source, encoding=source_encoding,
errors="strict", newline="") as f:
rows = list(csv.reader(f))
if not rows or rows[0] != ["주문번호", "품목"]:
raise ValueError("unexpected header")
if any(len(row) != 2 for row in rows[1:]):
raise ValueError("unexpected column count")
with open(destination, "x", encoding="utf-8", newline="") as f:
csv.writer(f, lineterminator="\n").writerows(rows)
return rows
# 파일의 인코딩 계약을 확인한 뒤 사용하는 호출 예시
# convert_orders("orders-cp949.csv", "orders-utf8.csv", "cp949")
newline=""은 CSV 모듈이 줄바꿈을 처리하도록 파일을 여는 옵션이다. 인코딩을 맞추는 옵션과 별개다. csv.reader의 기본 결과는 문자열이므로, 이 예제에서는 주문번호 001을 숫자 1로 바꾸지 않는다. CSV 파일 열기와 반환형
출력의 "x" 모드는 같은 이름의 기존 파일이 있으면 실패하게 한다. 잘못된 디코딩과 열 검사는 출력 파일을 만들기 전에 끝낸다. 다만 출력 도중 디스크 오류나 프로세스 중단이 생기면 부분 파일이 남을 수 있다. 이 코드는 충돌 없는 파일 교체나 장애 시 원자적 저장까지 구현한 도구가 아니다. 운영에서 그런 보장이 필요하면 임시 파일·검증·교체 절차를 별도로 설계한다.
재현 예제 ZIP을 새 폴더에 풀고 다음 명령으로 실행할 수 있다. Python 3.11 이상에서 외부 패키지 없이 동작하도록 작성했으며, 직접 확인한 버전은 앞서 밝힌 두 버전이다. 개인 파일을 읽지 않고 임시 폴더에 합성 파일을 만들었다가 정리한다.
python3 encoding_lab.py
실행 결과는 JSON으로 출력된다. 이 글의 3.15.0 관측 결과와 비교할 수 있다. 3.15 이전에는 기본 UTF-8 모드에 대한 검사가 제외되므로 전체 항목 수가 다를 수 있다.
| 직접 확인한 경로 | 관측 | 다음 판단 |
|---|---|---|
| CP949 → 명시적 CP949 읽기 → UTF-8 쓰기 | 한글·주문번호·두 데이터 행 보존 | 실제 입력 계약과 소비 프로그램으로 범위 확대 |
| CP949 → 잘못된 UTF-8 읽기 | UnicodeDecodeError, 출력 미생성 |
생산자의 인코딩 확인 후 원본에서 재시작 |
| 기존 출력 파일에 다시 쓰기 | FileExistsError, 기존 결과 유지 |
새 이름이나 별도 교체 절차 선택 |
| BOM 파일 → UTF-8 / UTF-8-SIG | 첫 열 불일치 / 일치 | 파일 계약에 BOM 허용 여부 기록 |
한글 바이트에 ignore 적용 |
예제 품목명이 빈 문자열로 변함 | 오류를 숨기지 말고 원본 보존 |
| 이모지를 CP949로 출력 | UnicodeEncodeError |
소비자가 받을 문자 범위와 형식을 다시 합의 |
이전 작업은 한 파일의 왕복에서 시작한다
먼저 기존 앱의 파일 읽기·쓰기 지점에서 생략된 encoding을 찾는다. PEP 686은 UTF-8 모드를 끈 상태에서 EncodingWarning을 이용해 영향 지점을 찾고, 수정 후 UTF-8 모드에서 시험하는 절차를 제시한다. 경고는 실행한 경로에서 나타나므로 테스트가 지나가지 않은 코드까지 확인했다는 뜻은 아니다. PEP 686의 이전 절차
아래 명령은 독자의 앱에 적용할 절차 예시다. 이 글에서는 같은 옵션을 합성 파일을 읽는 짧은 자식 프로세스에만 적용해 경고와 UTF-8 모드 동작을 확인했다.
python3 -X utf8=0 -X warn_default_encoding your_app.py
python3 -X utf8 your_app.py
모드를 끄는 일은 앱 전체를 이전 환경과 같게 만드는 보증이 아니다. 현재 환경의 로캘이 예전과 다르면 기본값도 다를 수 있다. 실제 장기 해결은 데이터 형식이 고정된 입출력에 그 형식을 명시하는 것이다. 반대로 운영체제 로캘을 의도적으로 따르는 도구라면 encoding="locale"이 적절할 수 있다.
검증용 입력에는 ASCII뿐 아니라 한글, 선행 0이 있는 식별자, BOM 유무, 사용하는 구분자·따옴표·줄바꿈을 포함한다. 이 글의 작은 예제가 모든 CSV 방언을 시험한 것은 아니므로, 실제 업무 형식의 대표 파일을 추가해야 한다. 출력 소비자가 CP949만 받는다면 Python 쪽만 UTF-8로 바꿔 전송해서는 안 된다. 양쪽이 받을 형식을 합의하고, 표현할 수 없는 문자는 어떤 업무 오류로 처리할지도 정한다.
완료 기록에는 입력 생산자와 인코딩 → 읽기 옵션 → 기대한 값·행 수 → 출력 인코딩과 BOM → 소비 프로그램의 확인 결과를 한 줄로 남기면 된다. 원본을 보존한 채 이 경로가 맞는지 확인해야 다음 파일에도 같은 판단을 적용할 수 있다.
인코딩과 CSV 구조가 맞은 뒤에도 주문 금액·권한·상태 같은 업무 규칙은 남는다. 그 다음 검증은 형식이 맞아도 업무 자동화가 실패하는 이유에서 이어 읽을 수 있다. 이 글을 저장해 이전 작업 때 다시 보거나, 소프트웨어 주제 모음과 RSS에서 후속 실전 글을 찾을 수 있다.
- Python 3.15는 UTF-8 모드를 기본 활성화한다. 기존 CP949 파일을 UTF-8로 자동 변환하거나 인코딩을 추측하는 기능은 아니다.
- 입력은 생산자가 정한 인코딩으로 엄격하게 읽고, 출력은 다음 프로그램이 받을 형식으로 명시한다. BOM·CSV 구문·업무 값은 따로 확인한다.
- 오류 없이 열렸다는 사실만으로 변환 성공을 판단하지 않는다. 원본을 보존하고 한글·식별자·행 수와 실제 소비 프로그램의 결과를 대조한다.
근거와 원자료
자료의 발행 시점과 이번 글에서 참고한 범위를 함께 기록합니다.
- Python 3.15.0 release ↗Python Software Foundation / CPython contributors · 2026
2026-10-09 출시와 UTF-8 기본값 포함. 3.15.0을 최신 패치라고 주장하지 않음.
- What’s new in Python 3.15 — UTF-8 default ↗Python Software Foundation / CPython contributors · 2026
3.15의 기본값 변경. v3.15.0 태그가 가리키는 d220409a13b77de7a6c30abe16ea90db01528edc의 Doc/whatsnew/3.15.rst도 대조.
- PEP 686 — Make UTF-8 mode default ↗Python Software Foundation / CPython contributors · 2026
UTF-8 기본 모드·비활성화·locale.getencoding·이전 절차. 지역별 실제 파일 인코딩을 보장하지 않음.
- Python 3.15 io — Text encoding ↗Python Software Foundation / CPython contributors · 2026
open/TextIOWrapper의 기본값, 명시적 인코딩과 바이너리 I/O, EncodingWarning.
- Python 3.15 — Python UTF-8 Mode ↗Python Software Foundation / CPython contributors · 2026
모드 활성화·sys.flags.utf8_mode·표준 스트림 override·시작 시점 설정.
- Python 3.15 codecs — Encodings and error handlers ↗Python Software Foundation / CPython contributors · 2026
CP949, strict/ignore, UTF-8-SIG의 BOM 의미. 인코딩 자동 추정 성공을 보장하지 않음.
- Python 3.15 csv — Reader and writer ↗Python Software Foundation / CPython contributors · 2026
newline 빈 문자열과 기본 문자열 반환·reader/writer. 특정 Excel UI나 모든 방언 실험의 근거 아님.
갱신 기록
Python 3.15.0·3.14.5의 macOS arm64 합성 파일 실험과 공식 문서를 바탕으로 작성. Windows·Excel·실제 고객 데이터는 실행하지 않았다.
오류를 발견했다면 댓글로 알려 주세요 →
이 글에 댓글 남기기
댓글 공개 방식을 확인하고 있습니다. 개인정보나 비밀 값은 적지 마세요.
댓글을 불러오는 중입니다.
내 댓글 확인·수정·삭제
등록 후 안내된 댓글 번호와 비밀번호를 입력하세요. 승인 대기 중인 댓글도 확인할 수 있습니다.