6 minute read

유니코드의 특성을 이용한 자모 분리 방법에 대해 알아보도록 하고 간단한 게임을 만들어 보자.

0. 목차

  1. 초성
  2. 중성
  3. 종성
  4. 한글 유니코드 구조
  5. 자모 분리 구현
  6. 영화 초성맞추기 게임

1. 한글 유니코드 구조

한글 유니코드를 이해하면 초성, 중성, 종성을 쉽게 분리할 수 있다.

1.1 유니코드 범위

구분 유니코드 범위 설명
한글 음절 0xAC00 ~ 0xD7A3 가 ~ 힣 (11,172자)
호환 자모 0x3131 ~ 0x3163 ㄱ ~ ㅣ
자모 0x1100 ~ 0x11FF 초성, 중성, 종성

1.2 한글 조합 공식

한글 음절은 다음 공식으로 계산된다:

한글 = 0xAC00 + (초성 × 21 × 28) + (중성 × 28) + 종성
  • 초성: 19개 (ㄱ, ㄲ, ㄴ, ㄷ, ㄸ, ㄹ, ㅁ, ㅂ, ㅃ, ㅅ, ㅆ, ㅇ, ㅈ, ㅉ, ㅊ, ㅋ, ㅌ, ㅍ, ㅎ)
  • 중성: 21개 (ㅏ, ㅐ, ㅑ, ㅒ, ㅓ, ㅔ, ㅕ, ㅖ, ㅗ, ㅘ, ㅙ, ㅚ, ㅛ, ㅜ, ㅝ, ㅞ, ㅟ, ㅠ, ㅡ, ㅢ, ㅣ)
  • 종성: 28개 (없음, ㄱ, ㄲ, ㄳ, ㄴ, ㄵ, ㄶ, ㄷ, ㄹ, ㄺ, ㄻ, ㄼ, ㄽ, ㄾ, ㄿ, ㅀ, ㅁ, ㅂ, ㅄ, ㅅ, ㅆ, ㅇ, ㅈ, ㅊ, ㅋ, ㅌ, ㅍ, ㅎ)

2. 초성

초성은 한글 음절의 첫소리이다. 19개가 있다.

public class Chosung {
    // 초성 배열 (인덱스 0~18)
    public static final char[] CHOSUNG = {
        'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ',
        'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
    };

    /**
     * 한글 문자에서 초성을 추출한다.
     * @param ch 한글 문자
     * @return 초성 문자
     */
    public static char getChosung(char ch) {
        if (ch < 0xAC00 || ch > 0xD7A3) {
            return ch; // 한글이 아니면 그대로 반환
        }
        int chosungIndex = (ch - 0xAC00) / (21 * 28);
        return CHOSUNG[chosungIndex];
    }
}

예시:

  • ‘가’ → 초성 ‘ㄱ’ (인덱스 0)
  • ‘나’ → 초성 ‘ㄴ’ (인덱스 2)
  • ‘힣’ → 초성 ‘ㅎ’ (인덱스 18)

3. 중성

중성은 한글 음절의 가운뎃소리(모음)이다. 21개가 있다.

public class Jungsung {
    // 중성 배열 (인덱스 0~20)
    public static final char[] JUNGSUNG = {
        'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ',
        'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'
    };

    /**
     * 한글 문자에서 중성을 추출한다.
     * @param ch 한글 문자
     * @return 중성 문자
     */
    public static char getJungsung(char ch) {
        if (ch < 0xAC00 || ch > 0xD7A3) {
            return ch;
        }
        int jungsungIndex = ((ch - 0xAC00) % (21 * 28)) / 28;
        return JUNGSUNG[jungsungIndex];
    }
}

예시:

  • ‘가’ → 중성 ‘ㅏ’ (인덱스 0)
  • ‘고’ → 중성 ‘ㅗ’ (인덱스 8)
  • ‘기’ → 중성 ‘ㅣ’ (인덱스 20)

4. 종성

종성은 한글 음절의 끝소리(받침)이다. 없는 경우를 포함해 28개가 있다.

public class Jongsung {
    // 종성 배열 (인덱스 0~27, 0은 종성 없음)
    public static final char[] JONGSUNG = {
        ' ', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ',
        'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ',
        'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
    };

    /**
     * 한글 문자에서 종성을 추출한다.
     * @param ch 한글 문자
     * @return 종성 문자 (없으면 공백)
     */
    public static char getJongsung(char ch) {
        if (ch < 0xAC00 || ch > 0xD7A3) {
            return ch;
        }
        int jongsungIndex = (ch - 0xAC00) % 28;
        return JONGSUNG[jongsungIndex];
    }

