Hangeul Automata

Author

차상진

Published

April 30, 2025

Hangeul Automata

1. 목표

한글 자소, 숫자, 백스페이스를 입력받아, 완성형 문장을 만드는 오토마타를 구현

2. 설명

state: 상태

char: 입력을 자소 단위로 쪼갠 것

ex) ['ㄱ','ㅏ','ㄴ','ㅏ','ㄷ','ㅣ','1','<'] 가 input이라면 'ㄱ','ㅏ', ... , '<'이 char이고 'ㄱ'을 입력받았을 때 state는 중성(모음)을 입력받길 기대하므로 state = 중성이다.

ㅁㅏㄴ (순으로 키보드를 누르면 “만”이 완성되고, ㅁㅏㄴㅏ 순으로 키보드를 누르면 “마나”가 완성)

초성 입력 -> 중성 -> 종성 이후에 자음이 오는지 모음이 오는지에 따라서 입력된 종성의 state가 다르게 결정된다!!

핵심

1. state에서 종성보다는 종성 후보가 적합하다. 왜냐하면 자음이 들어온 그 순간에는 그 자음이 종성인지, 새로운 음절의 초성인지 판단할 수 없기 때문이다.

2. 다음 입력을 보기 위해서 next_char이 필요하다

3. 경우의 수

state = 초성

  1. 입력: 자음 -> 초성 저장
  2. 입력: 모음 -> 다음 입력까지 보고 이중모음이면 merge, 아니면 모음 출력

state = 중성

  1. 입력: 자음 -> 이전 내용 출력, 초성 저장
  2. 입력: 모음 -> 다음 입력까지 보고 이중모음이면 merge, 아니면 결합하여 출력

state = 종성후보

  1. 입력: 자음 + 자음 -> 이중자음이면 merge, 아니라면 첫 번째 자음은 종성, 두 번째 자음은 새로운 초성
  2. 입력: 자음 + 모음 -> 자음은 종성이 되어서 출력, 모음은 그냥 모음 출력
  3. 입력: 모음 + 모음 -> 이중모음이면 merge 후 출력, 아니라면 따로따로 출력
  4. 입력: 모음 + 자음 -> 모음이기에 이전 내용 및 모음 출력, 자음은 새로운 글자의 초성
  5. 입력: 모음 -> 모음이기에 이전 내용 및 모음 출력
  6. 입력: 자음 -> 종성이 되고 출력

숫자 구현 1. 숫자는 한글과 절대 결합이 불가능하므로 그냥 이어붙히기

지우기 구현 1. 종성이 있는지 확인 -> 이중 종성이라면 분리해서 뒷글자 삭제, 아니라면 그냥 삭제 2. 종성이 없으면 중성이 있는지 확인 -> 이중 중성이라면 분리하여 뒷 글자 삭제, 아니라면 그냥 삭제 3. 중성도 없으면 초성이 있는지 확인 -> 있으면 초성 삭제 4. 만약 숫자라면 그냥 삭제

4. 구현

- 자모 및 이중모음, 이중 자음, 함수 선언

# 자모 리스트
CHOSUNG_LIST = [
    'ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ',
    'ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'
]
JUNGSUNG_LIST = [
    'ㅏ','ㅐ','ㅑ','ㅒ','ㅓ','ㅔ','ㅕ','ㅖ','ㅗ','ㅘ','ㅙ','ㅚ',
    'ㅛ','ㅜ','ㅝ','ㅞ','ㅟ','ㅠ','ㅡ','ㅢ','ㅣ'
]
JONGSUNG_LIST = [
    '', 'ㄱ','ㄲ','ㄳ','ㄴ','ㄵ','ㄶ','ㄷ','ㄹ','ㄺ','ㄻ','ㄼ','ㄽ','ㄾ','ㄿ','ㅀ',
    'ㅁ','ㅂ','ㅄ','ㅅ','ㅆ','ㅇ','ㅈ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'
]

DOUBLE_JONG_LIST = [
    ('ㄱ', 'ㅅ', 'ㄳ'),
    ('ㄴ', 'ㅈ', 'ㄵ'),
    ('ㄴ', 'ㅎ', 'ㄶ'),
    ('ㄹ', 'ㄱ', 'ㄺ'),
    ('ㄹ', 'ㅁ', 'ㄻ'),
    ('ㄹ', 'ㅂ', 'ㄼ'),
    ('ㄹ', 'ㅅ', 'ㄽ'),
    ('ㄹ', 'ㅌ', 'ㄾ'),
    ('ㄹ', 'ㅍ', 'ㄿ'),
    ('ㄹ', 'ㅎ', 'ㅀ'),
    ('ㅂ', 'ㅅ', 'ㅄ')
]

DOUBLE_JUNG_LIST = [
    ('ㅗ', 'ㅏ', 'ㅘ'),
    ('ㅗ', 'ㅐ', 'ㅙ'),
    ('ㅗ', 'ㅣ', 'ㅚ'),
    ('ㅜ', 'ㅓ', 'ㅝ'),
    ('ㅜ', 'ㅔ', 'ㅞ'),
    ('ㅜ', 'ㅣ', 'ㅟ'),
    ('ㅡ', 'ㅣ', 'ㅢ')
]

