Java - 문자 인코딩

INHEES·2025년 1월 20일

금일은 Java 의 문자 인코딩에 관하여 알아보겠습니다.

목차

  • 컴퓨터와 데이터
  • 컴퓨터와 문자 인코딩
  • 문자 집합 조회
  • 문자 인코딩 예제

컴퓨터와 데이터

텍스트 데이터가 어떤 원리로 사용해서 만들어지는지 알지 못한다면 실무에서 한글이 깨지는 현상을 해결 가능하다.

RAM 은 0과 1이라는 이진수를 통해 데이터를 저장하는 메모리의 한 종류입니다.

초당 수십억 번의 데이터 접근을 처리 가능합니다.

10진수의 데이터를 표현하기 위해서는 3울 이진수로 표현할 때 11(2) 로 표현되고 하나의 0 또는 1인 1bit 의 크기를 나타내며 즉 2 bit 이상 사용해야 합니다.

1 bit 가 늘어날수록 표현 가능한 데이터의 수는 2배씩 증가하게 되는 것이다. 다들 아시겠지만 1Byte = 8bit 입니다.

음수 표현!

음수를 표현하기 위해서는 처음 1 bit 를 음수와 양수를 구분하는데 사용 하며 나머지 7 bit 로 숫자 범위를 사용합니다.

때문에 8bit 로 표현 가능한 범위는 -127 ~ 128 까지 표현 가능합니다.


컴퓨터와 문자 인코딩

숫자가 아닌 문자를 메모리에 저장하기 위해서는 문자 집합(character set) 을 활용해야 합니다.

우리가 흔히 알고 있는 아스키 코드 표를 생각하시면 됩니다.

  • 문자 인코딩 : 문자 집합을 통해 문자를 숫자로 변환하는것
  • 문자 디코딩 : 문자 집합을 통해 숫자를 문자로 변환하는 것

ASCII 문자 집합

서로 다른 컴퓨터 간에 올바르게 표시되지 않는 문제를 해결하기위해 호환성 문제를 해결하기 위해 ASCII 라는 표준 문자 집합이 있습니다.

기본적으로 알파벳, 숫자, 키보드의 특수문자, 엔터와 같은 기본적인 7 bit 를 활용하여 총 128가지 문자를 표현할 수 있습니다.

또한 확장된 ASCII 가 있으며 기존 ASCII에 서유럽 문자의 추가 필요 했으며 8 bit 를 사용한 256 가지의 표현이 가능하기도 합니다.

이밖에도 한글 문자 집합이 있으며 16 bit 를 사용하여 표현이 가능하며 영어를 사용하며 1byte, 한글을 사용하면 2byte 를 메모리에 저장하게 됩니다.

전세계 문자 집합

EUC-KR이나 MS949 같은 한글 문자표를 PC에 설치하지 않으면 다른 나라 사람들은 한글로 작성된 문서를 열어 볼 수 없습니다.

각 나라의 문자표가 필요했고 호환성 문제를 해결해야 했습니다.

해결방안으로 유니코드, UTF-16, UTF-8 등 전 세계의 모든 문자들을 표현 가능하도록 하였습니다.

UTF - 8

  • 8bit(1byte) 기반, 가변길이 인코딩

  • 1byte ~ 4byte를 사용해서 문자를 인코딩

    • 1byte: ASCII, 영문, 기본 라틴 문자
    • 2byte: 그리스어, 히브리어 라틴 확장 문자
    • 3byte: 한글, 한자, 일본어
    • 4byte: 이모지, 고대문자등
  • 단점: 상대적으로 사용이 복잡함

    • UTF-8 에서는 각 문자가 가변 길이로 인코딩 되므로 작업이 복잡합니다.

현대의 사실상 UTF-8 이 표준 인코딩 기술이 된 이유

  • 저장 공간 절약과 네트워크 효율성

    • UTF-8 은 ASCII 문자를 포함한 많은 서양 문자에 대해 1바이트를 사용하며 영문 텍스트에서 UTF-16배 더 효율적이며 웹상에 문서의 80% 이상이 영문 문서이기 때문에 효율적이다.
    • 데이터를 네트워크로 전달할 때는 매우 큰 효율의 차이를 보입니다.
  • ASCII 와의 호환성

    • UTF-8 과 ASCII 와 호환되며 UTF-8 로 인코딩 된 테스트에서 ASCII 범위의 있는 문자는 기존 ASCII 와 동일한 방식으로 처리되며 호환성을 유지하면서도 전 세계의 모든 문자를 표현 가능합니다.
  • 2008년 W3C 웹 표준에 UTF-8 채택

  • 현재 대부분의 웹사이트와 애플리케이션에서 기본 인코딩으로 사용

