Windows에서 파이썬 cp949 오류 없애기: 로그·파일·서브프로세스 3곳 총정리

Windows에서 파이썬 cp949

Windows에서 파이썬 스크립트를 돌리다가 UnicodeDecodeError나 UnicodeEncodeError 메시지에 ‘cp949’라는 단어가 찍혀 있어서 검색하셨다면, 답은 명확합니다. 표준 출력(로그·print)은 스트림 인코딩을 UTF-8로 재설정하고, 파일은 open()에 encoding='utf-8'을 항상 명시하며, 서브프로세스는 자식 프로세스가 실제로 쓰는 인코딩에 맞춰 encoding 인자를 지정해야 세 곳 모두에서 오류가 사라집니다. 원인은 하나입니다. 한국어 Windows의 로컬 코드페이지가 cp949로 잡혀 있고, 파이썬이 인코딩을 따로 지정하지 않으면 이 로케일 값을 기본값으로 쓰기 때문입니다. 아래에서 세 지점을 실제 코드로 하나씩 잡아보겠습니다.

왜 하필 cp949 에러가 날까요?

Windows는 지역 설정이 한국어일 경우 시스템 기본 ANSI 코드페이지를 cp949로 지정합니다. 파이썬은 open()이나 print()처럼 인코딩을 명시하지 않는 함수를 호출하면 locale.getpreferredencoding(False) 값을 기본 인코딩으로 사용하는데, 이 값이 바로 그 cp949입니다.

직접 확인해보면 바로 나옵니다.

import locale
print(locale.getpreferredencoding(False))

한국어 Windows에서는 cp949가 출력됩니다. 반면 같은 코드를 리눅스나 macOS에서 돌리면 대부분 UTF-8이 나옵니다. 즉 같은 스크립트라도 운영체제·로케일에 따라 기본 인코딩이 달라지는 게 문제의 뿌리입니다. 개발할 때는 UTF-8 환경(리눅스 서버, macOS)에서 테스트하고 배포는 한국어 Windows 노트북에서 하는 구성이라면 이 차이 때문에 로컬에서는 안 나던 에러가 배포 환경에서만 재현되는 경우가 흔합니다.

이 로케일 종속성 자체를 없애는 근본 해법이 UTF-8 모드인데, 관련 내용은 아래 5번째 항목에서 다룹니다. 공식 사양은 PEP 686 문서에서 확인할 수 있고, cp949 인코딩 자체의 배경은 위키백과 CP949 문서에 정리돼 있습니다.

print·로그 출력에서 UnicodeEncodeError 잡는 법

이모지나 특수 기호가 섞인 문자열을 print()로 찍을 때 'cp949' codec can't encode character 에러가 나는 경우가 대표적입니다. cp949 코드페이지에는 이모지 같은 문자가 아예 정의돼 있지 않아서 인코딩 자체가 불가능하기 때문입니다.

Python 3.7부터 제공되는 TextIOWrapper.reconfigure()로 표준 스트림 인코딩을 바꿔주면 해결됩니다.

import sys
import logging

sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logging.info("한글 로그 테스트 ✅ - 오류 없이 출력됩니다")

Windows 터미널에서 파이썬 인코딩 오류 메시지가 표시된 화면

logging.basicConfig()의 StreamHandler는 별도 인코딩 인자가 없고 지정된 스트림(기본값은 sys.stderr)의 인코딩을 그대로 씁니다. 그래서 로깅을 설정하기 전에 sys.stdout·sys.stderr를 먼저 재설정해두는 순서가 중요합니다. 파일로 로그를 남기는 FileHandler는 별도로 encoding='utf-8'을 넣어야 합니다.

handler = logging.FileHandler('app.log', encoding='utf-8')

여기서 주의할 점 하나. sys.stdout.reconfigure()로 파이썬 내부 인코딩을 UTF-8로 바꿔도, 구형 cmd.exe 콘솔의 코드페이지 자체가 여전히 cp949라면 화면에는 물음표나 깨진 글자가 보일 수 있습니다. 인코딩 재설정은 파이썬이 만드는 바이트 시퀀스를 바꾸는 것이고, 그 바이트를 화면에 어떻게 렌더링할지는 터미널 쪽 코드페이지 몫이라 둘은 별개의 문제입니다.

파일 입출력은 encoding=’utf-8′ 명시가 우선입니다

가장 흔한 패턴은 다른 개발 환경(리눅스 서버, macOS, 또는 VS Code)에서 UTF-8로 저장한 .csv나 .txt 파일을 한국어 Windows에서 open()으로 열 때 나는 UnicodeDecodeError입니다. 인코딩을 지정하지 않으면 파이썬이 cp949로 디코딩을 시도하다가 UTF-8 바이트 시퀀스를 만나 깨지는 것입니다.

with open('report.csv', encoding='utf-8') as f:
    data = f.read()

with open('output.json', 'w', encoding='utf-8') as f:
    f.write(data)

읽기·쓰기 양쪽 모두에 encoding='utf-8'을 붙이는 습관이 필요합니다. 반대로 옛날에 엑셀에서 저장한 cp949 원본 파일을 다뤄야 한다면 encoding='cp949'를 명시해서 그 파일의 실제 인코딩에 맞춰야지, 무조건 UTF-8로 통일하는 게 정답은 아닙니다.

코드베이스 전체에서 인코딩이 빠진 open() 호출을 찾고 싶다면 Python 3.10부터 지원하는 EncodingWarning이 유용합니다.

