Spring Response Encoding UTF-8, EUC-KR, KSC5601
포스트
취소

Spring Response Encoding UTF-8, EUC-KR, KSC5601

Keywords: Spring framework, Kotlin, UTF-8, EUC-KR, KSC5601, KS X 1001, HttpMessageConverter, ObjectMapper, JsonSerialize

Response DTO의 인코딩에 대해서 알아볼 일이 있었다. Spring Framework에서 Response DTO를 JSON String & ByteArray로 serialize하는 구조에 대해서 알아보게 되었다.

  • Response body의 encoding을 customize하려면 어떻게 해야할까?
  • 서버 전체의 encoding을 변경하는 것은 영향 범위가 너무 크다. Response body의 특정 필드의 encoding만 변경할 수 있을까?

UTF-8, EUC-KR, KSC5601

해결하고자 하는 문제는, 기존에 UTF-8로 인코딩되던 값을 KSC5601로 인코딩하는 것이었다.

Spring 이야기를 하기 전에, 한글 인코딩 방식들의 차이부터 알아보자.

생각해보니 과거에 인코딩 관련하여 정리한 포스트가 있었다: https://ssaemo.tistory.com/28

당시에는 EUC-KR 인코딩에 대해서 대략적으로만 이해하고 넘어갔었다

Kotlin/JVM에서 String->ByteArray 변환 구현: String.toByteArray(Charset)

Kotlin/JVM에서는 toByteArray(Charset) extension 함수를 통해 String을 encode 할 수 있다.

java.nio.charset.Charset에는 java.nio.charset.StandardCharsets가 포함된다.

1
"가나다라".toByteArray(Charset.forName("EUC-KR"))

인코딩된 ByteArray를 hex 값으로 보고싶다면 아래 함수를 사용할 수 있다.

1
2
3
4
5
6
7
fun printIt(string: String) {
    println(string)
    for (i in string.indices) {
        print(String.format("U+%04X ", string.codePointAt(i)))
    }
    println()
}

그럼 Charset의 name을 KSC5601로 지정하면 되는걸까? 그렇지 않다. KSC5601로 호출 시, EUC-KR Charset이 나온다.

1
"가나다라".toByteArray(Charset.forName("KSC5601")) // same as EUC-KR

EUC-KR과 다른 인코딩 방식들의 차이

아래 문서들을 참고했다

UTF-8

  • 유니코드를 8 bit(1 byte) 단위로 인코딩한 것.
  • ASCII code는 1바이트로 처리된다. 한글 1글자를 3바이트로 표현

EUC-KR

  • EUC-KR = KSC5601(한글) + KSC5636(영문)
  • KSC5601로 한글을 표현하기 때문에 모든 한글을 표현하지 못한다.

EUC: Extended Unix Code

KSC5601-87 (KS X 1001)

  • 전각문자, 기호, 한글, 한자 등을 표현 가능
  • 모든 한글을 표현하지 못함
  • 한글 1글자를 2바이트로 표현 (2바이트 완성형)
  • ASCII code를 포함하지 않는다 (반각 영문자를 표현할 수 없음)

나무위키: 완성형

조합형? 위키피디아에서: 한글 부분은 기본적으로 2바이트 완성형 코드이지만, 부속서 3에서 2바이트 조합형 코드도 보조 부호계로서 규정되어 있다.

한글 조합형 인코딩

  • MSB(가장 좌측 비트)는 항상 1이다
  • 초성, 중성, 종성을 각 5비트로 변환한다
  • 1 + 초성 (5 bits) + 중성 (5 bits) + 종성 (5 bits) = 16 bits = 2 bytes
  • 참고

특정 한글의 KSC5601 인코딩 hex 값이 궁금하다면 이 페이지를 참조: https://crazybrain.tistory.com/33

KS C 5601은 KS X 1001의 옛 이름이라고 한다: https://ko.wikipedia.org/wiki/KS_X_1001