참고: 한글 윈도우의 경우 기존 윈도우와 호환성 때문에 기본 인코딩을 MS949로 유지한다. 한글 윈도우도 기본 인코딩
을 UTF-8로 변경하려고 노력중이다.

문자 집합 조회

문자 집합을 사용해서 문자 인코딩을 사용하는 코드 예제입니다.

    public static void main(String[] args) {
        // 이용 가능한 모든 Charset 자바 + OS
        SortedMap<String, Charset> charsets = Charset.availableCharsets();
        for (String charsetName : charsets.keySet()) {
            System.out.println("charsetName = " + charsetName);
        }

        System.out.println("=====");

        // 문자로 조회(대소문자 구분X), MS949, ms949, x-windows-949
        Charset charset1 = Charset.forName("MS949");
        System.out.println("charset1 = " + charset1);

        // 별칭 조회
        Set<String> aliases = charset1.aliases();
        for (String alias : aliases) {
            System.out.println("alias = " + alias);
        }

        // UTF-8 문자로 조회
        Charset charset2 = Charset.forName("UTF-8");
        System.out.println("charset2 = " + charset2);

        // UTF-8 상수로 조회
        Charset charset3 = StandardCharsets.UTF_8;
        System.out.println("charset3 = " + charset3);

        // 시스템의 기본 Charset 조회
        Charset defaultCharset = Charset.defaultCharset();
        System.out.println("defaultCharset = " + defaultCharset);
    }

코드를 보면 java + Os 에서 사용가능한 Charset, charsetName 의 이름 및 별칭 조회, 문자 및 상수 그리고 기본 Charset 조회 등을 확인 가능합니다.

주요 메서드로는 다음과 같습니다.

Charset.forName() // 특정 문자 집합을 지정해서 찾을 때

StandardCharsets.UTF_8 // 자주 사용하는 집합은 상수로 지정되어 있다. 

public final class StandardCharsets {
   public static final Charset US_ASCII = sun.nio.cs.US_ASCII.INSTANCE;
   public static final Charset ISO_8859_1 = sun.nio.cs.ISO_8859_1.INSTANCE;
   public static final Charset UTF_8 = sun.nio.cs.UTF_8.INSTANCE;
   public static final Charset UTF_16BE = new sun.nio.cs.UTF_16BE();
   public static final Charset UTF_16LE = new sun.nio.cs.UTF_16LE();
   public static final Charset UTF_16 = new sun.nio.cs.UTF_16();
}

Charset.defaultCharset() // 현재 시스템에서 사용하는 기본 문자 집합을 반환

문자 인코딩 예제

Charset(문자 집합) 을 사용해서 실제 문제 인코딩을 해보겠습니다.


    private static final Charset EUC_KR = Charset.forName("EUC-KR");
    private static final Charset MS_949 = Charset.forName("MS949");
    
        public static void main(String[] args) {
        System.out.println("== ASCII 영문 처리 ==");
        encoding("A", US_ASCII);
        encoding("A", ISO_8859_1);
        encoding("A", EUC_KR);
        encoding("A", UTF_8);
        encoding("A", UTF_16BE);

        System.out.println("== 한글 지원 ==");
        encoding("가", EUC_KR);
        encoding("가", MS_949);
        encoding("가", UTF_8);
        encoding("가", UTF_16BE);
    }
    
    
    private static void encoding(String text, Charset charset) {
        byte[] bytes = text.getBytes(charset);
        System.out.printf("%s -> [%s] 인코딩 -> %s %sbyte\n", text, charset, Arrays.toString(bytes), bytes.length);
    }
  • 문자를 컴퓨터가 이해할 수 있는 숫자(byte)로 변경하는 것을 문자 인코딩이라 한다.

  • String.getByte(Charset) 메서드를 사용하면 String 문자를 byte 배열로 변경할 수 있다.

  • 이때 중요한 점이 있는데, 문자를 byte로 변경하려면 문자 집합이 필요하다는 점이다. String.getByte() 의 인자로 Charset 객체를 전달하여 원하는 문자 집합을 선택하여 byte 로 변환이 가능하다.

