원본 글: https://www.baeldung.com/java-char-encoding
해당 글에서는 Java에서 인코딩을 어떻게 다루고 있는지 얘기합니다.
1. Importance of Character Encoding
먼저 인코딩의 중요성에 대해 알아야합니다.
아랍어, 일본어, 중국어 등 다양한 언어가 사용되고 있고, 이 또한 컴퓨터 세계에서는 '0'또는 '1'로 표현되어야합니다. 즉, 컴퓨터는 모든 언어체계를 알고 있어야하는데 그러다보니 인코딩이라는 개념을 생각하게 되었습니다. 만약 인코딩을 거치지 않으면 우리가 전달하고자하는 정보를 컴퓨터에 제대로 전달 못하고, 보안 취약점까지 야기할 수 있습니다.
@Test
public void default_encoding_test() {
try {
System.out.println(decodeText("The façade pattern is a software design pattern.", "US-ASCII"));
} catch (IOException e){
System.out.println("error");
}
}
private String decodeText(String input, String encoding) throws IOException {
return
new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(input.getBytes()),
Charset.forName(encoding)))
.readLine();
}
이런 코드가 있다고 생각해봅니다. decodeText()가 어떻게 구현되었는지는 중요하지 않습니다. 중요한건 인코딩의 체계가 다르면 어떤 현상이 나오는지 확인하면 됩니다.

