오라클 MS949 Characterset에서 이모지 저장
프론트에서 글을 쓰고 저장한 뒤, 다시 글을 불러오는 과정에서 이모지가 ? ? 로 저장이 되는 문제가 발생했다.
프론트 -> 스프링 컨트롤러까지는 값이 잘 넘어오는데 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;
}
);
}