영문
US-ASCII, ISO-8859-1, EUC-KR, MS949, UTF-8은 모두 ASCII와 호환된다

한글
EUC-KR, MS949는 한글 인코딩에 2byte를 사용하고 같은 값으로 인코딩한다.

참고 : 자바에서의 byte 표현에서 음수는 메모리에
첫번째 bit에 1로 표현되어 저장되지만 
디코딩을 하게되면 음수로 표현된다. 

인코딩과 디코딩 그리고 다양한 예시에 대해 알아보겠습니다.


    private static final Charset EUC_KR = Charset.forName("EUC-KR");
    private static final Charset MS_949 = Charset.forName("MS949");

	public static void main(String[] args){
        System.out.println("== 영문 ASCII 인코딩 ==");
        test("A", US_ASCII, US_ASCII);
        test("A", US_ASCII, ISO_8859_1); // ASCII 확장(LATIN-1)
    	test("A", US_ASCII, UTF_16BE); // UTF_16 디코딩 실패
        
        
        System.out.println("== 한글 인코딩 - 기본 ==");
        test("가", US_ASCII, US_ASCII); // X
        test("가", ISO_8859_1, ISO_8859_1); // X
        test("가", EUC_KR, EUC_KR);
        test("가", UTF_8, UTF_8);
        
        System.out.println("== 한글 인코딩 - 디코딩이 다른 경우 ==");
        test("가", EUC_KR, MS_949);
        test("뷁", MS_949, EUC_KR); // 인코딩 가능, 디코딩 X
        test("가", EUC_KR, UTF_8); // X
        test("가", MS_949, UTF_8); // X
        test("가", UTF_8, MS_949); // X

        System.out.println("== 영문 인코딩 - 디코딩이 다른 경우");
        test("A", EUC_KR, UTF_8);
        test("A", MS_949, UTF_8);
        test("A", UTF_8, MS_949);
        test("A", UTF_8, UTF_16BE); // X
    }

    private static void test(String text, Charset encodingCharset, Charset decodingCharset) {
        byte[] encoded = text.getBytes(encodingCharset);
        String decoded = new String(encoded, decodingCharset);
        System.out.printf("%s -> [%s] 인코딩 -> %s %sbyte -> [%s] 디코딩 -> %s\n",
                text, encodingCharset, Arrays.toString(encoded), encoded.length,
                decodingCharset, decoded);
    }
    

해당 문자들을 디코딩 할 경우 ? 라는 특수문자가 나오게 되면 실패한다는 의미입니다.

인코딩과 디코딩의 Charset 의 경우의 수 에 따라 test 함수의 결과값이 다른것을 관찰할 수 있습니다.

한글의 인코딩과 - 디코딩이 다를 경우

  • 와 같이 자주 사용하는 한글은 EUC-KR, MS949 서로 호환된다

  • 한글을 인코딩할 때 UTF-8과 EUC-KR(MS949)는 서로 호환되지 않는다.

영어의 인코딩과 - 디코딩이 다를 경우

  • 영문 인코딩 같은 경우 ASCII에 포함되는 영문은 UTF-16을 제외한 대부분의 문자 집합에서 호환된다.

한글이 깨지는 가장 큰 2가지 이유

  • EUC-KR(MS949), UTF-8이 서로 호환되지 않음

    • 한글이 깨지는 대부분의 문제는 UTF-8로 인코딩한 한글을 EUC-KR(MS949)로 디코딩하거나 또는 EUCKR(MS949)로 인코딩한 한글을 UTF-8로 디코딩할 때 발생한다.
  • EUC-KR(MS949) 또는 UTF-8로 인코딩한 한글을 ISO-8859-1 로 디코딩 할 때

    • EUC-KR(MS949) 또는 UTF-8로 인코딩한 한글을 한글을 지원하지 않는 ISO-8859-1 로 디코딩 할 때 발생한다.

참고자료

inflearn - java adv

profile
이유를 찾아보자

0개의 댓글