지난번에 자동화가 일주일간 죽어 있던 이야기를 썼어요. 범인은 제가 넣은 한글 주석이었고, 파일을 읽는 프로그램이 다른 문자 규칙으로 해석하면서 벌어진 일이었습니다.
그 일을 겪고 나서 “같은 실수가 다른 스크립트에도 있으면 어떡하지?” 싶었어요. 그래서 자동화에 쓰는 다른 스크립트들도 점검했습니다. 결과가 뜻밖이었어요. 같은 원인인데 증상이 정반대로 나타나더라고요.
두 얼굴 — 같은 원인, 정반대 증상
배치 파일 (.cmd) | PowerShell 스크립트 (.ps1) | |
|---|---|---|
| 한글이 잘못 읽히면 | 그 자리에서 죽는다 | 계속 실행된다 |
| 종료 상태 | 실패(비정상 종료) | 정상 종료 |
| 남는 기록 | 뚝 끊긴 로그 | 평소와 똑같은 로그 |
| 발견 시점 | 결국은 발각됨 | 아무도 모름 |
| 진짜 피해 | 작업이 안 돎 | 틀린 값으로 돎 |
왼쪽은 제가 지난번에 겪은 일이에요. 요란하게 죽었습니다. 오른쪽은 이번 점검에서 확인한 건데, 아무 일도 없었던 것처럼 끝납니다. 그런데 파일 안의 한글은 반쪽만 읽힌 상태예요.
점검 당시 예약 작업 4종의 상태는 이랬어요. 넷 다 멀쩡히 돌고 있었습니다.
| 예약 작업 | 실행기 | 마지막 결과 | 최근 실행 |
|---|---|---|---|
| LifeCoordinator-Closing | pwsh.exe (PS7) | 0 | 08-17 23:00 |
| LifeCoordinator-ProjectScan | pwsh.exe | 0 | 08-18 02:00 |
| LifeCoordinator-DevlogSync | pwsh.exe | 0 | 08-18 02:30 |
| LifeCoordinator-VaultBackup | pwsh.exe | 0 | 08-18 02:35 |
실행기가 전부 PowerShell 7이라 BOM 없는 파일도 UTF-8로 읽혀 한글이 멀쩡했던 거예요. 다만 트리거를 powershell.exe로 바꾸거나 다른 경로에서 5.1로 부르는 순간 같은 파일이 다르게 읽힙니다. 그래서 AI의 점검 보고서는 이 스크립트들이 pwsh(PowerShell 7) 전용이라는 전제를 파일에 적어 두라고도 권했어요. 지금 안전한 게 우연이 아니라 선택이라는 걸 남겨 두자는 거죠.
왜 이렇게 갈릴까 — “누가 어떤 규칙으로 읽는다고 가정하나”
핵심은 읽는 쪽의 가정입니다. 파일에는 정답이 안 적혀 있어요. 그냥 바이트만 있고, 읽는 프로그램이 “이건 아마 이 규칙으로 쓰였겠지” 하고 추측해서 해석합니다.
- PowerShell 7은 별다른 표시가 없으면 UTF-8이라고 가정합니다. 요즘 방식이죠.
- Windows PowerShell 5.1(윈도우에 원래 깔려 있는 구버전)은 시스템 ANSI 코드페이지라고 가정합니다. 한국어 윈도우면 CP949죠.
코드페이지: 바이트를 어떤 문자로 해석할지 정한 표예요. CP949와 UTF-8은 한글을 서로 다른 바이트로 쓰기 때문에, 한쪽으로 저장한 파일을 다른 쪽으로 읽으면 글자가 어긋납니다.
즉 같은 파일을 두 버전이 다르게 읽습니다. 그리고 여기서 갈리는 게, PowerShell은 글자가 깨져도 문법이 안 깨지면 그냥 실행된다는 거예요. 깨진 한글은 대개 주석이나 문자열 안에 있으니 문법은 멀쩡하거든요. 그래서 정상 종료됩니다.
말로만 하면 안 믿기니 숫자로 봤어요. AI에게 자동화용 예약 스크립트 4종도 같은 문제인지 점검해 달라고 했더니, AI는 콘솔 인코딩의 영향을 빼려고 화면에 찍힌 글자가 아니라 코드포인트 숫자로만 셌습니다. 대상은 Invoke-Closing.ps1의 첫 한글 줄. 같은 줄을 두 버전으로 읽어 한글 글자 수, 깨진 문자(보고서 표기로는 «Latin-1 깨진 문자») 수, 줄 길이를 비교했어요. 결과는 이랬습니다.
- PowerShell 7: 한글 6자, 깨진 문자 0개, 길이 41
- Windows PowerShell 5.1: 한글 3자, 깨진 문자 1개, 길이 44
한글이 절반만 살아남았고, 길이까지 달라졌습니다. 그런데 스크립트는 아무 불평 없이 잘 돌아요.
이게 왜 위험하냐면, 그 한글이 주석이면 로그만 지저분해지고 끝납니다. 그런데 조건 비교(if $x -eq "완료")나 경로, 검색 패턴에 한글이 들어 있으면 어떻게 될까요? 비교가 조용히 실패하고, 엉뚱한 분기로 흘러갑니다. 프로그램은 정상 종료되고, 결과만 틀려요. 이건 죽는 것보다 훨씬 늦게 발견됩니다.

