ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [etc.] 유니코드를 통한 한글 자모 분리 알고리즘
    쾌락없는 책임 (공부)/짜잘쓰 2023. 1. 10. 17:42
    반응형

    개요

    심심풀이로 만드는 프로젝트에서 단어의 자모를 분리해야 하는 알고리즘을 짜야 했습니다. 그래서 '단어' 라는 놈이 있으면 'ㄷㅏㄴㅇㅓ' 이런 식으로 쪼개야 하는 것이죠. 영어의 경우에는 알파베이 조합이 되지 않아 단순히 잘라주면 되지만 한글은 조금 복잡하게 되어서 이를 위한 보편적인 알고리즘이 필요했습니다.

     

    때문에 이를 위해서 '유니코드' 에서 한글이 어떻게 구성이 되는지, 이러한 원리를 통해서 어떻게 자모 분리를 할 수 있는지를 알아보도록 하겠습니다.

     

     

    전제 1 : 한글의 초성, 중성, 종성

    세종대왕님께서 한글을 만들 때와는 모양이 달라졌지만 현대 한글에서는 초성 19개, 중성 21개, 종성 28개가 있습니다.

    [혹시나 복붙하시는 분들을 위해]
    ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ
    ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ
     ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ
    • 초성 : ㄱㄲㄴㄷㄸ / ㄹㅁㅂㅃㅅ / ㅆㅇㅈㅉㅊ / ㅋㅌㅍㅎ   (19개)
    • 중성 : ㅏㅐㅑㅒㅓ / ㅔㅕㅖㅗㅘ / ㅙㅚㅛㅜㅝ / ㅞㅟㅠㅡㅢ / ㅣ    (21개)
    • 종성 : ㄱㄲㄳㄴㄵ / ㄶㄷㄹㄺㄻ / ㄼㄽㄾㄿㅀ / ㅁㅂㅄㅅㅆ / ㅇㅈㅊㅋㅌ / ㅍㅎ (공백)     (28개)

    이 중 종성의 경우에는 '가' 같은 단어를 위해 공백을 고려해야 합니다.

     

     

    전제 2 : 옛 한글은 고려하지 않음

    그렇습니다. 현대 국어에 맞추는 목적이므로 ᅘ 같은 옛 글자들은 고려하지 않습니다. 때문에 유니코드에서 고려한 현대 한글 글자는 11172 글자입니다.

     

     

    그럼 유니코드의 구성은 어떻게 되는가

    아래 블로그의 표가 이해를 가장 잘 돕는거 같습니다.

     

    유니코드(UTF-8) 한글 코드표, 한글코드 범위 {AC00-D7AF}

    유니코드(UTF-8) 한글 코드표, 한글코드 범위 {AC00-D7AF} U+AC00 to U+AD00 0 1 2 3 4 5 6 7 8 9 A B C D E F UTF8: 234, 176, 128; UNICODE: AC0 가 각 갂 갃 간 갅 갆 갇 갈 갉 갊 갋 갌 갍 갎 갏 UTF8: 234, 176, 144; UNICODE: AC1 감

    jjeong.tistory.com

    일단 유니코드의 '가' 와 관련된 순서를 보는걸 추천드립니다. '가' 부터 시작해서 '갛' 까지 순서대로 온 뒤 이후 'ㅏ'를 'ㅐ'로 변경해서 순서를 넣어놨습니다. 이를 통해서 종성, 중성, 초성 순으로 변경하게 되는 것입니다.

     

    그러면 '가'에서 '개' 까지는 28의 숫자가 차이나게 되고 '가' 에서 다음 초성인 '까'는 21 * 28 차이인 588 차이가 나게 됩니다. 물론 유니코드 상으로요.

     

    그러면 '깍'은 까 에서 1 증가한 코드 '\UAE4D' 입니다. 여기서 '초성'이 몇번째 초성인지를 어떨게 알 수 있을까요? 이 부분은 아주 간단하게 '가' 와의 차이에서 중성, 종성 수로 나누어주면 됩니다.

    1. '깍'의 유니코드 AE4D는 10진수로 44621
    2. '가'의 유니코드는 AC00으로 10진수로 44032
    3. 이 둘을 빼면 44621 - 44032 = 589
    4. 이를 초성, 종성 수로 나누면 589 / 588 = 1. ....
    5. 그래서 초성의 인덱스는 1번이다!

    이렇게 초성의 인덱스를 구할 수 있게 되는 것입니다. 한마디로 초성이 변경될 때마다 유니코드가 588증가하므로 이를 이용한 계산이라고 할 수 있습니다.

     

    이후 중성, 종성의 인덱스도 경우에도 이를 통해서 알아낼 수 있게 됩니다. 문자의 유니코드 'unicode'가 있고 '가'의 유니코드가 0xAC00 이라 하면

    • 초성 : (unicode - 0xAC00) / 21 / 28
    • 중성 : (unicode - 0xAC00 - (초성 * 21 * 28)) / 28
    • 종성 : unicode - 0xAC00 - (초성 * 21 * 28) - (중성 * 28)

    의 식을 통해서 인덱스를 만들 수 있게 되는 것입니다. 이런식으고 종성, 중성, 초성으로 변경된다는 사실을 이용해 인덱스를 추출할 수 있게 됩니다.

     

     

    코드로 써본 알고리즘

    이제 이론을 끝냈으니 코드를 써보겠습니다. 프로젝트가 원리만 알아내는 간단한 플젝이라 유니티로 작업해 C#으로 작성하게 되었습니다. 그래도 얼추 읽을만한 코드라고 생각됩니다.

     

    private const int KOR_UNICODE_START = 0xAC00;  
    public string input;

     

    일단 클래스에 '가'의 시작 코드 그리고 문자열 input을 정의해 줍니다.

     

    List<char> FirstSeong = new List<char>()
    {
        'ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ',
        'ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'
    }; // 19
    
    List<char> MidSeong = new List<char>()
    {
        'ㅏ','ㅐ','ㅑ','ㅒ','ㅓ','ㅔ','ㅕ','ㅖ',
        'ㅗ','ㅘ','ㅙ','ㅚ','ㅛ','ㅜ','ㅝ','ㅞ',
        'ㅟ','ㅠ','ㅡ','ㅢ','ㅣ'
    }; // 21
    
    List<char> EndSeong = new List<char>()
    {
        'ㄱ','ㄲ','ㄳ','ㄴ','ㄵ','ㄶ','ㄷ','ㄹ',
        'ㄺ','ㄻ','ㄼ','ㄽ','ㄾ','ㄿ','ㅀ','ㅁ',
        'ㅂ','ㅄ','ㅅ','ㅆ','ㅇ','ㅈ','ㅊ','ㅋ',
        'ㅌ','ㅍ','ㅎ'
    }; // 27 공백 제거

     

    이후 인덱스를 위해서 초성, 중성, 종성의 리스트를 3개 준비했습니다. 글 띄우기나 변수 이름이 이상하지만 리팩토링은 나중에 한다고 생각해요.

    또한 종성에서 공백은 고려하지 않으니 공백은 제거했습니다.

     

    public string SliceString()
    {
        StringBuilder result = new StringBuilder();
    
        for(int i = 0; i < input.Length; i++)
        {
            if(input[i] == ' ')
            {
                result.Append(' ');
                continue;
            }
            SliceOneKorWord(input[i], result);
        }
    
        Debug.Log($"{result.ToString()}");
        return result.ToString();
    }

     

    이후 문자열을 자르는 함수를 준비합니다. 일단 변경이 많을것으로 예상되니 StringBuilder를 사용하고 이후 문자열의 각 문자를 하나하나 보도록 합니다. 자모 분리의 경우 아래 나올 함수에서 하게 될겁니다. 

     

    void SliceOneKorWord(char word, StringBuilder array)
    {
        int indexGap = word - KOR_UNICODE_START;
    
        int first = indexGap / 21 / 28;
        int mid  = (indexGap - (first * 21 * 28)) / 28;
        int end   = indexGap - (first * 21 * 28) - (mid * 28) - 1;
    
        array.Append(FirstSeong[first]);
        array.Append(MidSeong[mid]);
    
        if(end > 0)
            array.Append(EndSeong[end]);
    }

     

    이후 공식을 통해서 인덱스에 해당하는 부분들을 넣어줍니다. 이러면 분리가 잘 되게 됩니다. 유니티에서는 UTF-8 방식으로 유니코드를 저장해주기에 별 문제 없이 알고리즘을 코드로 잘 쓸 수 있었습니다.

    반응형

    '쾌락없는 책임 (공부) > 짜잘쓰' 카테고리의 다른 글

    L value & R value - 둘 간의 차이점  (2) 2021.12.20

    댓글

Designed by Tistory.