한글 오토마타

프로그래밍 2009.11.04 23:30 Posted by 아일레프

최근 한/영 자동 고침을 지원하는 TextBox를 만들어야 했다. Low level api를 사용하지 않고 단순한 문자열 변환만을 이용해 한/영 고침을 지원하기 위해 어쩔 수 없이 한글 Automata가 필요하게 되었다. 예를 들어, tkfkd 이라고 사용자가 입력했을 때 "사랑"이라고 출력이 나와야 하는데 이는 다음과 같은 동작을 통해 이루어 질 수 있다.

tkfkd -> ㅅㅏㄹㅏㅇ -> 사랑

이 때 ㅅㅏㄹㅏㅇ -> 사랑 으로 변환하기 위해 사용되어야 하는 것이 한글 Automata이다. 웹검색을 통해 알아봤는데 두벌식 한글 입력을 위해서 imhangul에서 사용한 Automata 사이트에 있는 아래 Automata가 가장 괜찮아 보였다.

 

하지만 위 Automata는 키보드의 즉각적인 입력을 바로 한글로 나타내는 것에 목적이 있지만 내가 만들어야 하는 것은 처음과 끝이 정해진 한글 호환 자모로만 이루어진 문자열을 한글로 변경하는 것이기에 위 사이트의 Automata보다 간편해질 수 있는 여지가 있다. 또한 위 Automata는 다음 두가지 측면에서 나와 같은 초보 프로그래머가 구현하기 까다로운 측면이 있었다.

  1. 특정 State는 이전 State의 정보를 필요로 한다.

    위 사이트를 가보면 CxSy이라는 것은 x State까지의 입력을 Commit하고, y State로 이동하는 것을 의미한다고 되어있다. 위 그림에서 5번 State를 보자 5번 state에서 모음입력이 오면 C3S3이므로 3번 State까지의 입력을 Commit하고 3번 State로 이동하게 된다. 이는 9번 State도 마찬가지이다. 자, 어떻게 state가 이전 state의 정보를 가지고 있게 할 것인가? 내 머리로 깔끔한 해결책을 찾기에는 무리였다.

  2. 각 State의 동작을 정하는 분기 조건이 일치하지 않는다.

    위 그림을 보면 특정 State는 분기 조건이 자음입력, 모음 입력 밖에 없는데, 어떤 State는 분기 조건이 v, vc, l, t 4가지 이다. 분기조건이 C, vc, v 3가지인 State도 있다. 이것은 내가 프로그램을 짜기 곤란하게 만들었는데 왜냐면 나는 각 State를 특정 분기가 일어났을 때의 행동을 정의하는 특정 Interface를 구현하게 해서 최종적으로 StatePattern을 사용하고 싶었기 때문이다.

    그래서 새로운 한글 Automata를 구현하기로 결정했다. 그 녀석은 다음과 같다.

    쩝, Automata라기 보다는 State로 표현된 흐름도라고 보는 게 좋을 것 같다. 워낙 허접해서..

    State를 표현하는 원에는 숫자가 있는데 이는 각 State가 관심이 있는 버퍼의 index를 말한다. 그리고 분기조건은 버퍼[관심있는 index]가 자음인지, 모음인지, 글자 end인지 판단하는 것이다.

    또한 Commit, Canbe중C, Canbe자C, Combine등은 함수를 칭한다. 다음과 같은 동작을 한다.

  3. Commit(bu[0~n]) : 지정된 버퍼의 한글 호환 자모를 꺼낸 후 조합해 출력에 더한다. 예를 들어서 Commit(bu0,1)은 버퍼 0번 1번에 있는 한글 호환 자모를 조합해 출력에 더한다는 것이다. Commit(ㅁㅏㅁ)이면 '맘'을 반환한다.
  4. Canbe중C(모음1, 모음2) : 모음 1, 모음 2가 복모음으로 조합될 수 있는 지 검사한다. Canbe중C(ㅗ, ㅏ)이면 true를 반환하게 된다.
  5. Canbe자C(자음1, 자음2) : 자음 1, 자음 2가 복자음으로 조합될 수 있는 지 검사한다. Canbe자C(ㄹ, ㅁ)이면 true를 반환하게 된다.
  6. Combine(문자, 문자) 는 복모음, 복자음으로 조합 가능한 두 한글 호환 자모를 입력 받아 조합한 뒤 반환한다. 예를 들어 Combine(ㄹ, ㅁ)은 ㄻ을 반환한다.

    버퍼가 [ㅅㅏㄹㅏㅇ]이었을 때 동작과정을 보면 다음과 같다.

  7. 0 State (관심 index : 0) - 버퍼[0]이 자음이므로 1자 State로 이동
  8. 1자 State (관심 index : 1) - 버퍼[1]이 모음이므로 2모 State로 이동
  9. 2모 State (관심 index : 2) - 버퍼[2]이 자음이므로 3자 State로 이동
  10. 3자 State (관심 index : 3) - 버퍼[3]이 모음이므로 Commit(bu0,1)를 수행하고 0 State로 이동한다.

    버퍼 - [ㄹㅏㅇ], 출력 - 사

  11. 0 State (관심 index : 0) - 버퍼[0]이 자음이므로 1자 State로 이동
  12. 1자 State (관심 index : 1) - 버퍼[1]이 모음이므로 2모 State로 이동
  13. 2모 State (관심 index : 2) - 버퍼[2]이 자음이므로 3자 State로 이동
  14. 3자 State (관심 index : 3) - 버퍼[3]이 BufferEnd이므로 Commit(bu0,1,2)를 수행하고 0State로 이동한다.

    버퍼 - [], 출력 - 사랑

  15. 0State (관심 index : 0) - Buffer End이므로 Automata를 종료한다.

      

    버퍼가 [ㅁㅏㄹㅁㅇㅡㅁ] 이었을 때의 동작과정은 다음과 같다.

  16. 0 State (관심 index : 0) - 버퍼[0]이 자음이므로 1자 State로 이동
  17. 1자 State (관심 index : 1) - 버퍼[1]이 모음이므로 2모 State로 이동
  18. 2모 State (관심 index : 2) - 버퍼[2]이 자음이므로 3자 State로 이동
  19. 3자 State (관심 index : 3) - 버퍼[3]이 자음이므로 4자 State로 이동
  20. 4자 State (관심 index : 4) - 버퍼[4]이 자음이므로 CanBe자C(bu2,3)인지 검사, CanBe자C(ㄹ,ㅁ)이므로 true, 따라서 버퍼 2,3을 합한다. 이 때 버퍼는 [ㅁㅏㄻㅇㅡㅁ] 가 된다. 그리고 Commit(bu0,1,2)를 수행한 후 State0으로 이동한다.

    버퍼 - [ㅇㅡㅁ] 출력 - 맑

  21. 0 State (관심 index : 0) - 버퍼[0]이 자음이므로 1자 State로 이동
  22. 1자 State (관심 index : 1) - 버퍼[1]이 모음이므로 2모 State로 이동
  23. 2모 State (관심 index : 2) - 버퍼[2]이 자음이므로 3자 State로 이동
  24. 3자 State (관심 index : 3) - 버퍼[3]이 BufferEnd이므로 Commit(bu0,1,2)를 수행하고 0State로 이동한다.

       

    버퍼 - [], 출력 - 맑음

      

    이제는 구현만 남았다. Commit, Canbe중C, Canbe자C, Combine각각의 함수를 만들고 각 State를 다음의 Interface를 구현하게 한다.

    public interface IHangulAutomataState

    {

    HangulAutomataContext HangulContext { get; set; }

    IHangulAutomataState NextState { get; }

    int InteresteBufferIndex { get; }

    void DoStateActionWhenConsonantComes();

    void DoStateActionWhenVowelComes();

    void DoStateActionWhenBufferEndComes();

    }

    public class HangulAutomataContext

    {

    public List<char> Buffer { get; set; }

    public string OutString { get; set; }

    }

    그리고 HangulAutomata는 다음과 같이 구현하면 되겠다.

       

    public class HangulAutomata

    {

    public static string ConvertToCompleteHangul(string sentence)

    {

    HangulAutomataContext context = new HangulAutomataContext { Buffer = sentence.ToCharArray().ToList(), OutString = string.Empty };

    IHangulAutomataState automataState = HangulAutomataFactory.GetAutomataState(0, JamoType.None, context);

    while (true)

    {

    if (automataState.InteresteBufferIndex == context.Buffer.Count)

    {

    automataState.DoStateActionWhenBufferEndComes();

    if (context.Buffer.Count == 0)

    {

    return context.OutString;

    }

    }

    else

    {

    switch (GetJamoType(automataState.HangulContext.Buffer[automataState.InterestedBufferIndex]))

    {

    case JamoType.Consonant:

    automataState.DoStateActionWhenConsonantComes();

    break;

    case JamoType.Vowel:

    automataState.DoStateActionWhenVowelComes();

    break;

    }

    }

    automataState = automataState.NextState;

    }

    }

    }

       

       

신고