    /**
     * 종성이 있는지 확인한다.
     */
    public static boolean hasJongsung(char ch) {
        if (ch < 0xAC00 || ch > 0xD7A3) {
            return false;
        }
        return (ch - 0xAC00) % 28 != 0;
    }
}

예시:

  • ‘가’ → 종성 없음 (인덱스 0)
  • ‘간’ → 종성 ‘ㄴ’ (인덱스 4)
  • ‘강’ → 종성 ‘ㅇ’ (인덱스 21)

5. 자모 분리 구현

초성, 중성, 종성을 모두 분리하는 통합 클래스를 만들어보자.

public class KoreanUtil {

    private static final int HANGUL_BASE = 0xAC00;
    private static final int HANGUL_END = 0xD7A3;
    private static final int CHOSUNG_COUNT = 19;
    private static final int JUNGSUNG_COUNT = 21;
    private static final int JONGSUNG_COUNT = 28;

    private static final char[] CHOSUNG = {
        'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ',
        'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
    };

    private static final char[] JUNGSUNG = {
        'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ',
        'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'
    };

    private static final char[] JONGSUNG = {
        ' ', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ',
        'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ',
        'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
    };

    /**
     * 한글인지 확인한다.
     */
    public static boolean isKorean(char ch) {
        return ch >= HANGUL_BASE && ch <= HANGUL_END;
    }

    /**
     * 문자를 초성, 중성, 종성으로 분리한다.
     * @return [초성, 중성, 종성] 배열
     */
    public static char[] decompose(char ch) {
        if (!isKorean(ch)) {
            return new char[]{ch};
        }

        int unicode = ch - HANGUL_BASE;
        int chosungIdx = unicode / (JUNGSUNG_COUNT * JONGSUNG_COUNT);
        int jungsungIdx = (unicode % (JUNGSUNG_COUNT * JONGSUNG_COUNT)) / JONGSUNG_COUNT;
        int jongsungIdx = unicode % JONGSUNG_COUNT;

        if (jongsungIdx == 0) {
            return new char[]{CHOSUNG[chosungIdx], JUNGSUNG[jungsungIdx]};
        }
        return new char[]{CHOSUNG[chosungIdx], JUNGSUNG[jungsungIdx], JONGSUNG[jongsungIdx]};
    }

    /**
     * 문자열에서 초성만 추출한다.
     */
    public static String extractChosung(String str) {
        StringBuilder sb = new StringBuilder();
        for (char ch : str.toCharArray()) {
            if (isKorean(ch)) {
                int unicode = ch - HANGUL_BASE;
                int chosungIdx = unicode / (JUNGSUNG_COUNT * JONGSUNG_COUNT);
                sb.append(CHOSUNG[chosungIdx]);
            } else if (ch == ' ') {
                sb.append(' ');
            }
        }
        return sb.toString();
    }

    /**
     * 문자열을 모든 자모로 분리한다.
     */
    public static String decomposeAll(String str) {
        StringBuilder sb = new StringBuilder();
        for (char ch : str.toCharArray()) {
            char[] decomposed = decompose(ch);
            for (char c : decomposed) {
                sb.append(c);
            }
        }
        return sb.toString();
    }
}

사용 예시:

public static void main(String[] args) {
    // 초성 추출
    System.out.println(KoreanUtil.extractChosung("안녕하세요")); // ㅇㄴㅎㅅㅇ
    System.out.println(KoreanUtil.extractChosung("기생충"));     // ㄱㅅㅊ

    // 자모 분리
    System.out.println(KoreanUtil.decomposeAll("한글")); // ㅎㅏㄴㄱㅡㄹ

    // 개별 문자 분리
    char[] decomposed = KoreanUtil.decompose('한');
    System.out.println(Arrays.toString(decomposed)); // [ㅎ, ㅏ, ㄴ]
}

6. 영화 초성맞추기 게임 예제

이제 배운 내용을 활용해서 영화 초성맞추기 게임을 만들어보자.

import java.util.*;

public class MovieChosungGame {

    private static final String[] MOVIES = {
        "기생충", "올드보이", "괴물", "살인의 추억", "타짜",
        "부산행", "신과함께", "아저씨", "범죄와의 전쟁", "암살",
        "광해", "베테랑", "국제시장", "해운대", "도둑들",
        "명량", "관상", "변호인", "왕의 남자", "태극기 휘날리며"
    };

