티스토리 뷰

검색어 자동완성 기능을 구현하기 앞서, 자동완성 기능에 대해서 명확히 알아보도록 하겠습니다. 자동완성 기능이라면, 모두들 아시겠지만, 네이버의 검색어를 입력하는 창에  을 입력하면 으로 시작되는 단어들을 자동으로 추천해주는(괴물, 국민은행, 김진희 기자와 같은) 기능을 말하죠?

 

그런데, 웹에서 이 기능이 구현되는데, 버튼을 클릭해서 페이지가 리프레쉬 되면서 데이터를 건네주고 받아오는 것이 아니라 어떤 버튼을 누르지 않아도 사용자가 입력한  이란 값이 전송이 되어서, 으로 시작되는 괴물, 국민은행 과 같은 단어들을 실시간으로 받아서 출력해 준다는 사실이, 예전의 웹관련 지식만을 가지고 있었던 사람들에겐(저 같은) 놀라운 사실일 것입니다. 이 것이 가능한 것은, AJAX(Asynchronous JavaScript and XML)라는 기법이 있기 때문입니다. AJAX에 대해 처음 들으시는 분은, 웹에서 AJAX와 관련된 정보를 찾아보시구요

 

자동완성 기능이 어떻게 동작되는지에 대해서 간단히 설명을 하자면,

사용자가 을 입력하는 순간, 웹브라우저는 자동으로 사용자가 입력한 값을() 서버(cgi,php와 같은 페이지)로 전송합니다. 그럼 서버에서는 을 받아서, 으로 시작하는 추천 검색어를 검색해서 웹브라우저로 보내주면 브라우저에서 출력해 줍니다. 이 모든 것이 실시간으로 이루어 집니다.

 

 AJAX 기법이 없다면, 검색어 자동완성 기능 구현이 불가능(?) 했겠지만, 자동완성 기능을 구현데 있어서 AJAX와 관련된 부분은 사실 그리 어려운 것이 아닙니다.(저의 생각엔) 왜냐하면 이미 다 공개가 되어 있기 때문이죠 시중에 AJAX 관련 책을 사면, 검색어 자동완성 기능에 대해 AJAX를 사용해 구현하는 기법에 대해 자세히 나와 있습니다.

사실 어려움은, 추천 검색어를 어떻게 구축하는지, 어떻게 빠른 속도로 검색을 하는지에 있습니다.

그것은, 다음과 같은 문제들로 생각해 볼 수 있습니다.

-             특정 음절로(,) 시작되는 수많은 검색어들 중에 어떤 단어를 추천 검색어로 보여줄 것인가

-             자료를 어떻게 구축해야(어떤 자료 구조를 사용해야), 가장 효율적이고 빠르게 추천단어를 검색할 수 있는가.

-             을 입력했는데, 괴물, 국민은행 같은 단어들이 보여진다는건, 단어 한 글자 한 글자를 자소 단위로 분리했기 �문에 가능하다는 것을 알 수 있습니다. , 괴물 -> ㄱㅗㅣㅁㅜㄹ 같은 형태로 분리해야 합니다.

 

그럼 가장 먼저, 위 문제들 중, 한글을 자소 단위로 분리하는 것에 대해 알아보고자 합니다.

한글을 자소 단위로 분리하는 과정을 이해하기 위해서는, 먼저 한글의 코드 체계에 대해서 알아야 합니다. 한국어 형태소 분석과 정보검색 책이나  웹에서도 많은 자료가 있으니 참고하시면 될 것 같습니다. 간단히 설명을 하자면(책은 모두 서울에 두고 고향에 내려와서 정확하지 않더라도 이해해 주세요 ^^;;), 한글 코드는 대략, 완성형 코드와 조합형 코드로 나뉩니다.

완성형 코드는 자주 쓰이는 한글 2350자를 골라 가나다 순으로 배치하여, 0xB0A1~0xC8FE(상위 바이트 0xB0 ~ 0xC8, 하위 바이트 0xA1~ 0xFE)까지 차례대로 코드를 부여한 것입니다. 완성형 코드는, 자주 사용되는 2350자에 0xB0A1~0xC8FE까지 차례대로 코드를 부여한 것이므로, 완성형 코드로 표현된 한글 음절에는 어떤 자소 정보도 표현되어 있지 않습니다.

반면, 조합형 코드는 사용하는 2바이트 중에서, 상위 1비트는 아스키 코드와 구분하기 위해 사용하고, 다음 5비트는 초성, 5비트는 중성, 5비트는 종성에 대한 정보를 저장합니다. 다시 말해서, 조합형 코드로 표현된 한글은 자소 단위로 분리할 수 있는 것입니다.

그리고 마지막으로, 현재 윈도우에서 사용하고 있는 한글 코드는 Microsoft의 통합형 한글 코드입니다. 처음에는 확장 완성형 코드라고 불렸던 이 코드는, 2350자 밖에 표현하지 못하는 완성형 코드에 한글을 더 추가하여 만든 것입니다. 이를테면 기존 완성형 코드에서는, 과 같은 음절를 표현할 수 없습니다. 따라서, 확장완성형 코드는 기존의 KSC-5601(완성형 코드) 코드영역을 그대로 두고 0x8141~0xC6FE에 해당하는 부분 중 KSC-5601에 포함되지 않는 부분에 KSC-5601로는 두 바이트로 표시할 수 없는 한글 문자들을 가나다순으로 배치하였습니다.