python -X warn_default_encoding your_script.py

인코딩을 지정하지 않은 open() 호출마다 EncodingWarning: 'encoding' argument not specified 경고가 출력돼서, 대형 프로젝트에서 누락된 지점을 하나씩 추적할 수 있습니다. 관련 스펙은 PEP 597에 정의돼 있습니다.

서브프로세스에서 한글이 깨지는 진짜 이유

subprocess.run()에 text=True만 넣고 encoding을 생략하면, 파이썬은 자식 프로세스가 출력한 바이트를 부모 프로세스의 로케일 인코딩(cp949)으로 디코딩하려 시도합니다. 자식이 git처럼 UTF-8로 출력하는 프로그램이라면 여기서 디코딩 오류가 납니다.

노트북 화면에 UTF-8 인코딩 코드를 작성 중인 개발자

import subprocess

result = subprocess.run(
    ['git', 'log', '-1', '--format=%s'],
    capture_output=True,
    text=True,
    encoding='utf-8',
    errors='replace',
)
print(result.stdout)

errors='replace'를 함께 넣으면 디코딩이 안 되는 바이트를 예외 대신 � 문자로 치환해서 최소한 스크립트가 죽지는 않게 만들 수 있습니다. 다만 이 방식이 항상 맞는 건 아닙니다. 호출하는 대상이 오래된 Windows 전용 CLI 도구라 실제로 cp949 바이트를 출력하는 경우라면 encoding='utf-8'을 지정해도 여전히 깨지거나 예외가 납니다. 이럴 땐 자식 프로그램이 실제로 어떤 바이트를 내보내는지 capture_output=True, text=False로 먼저 raw bytes를 받아 확인한 뒤, 거기 맞는 인코딩(cp949 혹은 utf-8)을 지정해야 합니다.

자식 프로세스가 또 다른 파이썬 스크립트라면, 그 스크립트의 출력 인코딩 자체를 UTF-8로 강제하는 방법도 있습니다.

import os, subprocess

env = os.environ.copy()
env['PYTHONUTF8'] = '1'

result = subprocess.run(
    ['python', 'child_script.py'],
    env=env,
    capture_output=True,
    text=True,
    encoding='utf-8',
)

PYTHONUTF8=1로 세 곳을 한 번에 묶고, 이 함정은 조심하세요

매번 encoding='utf-8'을 붙이는 대신, Python 3.7부터 지원되는 UTF-8 모드(PEP 540)를 켜면 표준 스트림·open() 기본 인코딩·파일 시스템 인코딩이 한꺼번에 UTF-8로 바뀝니다.

# cmd
set PYTHONUTF8=1
python app.py

# PowerShell
$env:PYTHONUTF8="1"
python app.py

python -X utf8 app.py처럼 실행 시 옵션으로 켤 수도 있습니다. 노트북 서버처럼 자동 배포 스크립트를 반복 실행하는 환경이라면, 서비스 실행 커맨드 자체에 이 환경변수를 박아두는 편이 매 스크립트에 encoding 인자를 붙이는 것보다 관리 부담이 적습니다.

다만 트레이드오프가 두 가지 있습니다. 첫 번째, UTF-8 모드는 인코딩을 생략한 모든 open() 호출에 영향을 줍니다. 지금까지 인코딩을 지정하지 않은 채로 “우연히” cp949 파일을 cp949 로케일에서 잘 읽고 있던 코드가 있다면, UTF-8 모드를 켜는 순간 그 파일 읽기가 오히려 깨지기 시작합니다. 두 번째, PYTHONUTF8=1은 파이썬 내부 처리만 UTF-8로 바꿀 뿐 cmd.exe 콘솔 자체의 코드페이지는 그대로 두므로, 콘솔에 직접 한글을 찍는 부분에서 여전히 깨져 보일 수 있습니다. 이 경우 실행 전에 chcp 65001을 한 번 실행하거나, 코드페이지를 자동으로 UTF-8로 다루는 Windows Terminal을 쓰는 쪽이 편합니다. 참고로 PEP 686에 따라 이 UTF-8 모드는 Python 3.15부터 기본값으로 바뀔 예정이라, 그 전까지는 환경변수나 -X utf8 옵션으로 명시적으로 켜야 합니다.

세 지점을 표로 정리하면 다음과 같습니다.

지점 인코딩 미지정 시 기본값 해결 코드 비고
표준출력(print·log) 콘솔 로케일(cp949) sys.stdout.reconfigure(encoding='utf-8') Python 3.7+
파일 open() locale.getpreferredencoding() (cp949) open(path, encoding='utf-8') -X warn_default_encoding으로 누락 탐지
subprocess 부모 로케일(cp949)로 디코딩 encoding= 값을 자식이 실제 쓰는 인코딩에 맞춤 자식이 파이썬이면 env['PYTHONUTF8']='1'

지금 바로 할 수 있는 조치는 두 가지입니다. 스크립트 상단에 sys.stdout.reconfigure(encoding='utf-8')를 넣고, 코드베이스에서 -X warn_default_encoding 옵션으로 인코딩 누락 지점을 찾아 encoding='utf-8'을 채워 넣으세요. 배포 자동화 스크립트라면 실행 환경변수에 PYTHONUTF8=1을 추가해두는 것만으로 위 세 곳의 오류 대부분이 한 번에 정리됩니다.

SQLAlchemy 비동기 세션에서 greenlet_spawn 오류가 나는 이유와 해법

Leave a Comment