왜 헷갈리나 — 둘 다 “인코딩 문제”라고 부르기 때문
제가 헷갈렸던 이유가 여기 있어요. 두 현상을 똑같이 “한글 인코딩 문제”라고 부르거든요. 그래서 한쪽을 겪고 나면 다른 쪽도 같은 모습일 거라고 지레짐작하게 됩니다.
한글이 컴퓨터에서 한 글자에 여러 바이트를 쓴다는 사실은 예전에 글자 수를 잘못 셌던 일에서 이미 데인 적이 있어요. 그런데 그건 몇 바이트냐의 문제였고, 이번 건 누가 어떤 규칙으로 읽느냐의 문제입니다. 같은 한글이지만 아픈 지점이 달라요.
구분법과 처방
구분법은 간단합니다. 스크립트가 죽었나, 아니면 조용히 끝났나를 보세요.
- 죽었다 → 파일을 읽는 쪽이 문법까지 깨진 겁니다(배치 파일이 대표적). 로그가 중간에 뚝 끊깁니다.
- 조용히 끝났는데 결과가 이상하다 → 글자만 깨지고 문법은 살아남은 겁니다. 이때는 로그가 멀쩡해서 더 안 보여요.
처방도 다릅니다.
배치 파일은 아예 한글을 안 쓰는 게 답이었어요. 주석까지 영어로 바꾸고, 파일 맨 위에 “여기 한글 금지”라고 경고를 남겼습니다.
PowerShell 쪽은 3바이트면 끝납니다. 파일 맨 앞에 EF BB BF, 즉 BOM을 넣으면 구버전도 추측을 그만두고 최신 버전과 똑같이 읽어요.
BOM(Byte Order Mark): 파일 맨 앞에 붙는 3바이트짜리 표식으로, 읽는 쪽에 “이 파일은 UTF-8이다”라고 알려주는 역할을 합니다. 화면에는 안 보여요.
확인과 부착은 각각 한 줄입니다.
# 확인 — 앞 3바이트가 EF BB BF 인가
Format-Hex .check-target.ps1 -Count 3
# 부착 — 내용은 그대로 두고 인코딩만 다시 지정해 저장
$t = Get-Content .check-target.ps1 -Raw
Set-Content .check-target.ps1 -Value $t -Encoding utf8BOM -NoNewline
실제 부착은 이 스크립트들이 있는 레포를 맡은 오케스트레이터 AI가 했어요. 대상 스크립트 9개 전부에 3바이트씩 붙였고 본문은 한 글자도 안 바꿨습니다. 커밋에 남은 검증은 세 가지예요. PowerShell 파서로 9개 전부 구문 검사(에러 0), 두 버전으로 다시 읽기(둘 다 6자·0개·41로 같아짐), 그리고 예약 작업 하나를 실제로 돌려 결과 코드 0과 로그의 한글이 정상인지 확인.
여기서 AI가 한 번 헛디딘 게 있어요. 처음 보고서에서 AI는 4종 모두 “BOM 없음”이라고 판정했는데, 판정에 쓴 명령이 결함이었습니다.
head -c3 | od -An -tx1 | tr -d ' '
이 출력은 efbbbf 뒤에 개행이 남아서, = "efbbbf" 비교가 항상 실패하는 코드였어요. BOM이 있어도 “없음”이 나옵니다. 다행히 엔진별 오독 수치(6자 대 3자)는 코드포인트로 잰 거라 유효했고, 부착 뒤 다시 확인한 첫 3바이트는 4종 모두 efbbbf였습니다. AI는 바이트를 판정할 땐 xxd -p처럼 개행을 남기지 않는 도구를 써야 한다는 걸 교훈으로 적었어요.
점검 대상을 정할 때 놓친 것
마지막으로 제가 틀렸던 걸 하나 적어둘게요. 처음에 저는 자동으로 도는 스크립트 4개만 점검 대상으로 잡았습니다. “예약된 것만 위험하다”고 생각했거든요.
그런데 실제로는 여러 스크립트가 공통으로 불러 쓰는 파일 1개(Sync-RepoMain.ps1)와, 손으로 자주 실행하는 등록 스크립트 4개(Register-*.ps1)가 빠져 있었어요. 점검 요청은 예약 작업 4종이었는데, 스크립트 레포를 맡은 오케스트레이터 AI가 자체 점검에서 범위를 9종으로 넓혀 BOM을 붙였어요. 생각해보면 위험은 오히려 후자 쪽입니다. 공통 파일은 문제가 생기면 그걸 쓰는 전부에 번지고, 손으로 돌리는 스크립트는 그때그때 다른 환경에서 실행되니까요.
“자동으로 도는 것만 위험하다”가 제 착각이었습니다. 인코딩은 읽는 쪽이 누구냐에 달린 문제라, 언제 어떤 프로그램이 그 파일을 열지 모른다면 전부 대상이에요.
그래서 이번 사건에서 제가 챙긴 교훈은 하나입니다. 요란하게 죽는 실패가, 조용히 잘못 도는 것보다 낫습니다. 죽으면 일주일 만에라도 들키지만, 반쪽만 읽힌 파일은 아무도 안 알려주거든요.