# 상태 초기화
def reset_state():
    return {
        'output': [],
        'cho': '',
        'jung': '',
        'jong': '',
        'state': '초성'
    }

# 글자 조합
def combine_jamos(cho, jung, jong=''):
    cho_idx = CHOSUNG_LIST.index(cho)
    jung_idx = JUNGSUNG_LIST.index(jung)
    jong_idx = JONGSUNG_LIST.index(jong) if jong else 0
    return chr(0xAC00 + 588 * cho_idx + 28 * jung_idx + jong_idx)

# 이중 중성 처리
def merge_double_jung(j1, j2):
    return DOUBLE_JUNG.get((j1, j2), None)
    
# 이중 종성 처리
def normalize_jong(jong):
    if jong in DOUBLE_JONG:
        return DOUBLE_JONG[jong]
    return jong
    
# 글자 출력 및 상태 초기화
def flush(state):
    result = ''

    if state['cho'] and state['jung']:
        jong = normalize_jong(state['jong']) if state['jong'] else ''
        result = combine_jamos(state['cho'], state['jung'], jong)
    elif state['jung']:
        result = state['jung']
    elif state['cho']:
        result = state['cho']
    
    # 상태 초기화
    state['cho'] = ''
    state['jung'] = ''
    state['jong'] = ''
    state['state'] = '초성'
    
    return result

# 숫자인지 확인
def is_number(char):
    return char.isdigit()

def is_hangul_syllable(char):
    return 0xAC00 <= ord(char) <= 0xD7A3


def decompose(syllable):
    if not is_hangul_syllable(syllable):
        return syllable, '', ''
    
    code = ord(syllable) - 0xAC00
    cho = code // 588
    jung = (code % 588) // 28
    jong = code % 28
    return CHOSUNG_LIST[cho], JUNGSUNG_LIST[jung], JONGSUNG_LIST[jong]

def compose(cho, jung, jong=''):
    if cho not in CHOSUNG_LIST or jung not in JUNGSUNG_LIST:
        return cho + jung + jong  # 비정상 조합은 그냥 이어 붙임
    
    cho_index = CHOSUNG_LIST.index(cho)
    jung_index = JUNGSUNG_LIST.index(jung)
    jong_index = JONGSUNG_LIST.index(jong) if jong in JONGSUNG_LIST else 0
    
    code = 0xAC00 + (cho_index * 588) + (jung_index * 28) + jong_index
    return chr(code)

DOUBLE_JUNG = {(a , b): c for a, b, c in DOUBLE_JUNG_LIST}
DOUBLE_JONG = {a + b: c for a, b, c in DOUBLE_JONG_LIST}