해당 코드를 실행하면 기대하던 값이 아닌 저런 깨진 문자열을 볼 수 있습니다.
해당 현상은 실제 프로그래밍하면서도 자주 목격될 수 있는데요, 이렇기에 인코딩에 대한 학습이 필요함을 알 수 있습니다.
2. Fundamentals
먼저 encoding, charsets, code point에 대해서 알아야합니다.
- encoding: 컴퓨터는 현실세계의 데이터들을 0과 1로 표현하는데 어떤 문자에 대하여 매핑되는 특정 체계입니다. 예를 들어서 'T'는 US-ASCII로 인코딩하면 '01010100'이 됩니다.
- Charsets: 문자들이 매핑되어가면서 여러 집합을 가질 수 있습니다. 이런 집합을 Charsets이라고하며, 유명한 ASCII코드는 128개의 문자셋을 지니고 있습니다.
- Code Point: 코드 포인트는 특정 문자에 대한 정수 참조입니다.
문자 | Unicode CodePoint | Encoding(UTF-8) | Encoding(UTF-16) | Encoding(ISO-8859-1) | 설명 |
A | U+0041 | 41 | 00 41 | 41 | 영어 대문자 'A' |
3. Understanding Encoding Schemes
문자 인코딩은 인코딩되는 문자 길이에 따라 다양한 형태를 취할 수 있는데, 보통 인코딩해야할 문자의 바이트수가 길어지면 더 많은 이진숫자가 필요하다는 뜻입니다.
널리 사용되는 인코딩 방식중 2개를 확인해보겠습니다.
3.1 Single-Byte Encoding
대표적으로 ASCII 체계가 있습니다. 보통 데이터를 처리하기 위한 한 바이트(8bit)지만 ASCII는 7비트만 사용합니다.
그렇기에 2^7=128가지의 character set을 저장할 수 있습니다.
@Test
public void ascii_encoding_test() {
try {
Assertions.assertEquals(convertToBinary("T", "US-ASCII"), "01010100");
} catch (UnsupportedEncodingException e) {
System.out.println("error");
}
}
private String convertToBinary(String input, String encoding)
throws UnsupportedEncodingException {
byte[] encodedInput = input.getBytes(encoding);
return IntStream.range(0, encodedInput.length)
.mapToObj(i -> String.format("%8s", Integer.toBinaryString(encodedInput[i] & 0xFF))
.replace(" ", "0")) // 음수를 피하기 위해 & 0xFF 사용
.collect(Collectors.joining(" "));
}
이렇듯 "T" 문자열은 "US-ASCII" 인코딩 체계를 거치면 "0101011"로 변환되는것을 확인할 수 있습니다.
문제는 7비트로 모든 언어에 대한 정보를 표현하기란 불가능에 가깝습니다. 추가로 'ASCII 확장' 이라는 인코딩 체계도 등장하였지만 그리 효과적이지 않았습니다.
결국 ISO-8859-1이라는 인코딩 체계가 대세를 이루게 되었습니다.
3.2 Multi-Byte Encodig
점점 수용해야할 문자가 늘어날수록 Single-byte 인코딩 체계는 힘을 잃어갔습니다.
공간 사용량이 많지만 결국 이 모든 요구사항을 수용하기 위해서는 Multi-Byte Encoding체계를 구축해야했고, 대표적으로 BIG5와 SHIFT-JIS라는 인코딩 체계가 탄생하였습니다.
대표적으로 중국어 같은 문자열을 해결하기 위해 만들어졌습니다. 아까 사용했던 테스트에 추가 검증 구문을 추가해봐서 테스트해봅니다.
Assertions.assertEquals(convertToBinary("語", "Big5"), "10111011 01111001");
위의 구문을 보시면 2개의 바이트로 표현되고 있고, 이렇기에 Multi-Byte Encoding이라고 불려지는것을 알 수 있습니다.
4. Unicode
인코딩이 중요한만큼 디코딩도 중요합니다. 인코딩 방식이 서로 다르면 디코딩에도 문제가 생기는데, 예를 들어서 어떤 파일에 특정 인코딩방식으로 저장되었는데 다른 방식으로 디코딩을 한다면 문자가 깨져보일 수 있습니다.
과거에는 지역마다 다른 인코딩 방식을 사용하였기에 전 세계에서 통일된 방식으로 문자를 다루는 것이 어려웠습니다.
이런 문제를 해결하고자 Unicode라는 표준이 만들어졌습니다.
Unicode는 전 세계 모든 문자를 표현할 수 있는 인코딩 방식입니다. Unicode덕분에 전 세계가 하나의 표준 인코딩방식으로 나타내질 수 있게 된 것입니다.
Unicode는 독창적인 표현방식이 있습니다. "T"문자는 유니코드 코드 포인트로 10진수로 84입니다. 하지만 유니코드에서는 이 코드를 'U+0054'로 표현합니다. 'U'는 유니코드라는 뜻이고 뒤의 숫자는 '84'라는 숫자를 16진수로 표현한 값입니다. 유니코드에는 1,114,112개의 코드 포인트가 있는데 이를 10진수로 표현하면 너무 길기에 16진수로 나타냅니다.
컴퓨터가 이해할 수 있는 비트로 변환하는 방법으로는 유니코드 내 인코딩 방식에 따라 다릅니다. 주요 인코딩 방식 UTF-8, UTF-32 등에 대해 확인해보겠습니다.
4.1 UTF-32
UTF-32는 유니코드로 정의된 모든 코드 포인트를 표현하기 위한 4바이트 유니코드용 인코딩 체계입니다. 4바이트를 사용하기에 공간적 비효율이 발생합니다.
Assertions.assertEquals(convertToBinary("T", "UTF-32"), "00000000 00000000 00000000 01010100");
앞의 3바이트는 낭비됨을 볼 수 있습니다.
4.2 UTF-8
UTF-8은 가변길이 바이트를 사용하는 또 다른 유니코드 인코딩 방식입니다. 보통 단일 바이트를 사용하지만 필요한 경우 더 많은 수의 바이트를 사용하여 공간을 절약할 수 있습니다.
Assertions.assertEquals(convertToBinary("T", "UTF-8"), "01010100");
Assertions.assertEquals(convertToBinary("語", "UTF-8"), "11101000 10101010 10011110");
위의 코드의 경우 단일 바이트만 사용하지만 밑의 중국어는 3바이트를 사용함을 알 수 있습니다.
이처럼 UTF-8은 유연하게 인코딩을 진행할 수 있어서 보통 웹에서 자주 쓰이는 인코딩방식 중 하나입니다.
4.3 UTF-8 vs UTF-16
두 인코딩방식의 차이는 인코딩할 때 사용하는 최소 크기에 있습니다.
UTF-8은 1바이트(8비트)를 사용하며, UTF-16은 2바이트(16비트)를 사용합니다.
만약 ASCII 문자만 포함된 파일이라면 공간을 적게 쓰는 UTF-8이 더 효율적일 것입니다.
2바이트로 표현될 수 있는 문자에 대하여 UTF-16이 UTF-8보다 더 빠르게 변환될 수 있습니다.
그렇지만 2바이틀 사용하는 UTF-16은 바이트 순서(빅 엔디안, 리틀 엔디안)에 따라 표현 방식이 달라질 위험이 있습니다.
그렇기에 UTF-8은 공간을 아끼고 ASCII와 호환이 뛰어난 텍스트파일 저장 및 네트워크 통신에 적합합니다.
UTF-16은 프로그램에서 메모리자체에서 문자열을 처리할 때 적합합니다.
5. Encoding Support in Java
Java는 당연하게도 요즘 쓰이는 인코딩방식 모두를 지원합니다.
5.1 Default Charset Before Java18
Java18 이전에는 JVM이 시작할 때 Charset을 지정하였습니다. 이는 JVM이 실행되는 환경의 locale, charset정보를 가지고 수행되었습니다.
예를 들어서 MacOS에서는 UTF-8이 기본 charset이였습니다. Window에서는 windows-1252 라는 charset을 따라갔습니다.
많은 Java API에서는 위와같은 기준으로 실행되었습니다. 파일을 읽거나 쓰거나 입력을 받거나 URL을 인코딩 하는 등
그렇기에 문제를 야기할 수 있었습니다. 따로 인코딩 방식을 명시하지 않는다면 실행되는 환경에 따라 인코딩 방식이 각각 다른것이였죠.
5.2 Problems With the Default Charset Before Java18
실행되는 운영체제에 따라 애플리케이션 시스템이 다르게 동작할 수 있다는 것을 의미합니다.
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();
이런 코드를 mac에서 실행하면 UTF-8로, Window에서 실행하면 Windows-1252 방식으로 같은 문자열이 디코딩됩니다.
이런 경우 데이터가 유실되거나 제대로 나타나지 않을 수 있습니다.
5.3 Can We Override the Default Charset ?
시스템이 실행될 때 설정들을 Override하여 위의 문제를 임시적으로 해결할 수 있습니다.
-Dfile.encoding="UTF-8"
-Dsun.jnu.encoding="UTF-8"
- file.encodig: 시스템의 기본 charset을 지정합니다.
- sun.jnu.encodig: 파일 경로의 인코딩/디코딩할 때의 charset을 지정합니다.
하지만 문제는 Java에서 읽을때만 적용되며, 공식 문서에는 제공되지 않는 방법입니다. 그리고 강제로 charset을 지정함으로써 예상치 못한 문제가 야기될 수 있다는 단점이 있습니다. 그렇기에 Override하여 charset을 지정하는 것은 권장하지 않습니다.
결국 우리는 명시적으로 인코딩 정보를 Java에게 알려줘야합니다.
5.4 Default Charset Since Java18
Java18에서는 기본 charset을 UTF-8로 지정함으로써 기존 문제를 해결하였습니다.
UTF-8로 지정된 이유에 대해서는 위에 설명한 장점들로 인해서 설정되었다고 보시면 될 것 같습니다.
5.5 MalformedInputException
byte를 디코딩할 때 맞는 charset이 없거나 하는 등의 문제가 발생할 수 있습니다.
이런 경우 세 가지의 전략에 따라 동작하게 됩니다.
- IGNORE: 잘못된 문자를 무시하고 동작을 수행합니다.
- REPLACE: 잘못된 문자를 특정문자로 대체하고 동작을 수행합니다.
- REPORT: MalofrmedInputException 에러를 발생시킵니다.
CharsetDecoder에 대해서 MalformedInputAction은 REPORT로 동작하고 InputStreamReader에 있는 기본 디코더의 MalformedInputAction은 REPLACE로 동작합니다.
이러한 동작을 확인하기 위해 테스트코드에서 사용하는 유틸함수를 수정합니다.
private String decodeText(String input, Charset charset,
CodingErrorAction codingErrorAction) throws IOException {
CharsetDecoder charsetDecoder = charset.newDecoder();
charsetDecoder.onMalformedInput(codingErrorAction);
return
new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(input.getBytes()),
charsetDecoder)).readLine();
}
이렇게 작성하면 알 수 없는 문자가 들어왔을 경우 대처가 가능해집니다.
먼저 IGNORE의 경우를 테스트합니다. 원본 글에서는 모르는 문자에 대해 '?'로 대체되지만 실제 테스트값은 말그대로 무시되어버려서 이에 맞춰서 수정하였습니다.
@Test
public void set_ignore_test() {
try {
Assertions.assertEquals(
"The faade pattern is a software design pattern.",
decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.IGNORE));
} catch (IOException e) {
System.out.println("error");
}
}
다음은 REPLACE테스트입니다.
@Test
public void set_replace_test() {
try {
Assertions.assertEquals(
"The fa��ade pattern is a software design pattern.",
decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.REPLACE));
} catch (IOException e) {
System.out.println("error");
}
}
�라는 값으로 대체되는것을 확인할 수 있습니다.
다음은 REPORT 테스트입니다.
@Test
public void set_report_test() {
Assertions.assertThrows(
MalformedInputException.class,
() -> decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.REPORT));
}
의도적으로 에러를 반환합니다.
해당 클래스들을 통해서 알 수없는 값에 실패전략을 세울 수 있게 되었습니다.
6. Other Places Where Encoding Is Important
보통 코딩을 하면서 이런 인코딩 문제를 겪을 일이 크지 않습니다.
다만, 문제를 겪으면 잘못하면 데이터가 유실될 수 있기에 어떤 분야에서 문제가 발견될 수 있는지 확인해야합니다.
원본글에서 소개하고 있는것들이 있습니다. 좀 더 자세하게 설명하고 있지만 이 글에 담기에는 크게 중요하지 않은 내용들이 있기 때문에 종류만 적겠습니다.
1. 텍스트 에디터(vscode, 메모장 등)
2. 파일 시스템 - 운영체제에 따라 다름
3. 네트워크(FTP같은 프로토콜)
4. 데이터베이스
5. 웹브라우저
7. 결론
인코딩에 대해서 학습하였습니다.
자세하게 좀 다뤘지만 그만큼 머리에서 잊혀지지 않았으면 합니다.
'Baeldung번역&공부 > Java-string' 카테고리의 다른 글
Java Text Blocks (Java15이상) (0) | 2025.01.31 |
---|---|
Multi-line을 정의하는 여러 방법(Java Multi-line String) (0) | 2025.01.30 |
String 비교법(Comparing Strings in Java) (0) | 2025.01.28 |
문자열 순회 방법들(How to Iterate Over the String Characters in Java) (0) | 2025.01.27 |
String Pool에 대하여(Guide to Java String Pool) (0) | 2025.01.26 |