수 많은 우문은 현답을 만든다

한글이 깨질때 본문

개발지식/Springboot

한글이 깨질때

aiden.jo 2022. 7. 27. 15:34

안녕하세요 조영호입니다.
오늘은 한글 인코딩으로 이틀간 고생했던 경험을 공유하고자 합니다. 혹시라도 누군가에게 도움이 되시길 바라면서..

 

상황 설명

저는 VM 2개와 Rabbitmq 1개 사이에서 데이터를 주고받는 서비스를 개발하고 있었습니다. VM들에는 각각 A서비스, B서비스가 실행되고 있는 상태였습니다. 한글 인코딩 문제는, A서비스에서 "message" : "한글메세지" 형태로 메세지를 Rabbitmq로 보내서 B서비스에서 메세지를 Json Parsing 할때 예외가 발생했습니다.

 

 

예외 내용

예외의 내용은 대략 아래와 같았습니다.
Json Parsing Exception : ':' is not expected value on "message" : "+#)!#*(":#(@".

메세지가 전달되는 과정에서 message값의 한글이 깨졌고 그 중에 double quotation(")이 한개 포함되는 바람에 예외가 발생했습니다.

 

 

원인 추적

수 많은 경우의 수를 의심해보았고 제게는 많은 경험이 되었습니다.


1. Rabbitmq 로 메세지를 주고 받을때는 byte[]로 담아서 오기 때문에 보낼때 UTF8로 보내는 것을 까먹지 않았나?

return message.getBytes(StandardCharsets.UTF_8);

-> 아니다. 분명 최종 결과의 캐릭터셋을 UTF8로 설정 해 놓았다.

 

2. 혹시 서버의 언어 설정때문에 그런걸까?

-> 아니다. Linux 명령어 locale 을 입력하면 서버의 언어 설정을 확인할 수 있는데 en_US.utf8에서 ko_KR.UTF-8로 바꾸었으나 깨지는 것은 마찬가지였다.

$ locale
LANG=ko_KR.UTF-8
LANGUAGE=ko_KR



3. 조금 더 범위를 좁혀서, VM에 떠있는 JVM의 언어 설정 때문이 아닐까?

-> 아니다. Java application을 실행할때 JVM Option으로 -Dfile.encoding=UTF-8 -Dclient.encoding.override=UTF-8 을 추가해보았지만 문제는 해결되지 않았다.

 

 

4. 자 이제는 애플리케이션 문제라는건데.. Springboot application 자체에서 인코딩을 설정하면 해결이 될까?

server.servlet.encoding.charset: UTF-8

-> 아니다. application.properties 에 UTF8 설정을 할 수 있으나 이것으로도 해결되지 않았다.

 


5. 시스템 간의 데이터 전달이니까 Base64 decoding 을 빼먹은건 아닐까?

-> 아니다. 애초에 A서비스에서 Base64 encoding을 하지 않았기 때문에 decoding 문제는 아니다.

 

6. 오리무중. 포기하고 싶은 마음이 굴뚝같다!!!! 정말!!

잠시 후 머리를 식히고 조금 더 범위를 좁혀볼 수 있을지 생각해보았다... 여러분은 의심되는 것이 떠오르셨습니까?

-> 정답은 1번에 있었다.

 

필자는 분명 1번에서 최종 반환값을 UTF8로 전달했다.
조금 더 이전에 동작하는 로직을 살펴보니, 팀 시니어분이 Freemarker를 사용해서 Map에 message를 담는 로직을 추가했던 것이다.

* 프리마커란, 특정 자바 객체의 데이터를 생성해서 freemarker 템플릿(앞에서 말한 특정 객체의 이름)에 넣어주면 freemarker에서 템플릿에 맞게 데이터를 넣어준다. (조금 오래된 기술인지 찾아보면 front-end에서만 많이 사용하고 있는 것 같다)

 

프리마커를 사용한 것이 문제라는 것이 아니고, 여기서 message 값을 넘겨줄때 특수문자 등에 backslash('\')를 넣어주는 방어로직이 없었던 것이다. 사실 한글이 서비스 간에는 깨질 수 있지만 실제 메세지가 나갈때는 UTF8 처리가 되어 한글로 잘 나간다. (중요한 사실)

 

 

문제 해결

자 그럼 다음과 같이 모든 경우의 수를 치환해줘야 할까?

message.replace('"', '\"' ... ...


아니다. 프로그램을 짤때 항상 완벽하다고 생각하지 않는 습관이 있어서 이미 검증된 라이브러리가 없을까 탐색해보았다.

오늘 나를 살린 주인공은 StringUtil.javaStringEnc() 메소드다. 사용 방법은 아래와 같다.

map.put("message", StringUtil.javaStringEnc(message));

 

javaStringEnc()이 궁금한 분은 아래 내용을 펼쳐보시면 됩니다!

더보기
public static String javaStringEnc(String s) {
    int ln = s.length();
    for (int i = 0; i < ln; i++) {
        char c = s.charAt(i);
        if (c == '"' || c == '\\' || c < 0x20) {
            StringBuilder b = new StringBuilder(ln + 4);
            b.append(s.substring(0, i));
            while (true) {
                if (c == '"') {
                    b.append("\\\"");
                } else if (c == '\\') {
                    b.append("\\\\");
                } else if (c < 0x20) {
                    if (c == '\n') {
                        b.append("\\n");
                    } else if (c == '\r') {
                        b.append("\\r");
                    } else if (c == '\f') {
                        b.append("\\f");
                    } else if (c == '\b') {
                        b.append("\\b");
                    } else if (c == '\t') {
                        b.append("\\t");
                    } else {
                        b.append("\\u00");
                        int x = c / 0x10;
                        b.append((char)
                                (x < 0xA ? x + '0' : x - 0xA + 'a'));
                        x = c & 0xF;
                        b.append((char)
                                (x < 0xA ? x + '0' : x - 0xA + 'a'));
                    }
                } else {
                    b.append(c);
                }
                i++;
                if (i >= ln) {
                    return b.toString();
                }
                c = s.charAt(i);
            }
        } // if has to be escaped
    } // for each characters
    return s;
}

 

항상 알보나면 친절한 자바의 세계다.

부족한 저처럼 너무 많은 고생을 하지 마시고 바로 원인을 찾으시는데 도움 되시길 바랍니다.




감사합니다. 

 

 

 

[참고]
https://sjh836.tistory.com/132

https://effectivesquid.tistory.com/entry/Base64-%EC%9D%B8%EC%BD%94%EB%94%A9%EC%9D%B4%EB%9E%80