CP949 (Code Page 949)

  • EUC-KR의 확장
  • 한글 windows 메모장 저장 시 ANSI가 이에 해당

한글 인코딩 방식 차이 요약

  • 설명이 길었다. 요약
  • UTF-8과 EUC-KR(KSC5601)의 한글 인코딩은 바이트 수도 다르고, KSC5601 한글 완성형은 UTF-8 한글 인코딩과 호환되지 않는다.
  • EUC-KR은 KSC5601(KS X 1001)을 포함한다.

Spring framework와 JSON serialization, ObjectMapper

fastxml의 Jacson ObjectMapper 클래스가 있다. Spring은 이 클래스를 사용하여 JSON 등 mapping을 구현했다.

JsonSerializer interface를 구현하여 ObjectMapper serialization을 customize할 수 있는데, 기본적으로 String으로 serialize한다.

위와 같이 특수한 경우에는 String이 아닌 ByteArray로 직접 serialize할 수 있다.

1
2
3
4
5
6
7
8
9
class MyStringSerializer : JsonSerializer<String>() {
    override fun serialize(value: String, gen: JsonGenerator, serializers: SerializerProvider) {
        val converted = convertHalfToFullWidthCharacter(value)
        gen.writeString(converted)

        // val bytes = convertToSomething(value)
        // gen.writeUTF8String(bytes, 0, bytes.size)
    }
}

Spring MVC HttpMessageConverter

ObjectMapper보다 좀더 상위 수준에서, HTTP response body의 변환은 HttpMessageConverter interface에서 담당한다

Jackson의 HttpMessageConverter 구현체

HttpMessageConverter의 구현체가 MappingJackson2HttpMessageConverter이다. 이 클래스의 구현이 궁금하다면 AbstractJackson2HttpMessageConverter.writeInternal() 메소드를 보자.

HttpMessageConverter.write() 메소드 파라미터 중 HttpOutputMessage가 있는데, AbstractGenericHttpMessageConverter.write()의 구현을 보면 HttpOutputMessage.getHeaders()를 통해 HttpHeaders 객체를 얻은 후 addDefaultHeaders() 메소드를 통해 헤더 객체 변경하니 테스트 등에서 HttpOutputMessage 파라미터 구현 시 유의하도록 하자. (default header가 여기에 설정됨. e.g. Content-Type header와 default charset)

  • AbstractJackson2HttpMessageConverter.ENCODINGS (com.fasterxml.jackson.core.JsonEncoding enum의 값들을 포함)
  • UTF8~UTF32만 존재함. EUC-KR은 존재하지 않음 (EUC-KR과 같이 매칭되는 encoding을 찾지 못할 경우 UTF-8)

JsonSerialize

Jackson에서 JsonSerialize라는 어노테이션을 제공한다. 이를 통해 특정 필드에 어떤 serializer를 사용할 것인지 지정할 수 있다. (key, value 각각 설정 가능)

1
2
3
4
class MyResponse(
    @JsonSerialize(using = KSC5601Serializer::class)
    val s: String,
)

Conclusion

허무하지만 나중에 알고보니 오류 원인은 인코딩 문제가 아니었다.

하지만 과정에서 여러 가지 지식을 배울 수 있었다.

  • Kotlin/JVM String->ByteArray 변환 방법
  • 다양한 한글 인코딩 방식들의 차이
  • Spring Framework에서 encoding, serialization을 처리하는 구조 (HttpMessageConverter, ObjectMapper의 역할)
  • Jackson @JsonSerialize 어노테이션

문제 해결 과정에서 ChatGPT의 도움도 받아보았는데, 결과적으로는 ChatGPT가 도움이 된 것 같기도 하고, 잘못된 정보로 방해가 된 것 같기도 하고…

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

[TIL] HTTP Redirection with location header

Spring WebFlux WebClient Builder bean 사용 이유 (ObjectMapper bean)