    private static final Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        System.out.println("=================================");
        System.out.println("   영화 초성 맞추기 게임");
        System.out.println("=================================");
        System.out.println("초성을 보고 영화 제목을 맞춰보세요!");
        System.out.println("종료하려면 'quit'을 입력하세요.\n");

        int score = 0;
        int totalGames = 0;
        Random random = new Random();

        while (true) {
            // 랜덤 영화 선택
            String movie = MOVIES[random.nextInt(MOVIES.length)];
            String chosung = KoreanUtil.extractChosung(movie);

            System.out.println("\n문제: " + chosung);
            System.out.println("힌트: " + movie.length() + "글자 영화입니다.");
            System.out.print("정답: ");

            String answer = scanner.nextLine().trim();

            if (answer.equalsIgnoreCase("quit")) {
                break;
            }

            totalGames++;

            if (answer.equals(movie)) {
                System.out.println("정답입니다! 🎉");
                score++;
            } else {
                System.out.println("틀렸습니다. 정답은 '" + movie + "' 입니다.");
            }

            System.out.println("현재 점수: " + score + " / " + totalGames);
        }

        System.out.println("\n=================================");
        System.out.println("게임 종료!");
        System.out.println("최종 점수: " + score + " / " + totalGames);
        if (totalGames > 0) {
            System.out.printf("정답률: %.1f%%\n", (score * 100.0 / totalGames));
        }
        System.out.println("=================================");
    }
}

게임 실행 예시:

=================================
   영화 초성 맞추기 게임
=================================
초성을 보고 영화 제목을 맞춰보세요!
종료하려면 'quit'을 입력하세요.

문제: ㄱㅅㅊ
힌트: 3글자 영화입니다.
정답: 기생충
정답입니다! 🎉
현재 점수: 1 / 1

문제: ㅂㅅㅎ
힌트: 3글자 영화입니다.
정답: 부산행
정답입니다! 🎉
현재 점수: 2 / 2

문제: ㅅㅇㅇ ㅊㅇ
힌트: 5글자 영화입니다.
정답: quit

=================================
게임 종료!
최종 점수: 2 / 2
정답률: 100.0%
=================================

7. 추가 응용: 초성 검색 기능

초성 검색은 실무에서도 많이 사용되는 기능이다. 연락처 앱이나 검색 엔진에서 자주 볼 수 있다.

public class ChosungSearch {

    /**
     * 초성 패턴으로 검색한다.
     * @param items 검색 대상 목록
     * @param chosungPattern 초성 패턴 (예: "ㄱㅅㅊ")
     * @return 매칭되는 항목 목록
     */
    public static List<String> search(List<String> items, String chosungPattern) {
        List<String> results = new ArrayList<>();

        for (String item : items) {
            String itemChosung = KoreanUtil.extractChosung(item);
            if (itemChosung.contains(chosungPattern)) {
                results.add(item);
            }
        }

        return results;
    }

    public static void main(String[] args) {
        List<String> contacts = Arrays.asList(
            "김철수", "김영희", "박지민", "이수현", "최민수",
            "강감찬", "이순신", "홍길동", "김유신", "세종대왕"
        );

        System.out.println("'ㄱㅊ' 검색 결과:");
        List<String> results = search(contacts, "ㄱㅊ");
        results.forEach(System.out::println);
        // 출력: 김철수, 강감찬

        System.out.println("\n'ㅎㄱㄷ' 검색 결과:");
        results = search(contacts, "ㅎㄱㄷ");
        results.forEach(System.out::println);
        // 출력: 홍길동
    }
}

8. 정리

한글 유니코드의 조합 공식을 이해하면 다양한 한글 처리 기능을 구현할 수 있다.

공식 설명
(ch - 0xAC00) / (21 × 28) 초성 인덱스
((ch - 0xAC00) % (21 × 28)) / 28 중성 인덱스
(ch - 0xAC00) % 28 종성 인덱스

활용 분야:

  • 초성 검색 (연락처, 검색엔진)
  • 한글 정렬
  • 자동완성
  • 한글 게임 (초성퀴즈, 끝말잇기 등)
  • 텍스트 분석

한글의 유니코드 구조를 이해하고 나면, 다양한 한글 처리 로직을 쉽게 구현할 수 있다.

Categories:

Updated:

Comments