# 역조합용 딕셔너리 (백스페이스 처리에 사용)
REVERSE_DOUBLE_JUNG = {c: (a, b) for a, b, c in DOUBLE_JUNG_LIST}
# 역조합을 이용해서 이중 중성을 분해한다.
REVERSE_DOUBLE_JONG = {c: (a, b) for a, b, c in DOUBLE_JONG_LIST}
# 역조합을 이용해서 이중 종성을 분해한다.
# 전체 오토마타
def automata(inputs):
    state = reset_state()

    i = 0
    while i < len(inputs):
        char = inputs[i]
        next_char = inputs[i + 1] if i + 1 < len(inputs) else None

        if char == '<':  # 백스페이스 처리
            if state['jong']:
                # 이중 종성일 경우
                if state['jong'] in REVERSE_DOUBLE_JONG:
                    first, second = REVERSE_DOUBLE_JONG[state['jong']]
                    state['jong'] = first  # 첫 자음만 남기고 나머지 삭제
                else:
                    state['jong'] = ''
            elif state['jung']:
                # 이중 모음일 경우
                if state['jung'] in REVERSE_DOUBLE_JUNG:
                    first, second = REVERSE_DOUBLE_JUNG[state['jung']]
                    state['jung'] = first  # 첫 모음만 남기고 나머지 삭제
                else:
                    state['jung'] = ''
            elif state['cho']:
                state['cho'] = ''
            elif state['output']:
                last = state['output'].pop()
                if last.isdigit():
                    pass
                elif is_hangul_syllable(last):
                    cho, jung, jong = decompose(last)
                    if jong:
                        # 이중 종성인지 확인
                        if jong in REVERSE_DOUBLE_JONG:
                            first, second = REVERSE_DOUBLE_JONG[jong]
                            state['cho'] = cho
                            state['jung'] = jung
                            state['jong'] = first
                        else:
                            state['cho'] = cho
                            state['jung'] = jung
                            state['jong'] = ''
                else:
                    # 종성이 없으면 중성 삭제
                    if jung in REVERSE_DOUBLE_JUNG:
                        first, _ = REVERSE_DOUBLE_JUNG[jung]
                        state['cho'] = cho
                        state['jung'] = first
                        state['jong'] = ''
                    else:
                        state['cho'] = cho
                        state['jung'] = ''
                        state['jong'] = ''
            i += 1
            continue

        if is_number(char):
            flushed = flush(state)
            if flushed:
                state['output'].append(flushed)
            state['output'].append(char)
            i += 1
            continue
        
        if state['state'] == '초성':
            # 1. 상태가 초성일 때 자음(= 초성)이 입력되는 경우
            if char in CHOSUNG_LIST:
                state['cho'] = char
                state['state'] = '중성'
                i += 1
            elif char in JUNGSUNG_LIST:
                # 2. 상태가 초성일 때 모음이 먼저 입력되는 경우 중 merged 되는 경우
                if next_char in JUNGSUNG_LIST: # char: 모음 next_char: 모음
                    merged = merge_double_jung(char, next_char)
                    if merged:
                        state['jung'] = merged
                        state['state'] = '종성후보'
                        i += 2
                        continue
                    else:
                        # 3. merged 되지 않는 경우
                        state['jung'] = char
                        state['state'] = '종성후보'
                        i += 1
                else: # 4. char과 next char 모두 모음이 아닌 경우
                    state['jung'] = char
                    flushed = flush(state)
                    if flushed:
                        state['output'].append(flushed)
                        state['state'] = '초성'
                        i += 1
                    continue

        elif state['state'] == '중성':
            if char in CHOSUNG_LIST:
                # 1. 자음이 들어오면 이전 것 flush 후 초성으로 저장
                flushed = flush(state)
                if flushed:
                    state['output'].append(flushed)
                state['cho'] = char
                state['state'] = '중성'
                i += 1

            elif char in JUNGSUNG_LIST:
                if next_char in JUNGSUNG_LIST:
                    # 2. 이중 모음일 수 있음
                    merged = merge_double_jung(char, next_char)
                    if merged:
                        state['jung'] = merged
                        state['state'] = '종성후보'
                        i += 2
                        continue
                # 3. 이중 모음이 아님
                state['jung'] = char
                state['state'] = '종성후보'
                i += 1
                
        elif state['state'] == '종성후보':
            # 1. char과 next_char가 모두 자음일 때
            if char in JONGSUNG_LIST and next_char in JONGSUNG_LIST:
                double_jong = normalize_jong(char + next_char)
                if double_jong in JONGSUNG_LIST:
                    state['jong'] = double_jong
                    flushed = flush(state)
                    if flushed:
                        state['output'].append(flushed)
                    state['state'] = '초성'
                    i += 2
                else:
                    # 2. 이중자음 불가 → char은 종성, next_char는 초성
                    state['jong'] = char
                    flushed = flush(state)
                    if flushed:
                        state['output'].append(flushed)
                    state['state'] = '초성'
                    i += 1

            # 3. char은 자음이고 next_char는 모음 → char은 초성
            elif char in JONGSUNG_LIST and next_char in JUNGSUNG_LIST:
                flushed = flush(state)
                if flushed:
                    state['output'].append(flushed)
                state['cho'] = char
                state['state'] = '중성'
                i += 1

            # 4. char과 next_char가 모두 모음일 때
            elif char in JUNGSUNG_LIST and next_char in JUNGSUNG_LIST:
                merged = merge_double_jung(char, next_char)
                if merged:
                    flushed = flush(state)
                    if flushed:
                        state['output'].append(flushed)
                    state['jung'] = merged
                    flushed = flush(state)
                    if flushed:
                        state['output'].append(flushed)
                    state['state'] = '초성'
                    i += 2
                else: 
                    # 5. char이 모음이고 next char도 모음이지만 이중모음이 안될때
                    flushed = flush(state)
                    if flushed:
                        state['output'].append(flushed)
                    state['jung'] = char
                    i += 1
                    flushed = flush(state)
                    if flushed:
                        state['output'].append(flushed)
                    state['state'] = '초성'

            # 6. char이 모음이고 next_char는 자음일 때
            elif char in JUNGSUNG_LIST and next_char in CHOSUNG_LIST:
                flushed = flush(state)
                if flushed:
                    state['output'].append(flushed)
                state['jung'] = char
                i += 1
                flushed = flush(state)
                if flushed:
                    state['output'].append(flushed)
                state['state'] = '초성'
            # 7. char이 단독 모음일 때,
            elif char in JUNGSUNG_LIST:
                # 예외 처리 또는 단독 출력
                flushed = flush(state)
                if flushed:
                    state['output'].append(flushed)
                state['jung'] = char
                flushed = flush(state)
                if flushed:
                    state['output'].append(flushed)
                state['state'] = '초성'
                i += 1
            # 8. char이 단독 자음일 때,
            elif char in CHOSUNG_LIST:
                state['jong'] = char
                flushed = flush(state)
                if flushed:
                    state['output'].append(flushed)
                state['state'] = '초성'
                i += 1

    flushed = flush(state)
    if flushed:
        state['output'].append(flushed)

    return ''.join(state['output'])

6. Test

inp = 'ㅇㅏㄴㄴㅕㅇㅎㅏㅅㅔㅇㅛ123<'
print(automata(inp))
안녕하세요12