자바, 스프링

오라클 MS949 Characterset에서 이모지 저장

malmijalls 2022. 9. 27. 11:44

 

프론트에서 글을 쓰고 저장한 뒤, 다시 글을 불러오는 과정에서 이모지가 ? ? 로 저장이 되는 문제가 발생했다.

 

프론트 -> 스프링 컨트롤러까지는 값이 잘 넘어오는데 db에 저장되는 순간 이모지가 ? ? 로 바뀌었는데 알고보니

오라클 db의 characterset이 MS949라 유니코드 이모지 저장이 안 되는 것이였다..

 

MS949가 유니코드 자체를 지원하지 않는 characterset인듯 한데

이미 운영중인 db의 세팅을 UTF-8로 변경하자니 기존 내용들을 전부 변환하기에는 큰 일이고

 

national characterset이 UTF-16이어서 일부 테이블의 열 속성만 NCLOB으로 변경할까 싶었지만

db쪽은 잘 모르는 상태에서 건드리지 않는게 좋겠다고 생각되어서

 

결국 백엔드나 프론트에서 해결해야 하는 상황이었다.

 

https://meetup.toast.com/posts/317

 

Java에서의 Emoji처리에 대해 : NHN Cloud Meetup

Java에서의 Emoji처리에 대해

meetup.toast.com

 

위 블로그에서 상세히 설명이 되어있는데 초보개발자 입장에서는 한번에 이해하기가 어려웠다.

 

여기저기 찾아보고 나서야 저 블로그의 글을 이해할 수 있었는데,

위 글을 참조하여 16진수 BMP를 참고하여 이모지를 판별하고,

유니코드 문자로 변환하는 코드를 만들 수 있었다.

 

유니코드 변환 자체를 프론트에서 한 다음 컨트롤러로 보내는 방법도 있을 거 같은데

아직 자바스크립트쪽을 잘 몰라서 일단 자바로 구현했다.

자바스크립트로 구현한 방법은 뒤에 작성되어있습니다.

//테스트 문자열
String s = "test 한글 🀀😊✅❤✔\\asde!#@(";
System.out.println("원본 문자:"+s);

//이모지,서로게이트->유니코드 문자로 변환
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
    //이모지 확인
      if (shouldUnicode(s, i)) {
         //해당 문자의 10진수 값
        Integer res = Character.codePointAt(s, i);
        //서로게이트는 4바이트, char 2개로 표현되므로 인덱스에 1을 더함.
        if(Character.isSurrogate(s.charAt(i)))
            i++;
        //유니코드(16진수로 변환) 저장, 앞에는 \\u{ 를 붙이고 뒤에는 }를 붙여 유니코드임을 구분(실제 들어가는 백슬래시는 1개!)
        sb.append("\\u{" + Integer.toHexString(res).toUpperCase()+"}");
      } else {
        sb.append(s.charAt(i));
      }
    }

System.out.println("유니코드:"+sb.toString());


private static boolean shouldUnicode(String s, int i) {
//문자 10진수 16진수(확인용)
System.out.println(s.charAt(i)+" "+s.codePointAt(i)+" "+Integer.toHexString(s.codePointAt(i)));

//16진수가 1000 미만(영어, 숫자 등)인 경우 변환x
if(s.codePointAt(i)<4096) return false;

//16진수 -> 10진수 변환
int val = Integer.parseInt(String.valueOf(Integer.toHexString(s.codePointAt(i))).substring(0,2),16 );

//4byte의 surrogate 유니코드들,
//혹은 hex 범위가 20-27로 시작하거나, 29-2B, 32,33로 시작하는 symbol이면 변환
if( Character.isSurrogate(s.charAt(i)) || (val >= 32 && val <= 39) || (val>=41 && val <=43)  || val==46 || val==50 || val==51 || val==77 ) {
    return true;
}
else 
    return false;
}

 

참고로 프론트에 뿌릴 때  \u{} 와 같은 형식으로 자바스크립트를 거쳐 뿌려주면

자동으로 해당 유니코드를 이모지로 해석해준다.

 

이모지 뿐 아니라 해당 characterset에서 지원하지 않는 언어도

위 블로그에 있는 BMP표를 참고하여 변환해주면 될것 같다.

 

또한 해당 코드를 만들어보면서 서로게이트라는 것도 처음 알게 되었다(링크 글 참조)

 

 

 

자바에서 유니코드 -> 이모지 변환을 확인해보고 싶다면 아래와 같이 입력하면 된다.

//유니코드 문자 -> 이모지 문자로 다시 변환

String uni = sb.toString(); //유니코드로 변환했던 문자
StringBuilder sb2 = new StringBuilder();

for(int i=0; i<uni.length(); i++) {
    if(uni.charAt(i) == '\\' &&  uni.charAt(i+1) == 'u'){    
        //유니코드는 4자리일수도, 5자리일수도 있음
        char[] c = Character.toChars(Integer.parseInt(uni.substring(i+3, uni.indexOf('}', i+1)), 16));
        sb2.append(c);
        i=uni.indexOf('\\', i+2)-1;
    }else{
        //일반문자는 그냥 저장
        sb2.append(uni.charAt(i));
    } 
}
System.out.println("다시 변환:"+sb2.toString());

 

 

 

+) 나중에 생각해보니 코드를 판별하는 부분에서

굳이 16진수를 10진수로 변환할 필요 없이 바로 16진수끼리 비교하면 되는데

왜 그 생각을 못했지 싶다..

 

--수정--

자바스크립트로 구현하면 다음과 같다.

프론트에서 서버로 전송할때 유니코드를 문자열로 변환하는 함수를 이용하고, 다시 프론트로 불러올때 유니코드를 해석하는 함수를 이용하면 된다.

/*이모지 범위 정규식 */
const regex =  /[\u{1d300}-\u{1d7ff}\u{1ee00}-\u{1eeff}\u{1f000}-\u{1f9ff}\u{2000}-\u{27ff}\u{2900}-\u{2bff}\u{2e00}-\u{2eff}\u{3200}-\u{33ff}]/ug;
 
 /*이모지 -> 유니코드 string 변환 */
 function emojiToUnicodeString(str){
     var converted = str.replace(regex, match => `\\u{${match.codePointAt(0).toString(16)}}`);
     return converted;
 }; 
           
 /*유니코드string -> 이모지 변환 */
 function unicodeStringToEmoji(text) {
  
     return text.toString().replace(/\\u{[\dA-F]{4}}/gi,  function(match) {
             return String.fromCharCode(parseInt(match.replace(/\\u{/g, '').replace(/}/g, ''), 16));
         } 
     ).replace(/\\u{[\dA-F]{5}}/gi, function(match) {
             var tmp1 = match.replace(/\\u{/g, '').replace(/}/g, '');
             var tmp2 = '0b'+(parseInt(tmp1, 16).toString(2)).padStart(8, '0'); 
             
             tmp2 = tmp2  - 0x10000;
             
             var padding = tmp2.toString(2).padStart(20, '0'); 
             const c1 = Number.parseInt(padding.substr(0, 10), 2) + 0xD800;
             const c2 = Number.parseInt(padding.substr(10), 2) + 0xDC00;
             const result = String.fromCharCode(c1,c2);
             return result;
         }
     );
 }