(http://www.w3c.or.kr/i18n/hangul-i18n/ko-code.html 참조)

 따라서, 한글을 자소 단위로 분리하기 위해서는, 윈도우에서 사용하는 확장 완형 코드를 조합형 코드로 변환한 후, 변환된 조합형 코드에서 자소를 추출해야 합니다. 일단 한글이 조합형 코드로 표현되어 있다면, 자소를 추출하는 것은 쉽습니다. AND 연산을 통해서 간단히 초성, 중성, 종성을 추출해 낼 수 있습니다. 추출해낸 자소를 다시 변환해 주어야 한다는 약간의 어려움이 있지만, 이 부분에 대해서는 나중에 다시 설명하고 일단 가장 먼저 구현해야 될 사항은, 확장 완성형 코드를 조합형 코드로 변환해야 하는 것이겠죠?

 

 그럼 어떻게 변환하는가에 대해 알아보겠습니다.

 먼저 확장 완성형 코드에 대해 다시 한번 살펴보면, 확장 완성형 코드는, 기존 완성형 코드 영역 0xB0A1~0xC8FE 0x8141~0xC6FE 부분에 한글을 추가한 것이라고 하였습니다. 그림으로 살펴보면, 아래 그림과 같습니다.

 

 

 

  그럼, 추가 code영역과 기존 완성형 코드 영역을 모두 포함하는 범위는 어떻게 될까요? 0x8141~0xC8FE가 되겠죠? (그림이 좀 틀린 것 같군요.. --;;)

이 확장 완성형 코드를 조합형 코드로 변환하는 과정에 대해서 개략적인 과정은 다음과 같습니다.

먼저 0x8141~0xC8FE 영역의 모든 한글 음절들을 파일에 써줍니다. 그림을 보면 0x8141~0xC8FE 영역 중에 확장 완성형 코드 영역도 아니고 완성형 코드 영역도 아닌 부분이 있지만, 이 부분도 무시하고 출력합니다. 그럼 확장 완성형 코드의 모든 한글이 파일에 써지겠죠? 이 파일을 마이크로소프트 워드(한글을 써봤는데, 좀 깨지는 부분이 있더군요)로 읽어와서 다른 이름으로 저장을 선택한 후 텍스트 파일로 저장하고 인코딩 방식은 한글 조합형을 택합니다. 이렇게 저장된 한글 조합형 파일에서 한글 음절의 조합형 코드 값을 읽어서 배열에 적절히 입력한 후, 이 배열을 이용해서 확장 완성형 코드를 조합형 코드로 변환합니다.

그럼 위 과정에 대해서 좀 더 상세히 설명하도록 하겠습니다.

먼저 0x8141~0xC8FE 영역의 모든 한글 음절들을 파일에 써준다고 하였습니다. 이 부분은 다음과 같이 for문을 중첩하여 간단히 구현할 수 있습니다.

 

#define KS_HIGH_FIRST 0x81

#define KS_LOW_FIRST 0x41

#define KS_HIGH_LAST 0xC8

#define KS_LOW_LAST 0xFE

 

void make_ks_code_file(char* fname)

{

             FILE* fp = fopen(fname,"wt");

             for(int a=KS_HIGH_FIRST;a<=KS_HIGH_LAST;a++)

                           for(int b=KS_LOW_FIRST;b<=KS_LOW_LAST;b++)

                                        fprintf(fp,"%d %d %c%c\n",a - KS_HIGH_FIRST,b - KS_LOW_FIRST,a,b);

             fclose(fp);

}

 

 소스를 보면 쉽게 이해하실 수 있을 것입니다. fname에는 출력할 파일 이름을 지정합니다. 상위 바이트와 하위 바이트를 하나씩 증가시키면서 파일에 한글 음절을 출력하는 것을 볼 수 있습니다. 출력된 파일을 메모장을 열어보면 다음과 같습니다.

0 0 

0 1 

0 2 

 .

 .

 .

71 187 

71 188 

71 189 

 중간 중간 한글이 깨져서 안 나오는 부분이 있는데, 이 부분은 위에 설명한대로 확장 완성형 코드 영역이 아닌 부분이므로 그냥 무시하면 됩니다.

자 그럼, 첫 번째 줄을 살펴봅시다.

 0 0  -> 처음 나오는 두 개의 숫자 0, 0은 조합형 코드가 저장될 2차원 배열에서의 인덱스를 나타냅니다. 그 다음 나오는 , 코드 값으로 0x8141을 갖는 확장 완성형 한글 코드겠죠? 위 소스코드에서 fprintf문을 살펴보면,

fprintf(fp,"%d %d %c%c\n",a - KS_HIGH_FIRST,b - KS_LOW_FIRST,a,b);

위 부분에서 %d %d 부분이 처음 나오는 2개의 인덱스를 출력하는 부분이고 그 다음 나오는 %c%c 부분이 바로 을 출력하는 부분입니다. 보통 %s를 이용해서 출력하지 않나 하고 의아해 하실 수도 있겠지만, 위와 같이 출력해도, 메모장으로 읽었을 때는 한글로 표시됩니다.

그럼 확정 완성형 코드의 모든 한글 음절이 위 파일에 출력되었습니다. 이 파일을 워드에서 읽어서 조합형으로 변환하여 저장합니다.(다른 이름으로 저장을 이용해) 그럼 조합형으로 저장된 이 파일은, 처음 나오는 배열의 인덱스, 두 개의 숫자는 원래 파일과 마찬가지로 그대로 저장될 것입니다. 하지만 뒤에 나오는 , , 과 같은 확장 완성형 한글 코드는 조합형 코드로 변환되어서 저장될 것입니다.

의 확장 완성형 한글 코드는 0x8141(이것은 우리가 직접 출력한 코드) 조합형 한글 코드는 0x6388입니다.(조합형 코드로 변환된 파일을 읽어보면 알 수 있다) 그럼 이 조합형 코드를 unsigned short 이차원 배열의 [0,0]에 값을 넣어줍니다. 그럼 나중에 이라는 한글을 조합형 코드로 변환할 때는 어떻게 할까요. 의 확장 완성형 코드는 0x8141, 상위 바이트 0x81, 하위 바이트는 0x41입니다. 상위 바이트에서는 확장 완성형 코드가 시작되는 상위 바이트 값(0x81)을 빼고, 하위 바이트에서도 마찬 가지고 0x41을 뺍니다. 그럼 [0,0]의 값이 나오겠죠? 아까 조합형 코드를 저장한 2차원 배열의 [0,0]째 인덱스 값을 가져오면, 바로 그 값이 의 조합형 코드가 되는 것입니다. 마찬가지로 이차원 배열의 [0,1]째 인덱스에는 의 조합형 코드 값이 들어가 있고, 확장 완성형 코드인 0x8142로부터 조합형 코드를 얻어내기 위해서는 [0x81  0x81, 0x42  0x41] = [0,1]과 같은 과정을 거쳐서 배열의 [0,1]째 값을 가져오면 될 것입니다. 의 조합형 코드가 배열에 저장되는 위치는 [0xC8  0x81, 0xFE  0x41] = [71, 189]이 되겠죠? 그럼 이 이차원 배열의 크기는 당연히

sizeof(unsigned short) * (0xC8  0x81 + 1) * (0xFE  0x41 + 1) =

2 * 72 * 190 = 27360이 됩니다.

잘 이해가 안 되신다면, 아래의 소스 코드를 보도록 합시다.

 

#define KS_HIGH_FIRST 0x81

#define KS_LOW_FIRST 0x41

#define KS_HIGH_LAST 0xC8

#define KS_LOW_LAST 0xFE

 

#define KS_HIGH_SIZE (KS_HIGH_LAST - KS_HIGH_FIRST + 1)

#define KS_LOW_SIZE (KS_LOW_LAST - KS_LOW_FIRST + 1)

 

int make_ks_to_kssm_table()

{

           FILE* fp = fopen("조합형 한글 코드.txt","rt");

           FILE* fp_out = fopen("ks2kssm.h","wt");

           char line[100];

           unsigned short ks2kssm[KS_HIGH_SIZE][KS_LOW_SIZE];

           memset(ks2kssm,0,sizeof(ks2kssm));

 

           int a = 0,b = 0;

           while(1)

           {

                        if(fscanf(fp,"%d %d %s",&a,&b,line) != 3)

                                     break;

                        if(line[0] >= 0)

                                     continue;

                        char temp = line[0];

                        line[0] = line[1];

                        line[1] = temp;

                        int kssm_code = *(unsigned short*)line;

                        ks2kssm[a][b]  = kssm_code;

           }

 

           fprintf(fp_out,"unsigned short ks2kssm[%d][%d] = {\n",KS_HIGH_SIZE,KS_LOW_SIZE);

           for(a=0;a

           {

                        for(b=0;b

                        {

                                     fprintf(fp_out,"\t%d" ,ks2kssm[a][b]);

                                     if(a != KS_HIGH_SIZE - 1 || b != KS_LOW_SIZE - 1)

                                                  fprintf(fp_out,",");

                                     fprintf(fp_out,"  /* %c%c */\n",a+KS_HIGH_FIRST,b+KS_LOW_FIRST);

                        }

           }

 

           fprintf(fp_out,"};\n\n");

 

           fclose(fp);

           fclose(fp_out);

           return TRUE;

}

 

 

첫번째 while 문에서는, 조합형 코드가 저장된 파일에서 조합형 코드를 읽어서 위에 설명한 원리대로 2차원 배열에 값을 입력합니다. 그런데 while문 안에 약간 이상한 부분이 있습니다.                        

char temp = line[0];

line[0] = line[1];

line[1] = temp;

 

위 부분이 바로 그 부분인데, 조합형 코드값을 읽어서, 상위 바위트와 하위 바이트 값을 바꾸고 있습니다. 이것은 인텔 CPU의 특성인 리틀 엔디언 이라는 것 때문인데, 일단은 이렇게만 알고 있고, 자세한 사항은 웹을 참고해보시기 바랍니다.

아무튼, 이렇게 생성된 2차원 배열을 이용해 완성형 코드를 조합형 코드로 변환할 수 있는 것입니다. 하지만 이 배열의 값은 메모리에 저장되어 있는 것입니다. 이 값을 파일에 저장해야 겠죠? ks2kssm.h 파일에 2차원 배열 형태로 값을 씁니다. 이 동작이 다음에 나오는 for문에서 이루어 지고 있습니다. 완성된 ks2kssm.h 파일을 보면 쉽게 이해하실 수 있을 것입니다.

 

그럼 이제 확장 완성형 코드를 조합형 코드로 변환하는 방법을 자세히 살펴보도록 하겠습니다. 먼저 ks2kssm.h 라는 파일을 인클루드 하고, 확장 완성형 코드의 상위 바이트에서 0x81을 뺀 값을 a, 하위 바이트에서는 0x41를 뺀 값을 b라고 한다면, ks2kssm[a][b]의 값을 읽어오는 것으로  조합형 코드로 변환할 수 있습니다. 이것은 아래와 같은 #define 문으로 나타낼 수 있습니다.

 

#define TO_KSSM(c) ks_to_kssm[c[0] - KS_HIGH_FIRST][c[1] - KS_LOW_FIRST]

 

c에는 물론, 조합형 코드로 변환하려는 확장 완성형 한글 코드가 들어가겠죠?

 

 지금까지 확장 완성형 코드를 조합형 코드로 변환하는 방법에 대해 알아봤습니다. 변환하는 방법에 대해 설명을 하다보니, 우리가 왜 이걸 하고 있는지도 까먹게 생겼군요. 조합형 코드로 변환하는 이유는 바로 한글 음절에서 자소를 추출하기 위해서였죠? 이렇게 변환된 조합형 코드에서 초성, 중성, 종성을 추출하는 건 매우 쉽습니다. 방법은 위에 설명했던 것처럼 5bit AND 연산을 하면 됩니다. 아래와 같은 조합형 코드값이 있다고 합시다.

위의 줄 숫자는 몇 번째 비트를 나타내는지를 보여주는 숫자이고, 그 아래 x는 무작위의 비트값입니다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

x

x

x

x

x

x

x

x

x

x

x

x

x

x

x

x

 

초성 코드는 2~6번째 비트에, 중성 코드는 7 ~ 11번째 비트, 종성 코드는 12 ~ 16번째 비트에 저장되어 있습니다. 따라서 각 비트 부분과 AND 연산을 통해 초성, 중성, 종성 값을 추출해 낼 수 있을 것입니다. 한글 조합형 코드의 초성, 중성, 종성 코드표는 아래와 같습니다.

 

비트조합 (bit)

10진 코드

16진 코드

초성(순번)

중성(순번)

종성(순번)

0 0 0 0 0

0

00

미정의

미정의

미정의

0 0 0 0 1

1

01

채움

미정의

채움

0 0 0 1 0

2

02

 (0x00)

채움

 (0x00)

0 0 0 1 1

3

03

 (0x01)

 (0x00)

 (0x01)

0 0 1 0 0

4

04

 (0x02)

 (0x01)

 (0x02)

0 0 1 0 1

5

05

 (0x03)

 (0x02)

 (0x03)

0 0 1 1 0

6

06

 (0x04)

 (0x03)

 (0x04)

0 0 1 1 1

7

07

 (0x05)

 (0x04)

 (0x05)

0 1 0 0 0

8

08

 (0x06)

미정의

 (0x06)

0 1 0 0 1

9

09

 (0x07)

미정의

 (0x07)

0 1 0 1 0

10

0A

 (0x08)

 (0x05)

 (0x08)

0 1 0 1 1

11

0B

 (0x09)

 (0x06)

 (0x09)

0 1 1 0 0

12

0C

 (0x0a)

 (0x07)

 (0x0a)

0 1 1 0 1

13

0D

 (0x0b)

 (0x08)

 (0x0b)

0 1 1 1 0

14

0E

 (0x0c)

 (0x09)

 (0x0c)

0 1 1 1 1

15

0F

 (0x0d)

 (0x0a)

 (0x0d)

1 0 0 0 0

16

10

 (0x0e)

미정의

 (0x0e)

1 0 0 0 1

17

11

 (0x0f)

미정의

 (0x0f)

1 0 0 1 0

18

12

 (0x10)

 (0x0b)

미정의

1 0 0 1 1

19

13

 (0x11)

 (0x0c)

 (0x10)

1 0 1 0 0

20

14

 (0x12)

 (0x0d)

 (0x11)

1 0 1 0 1

21

15

미정의

 (0x0e)

 (0x12)

1 0 1 1 0

22

16

미정의

 (0x0f)

 (0x13)

1 0 1 1 1

23

17

미정의

 (0x10)

 (0x14)

1 1 0 0 0

24

18

미정의

미정의

 (0x15)

1 1 0 0 1

25

19

미정의

미정의

 (0x16)

1 1 0 1 0

26

1A

미정의

 (0x11)

 (0x17)

1 1 0 1 1

27

1B

미정의

 (0x12)

 (0x18)

1 1 1 0 0

28

1C

미정의

 (0x13)

 (0x19)

1 1 1 0 1

29

1D

미정의

 (0x14)

 (0x1a)

1 1 1 1 0

30

1E

미정의

미정의

미정의

1 1 1 1 1

31

1F

미정의

미정의

미정의

 

에서 초성, 중성, 종성을 추출해 내면, 위 표에 나온대로, (10진 코드로) 초성은 2, 중성은 3, 종성은 2가 될 것입니다.

자 그럼 아래는 확장 완성형 소스코드에서 초성, 중성, 종성을 추출하는 소스입니다.

 

 

unsigned short to_kssm(char* ks)

{

          unsigned char* c = (unsigned char*)ks;

          int i = c[0] - KS_HIGH_FIRST;

          int j = c[1] - KS_LOW_FIRST;

          if(i < 0 || i >= KS_HIGH_SIZE)

                       return 0;

          if(j < 0 || j >= KS_LOW_SIZE)

                       return 0;

          return ks2kssm[i][j];

}

 

int convert_ks_to_3(char* ks,char* out)

{

          unsigned short kssm = to_kssm(ks);

          if(kssm == 0)

                       return FALSE;

                      

          out[2] = kssm & 31;

          if(out[2] < 1 || out[2] >29)

                       return FALSE;

          kssm >>= 5;

          out[1] = kssm & 31;

          if(out[1] < 2 || out[2] > 29)

                       return FALSE;

          kssm >>= 5;

          out[0] = kssm & 31;

          if(out[0] < 1 || out[2] > 20)

                       return FALSE;

 

          return TRUE;

}

 

 

 convert_ks_to_3 함수에서 ks 파라미터에는 확장 완성형 한글 코드를 넘겨줍니다. 그리고 out 파라미터에는 추출된 초성,중성,종성이 저장될 char형 배열을 넘겨줍니다. 크기는 3보다는 커야 합니다.

 그리 어려운 것은 아니므로 다른 설명 없이 넘어가도록 하겠습니다.

 

 자 이로써, 확장 완성형 한글 코드에서 초성, 중성, 종성을 추출하는 방법을 알아 보았습니다. 이로써 자동완성 기능에 필요한 한글을 자소 단위로 분리하는 기능 구현이 모두 끝난 것처럼 보입니다만, 안타깝게도 문제가 있습니다. 그것은, 종성에 관한 코드 처리, 그리고 복자음, 복모음을 아직 적절히 처리하지 못한다는 것입니다. 여기서 복자음, 복모음이란 자음 또는 모음 2개가 결합되어 있는 형태를 말합니다. , , ,  와 같이 말이죠.

 그럼 복자음, 복모음이 어떻게 문제가 되는지 살펴볼까요?

 네이버의 검색창에 라고 쳐봅시다. 그런 추천 검색어 중에 로 시작되는 단어도 나오지만, 로 시작되는 와우, 와우플포 와 같은 단어들도 나오는 것을 확인할 수 있습니다. 검색창에 를 입력한 후 를 입력할 수도 있기 때문이죠. 하지만 위의 코드표를 보면  13,  14의 코드 값을 갖습니다.

 우리가 를 입력했을 때, 이것을 자소 단위로 분리하면, ㅇ ㅗ, 코드 값으로 표현하면, 13 13이 될 것입니다. 그럼 와우를 분리하면, ㅇㅘㅇㅜ 이 되겠죠? 이 것을 코드 값으로 표현하면 13 14 13 20과 같이 표현될 것입니다. 그럼 우리가 를 입력했을 때, 자동 완성 프로그램은, 추천 검색어들 중, ㅇㅗ로 시작되는, 즉 코드 값이 13 13으로 시작되는 검색어를 찾을 것입니다. 하지만, 와우 13 14 13 20이라는 코드 값을 가지므로 의 추천 검색어에 포함될 수 없는거죠. 따라서, 복모음 를 분리해서 ㅗㅏ의 형태로 만들어야 합니다. 분리하는 방법에 대해서는 잠시 후에 살펴보기로 하고, 먼저 받침에 관한 문제를 살펴 보겠습니다. 받침에 관한 문제도 위와 비슷한 문제입니다.

우리가 을 입력했을 때 추천 검색어에는 갑상선이라는 단어도 나오지만, 가방라는 단어도 나옵니다. 그것은 을 입력한 후 를 입력하면   받침에 다음 음절의 초성으로 넘어갈 수 있기 때문입니다. 하지만 위의 코드 표에서 보면, 초성에서의  9, 종성에서의  19, 즉 같은 이라도 초성에서 나타나느냐, 종성에서 나타나느냐의 따라서 코드 값이 틀려집니다. 이런 차이를 변환하지 않고 그대로 사용한다면, 우리가 검색창에 이라고 입력을 했을 때, 추천 검색어에는 가방, 가볼만한곳과 같은 검색어는 나타나지 않을 것입니다. 따라서 종성에서 나타나는 코드 값을 초성의 것과 같은 값으로 변환해주어야 합니다. , 복자음은 항상 종성에서만 나타나는데, 이런 복자음은 2개의 자음으로 분리한 후, 초성의 코드 값으로 변환해야 합니다.

자 그럼 이제, 복자음, 복모음, 종성 코드 값을 어떻게 변환하는지에 대해 알아 보도록 하겠습니다.

코드 값 변환은 완성형 코드를 조합형 코드를 변환할 때와 마찬가지로 배열을 이용합니다. 하지만 배열의 크기는 훨씬 작겠죠?

먼저, 복모음, 복자음을 분해하는 방법에 대해서 알아보겠습니다.

  로 분리하는 과정에 대해서 살펴 보겠습니다.

조합형 코드표를 보면,  14,  13,  3의 코드 값을 갖습니다. 즉 중성의 코드 값이 14이면, 이것을 13, 3으로 변환해주면 됩니다. 이런 변환은 다음과 같은 배열을 생성하면 쉽게 할 수 있습니다.

 

char bok_moum[30][2] = {

       2,2,  /* 0 */

       2,2,  /* 1 */

       2,2,  /* 2 */

       2,2,  /* 3 */

       2,2,  /* 4 */

       2,2,  /* 5 */

       2,2,  /* 6 */

       2,2,  /* 7 */

       2,2,  /* 8 */

       2,2,  /* 9 */

       2,2,  /* 10 */

       2,2,  /* 11 */

       2,2,  /* 12 */

       2,2,  /* 13 */

       13,3,  /* 14 */

       13,4,  /* 15 */

       2,2,  /* 16 */

       2,2,  /* 17 */

       13,29,  /* 18 */

       2,2,  /* 19 */

       2,2,  /* 20 */

       20,7,  /* 21 */

       20,10,  /* 22 */

       20,29,  /* 23 */

       2,2,  /* 24 */

       2,2,  /* 25 */

       2,2,  /* 26 */

       2,2,  /* 27 */

       27,29,  /* 28 */

       2,2  /* 29 */

};

 , 위의 14번째 인덱스 값을 살펴보면, 13, 3, 즉 복모음이 원래 모음 두 개로 분리되어 있는 값을 가지고 있습니다. 중간 중간 보면, 2로 채워진 부분이 있는데, 이것은 중성의 채움 코드입니다. NULL과 같은 의미라고 볼 수 있습니다. 2로 채워진 부분은 복모음이 아닌 것이죠. 따라서 위 배열을 이용해, 복모음인지 아닌지, 복모음이면 분리된 모음의 코드 값을 얻어올 수 있습니다.

그렇다면, 위 배열은 어떻게 만들까요? 가장 좋은 방법은, 남이 만든 것을 웹에서 긁어오는 것이겠죠? 하지만, 웹에 저런 자료가 없다면, 조합형 코드표를 확인하면서 직접 제작할 수도 있습니다. 하지만, 우리 프로그래머는 그런 노가다 따위는 하지 않습니다. 좀 더 센스 있는 방법이 있습니다. 다음과 같은 텍스트 파일을 메모장으로 작성합니다.

 

와ㅗㅏ

왜ㅗㅐ

외ㅗㅣ

워ㅜㅓ

웨ㅜㅔ

위ㅜㅣ

의ㅡㅣ

 그리고 나서, 위 파일을 열어 한 줄씩 읽습니다. 읽어온 한 줄에서 다시 한음절씩 읽습니다. 읽어온 음절에서 중성을 추출합니다. 처음 줄은 14, 13, 3의 값이 나오겠죠? 이 값을 이용해 배열에 값을 입력하고, 완성된 배열을 코드 형태로 헤더 파일에 써줍니다. 그럼 실제 소스를 살펴보도록 하겠습니다.

아래 소스에서 복모음보자음변환표.txt 파일을 읽어오고 있습니다. 이 파일의 내용은 제가 직접 작성한 텍스트 파일입니다.

 

�ㄱㅅ

앉ㄴㅈ

않ㄴㅎ

앍ㄹㄱ

앎ㄹㅁ

�ㄹㅂ

�ㄹㅅ

�ㄹㅌ

�ㄹㅍ

앓ㄹㅎ

�ㅂㅅ

와ㅗㅏ

왜ㅗㅐ

외ㅗㅣ

워ㅜㅓ

웨ㅜㅔ

위ㅜㅣ

의ㅡㅣ

 

이 파일은 내용은 위와 같습니다.

복자음과 복모음 그리고 분리된 자음 또는 모음으로 구성이 되어있습니다.

초성으로 나타나는 은 아래 소스에서 쓰이진 않지만, 복자음과 복모음을 나타내기 위해서 자리를 채우고 있다고 생각하시면 됩니다.

 

int make_bok_mj_convert_table()

{

             FILE* fp_in = fopen("복모음복자음변환표.txt","rt");

             if(fp_in == NULL)

             {

                           printf("복모음복자음변환표.txt 파일을 읽을 수 없습니다.\n");

                           getch();

                           return 0;

             }

 

             memset(bok_moum,JUNGSUNG_NULL,sizeof(bok_moum));

             memset(bok_jaum,JONGSUNG_NULL,sizeof(bok_jaum));

             char line[1024];

             char out[3];

             while(fgets(line,1024,fp_in))

             {

                           if(line[0] >= 0)

                                        continue;

                           char* p_line = line;

                           convert_ks_to_3(p_line,out);

                           if(out[0] < 0)

                                        continue;

                           p_line+=2;

                           if(out[2] == JONGSUNG_NULL)

                           { //복모음 변환

                                        int index = out[1];

                                        ASSERT(index > 0 && index < 30,"index가 범위를 벗어남");

            

                                        convert_ks_to_3(p_line,out);

                                        p_line+=2;

                                        bok_moum[index][0] = out[1];

 

                                        convert_ks_to_3(p_line,out);

                                        p_line+=2;

                                        bok_moum[index][1] = out[1];

                           }

                           else

                           { //복자음 변환

                                        int index = out[2];

                                        ASSERT(index > 0 && index < 30,"index가 범위를 벗어남");

 

                                        convert_ks_to_3(p_line,out);

                                        p_line+=2;

                                        bok_jaum[index][0] = out[0];

 

                                        convert_ks_to_3(p_line,out);

                                        p_line+=2;

                                        bok_jaum[index][1] = out[0];

                           }

             }

 

             fclose(fp_in);

 

             char* bname = "bok_moum";

 

SSS:

             fprintf(fp_out,"char %s[30][2] = {\n",bname);

 

             for(int i=0;i<30;i++)

             {

                           if(!strcmp(bname,"bok_moum"))

                                        fprintf(fp_out,"\t%d,%d",bok_moum[i][0],bok_moum[i][1]);

                           else

                                        fprintf(fp_out,"\t%d,%d",bok_jaum[i][0],bok_jaum[i][1]);

                           if(i != 29)

                                        fprintf(fp_out,",");

                           fprintf(fp_out,"  /* %d */",i);

                           fprintf(fp_out,"\n");

             }

             fprintf(fp_out,"};\n\n");

 

             if(!strcmp(bname,"bok_moum"))

             {

                           bname = "bok_jaum";

                           goto SSS;

             }

             return TRUE;

}

 

while문 안에서, 복모음보자음변환표.txt 파일을 한 줄씩 읽은 후, 첫 음절을 읽은 후, 종성 코드가 채움(NULL)이 아닐 때에는, 즉 받침이 있을 경우에는 복자음 변환 테이블에 값을 넣어주고, 채움(NULL)일 경우, 즉 받침이 없을 경우에는 복모음 변환 테이블에 값을 넣어줍니다.

그렇게 배열에 모든 값이 들어가면, for문에서 이 배열의 값을 헤더파일로 출력해 줍니다. 그래서 나중에 복모음, 복자음을 분리할 때는 이 헤더파일을 인클루드 한 후, 미리 정의된 배열을 이용하면 되겠습니다.

 

그럼 이제, 종성 코드를 초성 코드로 변환하는 일이 남았습니다.

종성 코드를 초성 코드로 변환하는 것도 거의 위와 비슷한 원리로 이루어집니다. 먼저 아래와 같은 텍스트 파일을 만들어 줍니다.

 

종성2초성.txt

 

 

 위 파일을 열어, 한 음절씩 읽은 후, 초성과 종성 코드를 추출합니다.

 그리고 char형 일차원 배열을 선언한 후, 배열의 종성 코드가 가리키는 위치에 추출한 초성 코드를 입력하면, 간단히 변환 테이블이 완성됩니다.

 어렵지 않으므로 바로 소스코드를 보도록 하겠습니다.

 

int make_jongsung2chosung()

{

             memset(jong2cho,0,sizeof(jong2cho));

             FILE* fp_in = fopen("종성2초성.txt","rt");

             if(fp_in == NULL)

             {

                           printf("중성2초성.txt 파일을 찾을 수 없습니다.\n");

                           getch();

                           return 0;

             }

             char line[1024];

             while(fgets(line,1024,fp_in))

             {

                           if(line[0] >= 0)

                                        continue;

                           char out[3];

                           convert_ks_to_3(line,out);

                           ASSERT(out[0] > 0,"범위를 벗어남");

                           jong2cho[out[2]] = out[0];

             }

             fclose(fp_in);

 

             fprintf(fp_out,"char jong2cho[30] = {\n");

             for(int i=0;i<30;i++)

             {

                           fprintf(fp_out,"\t%d ",jong2cho[i]);

                           if(i != 29)

                                        fprintf(fp_out,",");

                           fprintf(fp_out,"/* %d */",i);

                           fprintf(fp_out,"\n");

             }

             fprintf(fp_out,"};\n\n");

             return TRUE;

}

 

 지금까지 봐왔던 소스와 비슷하므로 따로 설명은 하지 않겠습니다.

 이렇게 해서 위의 함수들을 실행시키면 다음과 같은 헤더 파일이 생성됩니다.

 

char bok_moum[30][2] = {

             2,2,  /* 0 */

             2,2,  /* 1 */

             2,2,  /* 2 */

             2,2,  /* 3 */

             2,2,  /* 4 */

             2,2,  /* 5 */

             2,2,  /* 6 */

             2,2,  /* 7 */

             2,2,  /* 8 */

             2,2,  /* 9 */

             2,2,  /* 10 */

             2,2,  /* 11 */

             2,2,  /* 12 */

             2,2,  /* 13 */

             13,3,  /* 14 */

             13,4,  /* 15 */

             2,2,  /* 16 */

             2,2,  /* 17 */

             13,29,  /* 18 */

             2,2,  /* 19 */

             2,2,  /* 20 */

             20,7,  /* 21 */

             20,10,  /* 22 */

             20,29,  /* 23 */

             2,2,  /* 24 */

             2,2,  /* 25 */

             2,2,  /* 26 */

             2,2,  /* 27 */

             27,29,  /* 28 */

             2,2  /* 29 */

};

 

char bok_jaum[30][2] = {

             1,1,  /* 0 */

             1,1,  /* 1 */

             1,1,  /* 2 */

             1,1,  /* 3 */

             2,11,  /* 4 */

             1,1,  /* 5 */

             4,14,  /* 6 */

             4,20,  /* 7 */

             1,1,  /* 8 */

             1,1,  /* 9 */

             7,2,  /* 10 */

             7,8,  /* 11 */

             7,9,  /* 12 */

             7,11,  /* 13 */

             7,18,  /* 14 */

             7,19,  /* 15 */

             7,20,  /* 16 */

             1,1,  /* 17 */

             1,1,  /* 18 */

             1,1,  /* 19 */

             9,11,  /* 20 */

             1,1,  /* 21 */

             1,1,  /* 22 */

             1,1,  /* 23 */

             1,1,  /* 24 */

             1,1,  /* 25 */

             1,1,  /* 26 */

             1,1,  /* 27 */

             1,1,  /* 28 */

             1,1  /* 29 */

};

 

char jong2cho[30] = {

             0 ,/* 0 */

             0 ,/* 1 */

             2 ,/* 2 */

             3 ,/* 3 */

             0 ,/* 4 */

             4 ,/* 5 */

             0 ,/* 6 */

             0 ,/* 7 */

             5 ,/* 8 */

             7 ,/* 9 */

             0 ,/* 10 */

             0 ,/* 11 */

             0 ,/* 12 */

             0 ,/* 13 */

             0 ,/* 14 */

             0 ,/* 15 */

             0 ,/* 16 */

             8 ,/* 17 */

             0 ,/* 18 */

             9 ,/* 19 */

             0 ,/* 20 */

             11 ,/* 21 */

             12 ,/* 22 */

             13 ,/* 23 */

             14 ,/* 24 */

             16 ,/* 25 */

             17 ,/* 26 */

             18 ,/* 27 */

             19 ,/* 28 */

             20 /* 29 */

};

 

 완성된 위 헤더 파일을 인클루드 한 후, 위 배열들을 이용해, 복자음, 복모음을 분리하고, 종성 코드를 초성 코드로 변환할 수 있습니다.

 

 자 이제 드디어, 음절을 검색어 자동완성 기능에 필요한 자소 단위로 분리할 수 있는 준비가 모두 끝났습니다. 이제는 우리의 목적은 만들어진 변환 테이블을 가지고, 한글 문자열이 들어왔을 때, 그것을 자동완성 기능에 필요한 자소 단위로 형태로 분리하는 함수를 만드는 것입니다.

 말이 필요 없습니다. 바로 완성된 함수를 보도록 하겠습니다. 프로그래머에겐 가장 좋은 설명이 바로 소스입니다. (조금씩 글 쓰는게 지쳐가는 가는 것 같습니다. -_-;;)

 

int dsbl_umjul(char* c,char* out)

{

             char ret[3];

             convert_ks_to_3(c,ret);

 

             if(!(ret[0] >= 1 && ret[0] <= 20))

                           return FALSE;

             if(!(ret[1] >= 2 && ret[1] <= 29))

                           return FALSE;

             if(!(ret[2] >= 1 && ret[2] <= 29))

                           return FALSE;

 

             char* p_out = out;

             int out_size = 0;

             *out++ = ret[0]; //초성 코드를 출력에 기록

 

             int jamo = ret[1];

             if(jamo != JUNGSUNG_NULL)

             {

                           if(bok_moum[jamo][0] != JUNGSUNG_NULL)

                           {

                                        //복모음일 경우에는 분리해서 출력에 기록

                                        *out++ = bok_moum[jamo][0] * -1; //중성 코드의 경우, 초성 코드와 구분하기 위해 -1을 곱한다.

                                        *out++ = bok_moum[jamo][1] * -1; //중성 코드의 경우, 초성 코드와 구분하기 위해 -1을 곱한다.

                           }

                           else //복모음이 아닐경우 발로 출력에 기록

                                        *out++ = jamo * -1; //중성 코드의 경우, 초성 코드와 구분하기 위해 -1을 곱한다.

             }

 

             jamo = ret[2];

             if(jamo != JONGSUNG_NULL)

             {

                           if(bok_jaum[jamo][0] != JONGSUNG_NULL)

                           {

                                        //복자음일 경우 분리해서 출력에 기록

                                        *out++ = bok_jaum[jamo][0];

                                        *out++ = bok_jaum[jamo][1];

                           }

                           else

                           {

                                        *out++ = jong2cho[jamo]; //종성 코드를 초성 코드로 변환해서 출력에 기록

                                        ASSERT(jong2cho[jamo] >= 2,"범위를 벗어남");

                           }

             }

             return (out - p_out); //결과물의 길이를 반환한다.

}

 

 자 드디어, 우리가 20페이지가 넘도록 살펴보았던 음절을 자소 단위로 분리하는 함수가 나왔습니다. 여기서 말하는 자소 단위로 분리는 위에서 살펴봤던 것처럼 좀 더 특별한 것이 있습니다. 일단 복모음과 복자음 모두 분리되어야 하고, 종성 코드는 초성 코드로 변환되어야 합니다.  dsbl_umjul이 바로 그런 함수이죠.(dsbl disassemble의 약자입니다. ^^)

 dsbl_umjul 함수의 입력은 파라미터 c에 그리고 출력은 파라미터 out에 들어갑니다. 파라미터 c에는 확장 완성형 코드 음절이 입력되고, out에는 분리된 자소 코드가 기록될 char형 배열을 넘겨주면 됩니다.

 c 을 넘겨주었다면, 위 함수를 호출한 후 out에는 2 3 9 11(ㄱㅏㅂㅅ) 값이 들어가 있을 것입니다.

 dsbl_umjul 함수는 음절을 분리하는 함수이고, 그렇다면 진짜 마지막으로 이제는 문자열을 자소 단위로 분리하는 함수가 나와야겠죠?

 역시 바로 소스 보도록 하겠습니다.

 

int dsbl_string(char* str,char* out,int buf_size,int* p_out_size)

{

             char* p_str = str;

             char* p_out = out;

             while(*p_str)

             {

                           if(*p_str >= 0)

                           {

                                        if(*p_str == ' ')

                                                     p_str++;

                                        else

                                                     *p_out++ = *p_str++;

                                        continue;

                           }

                           if(p_out - out + 5 > buf_size)

                                        return p_str - str;

                           int len = dsbl_umjul(p_str,p_out);

                           if(len == 0)

                           {

                                        printf("변환 실패\n");

                                        return -1;

                           }

 

                           p_out += len;

                           p_str += 2;

             }

             if(p_out - out < buf_size)

                           *p_out = NULL;

             if(p_out_size)

                           *p_out_size = p_out - out;

             return p_str - str;

}

 

 역시 함수가 별로 어렵지 않습니다. 그래서 다른 설명 없이 파라미터에 대해서만 알아보고 넘어가도록 하겠습니다.

 str은 역시 예상하셨던 것처럼 분리할 문자열이 들어갑니다. 박지성 연봉 이와 같은 문자열이 들어갈 수 있겠죠? 그 다음 나오는 out 파라미터는 역시 예상했던 것처럼 자소 단위로 분리된 결과물이 기록되어 나오는 버퍼 입니다. 그 다음 buf_size out에 넘겨주는 버퍼의 크기 p_out_size에는 int형 주소를 넘겨주면, 분리된 자소의 길이가 저장됩니다.

 

지금까지 한글을 검색어 자동완성 기능해 필요한 형태의 자소 단위로 분리하는 방법에 대해서 알아봤습니다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함