오류 모드
고친 사람 github-actions[bot]
오류 모드는 글자 변환이 중간에 막혔을 때 멈출지 계속 갈지를 정해 줍니다. 바이트를 글자로 읽다 보면 규칙에 안 맞는 바이트를 만나는 때가 있습니다. 멈추는 쪽은 오류를 냅니다. 계속 가는 쪽은 그 대목을 표시로 바꾸거나 건너뜁니다.
쉽고 빠른 이해
글자 변환이 막혔을 때 어떻게 할지를 미리 정해 둡니다. 깨진 바이트가 섞인 파일을 읽다가
멈출지, 깨진 곳에 � 를 찍고 끝까지 읽을지를 이 설정이 가릅니다.
어느 쪽도 늘 맞지는 않아서 고를 수 있게 해 둡니다. 멈추면 깨졌다는 것은 바로 알지만 한 바이트 때문에 나머지를 다 못 받습니다. 계속 가면 끝까지 읽지만 원래 바이트를 잃습니다.
- 변환기가 바이트를 차례로 읽어 글자로 바꿉니다
- 규칙에 안 맞는 바이트를 만나면 이 설정을 봅니다
- 멈추는 설정이면 오류를 냅니다. 계속 가는 설정이면 표시를 넣거나 건너뛴 뒤 다음으로 갑니다
계속 가는 쪽의 대가는 더 눈에 안 띕니다. 결과만 보고는 무엇이 깨졌는지 알 수 없습니다. 되돌릴 수도 없습니다. 기본이 계속 가는 쪽인 도구를 모르고 쓰면 깨진 데이터가 오류 없이 저장됩니다.
상세
이 절은 변환이 막히는 두 경우, 막혔을 때의 대응 넷, 언어별 이름, 모드를 고르는 기준을 차례로 봅니다.
번역가가 원고를 옮기다 모르는 낱말을 만났다고 해 봅시다. 번역을 멈추고 의뢰인에게 물을 수 있습니다. 그 낱말 대신 「판독 불가」라고 적고 계속 옮길 수도 있습니다. 아무 말 없이 빼고 넘어갈 수도 있습니다.
오류 모드는 이 선택을 변환 전에 정해 둡니다. 변환하는 프로그램은 도중에 사람에게 물을 수 없기 때문입니다.
변환이 막히는 두 경우
변환은 두 경우에 막힙니다. 바이트를 글자로 읽다가 규칙에 안 맞는 바이트를 만난 경우와, 글자를 바이트로 적다가 그 규칙에 없는 글자를 만난 경우입니다. 이 둘을 정확히 말하려면 낱말 몇 개가 필요해서 그것부터 풉니다.
유니코드는 세상의 글자를 모아 글자마다 번호를 매긴 목록입니다. 어느 나라 글자든 번호 하나로 가리킬 수 있게 하려고 만든 목록입니다.
이 번호를 코드 포인트라고 부릅니다. 「한」의 코드 포인트는 16진수로 적어 U+D55C 입니다.
컴퓨터는 이 번호를 바이트로 바꿔 담습니다. 번호를 어떤 바이트로 적을지 정한 규칙이 문자 인코딩입니다. 같은 번호라도 규칙이 다르면 다른 바이트가 됩니다.
UTF-8(Unicode Transformation Format 8-bit, 유니코드 변환 형식 8비트)이 가장 흔히 쓰는 규칙입니다. 글자 하나를 1바이트에서 4바이트 사이로 적습니다.
글자를 바이트로 바꾸는 일을 인코딩이라고 합니다. 반대로 바이트를 글자로 되돌리는 일은 디코딩이라고 합니다.
두 일을 맡는 부품을 각각 인코더와 디코더라고 부릅니다. 변환이 막히는 것도 이 두 부품 안에서입니다.
오류 모드는 이 두 부품에 붙는 설정입니다. 부품을 만들 때 정해 두거나, 변환을 부를 때 인자로 넘깁니다.
디코더는 규칙에 안 맞는 바이트를 만나면 막힙니다. UTF-8 에서 바이트 FF 는 어떤 글자의
일부도 될 수 없습니다. 여러 바이트로 적는 글자가 중간에 잘려 앞 조각만 남은 경우도 같습니다.
인코더는 그 인코딩에 없는 글자를 만나면 막힙니다. ASCII(American Standard Code for Information Interchange, 미국 정보 교환 표준 부호)는 영문자·숫자·기호 128개만 담습니다. 「한」을 ASCII 로 적으라고 하면 적을 바이트가 없습니다.
막혔을 때 고르는 대응 넷
막혔을 때 할 수 있는 일은 넷으로 모입니다. 앞의 셋은 디코더와 인코더에 다 있습니다. 넷째는 인코더에만 있습니다.
| 대응 | 흔히 부르는 이름 | 막힌 대목에 남는 것 | 변환 |
|---|---|---|---|
| 멈춘다 | 엄격 모드 | 없습니다. 오류가 납니다 | 거기서 끝납니다 |
| 바꾼다 | 대체 모드 | 디코딩은 �, 인코딩은 흔히 ? |
끝까지 갑니다 |
| 건너뛴다 | 무시 모드 | 아무것도 안 남습니다 | 끝까지 갑니다 |
| 번호로 적는다 | 문자 참조 모드 | 한 꼴의 글자 번호 |
끝까지 갑니다 |
대체 모드가 디코딩에서 넣는 � 는 대체 문자입니다. 유니코드가 「여기 읽지 못한 글자가
있었다」는 표시로 따로 마련한 글자입니다. 번호는 U+FFFD 입니다. 화면에는 검은 마름모 안의
물음표로 보입니다.
문자 참조 모드의 한 은 HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어)이
글자를 번호로 적는 방법입니다. 54620 은 「한」의 코드 포인트 U+D55C 를 십진수로 적은 값입니다.
웹 페이지로 보여 줄 글이면 브라우저가 이 번호를 다시 「한」으로 그려 줍니다.
디코더 쪽 흐름을 그리면 아래와 같습니다. 엄격 모드만 빠져나갑니다. 나머지 둘은 다음 바이트로 돌아가 끝까지 읽습니다.
flowchart TD
A["다음 바이트를 본다"] --> B{"규칙에 맞나"}
B -->|맞다| C["글자로 바꿔 내보낸다"]
B -->|안 맞다| D{"오류 모드"}
D -->|엄격| E["오류를 내고 멈춘다"]
D -->|대체| F["대체 문자를 내보낸다"]
D -->|무시| G["아무것도 안 내보낸다"]
C --> A
F --> A
G --> A
무시 모드가 숨기는 문제
무시 모드는 흔적을 남기지 않습니다. 결과만 봐서는 무엇이 빠졌는지 알 수 없습니다. 빠진 바이트 앞뒤 조각이 붙어 원래 없던 낱말이 생기기도 합니다.
이것이 보안 구멍이 되기도 합니다. 입력 검증이 <script> 라는 글자를 찾아 막는다고 해
봅시다. <scr 와 ipt> 사이에 깨진 바이트가 끼어 있으면 검증은 이 낱말을 못 찾고 통과시킵니다.
그 뒤 디코더가 깨진 바이트를 지우면 두 조각이 붙어 <script> 가 됩니다. 검증을 지난 입력이
교차 사이트 스크립팅 공격 코드로 바뀐 것입니다. 남의 웹 페이지에 스크립트를 심어 방문자
브라우저에서 돌게 하는 공격입니다.
대체 모드는 이 문제를 피합니다. 깨진 대목에 � 가 끼어 있어서 앞뒤 조각이 붙지 않습니다.
다만 대체 모드도 원래 바이트를 버립니다. FF 가 � 로 바뀐 결과에서는 원래 무슨 바이트였는지
알 수 없습니다.
언어마다 다른 이름
이 소절은 파이썬·자바·브라우저가 같은 선택을 어떻게 부르는지 봅니다. 이름은 달라도 고르는 대응은 앞의 넷 가운데 하나입니다.
| 환경 | 엄격 | 대체 | 무시 | 아무것도 안 고르면 |
|---|---|---|---|---|
파이썬 errors 인자 |
strict |
replace |
ignore |
엄격 |
자바 CodingErrorAction |
REPORT |
REPLACE |
IGNORE |
쓰는 방법마다 다릅니다 |
| 브라우저 디코더 | fatal |
replacement |
없습니다 | 대체 |
브라우저 인코더는 엄격 모드와 html 이라는 이름의 문자 참조 모드 가운데 하나를 씁니다.
파이썬은 문자 참조 모드를 xmlcharrefreplace 라는 이름으로 인코딩에만 둡니다.
파이썬에서 디코딩할 때는 두 번째 인자로 모드를 고릅니다. 아래는 가운데에 FF 가 낀 세 바이트를
UTF-8 로 읽어 본 것입니다.
b = b"A\xffB"
b.decode("utf-8") # 예외
b.decode("utf-8", "replace") # 'A�B'
b.decode("utf-8", "ignore") # 'AB'
첫 줄은 아무 모드도 안 골랐으니 엄격 모드로 돌아 UnicodeDecodeError 예외를 냅니다. 대체
모드는 FF 를 � 하나로 바꿉니다. 무시 모드는 FF 를 지워 A 와 B 가 붙습니다.
인코딩도 같은 인자로 고릅니다. 「한」을 ASCII 로 적어 보면 두 모드의 차이가 보입니다.
s = "한"
s.encode("ascii", "replace") # b'?'
s.encode("ascii",
"xmlcharrefreplace") # b'한'
대체 모드는 「한」을 물음표 하나로 바꿉니다. 문자 참조 모드는 앞의 표에서 본 글자 번호를 적습니다.
자바는 어떤 방법으로 변환하느냐에 따라 기본이 갈립니다. 아래 코드의 UTF_8 은
StandardCharsets.UTF_8 을 줄여 적은 것입니다.
byte[] b = {0x41, (byte) 0xFF, 0x42};
new String(b, UTF_8); // "A�B"
var d = UTF_8.newDecoder();
d.decode(ByteBuffer.wrap(b)); // 예외
new String 은 늘 대체 모드로 읽습니다. 모드를 바꿀 방법이 없습니다. 반대 방향인 String.getBytes 도
같습니다. 적을 수 없는 글자는 그 인코딩의 기본 대체 바이트로 바뀝니다. 대개 ? 하나입니다.
newDecoder() 로 디코더를 직접 만들면 기본이 엄격 모드(REPORT)입니다. 그래서 둘째 호출은
MalformedInputException 을 냅니다. 모드를 바꾸려면 디코더의 onMalformedInput 에 다른 값을
넘깁니다.
자바스크립트의 TextDecoder 는 브라우저 디코더를 그대로 따릅니다. 기본은 대체
모드입니다. fatal: true 를 넘기면 엄격 모드로 바뀝니다.
모드를 고르는 기준
고르는 기준은 하나입니다. 원래 바이트를 잃어도 되는가입니다.
| 모드 | 맞는 경우 | 대가 |
|---|---|---|
| 엄격 | 데이터를 저장하거나 다른 시스템에 넘길 때 | 깨진 바이트 하나에 요청 전체가 실패합니다 |
| 대체 | 사람이 읽기만 할 때 · 로그 화면이나 미리보기 | 원래 바이트를 잃습니다 |
| 무시 | 거의 없습니다 | 흔적 없이 빠져 문제를 숨깁니다 |
흔한 사고는 기본값을 모르고 쓰는 것입니다. 자바의 new String 처럼 대체가 기본인 길로 요청
본문을 읽으면, 깨진 입력이 오류 없이 � 가 섞인 채 데이터베이스에 들어갑니다. 나중에
알아채도 원래 바이트는 이미 없습니다.
엄격 모드도 모든 깨짐을 잡아 주지는 않습니다. 잡는 것은 규칙에 안 맞는 바이트뿐입니다.
서유럽 글자용 인코딩인 ISO-8859-1 은 모든 바이트를 글자 하나씩에 대응시킵니다. 그래서 이 인코딩으로 읽으면 막히는 바이트가 없습니다. 한글 바이트를 그 규칙으로 잘못 읽어도 오류 없이 엉뚱한 글자가 나옵니다. 이름의 ISO 는 국제 표준화 기구(International Organization for Standardization)의 줄임입니다.
이렇게 규칙을 잘못 골라 글자가 엉뚱한 기호로 보이는 고장을 모지바케라고 합니다. 오류 모드는 어느 규칙으로 읽을지를 정하지 않습니다. 고른 규칙으로 읽다가 막혔을 때의 대응만 정합니다.
관련 항목
오류 모드가 붙는 변환 부품과 변환 과정
디코더 · 인코더 · 디코딩 · 인코딩 · 문자 인코딩
막힌 대목을 대신 채우는 표시
대체 문자 · 숫자 문자 참조 · HTML 엔티티 · 이스케이프 시퀀스
이 설정을 고르는 언어별 인터페이스
TextDecoder · TextEncoder · CharsetDecoder · CharsetEncoder · CodingErrorAction · codecs 모듈 · surrogateescape
이 설정이 다루는 글자 규칙과 번호 체계
UTF-8 · UTF-16 · ASCII · EUC-KR · ISO-8859-1 · Unicode · 코드 포인트
모드를 잘못 골라 생기는 문제
모지바케 · 교차 사이트 스크립팅 · 입력 검증 · 인코딩 불일치 · 데이터 손실
오류 모드 값을 정하는 표준 문서
WHATWG Encoding Standard · HTML Living Standard · Unicode 표준
다른 이름: error mode · 에러 모드 · 오류 처리 모드 · 디코딩 오류 처리 · 인코딩 오류 처리