MeCab-Ko 소개

MeCab-Ko는 한국어 형태소 분석을 위한 고성능 오픈소스 라이브러리입니다. 기존 C/C++ 기반의 은전한닢(mecab-ko)을 순수 Rust로 재구현하여, 메모리 안전성과 현대적인 개발 환경을 제공합니다.

최신 버전: v0.4.0 (2026-03-05) 🎉 crates.io 정식 배포!

  • 6개 크레이트 crates.io 정식 배포
  • 세종 코퍼스 호환 모드 강화 (후처리 파이프라인 완성)
  • 고유명사 ~200개 추가 (도시, 국가, 브랜드, 유명인)
  • 신조어 사전 v3.0 (511개)
  • 평가 데이터셋 확장 (160 → 300문장)
  • CI 자동 정확도 측정 (회귀 탐지)

왜 MeCab-Ko인가?

**한국어 자연어 처리(NLP)**의 첫 단계는 형태소 분석입니다. MeCab-Ko는 다음과 같은 장점을 제공합니다:

  • 높은 정확도: mecab-ko-dic 기반의 검증된 사전
  • 빠른 속도: Rust의 성능 + Zero-copy 사전 로딩
  • 다양한 플랫폼: Python, Node.js, WebAssembly, Rust
  • Elasticsearch 호환: Nori 분석기와 호환되는 API

형태소 분석이란?

형태소 분석(Morphological Analysis)은 문장을 가장 작은 의미 단위인 형태소로 분리하고, 각 형태소의 품사를 태깅하는 자연어 처리의 기본 과정입니다.

예를 들어, "아버지가방에들어가신다"라는 문장을 분석하면:

아버지    NNG,가족,*,*,*,*,아버지,아버지
가        JKS,*,*,*,*,*,가,가
방        NNG,장소,*,*,*,*,방,방
에        JKB,*,*,*,*,*,에,에
들어가    VV,*,*,*,*,*,들어가,들어가
시        EP,*,*,*,*,*,시,시
ㄴ다      EF,*,*,*,*,*,ㄴ다,ㄴ다
EOS

주요 특징

순수 Rust 구현

  • unsafe 코드 없이 메모리 안전성 보장
  • 크로스 플랫폼 지원 (Linux, macOS, Windows)
  • WASM 지원으로 브라우저에서도 실행 가능

한국어 최적화

  • 띄어쓰기 패널티를 통한 한국어 특화 분석
  • 한글 자모 분리/결합 유틸리티 내장
  • 세종 품사 태그 체계 기반
  • Unknown 단어 패턴 감지 및 처리 (v0.2.0)

고성능

  • Zero-copy 사전 로딩
  • 효율적인 Double-Array Trie 검색
  • Viterbi 알고리즘 최적 구현
  • 처리량: ~238K morphemes/sec

유연성

  • 사용자 사전 지원
  • 다양한 출력 포맷 (MeCab, Wakati, JSON, CSV 등)
  • 라이브러리 및 CLI 도구 제공
  • Elasticsearch Nori 호환 분석기

성능 지표

지표목표측정값상태
Throughput150K ops/sec238KPASS
Cold Start< 200ms132msPASS
Memory< 150MB145MBPASS

프로젝트 구조

mecab-ko/
├── rust/crates/
│   ├── mecab-ko/               # 통합 라이브러리
│   ├── mecab-ko-core/          # 핵심 분석 엔진
│   ├── mecab-ko-dict/          # 사전 관리
│   ├── mecab-ko-dict-builder/  # 사전 빌드 도구
│   ├── mecab-ko-dict-validator/# 사전 검증 도구
│   ├── mecab-ko-hangul/        # 한글 유틸리티
│   ├── mecab-ko-cli/           # CLI 도구
│   ├── mecab-ko-python/        # Python 바인딩
│   ├── mecab-ko-wasm/          # WASM 바인딩
│   ├── mecab-ko-node/          # Node.js 바인딩
│   └── mecab-ko-elasticsearch/ # ES/Nori 호환
└── docs/
    └── book/                   # 이 가이드북
Crate설명상태
mecab-ko사용자를 위한 통합 인터페이스v0.4.0 (crates.io)
mecab-ko-coreLattice, Viterbi, 미등록어 처리, 세종 호환v0.4.0 (crates.io)
mecab-ko-dict사전 로딩, Trie, 연접 비용 매트릭스v0.4.0 (crates.io)
mecab-ko-hangul자모 분리/결합, 문자 분류v0.4.0 (crates.io)
mecab-ko-dict-builder사전 빌드 도구v0.4.0 (crates.io)
mecab-ko-dict-validator사전 검증 도구v0.4.0 (crates.io)
mecab-ko-climecab-ko 명령줄 도구v0.4.0
mecab-ko-elasticsearchNori 호환 분석기v0.4.0
mecab-ko-wasmWebAssembly 바인딩v0.3.1 (npm)

다른 프로젝트와의 비교

프로젝트언어ThroughputMemory특징
mecab-ko (원본)C++18 MB/s~80 MB원조, 유지보수 중단
KiwiC++22 MB/s~150 MB독자 모델, 높은 정확도
LinderaRust12 MB/s~180 MB일본어 중심
MeCab-KoRust15 MB/s~145 MBmecab-ko 호환, 순수 Rust

v0.4.0 주요 변경사항

crates.io 정식 배포 🎉

  • 6개 크레이트 crates.io 정식 배포 완료
  • cargo add mecab-ko로 손쉬운 설치
  • MIT/Apache 2.0 듀얼 라이선스

세종 코퍼스 호환 모드 강화

  • 후처리 파이프라인 완성 (4단계)
    1. apply_decomposition_corrections(): 오분석 패턴 보정
    2. apply_token_merges(): 잘못 분해된 토큰 병합
    3. apply_lexicon_overrides(): 고빈도 어휘 매핑
    4. apply_context_corrections(): 컨텍스트 기반 품사 보정
  • EP/EC/ETM/ETN 패턴 확장

고유명사 및 신조어 확장

  • 고유명사 ~200개 추가 (도시, 국가, 브랜드, 대학, 유명인)
  • 신조어 사전 v3.0 (511개): AI/ML, 소셜미디어, MZ세대 용어

평가 인프라 강화

  • 평가 데이터셋: 160 → 300문장 확장
  • CI 자동 정확도 측정 (회귀 탐지)
  • 기준선: Token Accuracy 23.8% (세종 모드)

이전 버전 주요 기능

  • K-best Viterbi 경로 탐색 (v0.3.0)
  • LRU 캐싱 토크나이저 (v0.3.0)
  • 스트리밍 API (v0.3.0)
  • npm WASM 배포 (v0.3.1)

자세한 내용은 변경 이력을 참조하세요.

라이선스

Apache 2.0 또는 MIT 라이선스 중 선택하여 사용할 수 있습니다.

시작하기

다음 장에서 설치 방법과 빠른 시작 가이드를 확인하세요:

설치

MeCab-Ko는 여러 가지 방법으로 설치할 수 있습니다. 사용 목적에 따라 적절한 방법을 선택하세요.

요구 사항

시스템 요구 사항

  • 운영체제: Linux, macOS, Windows 10+
  • Rust: 1.75.0 이상 (라이브러리 사용 시)
  • 메모리: 최소 256MB (사전 로딩 포함)
  • Python: 3.8+ (Python 바인딩 사용 시)
  • Node.js: 18+ (Node.js 바인딩 사용 시)

Rust 설치

Rust가 설치되어 있지 않다면 rustup을 사용하여 설치하세요:

# Unix-like systems (Linux, macOS)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Windows (PowerShell)
# Visit https://rustup.rs/ and download rustup-init.exe

설치 확인:

rustc --version
# rustc 1.70.0 (or later)

cargo --version
# cargo 1.70.0 (or later)

CLI 도구 설치

Cargo를 통한 설치

가장 간단한 방법은 Cargo를 사용하는 것입니다:

cargo install mecab-ko-cli

설치 후 사용:

mecab-ko --version
mecab-ko "안녕하세요"

소스에서 빌드

최신 개발 버전을 사용하려면 소스에서 직접 빌드합니다:

# Clone the repository
git clone https://github.com/hephaex/mecab-ko.git
cd mecab-ko/rust

# Build in release mode
cargo build --release

# The binary is located at target/release/mecab-ko
./target/release/mecab-ko --version

시스템 경로에 설치:

# Option 1: Copy to a directory in PATH
sudo cp target/release/mecab-ko /usr/local/bin/

# Option 2: Add to PATH
export PATH="$PATH:$(pwd)/target/release"

라이브러리로 사용

crates.io에서 설치 (권장) 🎉

Rust 프로젝트에서 라이브러리로 사용하려면 Cargo.toml에 의존성을 추가하세요:

[dependencies]
mecab-ko = "0.4"

또는 cargo add 명령어를 사용합니다:

cargo add mecab-ko

특정 기능만 사용할 경우:

[dependencies]
# Core library only (Viterbi, Lattice, Sejong converter)
mecab-ko-core = "0.4"

# Hangul utilities only (자모 분리/결합)
mecab-ko-hangul = "0.4"

# Dictionary management only (사전 로딩)
mecab-ko-dict = "0.4"

# Dictionary builder (사전 빌드 도구)
mecab-ko-dict-builder = "0.4"

# Dictionary validator (사전 검증 도구)
mecab-ko-dict-validator = "0.4"

Feature Flags

mecab-ko 크레이트는 다음 feature를 제공합니다:

[dependencies]
mecab-ko = { version = "0.4", features = ["builder", "serde"] }
Feature설명
builder사전 빌더 기능 포함
serdeJSON 직렬화 지원
rayon병렬 처리 지원
zstd사전 압축 지원 (기본 활성화)

사전 설치

MeCab-Ko는 형태소 분석을 위해 사전이 필요합니다.

기본 사전

기본 사전은 라이브러리에 포함되어 있습니다. 별도 설치가 필요하지 않습니다.

커스텀 사전 경로

시스템에 설치된 mecab-ko-dic을 사용하려면:

# Using custom dictionary path
mecab-ko -d /path/to/mecab-ko-dic "분석할 텍스트"

일반적인 사전 경로:

운영체제경로
Linux/usr/share/mecab-ko-dic
macOS (Homebrew)/opt/homebrew/lib/mecab/dic/mecab-ko-dic
WindowsC:\Program Files\MeCab\dic\mecab-ko-dic

사용자 사전

사용자 정의 단어를 추가하려면 CSV 형식의 사용자 사전을 생성합니다:

# Create user dictionary file
cat > user.csv << EOF
딥러닝,NNG,-1000,딥러닝
머신러닝,NNG,-1000,머신러닝
앤트로픽,NNP,-1000,앤트로픽
EOF

# Use with --user-dic option
mecab-ko --user-dic user.csv "딥러닝과 머신러닝"

자세한 내용은 사용자 사전 장을 참조하세요.

설치 확인

설치가 완료되면 다음 명령으로 확인합니다:

# Check version
mecab-ko --version

# Simple test
echo "안녕하세요" | mecab-ko

예상 출력:

안녕    NNG
하      XSV
세요    EP+EF
EOS

문제 해결

빌드 오류

Rust 버전이 오래된 경우:

rustup update stable

사전을 찾을 수 없음

사전 경로를 명시적으로 지정하세요:

mecab-ko -d /path/to/dict "텍스트"

권한 문제 (Linux/macOS)

# Make binary executable
chmod +x target/release/mecab-ko

다음 단계

설치를 완료했다면 빠른 시작을 확인하세요.

빠른 시작

이 장에서는 MeCab-Ko를 사용하여 한국어 형태소 분석을 시작하는 방법을 설명합니다.

CLI 빠른 시작

기본 사용법

가장 간단한 형태소 분석:

mecab-ko "안녕하세요"

출력:

안녕    NNG
하      XSV
세요    EP+EF
EOS

파이프라인 사용

표준 입력을 통한 분석:

echo "오늘 날씨가 좋습니다" | mecab-ko

파일 분석:

cat input.txt | mecab-ko > output.txt

분리만 수행 (Wakati)

띄어쓰기로 분리된 형태소만 출력:

mecab-ko -O wakati "형태소 분석 테스트"

출력:

형태소 분석 테스트

JSON 출력

프로그래밍 친화적인 JSON 포맷:

mecab-ko -O json "안녕"

출력:

[
  {
    "surface": "안녕",
    "pos": "NNG",
    "start": 0,
    "end": 6
  }
]

라이브러리 빠른 시작

프로젝트 설정

새 Rust 프로젝트 생성:

cargo new my-nlp-project
cd my-nlp-project

Cargo.toml에 의존성 추가:

[dependencies]
mecab-ko = "0.2"

기본 형태소 분석

use mecab_ko::Tokenizer;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create tokenizer instance
    let tokenizer = Tokenizer::new()?;

    // Tokenize text
    let text = "아버지가방에들어가신다";
    let tokens = tokenizer.tokenize(text);

    // Print results
    for token in tokens {
        println!("{}\t{}", token.surface, token.pos);
    }

    Ok(())
}

다양한 분석 메서드

use mecab_ko::Tokenizer;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tokenizer = Tokenizer::new()?;
    let text = "오늘 날씨가 좋습니다";

    // Get morphemes only (surface forms)
    let morphs = tokenizer.morphs(text);
    println!("Morphs: {:?}", morphs);
    // Output: ["오늘", "날씨", "가", "좋", "습니다"]

    // Get nouns only
    let nouns = tokenizer.nouns(text);
    println!("Nouns: {:?}", nouns);
    // Output: ["오늘", "날씨"]

    // Get POS-tagged pairs
    let pos = tokenizer.pos(text);
    println!("POS: {:?}", pos);
    // Output: [("오늘", "NNG"), ("날씨", "NNG"), ("가", "JKS"), ...]

    Ok(())
}

한글 유틸리티 사용

use mecab_ko::hangul::{decompose, compose, is_hangul, has_jongseong};

fn main() {
    // Decompose Hangul syllable to Jamo
    if let Some((cho, jung, jong)) = decompose('한') {
        println!("초성: {}, 중성: {}, 종성: {:?}", cho, jung, jong);
        // Output: 초성: ㅎ, 중성: ㅏ, 종성: Some('ㄴ')
    }

    // Compose Jamo to Hangul syllable
    if let Some(c) = compose('ㅎ', 'ㅏ', Some('ㄴ')) {
        println!("Composed: {}", c);  // Output: 한
    }

    // Check if character is Hangul
    println!("Is '가' Hangul? {}", is_hangul('가'));  // true
    println!("Is 'a' Hangul? {}", is_hangul('a'));    // false

    // Check for jongseong (final consonant)
    println!("Has jongseong '한': {:?}", has_jongseong('한'));  // Some(true)
    println!("Has jongseong '하': {:?}", has_jongseong('하'));  // Some(false)
}

문자열 자모 분리/결합

use mecab_ko::hangul::{decompose_str, compose_str};

fn main() {
    // Decompose entire string
    let decomposed = decompose_str("한글");
    println!("{}", decomposed);  // Output: ㅎㅏㄴㄱㅡㄹ

    // Compose Jamo string back
    let composed = compose_str("ㅎㅏㄴㄱㅡㄹ");
    println!("{}", composed);    // Output: 한글
}

사용자 사전 활용

CSV 사전 생성

user.csv 파일:

# Custom dictionary for technical terms
딥러닝,NNG,-1000,딥러닝
머신러닝,NNG,-1000,머신러닝
자연어처리,NNG,-1000,자연어처리
챗GPT,NNP,-1000,챗지피티

CLI에서 사용

mecab-ko --user-dic user.csv "딥러닝 기술이 발전하고 있습니다"

라이브러리에서 사용

use mecab_ko_dict::UserDictionary;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create user dictionary
    let mut user_dict = UserDictionary::new();

    // Add entries programmatically
    user_dict.add_entry("딥러닝", "NNG", Some(-1000), None);
    user_dict.add_entry("챗GPT", "NNP", Some(-1000), Some("챗지피티".to_string()));

    // Or load from CSV file
    user_dict.load_from_csv("user.csv")?;

    // Check loaded entries
    println!("Loaded {} entries", user_dict.len());

    Ok(())
}

출력 포맷 선택

MeCab-Ko는 다양한 출력 포맷을 지원합니다:

포맷옵션설명
Default-O defaultMeCab 호환 포맷
Wakati-O wakati형태소만 공백 분리
JSON-O jsonJSON 배열
CSV-O csvCSV 포맷
Simple-O simple표면형/품사 쌍
POS-O pos표면형/품사 형식
Dump-O dump디버그용 상세 정보

예시:

# Simple format
mecab-ko -O simple "테스트"
# Output: 테스트/NNG

# CSV format
mecab-ko -O csv "테스트"
# Output:
# surface,pos,start,end,reading,lemma
# 테스트,NNG,0,9,,

다음 단계

기본 사용법 튜토리얼

이 튜토리얼에서는 MeCab-Ko의 기본적인 사용법을 단계별로 배웁니다.

목차

  1. 환경 설정
  2. 첫 번째 분석
  3. 토큰 정보 활용
  4. 다양한 분석 메서드
  5. 사용자 사전 적용

환경 설정

Rust 프로젝트 생성

# 새 프로젝트 생성
cargo new mecab-tutorial
cd mecab-tutorial

# Cargo.toml에 의존성 추가
cat >> Cargo.toml << 'EOF'
[dependencies]
mecab-ko = "0.2"
EOF

Python 환경 (선택사항)

# pip으로 설치
pip install mecab-ko

# 또는 Poetry 사용
poetry add mecab-ko

첫 번째 분석

Rust 예제

use mecab_ko::Tokenizer;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 토크나이저 생성
    let tokenizer = Tokenizer::new()?;

    // 텍스트 분석
    let text = "안녕하세요, 오늘 날씨가 좋네요!";
    let tokens = tokenizer.tokenize(text);

    // 결과 출력
    println!("입력: {}", text);
    println!("\n분석 결과:");
    println!("{:-<50}", "");

    for token in &tokens {
        println!("{:<10} {:>10}", token.surface, token.pos);
    }

    Ok(())
}

출력:

입력: 안녕하세요, 오늘 날씨가 좋네요!

분석 결과:
--------------------------------------------------
안녕             NNG
하               XSV
세요           EP+EF
,                 SC
오늘             NNG
날씨             NNG
가               JKS
좋               VA
네요           EF+EF
!                 SF

Python 예제

from mecab_ko import Mecab

# 분석기 생성
mecab = Mecab()

# 텍스트 분석
text = "안녕하세요, 오늘 날씨가 좋네요!"
result = mecab.pos(text)

print(f"입력: {text}")
print("\n분석 결과:")
for surface, pos in result:
    print(f"  {surface:<10} {pos:>10}")

토큰 정보 활용

토큰 구조체

각 토큰은 다음 정보를 포함합니다:

필드타입설명
surfaceString표면형 (실제 텍스트)
posString품사 태그
startusize시작 위치 (바이트)
endusize끝 위치 (바이트)
readingOption읽기 정보
lemmaOption기본형 (원형)

위치 정보 활용

use mecab_ko::Tokenizer;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tokenizer = Tokenizer::new()?;
    let text = "한국어 형태소 분석";
    let tokens = tokenizer.tokenize(text);

    println!("원문: {}", text);
    println!("\n토큰별 위치 정보:");

    for token in &tokens {
        // 원문에서 해당 토큰 추출
        let extracted = &text[token.start..token.end];
        println!(
            "  '{}' @ [{}, {}) = '{}'",
            token.surface, token.start, token.end, extracted
        );
    }

    Ok(())
}

출력:

원문: 한국어 형태소 분석

토큰별 위치 정보:
  '한국어' @ [0, 9) = '한국어'
  '형태소' @ [10, 19) = '형태소'
  '분석' @ [20, 26) = '분석'

품사 필터링

use mecab_ko::Tokenizer;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tokenizer = Tokenizer::new()?;
    let text = "오늘 날씨가 정말 좋습니다";
    let tokens = tokenizer.tokenize(text);

    // 명사만 추출 (NNG, NNP, NNB 등)
    let nouns: Vec<_> = tokens
        .iter()
        .filter(|t| t.pos.starts_with("NN"))
        .map(|t| &t.surface)
        .collect();

    println!("명사: {:?}", nouns);
    // 출력: 명사: ["오늘", "날씨"]

    // 동사/형용사 추출 (VV, VA)
    let verbs: Vec<_> = tokens
        .iter()
        .filter(|t| t.pos.starts_with("V"))
        .map(|t| &t.surface)
        .collect();

    println!("용언: {:?}", verbs);
    // 출력: 용언: ["좋"]

    Ok(())
}

다양한 분석 메서드

morphs() - 형태소 추출

#![allow(unused)]
fn main() {
let morphs = tokenizer.morphs("오늘 날씨가 좋습니다");
println!("{:?}", morphs);
// ["오늘", "날씨", "가", "좋", "습니다"]
}

nouns() - 명사 추출

#![allow(unused)]
fn main() {
let nouns = tokenizer.nouns("자연어 처리는 인공지능의 핵심 기술입니다");
println!("{:?}", nouns);
// ["자연어", "처리", "인공지능", "핵심", "기술"]
}

pos() - 품사 태깅

#![allow(unused)]
fn main() {
let pos_tagged = tokenizer.pos("형태소 분석");
for (surface, pos) in pos_tagged {
    println!("{}: {}", surface, pos);
}
// 형태소: NNG
// 분석: NNG
}

wakati() - 띄어쓰기 분리

#![allow(unused)]
fn main() {
let wakati = tokenizer.wakati("아버지가방에들어가신다");
println!("{}", wakati.join(" "));
// "아버지 가 방 에 들어가 시 ㄴ다"
}

사용자 사전 적용

CSV 사전 생성

user_dict.csv 파일을 생성합니다:

# 신조어 사전
# 형식: 표면형,품사,비용,읽기

# IT 용어
딥러닝,NNG,-2000,딥러닝
머신러닝,NNG,-2000,머신러닝
챗GPT,NNP,-2000,챗지피티
LLM,NNG,-2000,엘엘엠

# 브랜드명
삼성전자,NNP,-2000,삼성전자
네이버,NNP,-2000,네이버

# 신조어
갓생,NNG,-1500,갓생
소확행,NNG,-1500,소확행

사용자 사전 로드

use mecab_ko::Tokenizer;
use mecab_ko_dict::UserDictionary;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 사용자 사전 로드
    let mut user_dict = UserDictionary::new();
    user_dict.load_from_csv("user_dict.csv")?;

    println!("사용자 사전 로드: {} 항목", user_dict.len());

    // 토크나이저 생성 (사용자 사전 적용)
    let tokenizer = Tokenizer::with_user_dict(user_dict)?;

    // 분석 테스트
    let text = "딥러닝과 머신러닝으로 챗GPT를 만들었습니다";
    let tokens = tokenizer.tokenize(text);

    for token in &tokens {
        println!("{:<12} {}", token.surface, token.pos);
    }

    Ok(())
}

CLI에서 사용자 사전 사용

# 사용자 사전과 함께 분석
mecab-ko --user-dic user_dict.csv "딥러닝 기술이 발전하고 있습니다"

# JSON 출력과 함께
mecab-ko --user-dic user_dict.csv -O json "챗GPT가 인기입니다"

실습 예제

키워드 추출기

use mecab_ko::Tokenizer;
use std::collections::HashMap;

fn extract_keywords(text: &str, top_n: usize) -> Vec<(String, usize)> {
    let tokenizer = Tokenizer::new().expect("Failed to create tokenizer");
    let tokens = tokenizer.tokenize(text);

    // 명사만 카운트
    let mut counter: HashMap<String, usize> = HashMap::new();
    for token in tokens {
        if token.pos.starts_with("NN") && token.surface.chars().count() > 1 {
            *counter.entry(token.surface).or_insert(0) += 1;
        }
    }

    // 빈도순 정렬
    let mut keywords: Vec<_> = counter.into_iter().collect();
    keywords.sort_by(|a, b| b.1.cmp(&a.1));
    keywords.truncate(top_n);

    keywords
}

fn main() {
    let article = r#"
        인공지능 기술이 빠르게 발전하고 있습니다.
        특히 자연어 처리 분야에서 대규모 언어 모델의 발전이 눈부십니다.
        인공지능은 의료, 금융, 교육 등 다양한 분야에서 활용되고 있으며,
        앞으로 더 많은 분야에서 인공지능 기술이 적용될 것으로 예상됩니다.
    "#;

    let keywords = extract_keywords(article, 5);

    println!("상위 키워드:");
    for (word, count) in keywords {
        println!("  {} ({}회)", word, count);
    }
}

출력:

상위 키워드:
  인공지능 (3회)
  기술 (2회)
  분야 (3회)
  발전 (2회)
  언어 (1회)

다음 단계

참고 자료

고급 기능 튜토리얼

MeCab-Ko의 고급 기능을 활용하는 방법을 알아봅니다.

목차

  1. N-best 분석
  2. 복합명사 분해
  3. 스트리밍 처리
  4. 정확도 평가
  5. 사전 품질 검증

N-best 분석

형태소 분석에서 최적 경로 외에 여러 후보를 얻을 수 있습니다.

기본 사용법

use mecab_ko::{Tokenizer, TokenizerConfig};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = TokenizerConfig::default();
    config.nbest = 3;  // 상위 3개 결과

    let tokenizer = Tokenizer::with_config(config)?;
    let text = "아버지가방에들어가신다";

    let results = tokenizer.tokenize_nbest(text)?;

    for (i, tokens) in results.iter().enumerate() {
        println!("===== 후보 {} =====", i + 1);
        for token in tokens {
            println!("  {:<10} {}", token.surface, token.pos);
        }
        println!();
    }

    Ok(())
}

출력:

===== 후보 1 =====
  아버지       NNG
  가           JKS
  방           NNG
  에           JKB
  들어가       VV
  시           EP
  ㄴ다         EF

===== 후보 2 =====
  아버지       NNG
  가방         NNG
  에           JKB
  들어가       VV
  시           EP
  ㄴ다         EF

===== 후보 3 =====
  아버지       NNG
  가           JKS
  방           NNG
  에           JKB
  들어가시     VV+EP
  ㄴ다         EF

비용 기반 필터링

#![allow(unused)]
fn main() {
// theta 값으로 후보 범위 조정
// 낮은 theta = 더 다양한 후보
config.theta = 0.5;  // 기본값: 0.75

let tokenizer = Tokenizer::with_config(config)?;
}

복합명사 분해

DecompoundMode 설정

#![allow(unused)]
fn main() {
use mecab_ko_core::DecompoundMode;

// None: 분해하지 않음
// Discard: 원본 제거, 분해 결과만
// Mixed: 원본 유지, 분해 결과도 포함

let mut config = TokenizerConfig::default();
config.decompound_mode = DecompoundMode::Mixed;
}

분해 예제

use mecab_ko::{Tokenizer, TokenizerConfig};
use mecab_ko_core::DecompoundMode;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let text = "국립국어원에서 발표했습니다";

    // 분해 없음
    let config_none = TokenizerConfig {
        decompound_mode: DecompoundMode::None,
        ..Default::default()
    };
    let tokenizer_none = Tokenizer::with_config(config_none)?;
    println!("=== 분해 없음 ===");
    for t in tokenizer_none.tokenize(text) {
        println!("  {}", t.surface);
    }

    // Mixed 모드 (원본 + 분해)
    let config_mixed = TokenizerConfig {
        decompound_mode: DecompoundMode::Mixed,
        ..Default::default()
    };
    let tokenizer_mixed = Tokenizer::with_config(config_mixed)?;
    println!("\n=== Mixed 모드 ===");
    for t in tokenizer_mixed.tokenize(text) {
        println!("  {} (offset: {})", t.surface, t.start);
    }

    Ok(())
}

출력:

=== 분해 없음 ===
  국립국어원
  에서
  발표
  했
  습니다

=== Mixed 모드 ===
  국립국어원 (offset: 0)
  국립 (offset: 0)
  국어원 (offset: 6)
  에서 (offset: 15)
  발표 (offset: 21)
  했 (offset: 27)
  습니다 (offset: 30)

접두사/접미사 처리

v0.2.0에서 개선된 접두사/접미사 자동 감지:

#![allow(unused)]
fn main() {
// 접미사: 들, 님, 분, 꾼 등
// 접두사: 신, 구, 총, 부, 전, 후 등

let text = "선생님들께서 신기술을 소개합니다";
let tokens = tokenizer.tokenize(text);

for token in &tokens {
    if token.pos == "XSN" {
        println!("접미사 발견: {}", token.surface);
    }
    if token.pos == "XPN" {
        println!("접두사 발견: {}", token.surface);
    }
}
}

스트리밍 처리

대용량 텍스트를 메모리 효율적으로 처리합니다.

StreamingTokenizer

use mecab_ko::StreamingTokenizer;
use std::io::{BufRead, BufReader};
use std::fs::File;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tokenizer = StreamingTokenizer::new()?;

    // 파일 스트리밍 처리
    let file = File::open("large_corpus.txt")?;
    let reader = BufReader::new(file);

    let mut total_tokens = 0;
    let mut total_nouns = 0;

    for line in reader.lines() {
        let line = line?;
        if line.trim().is_empty() {
            continue;
        }

        let tokens = tokenizer.tokenize(&line);
        total_tokens += tokens.len();
        total_nouns += tokens.iter().filter(|t| t.pos.starts_with("NN")).count();
    }

    println!("총 토큰: {}", total_tokens);
    println!("총 명사: {}", total_nouns);

    Ok(())
}

배치 처리

use mecab_ko::Tokenizer;
use rayon::prelude::*;

fn process_batch(texts: &[String]) -> Vec<Vec<String>> {
    let tokenizer = Tokenizer::new().expect("Failed to create tokenizer");

    texts
        .par_iter()  // 병렬 처리
        .map(|text| {
            tokenizer
                .tokenize(text)
                .iter()
                .map(|t| t.surface.clone())
                .collect()
        })
        .collect()
}

fn main() {
    let texts = vec![
        "첫 번째 문장입니다.".to_string(),
        "두 번째 문장입니다.".to_string(),
        "세 번째 문장입니다.".to_string(),
    ];

    let results = process_batch(&texts);

    for (text, tokens) in texts.iter().zip(results.iter()) {
        println!("{} -> {:?}", text, tokens);
    }
}

정확도 평가

평가 데이터 형식 (TSV)

eval_data.tsv:

안녕하세요	안녕/NNG 하/XSV 세요/EP+EF
오늘 날씨가 좋습니다	오늘/NNG 날씨/NNG 가/JKS 좋/VA 습니다/EF

CLI로 평가

# 정확도 측정
mecab-ko evaluate --data eval_data.tsv

# 상세 출력
mecab-ko evaluate --data eval_data.tsv --verbose

# 품사별 정확도
mecab-ko evaluate --data eval_data.tsv --pos-detail

프로그래밍 방식 평가

use mecab_ko_core::evaluate::{Evaluator, EvalResult};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let evaluator = Evaluator::new()?;

    let result = evaluator.evaluate_file("eval_data.tsv")?;

    println!("=== 평가 결과 ===");
    println!("토큰 정확도: {:.2}%", result.token_accuracy * 100.0);
    println!("문장 정확도: {:.2}%", result.sentence_accuracy * 100.0);
    println!("품사 정확도: {:.2}%", result.pos_accuracy * 100.0);
    println!();
    println!("Precision: {:.2}%", result.precision * 100.0);
    println!("Recall: {:.2}%", result.recall * 100.0);
    println!("F1 Score: {:.2}%", result.f1_score * 100.0);

    // 품사별 정확도
    println!("\n=== 품사별 정확도 ===");
    for (pos, accuracy) in &result.pos_stats {
        println!("  {}: {:.2}%", pos, accuracy * 100.0);
    }

    Ok(())
}

출력:

=== 평가 결과 ===
토큰 정확도: 96.45%
문장 정확도: 89.23%
품사 정확도: 94.87%

Precision: 95.12%
Recall: 94.56%
F1 Score: 94.84%

=== 품사별 정확도 ===
  NNG: 97.23%
  NNP: 92.45%
  VV: 95.67%
  VA: 96.12%
  ...

사전 품질 검증

v0.2.0에서 추가된 사전 분석 기능을 사용합니다.

CLI로 검증

# 기본 검증
mecab-ko-dict-validator validate /path/to/dict

# 통계 분석 포함
mecab-ko-dict-validator validate /path/to/dict --analyze

# 자동 수정 제안
mecab-ko-dict-validator validate /path/to/dict --fix

# JSON 출력
mecab-ko-dict-validator validate /path/to/dict --output-format json

품사 분포 분석

use mecab_ko_dict_validator::{Analyzer, AnalysisReport};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let analyzer = Analyzer::new("/path/to/dict")?;
    let report = analyzer.analyze()?;

    println!("=== 품사 분포 ===");
    for stat in &report.pos_distribution.top_tags {
        println!(
            "  {}: {} ({:.1}%)",
            stat.tag,
            stat.count,
            stat.percentage
        );
    }

    println!("\n=== 비용 분포 ===");
    println!("  평균: {}", report.cost_distribution.mean);
    println!("  중앙값: {}", report.cost_distribution.median);
    println!("  표준편차: {}", report.cost_distribution.std_dev);

    // 이상치 탐지
    if !report.anomalies.is_empty() {
        println!("\n=== 이상치 ({}) ===", report.anomalies.len());
        for anomaly in report.anomalies.iter().take(5) {
            println!("  {} (비용: {})", anomaly.surface, anomaly.cost);
        }
    }

    Ok(())
}

일관성 검사

use mecab_ko_dict_validator::{Validator, ValidationResult};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let validator = Validator::new()?;
    let result = validator.validate("/path/to/dict")?;

    println!("=== 검증 결과 ===");
    println!("총 항목: {}", result.total_entries);
    println!("오류: {}", result.errors.len());
    println!("경고: {}", result.warnings.len());

    // 중복 검사
    if !result.duplicates.is_empty() {
        println!("\n중복 항목:");
        for dup in &result.duplicates {
            println!("  {} ({}회)", dup.surface, dup.count);
        }
    }

    // 품사 태그 검증
    for invalid in &result.invalid_pos_tags {
        println!("잘못된 품사: {} @ line {}", invalid.tag, invalid.line);
    }

    Ok(())
}

Unknown 단어 패턴

v0.2.0에서 개선된 미등록어 처리:

use mecab_ko_core::unknown::{WordPattern, detect_pattern, estimate_pos};

fn main() {
    let words = vec![
        "ChatGPT",      // CamelCase -> NNP
        "iPhone15",     // NumberUnit
        "삼성Galaxy",   // HangulAlphaMix -> NNG
        "김철수",       // ProperNoun -> NNP
    ];

    for word in words {
        let pattern = detect_pattern(word);
        let pos = estimate_pos(&pattern);
        println!("{}: {:?} -> {}", word, pattern, pos);
    }
}

출력:

ChatGPT: CamelCase -> NNP
iPhone15: NumberUnit -> NNG
삼성Galaxy: HangulAlphaMix -> NNG
김철수: ProperNoun -> NNP

실습: 문서 분석 파이프라인

use mecab_ko::{Tokenizer, TokenizerConfig};
use mecab_ko_core::DecompoundMode;
use std::collections::HashMap;

struct DocumentAnalyzer {
    tokenizer: Tokenizer,
}

impl DocumentAnalyzer {
    fn new() -> Result<Self, Box<dyn std::error::Error>> {
        let config = TokenizerConfig {
            decompound_mode: DecompoundMode::Mixed,
            ..Default::default()
        };
        let tokenizer = Tokenizer::with_config(config)?;
        Ok(Self { tokenizer })
    }

    fn analyze(&self, text: &str) -> DocumentStats {
        let tokens = self.tokenizer.tokenize(text);

        let mut pos_count: HashMap<String, usize> = HashMap::new();
        let mut noun_freq: HashMap<String, usize> = HashMap::new();

        for token in &tokens {
            // 품사 카운트
            let pos_category = &token.pos[..token.pos.len().min(2)];
            *pos_count.entry(pos_category.to_string()).or_insert(0) += 1;

            // 명사 빈도
            if token.pos.starts_with("NN") && token.surface.chars().count() > 1 {
                *noun_freq.entry(token.surface.clone()).or_insert(0) += 1;
            }
        }

        DocumentStats {
            total_tokens: tokens.len(),
            pos_distribution: pos_count,
            top_nouns: Self::top_n(&noun_freq, 10),
        }
    }

    fn top_n(freq: &HashMap<String, usize>, n: usize) -> Vec<(String, usize)> {
        let mut items: Vec<_> = freq.iter().map(|(k, v)| (k.clone(), *v)).collect();
        items.sort_by(|a, b| b.1.cmp(&a.1));
        items.truncate(n);
        items
    }
}

struct DocumentStats {
    total_tokens: usize,
    pos_distribution: HashMap<String, usize>,
    top_nouns: Vec<(String, usize)>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let analyzer = DocumentAnalyzer::new()?;

    let document = r#"
        인공지능 기술의 발전으로 자연어 처리 분야가 급격히 성장하고 있습니다.
        특히 대규모 언어 모델은 다양한 자연어 처리 작업에서 뛰어난 성능을 보이고 있습니다.
        한국어 형태소 분석기는 이러한 자연어 처리 파이프라인의 핵심 구성 요소입니다.
    "#;

    let stats = analyzer.analyze(document);

    println!("=== 문서 통계 ===");
    println!("총 토큰: {}", stats.total_tokens);

    println!("\n품사 분포:");
    for (pos, count) in &stats.pos_distribution {
        println!("  {}: {}", pos, count);
    }

    println!("\n상위 명사:");
    for (noun, count) in &stats.top_nouns {
        println!("  {}: {}회", noun, count);
    }

    Ok(())
}

다음 단계

웹 서버 통합 튜토리얼

MeCab-Ko를 웹 애플리케이션에 통합하는 방법을 알아봅니다.

목차

  1. Actix-web 통합
  2. Axum 통합
  3. REST API 설계
  4. 성능 최적화
  5. Docker 배포

Actix-web 통합

프로젝트 설정

cargo new mecab-server
cd mecab-server

Cargo.toml:

[package]
name = "mecab-server"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
mecab-ko = "0.2"
tokio = { version = "1", features = ["full"] }

기본 서버 구현

use actix_web::{web, App, HttpResponse, HttpServer};
use mecab_ko::Tokenizer;
use serde::{Deserialize, Serialize};
use std::sync::Arc;

// 요청/응답 구조체
#[derive(Deserialize)]
struct AnalyzeRequest {
    text: String,
    #[serde(default)]
    output_format: Option<String>,
}

#[derive(Serialize)]
struct Token {
    surface: String,
    pos: String,
    start: usize,
    end: usize,
}

#[derive(Serialize)]
struct AnalyzeResponse {
    tokens: Vec<Token>,
    morphs: Vec<String>,
    nouns: Vec<String>,
}

// 애플리케이션 상태
struct AppState {
    tokenizer: Tokenizer,
}

// 핸들러
async fn analyze(
    state: web::Data<Arc<AppState>>,
    req: web::Json<AnalyzeRequest>,
) -> HttpResponse {
    let tokens = state.tokenizer.tokenize(&req.text);

    let response = AnalyzeResponse {
        tokens: tokens
            .iter()
            .map(|t| Token {
                surface: t.surface.clone(),
                pos: t.pos.clone(),
                start: t.start,
                end: t.end,
            })
            .collect(),
        morphs: tokens.iter().map(|t| t.surface.clone()).collect(),
        nouns: tokens
            .iter()
            .filter(|t| t.pos.starts_with("NN"))
            .map(|t| t.surface.clone())
            .collect(),
    };

    HttpResponse::Ok().json(response)
}

async fn health() -> HttpResponse {
    HttpResponse::Ok().json(serde_json::json!({
        "status": "healthy",
        "version": env!("CARGO_PKG_VERSION")
    }))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 토크나이저 초기화 (한 번만)
    let tokenizer = Tokenizer::new().expect("Failed to initialize tokenizer");
    let state = Arc::new(AppState { tokenizer });

    println!("Server running at http://localhost:8080");

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(state.clone()))
            .route("/health", web::get().to(health))
            .route("/analyze", web::post().to(analyze))
    })
    .workers(num_cpus::get())  // CPU 코어 수만큼 워커
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

테스트

# 서버 실행
cargo run --release

# 분석 요청
curl -X POST http://localhost:8080/analyze \
  -H "Content-Type: application/json" \
  -d '{"text": "안녕하세요, 오늘 날씨가 좋네요!"}'

응답:

{
  "tokens": [
    {"surface": "안녕", "pos": "NNG", "start": 0, "end": 6},
    {"surface": "하", "pos": "XSV", "start": 6, "end": 9},
    ...
  ],
  "morphs": ["안녕", "하", "세요", ",", "오늘", "날씨", "가", "좋", "네요", "!"],
  "nouns": ["안녕", "오늘", "날씨"]
}

Axum 통합

프로젝트 설정

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
mecab-ko = "0.2"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"

Axum 서버 구현

use axum::{
    extract::State,
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use mecab_ko::Tokenizer;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tower_http::cors::CorsLayer;
use tracing_subscriber;

#[derive(Clone)]
struct AppState {
    tokenizer: Arc<Tokenizer>,
}

#[derive(Deserialize)]
struct AnalyzeRequest {
    text: String,
}

#[derive(Serialize)]
struct TokenResponse {
    surface: String,
    pos: String,
    start: usize,
    end: usize,
}

#[derive(Serialize)]
struct AnalyzeResponse {
    success: bool,
    data: AnalyzeData,
}

#[derive(Serialize)]
struct AnalyzeData {
    tokens: Vec<TokenResponse>,
    token_count: usize,
    noun_count: usize,
}

async fn analyze(
    State(state): State<AppState>,
    Json(payload): Json<AnalyzeRequest>,
) -> Result<Json<AnalyzeResponse>, StatusCode> {
    let tokens = state.tokenizer.tokenize(&payload.text);

    let token_responses: Vec<TokenResponse> = tokens
        .iter()
        .map(|t| TokenResponse {
            surface: t.surface.clone(),
            pos: t.pos.clone(),
            start: t.start,
            end: t.end,
        })
        .collect();

    let noun_count = tokens
        .iter()
        .filter(|t| t.pos.starts_with("NN"))
        .count();

    Ok(Json(AnalyzeResponse {
        success: true,
        data: AnalyzeData {
            token_count: token_responses.len(),
            noun_count,
            tokens: token_responses,
        },
    }))
}

async fn health() -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "status": "healthy",
        "service": "mecab-ko-api"
    }))
}

#[tokio::main]
async fn main() {
    // 로깅 설정
    tracing_subscriber::fmt::init();

    // 토크나이저 초기화
    let tokenizer = Arc::new(
        Tokenizer::new().expect("Failed to initialize tokenizer")
    );

    let state = AppState { tokenizer };

    // 라우터 설정
    let app = Router::new()
        .route("/health", get(health))
        .route("/analyze", post(analyze))
        .layer(CorsLayer::permissive())
        .with_state(state);

    // 서버 시작
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
        .await
        .unwrap();

    println!("Server running at http://localhost:8080");
    axum::serve(listener, app).await.unwrap();
}

REST API 설계

API 엔드포인트

MethodPathDescription
GET/health헬스 체크
POST/analyze텍스트 분석
POST/batch배치 분석
POST/nouns명사 추출
POST/pos품사 태깅

배치 분석 엔드포인트

#![allow(unused)]
fn main() {
#[derive(Deserialize)]
struct BatchRequest {
    texts: Vec<String>,
}

#[derive(Serialize)]
struct BatchResponse {
    results: Vec<AnalyzeData>,
    total_texts: usize,
    total_tokens: usize,
}

async fn batch_analyze(
    State(state): State<AppState>,
    Json(payload): Json<BatchRequest>,
) -> Json<BatchResponse> {
    let results: Vec<AnalyzeData> = payload
        .texts
        .iter()
        .map(|text| {
            let tokens = state.tokenizer.tokenize(text);
            AnalyzeData {
                token_count: tokens.len(),
                noun_count: tokens.iter().filter(|t| t.pos.starts_with("NN")).count(),
                tokens: tokens
                    .iter()
                    .map(|t| TokenResponse {
                        surface: t.surface.clone(),
                        pos: t.pos.clone(),
                        start: t.start,
                        end: t.end,
                    })
                    .collect(),
            }
        })
        .collect();

    let total_tokens: usize = results.iter().map(|r| r.token_count).sum();

    Json(BatchResponse {
        total_texts: results.len(),
        total_tokens,
        results,
    })
}
}

에러 처리

#![allow(unused)]
fn main() {
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};

#[derive(Serialize)]
struct ErrorResponse {
    success: bool,
    error: String,
    code: String,
}

enum AppError {
    InvalidInput(String),
    AnalysisFailed(String),
    InternalError(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, code, message) = match self {
            AppError::InvalidInput(msg) => {
                (StatusCode::BAD_REQUEST, "INVALID_INPUT", msg)
            }
            AppError::AnalysisFailed(msg) => {
                (StatusCode::UNPROCESSABLE_ENTITY, "ANALYSIS_FAILED", msg)
            }
            AppError::InternalError(msg) => {
                (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", msg)
            }
        };

        let body = Json(ErrorResponse {
            success: false,
            error: message,
            code: code.to_string(),
        });

        (status, body).into_response()
    }
}
}

성능 최적화

토크나이저 풀링

#![allow(unused)]
fn main() {
use std::sync::Arc;
use tokio::sync::Semaphore;

struct TokenizerPool {
    tokenizer: Arc<Tokenizer>,
    semaphore: Arc<Semaphore>,
}

impl TokenizerPool {
    fn new(max_concurrent: usize) -> Self {
        Self {
            tokenizer: Arc::new(Tokenizer::new().unwrap()),
            semaphore: Arc::new(Semaphore::new(max_concurrent)),
        }
    }

    async fn analyze(&self, text: &str) -> Vec<Token> {
        let _permit = self.semaphore.acquire().await.unwrap();
        self.tokenizer.tokenize(text)
    }
}
}

캐싱 (Redis)

#![allow(unused)]
fn main() {
use redis::{AsyncCommands, Client as RedisClient};
use serde_json;

struct CachedTokenizer {
    tokenizer: Tokenizer,
    redis: RedisClient,
    cache_ttl: usize,
}

impl CachedTokenizer {
    async fn analyze(&self, text: &str) -> Vec<TokenResponse> {
        let cache_key = format!("mecab:{}", md5::compute(text).0);

        // 캐시 확인
        let mut conn = self.redis.get_async_connection().await.unwrap();
        if let Ok(cached) = conn.get::<_, String>(&cache_key).await {
            if let Ok(tokens) = serde_json::from_str(&cached) {
                return tokens;
            }
        }

        // 분석 실행
        let tokens = self.tokenizer.tokenize(text);
        let response: Vec<TokenResponse> = tokens
            .iter()
            .map(|t| TokenResponse {
                surface: t.surface.clone(),
                pos: t.pos.clone(),
                start: t.start,
                end: t.end,
            })
            .collect();

        // 캐시 저장
        let json = serde_json::to_string(&response).unwrap();
        let _: () = conn
            .set_ex(&cache_key, json, self.cache_ttl)
            .await
            .unwrap();

        response
    }
}
}

요청 제한 (Rate Limiting)

#![allow(unused)]
fn main() {
use tower_governor::{
    governor::GovernorConfigBuilder,
    GovernorLayer,
};

fn create_rate_limiter() -> GovernorLayer {
    let config = GovernorConfigBuilder::default()
        .per_second(100)  // 초당 100 요청
        .burst_size(50)   // 버스트 50
        .finish()
        .unwrap();

    GovernorLayer::with_config(config)
}

// Router에 적용
let app = Router::new()
    .route("/analyze", post(analyze))
    .layer(create_rate_limiter());
}

Docker 배포

Dockerfile

# Build stage
FROM rust:1.75 as builder

WORKDIR /app
COPY . .

# 릴리스 빌드
RUN cargo build --release --bin mecab-server

# Runtime stage
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 바이너리 복사
COPY --from=builder /app/target/release/mecab-server /app/

# 사전 데이터 복사 (필요한 경우)
# COPY --from=builder /app/data/dict /app/data/dict

# 비루트 사용자
RUN useradd -m appuser
USER appuser

EXPOSE 8080

ENV RUST_LOG=info

CMD ["./mecab-server"]

docker-compose.yml

version: '3.8'

services:
  mecab-api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - RUST_LOG=info
      - MECAB_DIC_DIR=/app/data/dict
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '2'
          memory: 512M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - mecab-api

volumes:
  redis-data:

Nginx 설정

upstream mecab_backend {
    least_conn;
    server mecab-api:8080;
}

server {
    listen 80;

    location /api/ {
        proxy_pass http://mecab_backend/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 타임아웃 설정
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 30s;
    }

    location /health {
        proxy_pass http://mecab_backend/health;
    }
}

배포 명령

# 이미지 빌드
docker-compose build

# 서비스 시작
docker-compose up -d

# 스케일 아웃
docker-compose up -d --scale mecab-api=5

# 로그 확인
docker-compose logs -f mecab-api

# 상태 확인
docker-compose ps

클라이언트 예제

Python 클라이언트

import requests
from typing import List, Dict

class MecabClient:
    def __init__(self, base_url: str = "http://localhost:8080"):
        self.base_url = base_url

    def analyze(self, text: str) -> Dict:
        response = requests.post(
            f"{self.base_url}/analyze",
            json={"text": text}
        )
        response.raise_for_status()
        return response.json()

    def batch_analyze(self, texts: List[str]) -> Dict:
        response = requests.post(
            f"{self.base_url}/batch",
            json={"texts": texts}
        )
        response.raise_for_status()
        return response.json()

# 사용
client = MecabClient()
result = client.analyze("안녕하세요")
print(result)

JavaScript 클라이언트

class MecabClient {
  constructor(baseUrl = 'http://localhost:8080') {
    this.baseUrl = baseUrl;
  }

  async analyze(text) {
    const response = await fetch(`${this.baseUrl}/analyze`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text })
    });
    return response.json();
  }

  async batchAnalyze(texts) {
    const response = await fetch(`${this.baseUrl}/batch`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ texts })
    });
    return response.json();
  }
}

// 사용
const client = new MecabClient();
const result = await client.analyze('안녕하세요');
console.log(result);

다음 단계

CLI 사용법

mecab-ko 명령줄 도구는 터미널에서 빠르게 형태소 분석을 수행할 수 있는 인터페이스를 제공합니다.

기본 문법

mecab-ko [OPTIONS] [INPUT] [COMMAND]

입력 방식

직접 입력

mecab-ko "분석할 텍스트"

표준 입력 (stdin)

echo "분석할 텍스트" | mecab-ko
cat input.txt | mecab-ko

파이프라인

mecab-ko "첫 번째 문장" | grep "NNG"

옵션 상세

-d, --dicdir <PATH>

사전 경로를 지정합니다.

mecab-ko -d /path/to/mecab-ko-dic "테스트"

기본적으로 내장 사전을 사용하며, 외부 사전을 지정하면 해당 사전을 우선 사용합니다.

-u, --user-dic <PATH>

CSV 형식의 사용자 사전 파일을 로드합니다.

mecab-ko --user-dic user.csv "사용자 정의 단어"
mecab-ko -u custom.csv "테스트"

사용자 사전에 대한 자세한 내용은 사용자 사전 장을 참조하세요.

-O, --output-format <FORMAT>

출력 포맷을 지정합니다.

mecab-ko -O wakati "형태소 분석"
mecab-ko -O json "형태소 분석"
mecab-ko --output-format csv "형태소 분석"

사용 가능한 포맷:

포맷설명예시 출력
defaultMeCab 기본 포맷 (기본값)안녕\tNNG
wakati형태소만 공백 분리안녕 하 세요
jsonJSON 배열[{"surface":"안녕",...}]
csvCSV 헤더 포함surface,pos,start,end,...
pos표면형/품사 슬래시 구분안녕/NNG
simple표면형/품사 쌍 공백 분리안녕/NNG 하/XSV
dump디버그용 상세 정보[000] surface="안녕" ...

-N, --nbest <N>

상위 N개의 분석 결과를 출력합니다. (기본값: 1)

mecab-ko -N 3 "아버지가방에들어가신다"

--separator <SEP>

wakati 출력 시 형태소 간 구분자를 지정합니다. (기본값: 공백)

mecab-ko -O wakati --separator "/" "형태소 분석"
# Output: 형태소/분석

--no-line

라인별 처리를 비활성화하고 전체 텍스트를 하나로 처리합니다.

echo -e "첫째 줄\n둘째 줄" | mecab-ko --no-line

-a, --all

디버그용 전체 분석 결과를 표시합니다.

mecab-ko -a "테스트"

-q, --quiet

경고 메시지를 숨깁니다.

mecab-ko -q --user-dic user.csv "테스트"

서브커맨드

parse

형태소 분석을 수행합니다. (기본 동작)

mecab-ko parse "분석할 텍스트"
mecab-ko parse --help

dict

사전 정보를 표시합니다.

mecab-ko dict
mecab-ko dict /path/to/dictionary

출력 예시:

MeCab-Ko Dictionary Information
================================
Path: /path/to/dictionary
(Dictionary information)

evaluate

형태소 분석 정확도를 평가합니다. (v0.2.0+)

mecab-ko evaluate --input test.tsv --dicdir ./dict-output

옵션

옵션설명
--input <FILE>테스트 데이터 파일 (TSV 형식)
--dicdir <PATH>사전 경로
--sejong세종 호환 모드로 평가 (v0.3.1+)
--verbose틀린 문장 상세 출력

테스트 데이터 형식 (TSV)

텍스트\t토큰1/품사1 토큰2/품사2 ...
나는 학교에 갔다\t나/NP 는/JX 학교/NNG 에/JKB 갔/VV 다/EF

세종 호환 모드

--sejong 옵션을 사용하면 복합 태그(VV+EF)를 세종 코퍼스 형식(VV, EF)으로 분리하여 평가합니다.

# 기본 모드
mecab-ko evaluate --input test.tsv --dicdir ./dict-output
# Token Accuracy: 15.2%

# 세종 호환 모드
mecab-ko evaluate --input test.tsv --dicdir ./dict-output --sejong
# Token Accuracy: 16.8%

출력 지표

지표설명
Token Accuracy토큰 단위 정확도
Sentence Accuracy문장 단위 정확도
POS Accuracy품사 태그 정확도
Precision정밀도
Recall재현율
F1 ScoreF1 점수

version

버전 정보를 표시합니다.

mecab-ko version

출력 예시:

mecab-ko 0.3.1
Rust implementation of Korean morphological analyzer

Features:
  - MeCab-compatible analysis
  - User dictionary support
  - Multiple output formats
  - Sejong corpus compatibility (v0.3.1)

Repository: https://github.com/hephaex/mecab-ko

출력 포맷 상세

Default 포맷

MeCab 호환 형식입니다.

mecab-ko "안녕하세요"
안녕    NNG
하      XSV
세요    EP+EF
EOS

각 라인은 탭으로 구분된 표면형 \t 품사정보 형식이며, 분석이 끝나면 EOS가 출력됩니다.

Wakati 포맷

형태소만 공백으로 분리하여 출력합니다.

mecab-ko -O wakati "형태소 분석을 수행합니다"
형태소 분석 을 수행 합니다

JSON 포맷

프로그래밍에 편리한 JSON 배열 형식입니다.

mecab-ko -O json "안녕"
[
  {
    "surface": "안녕",
    "pos": "NNG",
    "start": 0,
    "end": 6
  }
]

필드 설명:

필드타입설명
surfacestring표면형
posstring품사 태그
startnumber시작 바이트 위치
endnumber끝 바이트 위치
readingstring?읽기 (있는 경우)
lemmastring?원형 (있는 경우)

CSV 포맷

스프레드시트 친화적인 CSV 형식입니다.

mecab-ko -O csv "테스트 문장"
surface,pos,start,end,reading,lemma
테스트,NNG,0,9,,
문장,NNG,10,16,,

POS 포맷

각 형태소를 표면형/품사 형식으로 출력합니다.

mecab-ko -O pos "안녕하세요"
안녕/NNG
하/XSV
세요/EP+EF

Simple 포맷

표면형/품사 쌍을 한 줄에 공백으로 연결합니다.

mecab-ko -O simple "안녕하세요"
안녕/NNG 하/XSV 세요/EP+EF

Dump 포맷

디버깅을 위한 상세 정보를 출력합니다.

mecab-ko -O dump "안녕"
[000] surface="안녕" pos=NNG span=[0,6)

사용 예시

명사 추출

mecab-ko "오늘 날씨가 좋습니다" | grep "NNG\|NNP"

형태소 수 세기

mecab-ko -O wakati "긴 문장을 분석합니다" | tr ' ' '\n' | wc -l

파일 일괄 처리

for file in *.txt; do
    mecab-ko -O json < "$file" > "${file%.txt}.json"
done

JSON을 jq로 처리

mecab-ko -O json "안녕하세요" | jq '.[] | .surface'

사용자 사전으로 분석 결과 비교

echo "딥러닝 기술" | mecab-ko
echo "딥러닝 기술" | mecab-ko --user-dic tech.csv

종료 코드

코드의미
0성공
1일반 오류
2명령줄 인자 오류

도움말

전체 옵션 목록:

mecab-ko --help
mecab-ko -h

특정 서브커맨드 도움말:

mecab-ko parse --help
mecab-ko dict --help

사용자 사전

사용자 사전은 기본 사전에 없는 단어를 추가하여 형태소 분석 품질을 향상시키는 기능입니다. 신조어, 전문 용어, 고유명사 등을 등록할 수 있습니다.

사전 파일 형식

CSV 포맷

사용자 사전은 CSV(Comma-Separated Values) 형식을 사용합니다.

# 주석 라인 (# 으로 시작)
# 표면형,품사,비용,읽기
딥러닝,NNG,-1000,딥러닝
머신러닝,NNG,-1000,머신러닝
챗GPT,NNP,-1000,챗지피티
앤트로픽,NNP,-1000,앤트로픽

필드 설명

필드필수설명예시
표면형O등록할 단어딥러닝
품사O품사 태그NNG, NNP, VV
비용X선택 우선도 (낮을수록 우선)-1000
읽기X발음 정보딥러닝

비용(Cost) 이해하기

비용은 형태소 분석 시 해당 단어의 선택 우선도를 결정합니다:

  • 음수 값: 우선 선택 (추천: -1000 ~ -500)
  • 0: 기본 비용
  • 양수 값: 후순위 선택
# 높은 우선순위 (신조어, 전문 용어)
딥러닝,NNG,-1000,
GPT,NNP,-1000,

# 보통 우선순위
인공지능,NNG,-500,

# 낮은 우선순위 (거의 사용되지 않는 단어)
오래된단어,NNG,100,

품사 태그

사용자 사전에서 자주 사용하는 품사 태그:

태그의미예시
NNG일반 명사딥러닝, 인공지능
NNP고유 명사앤트로픽, 오픈AI
NNB의존 명사것, 수, 등
VV동사분석하다, 처리하다
VA형용사빠르다, 좋다
MAG일반 부사매우, 아주
SL외국어API, GPU
SH한자韓國, 人工
SN숫자123, 456

전체 품사 태그 목록은 품사 태그 장을 참조하세요.

CLI에서 사용

기본 사용

mecab-ko --user-dic user.csv "딥러닝 기술이 발전하고 있습니다"

조용히 실행

사전 로드 메시지 숨기기:

mecab-ko -q --user-dic user.csv "테스트"

여러 사전 파일

현재는 하나의 사용자 사전만 지정 가능합니다. 여러 사전을 사용하려면 파일을 합치세요:

cat dict1.csv dict2.csv > combined.csv
mecab-ko --user-dic combined.csv "테스트"

라이브러리에서 사용

기본 사용법

use mecab_ko_dict::UserDictionary;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut dict = UserDictionary::new();

    // Add entries programmatically
    dict.add_entry("딥러닝", "NNG", Some(-1000), None);
    dict.add_entry("머신러닝", "NNG", Some(-1000), Some("머신러닝".to_string()));

    // Or load from file
    dict.load_from_csv("user.csv")?;

    println!("Loaded {} entries", dict.len());
    Ok(())
}

빌더 패턴

#![allow(unused)]
fn main() {
use mecab_ko_dict::UserDictionaryBuilder;

let dict = UserDictionaryBuilder::new()
    .default_cost(-500)              // Set default cost
    .add("딥러닝", "NNG")            // Uses default cost
    .add_with_cost("GPT", "NNP", -1000)
    .add_full("챗GPT", "NNP", -1000, Some("챗지피티"))
    .load_csv("extra.csv")?         // Load additional entries
    .build();
}

엔트리 조회

#![allow(unused)]
fn main() {
let entries = dict.lookup("딥러닝");
for entry in entries {
    println!("Surface: {}", entry.surface);
    println!("POS: {}", entry.pos);
    println!("Cost: {}", entry.cost);
    if let Some(reading) = &entry.reading {
        println!("Reading: {}", reading);
    }
}
}

문자열에서 로드

#![allow(unused)]
fn main() {
let csv_content = r#"
IT 용어 사전
딥러닝,NNG,-1000,
머신러닝,NNG,-1000,
자연어처리,NNG,-1000,자연어처리
"#;

dict.load_from_str(csv_content)?;
}

컨텍스트 ID 지정

고급 사용자를 위해 좌/우 문맥 ID를 직접 지정할 수 있습니다:

#![allow(unused)]
fn main() {
dict.add_entry_with_ids(
    "특수단어",
    "NNG",
    -1000,    // cost
    1234,     // left_id
    5678,     // right_id
    None,     // reading
);
}

사전 저장

#![allow(unused)]
fn main() {
dict.save_to_csv("output.csv")?;
}

도메인별 사전 예시

IT/기술 용어

# IT 기술 용어 사전
딥러닝,NNG,-1000,딥러닝
머신러닝,NNG,-1000,머신러닝
인공지능,NNG,-1000,인공지능
자연어처리,NNG,-1000,자연어처리
GPT,NNP,-1000,지피티
API,SL,-1000,에이피아이
클라우드,NNG,-1000,클라우드
쿠버네티스,NNP,-1000,쿠버네티스
도커,NNP,-1000,도커

브랜드/회사명

# 회사 및 브랜드명
앤트로픽,NNP,-1000,앤트로픽
오픈AI,NNP,-1000,오픈에이아이
구글,NNP,-1000,구글
마이크로소프트,NNP,-1000,마이크로소프트
메타,NNP,-1000,메타
테슬라,NNP,-1000,테슬라

신조어/인터넷 용어

# 신조어 및 인터넷 용어
갓생,NNG,-1000,갓생
소확행,NNG,-1000,소확행
워라밸,NNG,-1000,워라밸
MZ세대,NNG,-1000,엠지세대
플렉스,NNG,-1000,플렉스
TMI,NNG,-1000,티엠아이

사전 작성 팁

1. 적절한 비용 설정

# 일반적인 경우: -1000
신조어,NNG,-1000,

# 자주 사용되는 경우: -1500
아주자주쓰는단어,NNG,-1500,

# 덜 중요한 경우: -500
덜중요한단어,NNG,-500,

2. 같은 표면형, 다른 품사

하나의 단어가 여러 품사로 사용될 수 있습니다:

# '분석'은 명사와 동사 어간 모두 가능
분석,NNG,-1000,

3. 복합어 등록

띄어쓰기 없는 복합어를 하나의 단어로 등록:

자연어처리,NNG,-1000,자연어처리
인공지능,NNG,-1000,인공지능

4. 외래어 표기 변형

다양한 표기를 모두 등록:

# 같은 단어의 다른 표기
쿠버네티스,NNP,-1000,
쿠버네테스,NNP,-1000,
kubernetes,SL,-1000,

5. 읽기 정보 활용

발음이 표기와 다른 경우 읽기 정보 제공:

GPT,NNP,-1000,지피티
API,SL,-1000,에이피아이
CEO,SL,-1000,씨이오

검증 및 테스트

사전 로드 테스트

#![allow(unused)]
fn main() {
use mecab_ko_dict::UserDictionary;

fn test_dictionary() -> Result<(), Box<dyn std::error::Error>> {
    let mut dict = UserDictionary::new();
    dict.load_from_csv("user.csv")?;

    // Verify entry count
    println!("Total entries: {}", dict.len());

    // Test specific lookup
    let entries = dict.lookup("딥러닝");
    assert!(!entries.is_empty(), "Entry 'Deep Learning' not found");

    Ok(())
}
}

분석 결과 비교

# Without user dictionary
echo "딥러닝 기술" | mecab-ko

# With user dictionary
echo "딥러닝 기술" | mecab-ko --user-dic user.csv

문제 해결

엔트리가 적용되지 않음

  1. 비용이 너무 높은지 확인 (음수 값 사용)
  2. 품사 태그가 올바른지 확인
  3. CSV 형식이 정확한지 확인 (콤마 구분)

파싱 오류

Error: Invalid user dictionary format at line 5

해당 라인의 형식 확인:

  • 최소 2개 필드 (표면형, 품사) 필요
  • 빈 필드 허용 (예: 단어,NNG,,)
  • 콤마가 포함된 값은 큰따옴표로 감싸기

인코딩 문제

사전 파일은 UTF-8 인코딩을 사용해야 합니다:

# Check encoding
file user.csv

# Convert from EUC-KR to UTF-8
iconv -f EUC-KR -t UTF-8 user_euckr.csv > user.csv

출력 포맷

MeCab-Ko는 다양한 출력 포맷을 지원합니다. 용도에 따라 적절한 포맷을 선택하세요.

포맷 비교

포맷용도파싱 용이성가독성
defaultMeCab 호환중간좋음
wakati분리된 텍스트쉬움좋음
json프로그래밍매우 쉬움보통
csv스프레드시트쉬움보통
pos간단한 태깅쉬움좋음
simple한 줄 요약쉬움좋음
dump디버깅어려움상세

Default 포맷

MeCab 원본과 호환되는 기본 포맷입니다.

사용법

mecab-ko "안녕하세요"
mecab-ko -O default "안녕하세요"

출력 형식

표면형\t품사정보
...
EOS

예시

$ mecab-ko "오늘 날씨가 좋습니다"
오늘    NNG
날씨    NNG
가      JKS
좋      VA
습니다  EF
EOS

파싱

# Python
for line in output.split('\n'):
    if line == 'EOS':
        break
    surface, pos = line.split('\t')
#![allow(unused)]
fn main() {
// Rust
for line in output.lines() {
    if line == "EOS" {
        break;
    }
    let parts: Vec<&str> = line.split('\t').collect();
    let surface = parts[0];
    let pos = parts[1];
}
}

Wakati 포맷

형태소를 공백으로 분리하여 출력합니다. 텍스트 전처리에 유용합니다.

사용법

mecab-ko -O wakati "안녕하세요"

출력 형식

형태소1 형태소2 형태소3 ...

예시

$ mecab-ko -O wakati "오늘 날씨가 좋습니다"
오늘 날씨 가 좋 습니다

커스텀 구분자

# Slash separator
mecab-ko -O wakati --separator "/" "테스트"
# Output: 테/스/트

# Newline separator
mecab-ko -O wakati --separator $'\n' "테스트"

활용 예시

# Word count
mecab-ko -O wakati "긴 문장" | tr ' ' '\n' | wc -l

# Unique morphemes
mecab-ko -O wakati "문장" | tr ' ' '\n' | sort | uniq

# TF-IDF input preparation
cat document.txt | mecab-ko -O wakati > tokenized.txt

JSON 포맷

프로그래밍에 가장 편리한 구조화된 형식입니다.

사용법

mecab-ko -O json "안녕하세요"

출력 형식

[
  {
    "surface": "표면형",
    "pos": "품사",
    "start": 시작바이트,
    "end": 끝바이트,
    "reading": "읽기",
    "lemma": "원형"
  },
  ...
]

예시

$ mecab-ko -O json "안녕"
[
  {
    "surface": "안녕",
    "pos": "NNG",
    "start": 0,
    "end": 6
  }
]

필드 설명

필드타입필수설명
surfacestringO표면형 (원문 그대로)
posstringO품사 태그
startnumberO시작 바이트 위치
endnumberO끝 바이트 위치 (exclusive)
readingstringX읽기/발음
lemmastringX원형/기본형

활용 예시

# Extract surfaces with jq
mecab-ko -O json "안녕하세요" | jq '.[].surface'

# Filter nouns
mecab-ko -O json "오늘 날씨" | jq '[.[] | select(.pos | startswith("NN"))]'

# Count by POS
mecab-ko -O json "문장" | jq 'group_by(.pos) | map({pos: .[0].pos, count: length})'
# Python
import json
import subprocess

result = subprocess.run(
    ["mecab-ko", "-O", "json", "안녕하세요"],
    capture_output=True, text=True
)
tokens = json.loads(result.stdout)

for token in tokens:
    print(f"{token['surface']}: {token['pos']}")

CSV 포맷

스프레드시트 애플리케이션에서 열기 좋은 형식입니다.

사용법

mecab-ko -O csv "안녕하세요"

출력 형식

surface,pos,start,end,reading,lemma
표면형1,품사1,시작1,끝1,읽기1,원형1
...

예시

$ mecab-ko -O csv "오늘 날씨"
surface,pos,start,end,reading,lemma
오늘,NNG,0,6,,
날씨,NNG,7,13,,

활용 예시

# Save to file
mecab-ko -O csv "문장" > output.csv

# Open with spreadsheet application
libreoffice --calc output.csv

# Extract column
mecab-ko -O csv "문장" | cut -d',' -f1,2

POS 포맷

표면형과 품사를 슬래시로 연결하여 출력합니다.

사용법

mecab-ko -O pos "안녕하세요"

출력 형식

표면형1/품사1
표면형2/품사2
...

예시

$ mecab-ko -O pos "오늘 날씨가 좋습니다"
오늘/NNG
날씨/NNG
가/JKS
좋/VA
습니다/EF

활용 예시

# Filter specific POS
mecab-ko -O pos "문장" | grep "/NNG"

# Count POS tags
mecab-ko -O pos "문장" | cut -d'/' -f2 | sort | uniq -c

Simple 포맷

한 줄에 모든 분석 결과를 출력합니다.

사용법

mecab-ko -O simple "안녕하세요"

출력 형식

표면형1/품사1 표면형2/품사2 ...

예시

$ mecab-ko -O simple "오늘 날씨가 좋습니다"
오늘/NNG 날씨/NNG 가/JKS 좋/VA 습니다/EF

활용 예시

간결한 로그 출력이나 비교에 유용:

# Compare two sentences
echo "문장1: $(mecab-ko -O simple '첫 번째 문장')"
echo "문장2: $(mecab-ko -O simple '두 번째 문장')"

Dump 포맷

디버깅을 위한 상세 정보를 출력합니다.

사용법

mecab-ko -O dump "안녕하세요"

출력 형식

[인덱스] surface="표면형" pos=품사 span=[시작,끝)

예시

$ mecab-ko -O dump "안녕"
[000] surface="안녕" pos=NNG span=[0,6)

활용

  • 바이트 위치 확인
  • 토큰 인덱스 추적
  • 분석 결과 디버깅

포맷 선택 가이드

상황별 추천

상황추천 포맷
기존 MeCab 스크립트와 호환default
텍스트 전처리/토큰화wakati
프로그래밍에서 파싱json
엑셀/스프레드시트 분석csv
간단한 태깅 확인pos, simple
문제 디버깅dump

성능 고려

출력 포맷에 따른 성능 차이는 미미합니다. 용도에 맞는 포맷을 선택하세요.

커스텀 출력

프로그래밍 방식으로 커스텀 출력을 만들 수 있습니다:

#![allow(unused)]
fn main() {
use mecab_ko::Tokenizer;

let tokenizer = Tokenizer::new()?;
let tokens = tokenizer.tokenize("안녕하세요");

// Custom format: SURFACE[POS]
for token in tokens {
    print!("{}[{}] ", token.surface, token.pos);
}
println!();
// Output: 안녕[NNG] 하[XSV] 세요[EP+EF]
}
# Using shell
mecab-ko -O json "안녕하세요" | jq -r '.[] | "\(.surface)[\(.pos)]"' | tr '\n' ' '

라이브러리 API

이 장에서는 MeCab-Ko 라이브러리의 주요 API를 설명합니다.

크레이트 구조

mecab-ko (통합 라이브러리)
├── mecab-ko-core     (핵심 분석 엔진)
├── mecab-ko-dict     (사전 관리)
└── mecab-ko-hangul   (한글 유틸리티)

대부분의 경우 mecab-ko 크레이트만 사용하면 됩니다. 개별 기능만 필요한 경우 해당 크레이트를 직접 의존할 수 있습니다.

mecab-ko

Tokenizer

형태소 분석의 메인 인터페이스입니다.

#![allow(unused)]
fn main() {
use mecab_ko::Tokenizer;
}

생성

#![allow(unused)]
fn main() {
// Create with default dictionary
let tokenizer = Tokenizer::new()?;

// Create with custom dictionary path
let tokenizer = Tokenizer::with_dict("/path/to/dict")?;
}

메서드

tokenize(&self, text: &str) -> Vec<Token>

텍스트를 형태소로 분석합니다.

#![allow(unused)]
fn main() {
let tokens = tokenizer.tokenize("안녕하세요");
for token in tokens {
    println!("{}: {} ({}-{})",
        token.surface, token.pos, token.start, token.end);
}
}
morphs(&self, text: &str) -> Vec<String>

형태소(표면형)만 추출합니다. wakati와 동일합니다.

#![allow(unused)]
fn main() {
let morphs = tokenizer.morphs("오늘 날씨가 좋습니다");
// ["오늘", "날씨", "가", "좋", "습니다"]
}
wakati(&self, text: &str) -> Vec<String>

형태소를 분리하여 표면형 목록으로 반환합니다.

#![allow(unused)]
fn main() {
let words = tokenizer.wakati("형태소 분석");
// ["형태소", "분석"]
}
nouns(&self, text: &str) -> Vec<String>

명사(NN으로 시작하는 품사)만 추출합니다.

#![allow(unused)]
fn main() {
let nouns = tokenizer.nouns("오늘 날씨가 좋습니다");
// ["오늘", "날씨"]
}
pos(&self, text: &str) -> Vec<(String, String)>

(표면형, 품사) 쌍의 목록을 반환합니다.

#![allow(unused)]
fn main() {
let pos_tagged = tokenizer.pos("안녕하세요");
// [("안녕", "NNG"), ("하", "XSV"), ("세요", "EP+EF")]
}

Token

형태소 분석 결과를 나타내는 구조체입니다.

#![allow(unused)]
fn main() {
pub struct Token {
    pub surface: String,        // 표면형
    pub pos: String,            // 품사 태그
    pub start: usize,           // 시작 바이트 위치
    pub end: usize,             // 끝 바이트 위치
    pub reading: Option<String>, // 읽기
    pub lemma: Option<String>,  // 원형 (기본형)
}
}

예시:

#![allow(unused)]
fn main() {
let token = &tokens[0];
println!("Surface: {}", token.surface);
println!("POS: {}", token.pos);
println!("Position: {}-{}", token.start, token.end);
if let Some(reading) = &token.reading {
    println!("Reading: {}", reading);
}
}

mecab-ko-hangul

한글 처리를 위한 유틸리티 함수들입니다.

#![allow(unused)]
fn main() {
use mecab_ko::hangul::*;
// or
use mecab_ko_hangul::*;
}

자모 분리/결합

decompose(c: char) -> Option<(char, char, Option<char>)>

한글 음절을 초성, 중성, 종성으로 분해합니다.

#![allow(unused)]
fn main() {
let result = decompose('한');
assert_eq!(result, Some(('ㅎ', 'ㅏ', Some('ㄴ'))));

let result = decompose('가');
assert_eq!(result, Some(('ㄱ', 'ㅏ', None)));

let result = decompose('a');
assert_eq!(result, None);  // Not a Hangul syllable
}

compose(cho: char, jung: char, jong: Option<char>) -> Option<char>

초성, 중성, 종성을 결합하여 한글 음절을 만듭니다.

#![allow(unused)]
fn main() {
let c = compose('ㅎ', 'ㅏ', Some('ㄴ'));
assert_eq!(c, Some('한'));

let c = compose('ㄱ', 'ㅏ', None);
assert_eq!(c, Some('가'));
}

decompose_str(s: &str) -> String

문자열의 모든 한글 음절을 자모로 분해합니다.

#![allow(unused)]
fn main() {
let result = decompose_str("한글");
assert_eq!(result, "ㅎㅏㄴㄱㅡㄹ");

let result = decompose_str("Hello 한글");
assert_eq!(result, "Hello ㅎㅏㄴㄱㅡㄹ");
}

compose_str(s: &str) -> String

자모 문자열을 한글 음절로 결합합니다.

#![allow(unused)]
fn main() {
let result = compose_str("ㅎㅏㄴㄱㅡㄹ");
assert_eq!(result, "한글");
}

문자 판별

is_hangul(c: char) -> bool

한글(음절 또는 자모)인지 확인합니다.

#![allow(unused)]
fn main() {
assert!(is_hangul('가'));
assert!(is_hangul('ㄱ'));
assert!(is_hangul('ㅏ'));
assert!(!is_hangul('a'));
}

is_hangul_syllable(c: char) -> bool

완성형 한글 음절인지 확인합니다.

#![allow(unused)]
fn main() {
assert!(is_hangul_syllable('가'));
assert!(is_hangul_syllable('힣'));
assert!(!is_hangul_syllable('ㄱ'));  // Jamo, not syllable
}

is_jamo(c: char) -> bool

한글 자모인지 확인합니다.

#![allow(unused)]
fn main() {
assert!(is_jamo('ㄱ'));
assert!(is_jamo('ㅏ'));
assert!(!is_jamo('가'));  // Syllable, not jamo
}

is_choseong(c: char) -> bool

초성 자모인지 확인합니다.

#![allow(unused)]
fn main() {
assert!(is_choseong('ㄱ'));
assert!(is_choseong('ㅎ'));
assert!(!is_choseong('ㅏ'));  // Jungseong, not choseong
}

is_jungseong(c: char) -> bool

중성 자모인지 확인합니다.

#![allow(unused)]
fn main() {
assert!(is_jungseong('ㅏ'));
assert!(is_jungseong('ㅣ'));
assert!(!is_jungseong('ㄱ'));
}

has_jongseong(c: char) -> Option<bool>

한글 음절에 종성이 있는지 확인합니다.

#![allow(unused)]
fn main() {
assert_eq!(has_jongseong('한'), Some(true));
assert_eq!(has_jongseong('하'), Some(false));
assert_eq!(has_jongseong('a'), None);  // Not Hangul
}

문자 분류

classify_char(c: char) -> CharType

문자의 종류를 판별합니다.

#![allow(unused)]
fn main() {
use mecab_ko::CharType;

assert_eq!(classify_char('한'), CharType::HangulSyllable);
assert_eq!(classify_char('ㄱ'), CharType::HangulJamo);
assert_eq!(classify_char('韓'), CharType::Hanja);
assert_eq!(classify_char('ア'), CharType::Katakana);
assert_eq!(classify_char('あ'), CharType::Hiragana);
assert_eq!(classify_char('a'), CharType::Alphabet);
assert_eq!(classify_char('1'), CharType::Digit);
assert_eq!(classify_char(' '), CharType::Whitespace);
assert_eq!(classify_char('.'), CharType::Punctuation);
}

CharType 열거형

#![allow(unused)]
fn main() {
pub enum CharType {
    HangulSyllable,  // 한글 음절
    HangulJamo,      // 한글 자모
    Hanja,           // 한자
    Katakana,        // 가타카나
    Hiragana,        // 히라가나
    Alphabet,        // ASCII 알파벳
    Digit,           // 숫자
    Whitespace,      // 공백 문자
    Punctuation,     // 구두점
    Other,           // 기타
}
}

mecab-ko-dict

사전 관리 기능을 제공합니다.

#![allow(unused)]
fn main() {
use mecab_ko::dict::*;
// or
use mecab_ko_dict::*;
}

UserDictionary

사용자 정의 사전을 관리합니다.

생성 및 엔트리 추가

#![allow(unused)]
fn main() {
use mecab_ko_dict::UserDictionary;

let mut dict = UserDictionary::new();

// Add entry with surface, POS, cost, and optional reading
dict.add_entry("딥러닝", "NNG", Some(-1000), None);
dict.add_entry("챗GPT", "NNP", Some(-1000), Some("챗지피티".to_string()));
}

CSV 파일 로드

#![allow(unused)]
fn main() {
dict.load_from_csv("user.csv")?;
}

CSV 포맷:

# Comment line (starts with #)
표면형,품사,비용,읽기
딥러닝,NNG,-1000,딥러닝
챗GPT,NNP,-1000,챗지피티

CSV 문자열 로드

#![allow(unused)]
fn main() {
let csv_content = r#"
딥러닝,NNG,-1000,
머신러닝,NNG,-1000,
"#;
dict.load_from_str(csv_content)?;
}

엔트리 조회

#![allow(unused)]
fn main() {
let entries = dict.lookup("딥러닝");
for entry in entries {
    println!("{}: {} (cost: {})", entry.surface, entry.pos, entry.cost);
}
}

기타 메서드

#![allow(unused)]
fn main() {
// Number of entries
let count = dict.len();

// Check if empty
let is_empty = dict.is_empty();

// Clear all entries
dict.clear();

// Save to CSV file
dict.save_to_csv("output.csv")?;

// Get all entries
let all_entries = dict.entries();
}

UserDictionaryBuilder

빌더 패턴으로 사용자 사전을 생성합니다.

#![allow(unused)]
fn main() {
use mecab_ko_dict::UserDictionaryBuilder;

let dict = UserDictionaryBuilder::new()
    .default_cost(-500)
    .add("딥러닝", "NNG")
    .add_with_cost("머신러닝", "NNG", -300)
    .add_full("자연어처리", "NNG", -400, Some("자연어처리"))
    .load_csv("extra.csv")?
    .build();
}

Entry

사전 엔트리를 나타내는 구조체입니다.

#![allow(unused)]
fn main() {
pub struct Entry {
    pub surface: String,   // 표면형
    pub left_id: u16,      // 좌문맥 ID
    pub right_id: u16,     // 우문맥 ID
    pub cost: i16,         // 비용 (낮을수록 우선)
    pub feature: String,   // 품사 정보
}
}

UserEntry

사용자 사전 엔트리입니다.

#![allow(unused)]
fn main() {
pub struct UserEntry {
    pub surface: String,           // 표면형
    pub left_id: u16,              // 좌문맥 ID
    pub right_id: u16,             // 우문맥 ID
    pub cost: i16,                 // 비용
    pub pos: String,               // 품사 태그
    pub reading: Option<String>,   // 읽기
    pub lemma: Option<String>,     // 원형
}
}

에러 처리

mecab-ko-core 에러

#![allow(unused)]
fn main() {
use mecab_ko::Error;

match tokenizer.new() {
    Ok(t) => { /* use tokenizer */ }
    Err(Error::Dict(e)) => eprintln!("Dictionary error: {}", e),
    Err(Error::Init(msg)) => eprintln!("Init error: {}", msg),
    Err(e) => eprintln!("Error: {}", e),
}
}

mecab-ko-dict 에러

#![allow(unused)]
fn main() {
use mecab_ko_dict::error::DictError;

match dict.load_from_csv("user.csv") {
    Ok(_) => println!("Loaded successfully"),
    Err(DictError::Io(e)) => eprintln!("IO error: {}", e),
    Err(DictError::Format(msg)) => eprintln!("Format error: {}", msg),
    Err(DictError::Version { expected, found }) => {
        eprintln!("Version mismatch: expected {}, found {}", expected, found);
    }
}
}

전체 예시

use mecab_ko::{Tokenizer, hangul};
use mecab_ko_dict::UserDictionary;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create tokenizer
    let tokenizer = Tokenizer::new()?;

    // Basic tokenization
    let text = "오늘 날씨가 좋습니다";
    let tokens = tokenizer.tokenize(text);

    println!("=== Tokens ===");
    for token in &tokens {
        println!("{}\t{}\t[{},{})",
            token.surface, token.pos, token.start, token.end);
    }

    // Extract nouns
    println!("\n=== Nouns ===");
    let nouns = tokenizer.nouns(text);
    println!("{:?}", nouns);

    // Hangul utilities
    println!("\n=== Hangul ===");
    let (cho, jung, jong) = hangul::decompose('한').unwrap();
    println!("'한' = {} + {} + {:?}", cho, jung, jong);

    println!("Decomposed '한글': {}", hangul::decompose_str("한글"));

    // User dictionary
    println!("\n=== User Dictionary ===");
    let mut dict = UserDictionary::new();
    dict.add_entry("딥러닝", "NNG", Some(-1000), None);

    let entries = dict.lookup("딥러닝");
    for entry in entries {
        println!("{}: {} (cost: {})", entry.surface, entry.pos, entry.cost);
    }

    Ok(())
}

추가 문서

자세한 API 문서는 docs.rs에서 확인할 수 있습니다.

Rust API

MeCab-Ko는 Rust로 작성되어 안전하고 효율적인 API를 제공합니다.

빠른 시작

use mecab_ko::{Tagger, TaggerConfig};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 기본 설정으로 Tagger 생성
    let tagger = Tagger::new(TaggerConfig::default())?;

    // 문장 분석
    let result = tagger.parse("안녕하세요")?;

    for node in result.iter() {
        println!("{}\t{}", node.surface, node.feature);
    }

    Ok(())
}

주요 타입

Tagger

형태소 분석의 핵심 구조체입니다.

#![allow(unused)]
fn main() {
pub struct Tagger {
    // 내부 필드는 비공개
}

impl Tagger {
    /// 새로운 Tagger를 생성합니다.
    pub fn new(config: TaggerConfig) -> Result<Self, Error>;

    /// 문자열을 분석합니다.
    pub fn parse(&self, text: &str) -> Result<ParseResult, Error>;

    /// 여러 문장을 한 번에 분석합니다.
    pub fn parse_batch(&self, texts: &[&str]) -> Result<Vec<ParseResult>, Error>;

    /// 사전을 동적으로 추가합니다.
    pub fn add_user_dict(&mut self, dict_path: &Path) -> Result<(), Error>;
}
}

TaggerConfig

Tagger의 동작을 제어하는 설정 구조체입니다.

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct TaggerConfig {
    /// 사전 디렉토리 경로
    pub dict_dir: Option<PathBuf>,

    /// 사용자 사전 경로
    pub user_dict: Option<PathBuf>,

    /// 출력 포맷
    pub output_format: OutputFormat,

    /// 띄어쓰기 패널티
    pub space_penalty: i32,

    /// 최대 그룹화 크기
    pub max_grouping_size: usize,

    /// 부분 처리 활성화
    pub partial: bool,

    /// 전부 출력 (N-best)
    pub all_morphs: bool,

    /// N-best 개수
    pub nbest: usize,

    /// theta (N-best용)
    pub theta: f32,
}

impl Default for TaggerConfig {
    fn default() -> Self {
        Self {
            dict_dir: None,
            user_dict: None,
            output_format: OutputFormat::default(),
            space_penalty: -1000,
            max_grouping_size: 24,
            partial: false,
            all_morphs: false,
            nbest: 1,
            theta: 0.75,
        }
    }
}
}

ParseResult

분석 결과를 담는 구조체입니다.

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct ParseResult {
    /// 원본 텍스트
    pub text: String,

    /// 분석된 노드 리스트
    pub nodes: Vec<Node>,
}

impl ParseResult {
    /// 노드 반복자를 반환합니다.
    pub fn iter(&self) -> impl Iterator<Item = &Node>;

    /// 특정 포맷으로 문자열 출력
    pub fn format(&self, format: OutputFormat) -> String;

    /// JSON 직렬화
    pub fn to_json(&self) -> serde_json::Value;
}
}

Node

분석된 형태소 노드입니다.

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
pub struct Node {
    /// 표면형 (실제 텍스트)
    pub surface: String,

    /// 품사 및 의미 정보
    pub feature: String,

    /// 시작 위치 (바이트 단위)
    pub start: usize,

    /// 길이 (바이트 단위)
    pub length: usize,

    /// 품사 ID
    pub pos_id: u16,

    /// 비용
    pub cost: i32,
}

impl Node {
    /// 품사 태그만 추출
    pub fn pos(&self) -> &str;

    /// 기본형 (원형) 추출
    pub fn base_form(&self) -> Option<&str>;

    /// 읽기 정보
    pub fn reading(&self) -> Option<&str>;
}
}

OutputFormat

출력 포맷을 지정합니다.

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
    /// MeCab 기본 포맷
    MeCab,

    /// 공백으로 구분된 형태소만
    Wakati,

    /// JSON 포맷
    Json,

    /// CSV 포맷
    Csv,

    /// 품사 태그만
    PosOnly,

    /// 커스텀 포맷
    Custom(String),
}
}

사용 예제

기본 사용

#![allow(unused)]
fn main() {
use mecab_ko::{Tagger, TaggerConfig};

let tagger = Tagger::new(TaggerConfig::default())?;
let result = tagger.parse("형태소 분석을 시작합니다.")?;

for node in result.iter() {
    println!("표면형: {}", node.surface);
    println!("품사: {}", node.pos());
    if let Some(base) = node.base_form() {
        println!("기본형: {}", base);
    }
    println!("---");
}
}

사용자 사전 추가

#![allow(unused)]
fn main() {
use mecab_ko::{Tagger, TaggerConfig};
use std::path::Path;

let mut config = TaggerConfig::default();
config.user_dict = Some(Path::new("user.csv").to_path_buf());

let tagger = Tagger::new(config)?;
let result = tagger.parse("딥러닝과 머신러닝")?;
}

N-best 분석

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();
config.nbest = 3;
config.theta = 0.75;

let tagger = Tagger::new(config)?;
let results = tagger.parse_nbest("아버지가방에들어가신다")?;

for (i, result) in results.iter().enumerate() {
    println!("=== 후보 {} ===", i + 1);
    println!("{}", result.format(OutputFormat::MeCab));
}
}

병렬 처리

#![allow(unused)]
fn main() {
use rayon::prelude::*;

let tagger = Tagger::new(TaggerConfig::default())?;
let sentences = vec![
    "첫 번째 문장",
    "두 번째 문장",
    "세 번째 문장",
];

let results: Vec<_> = sentences
    .par_iter()
    .map(|&text| tagger.parse(text))
    .collect::<Result<_, _>>()?;
}

커스텀 출력 포맷

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();
config.output_format = OutputFormat::Custom("%m\t%f[0]\t%f[6]\n".to_string());

let tagger = Tagger::new(config)?;
let result = tagger.parse("분석할 텍스트")?;
println!("{}", result.format(config.output_format));
}

스트림 처리

#![allow(unused)]
fn main() {
use std::io::{BufRead, BufReader};
use std::fs::File;

let tagger = Tagger::new(TaggerConfig::default())?;
let file = File::open("large_text.txt")?;
let reader = BufReader::new(file);

for line in reader.lines() {
    let line = line?;
    if !line.trim().is_empty() {
        let result = tagger.parse(&line)?;
        // 결과 처리
    }
}
}

Error 처리

MeCab-Ko는 Result 타입을 사용하여 에러를 처리합니다.

#![allow(unused)]
fn main() {
use mecab_ko::{Error, ErrorKind};

match tagger.parse(text) {
    Ok(result) => {
        // 성공 처리
    }
    Err(e) => match e.kind() {
        ErrorKind::DictNotFound => {
            eprintln!("사전을 찾을 수 없습니다: {}", e);
        }
        ErrorKind::ParseError => {
            eprintln!("파싱 오류: {}", e);
        }
        ErrorKind::InvalidConfig => {
            eprintln!("잘못된 설정: {}", e);
        }
        _ => {
            eprintln!("기타 오류: {}", e);
        }
    }
}
}

Feature Flags

Cargo.toml에서 다음 feature를 사용할 수 있습니다:

[dependencies]
mecab-ko = { version = "0.1", features = ["builder", "python", "wasm"] }
Feature설명
builder사전 빌더 기능 포함
pythonPython 바인딩 포함
wasmWASM 지원 포함
rayon병렬 처리 지원
serdeJSON 직렬화 지원

성능 최적화

Tagger 재사용

Tagger 생성은 비용이 큰 작업입니다. 가능하면 재사용하세요.

#![allow(unused)]
fn main() {
// Bad
for text in texts {
    let tagger = Tagger::new(config.clone())?; // 매번 생성
    let result = tagger.parse(text)?;
}

// Good
let tagger = Tagger::new(config)?; // 한 번만 생성
for text in texts {
    let result = tagger.parse(text)?;
}
}

배치 처리

여러 문장을 한 번에 처리하면 더 효율적입니다.

#![allow(unused)]
fn main() {
let results = tagger.parse_batch(&texts)?; // 배치 처리
}

Arc 공유

멀티스레드 환경에서는 Arc로 공유하세요.

#![allow(unused)]
fn main() {
use std::sync::Arc;
use rayon::prelude::*;

let tagger = Arc::new(Tagger::new(config)?);

texts.par_iter()
    .map(|text| {
        let tagger = Arc::clone(&tagger);
        tagger.parse(text)
    })
    .collect::<Result<Vec<_>, _>>()?;
}

전체 API 문서

상세한 API 문서는 rustdoc을 참조하세요.

관련 문서

Python 바인딩

MeCab-Ko는 PyO3를 사용하여 Python 바인딩을 제공합니다.

설치

pip install mecab-ko

소스에서 설치:

cd python
pip install maturin
maturin develop --release

빠른 시작

from mecab_ko import Tagger

# Tagger 생성
tagger = Tagger()

# 문장 분석
result = tagger.parse("안녕하세요")
print(result)

# 노드 단위 처리
for node in tagger.parse_nodes("형태소 분석"):
    print(f"{node.surface}\t{node.pos}\t{node.feature}")

API 레퍼런스

Tagger 클래스

형태소 분석기의 메인 클래스입니다.

class Tagger:
    """MeCab-Ko 형태소 분석기"""

    def __init__(
        self,
        dict_dir: str | None = None,
        user_dict: str | None = None,
        output_format: str = "mecab",
        space_penalty: int = -1000,
    ):
        """
        새로운 Tagger를 생성합니다.

        Args:
            dict_dir: 사전 디렉토리 경로
            user_dict: 사용자 사전 파일 경로
            output_format: 출력 포맷 ("mecab", "wakati", "json", "csv")
            space_penalty: 띄어쓰기 패널티 (기본값: -1000)

        Raises:
            RuntimeError: 사전을 찾을 수 없거나 초기화 실패 시
        """
        ...

    def parse(self, text: str) -> str:
        """
        텍스트를 분석하고 문자열로 반환합니다.

        Args:
            text: 분석할 텍스트

        Returns:
            분석 결과 문자열

        Raises:
            RuntimeError: 파싱 실패 시
        """
        ...

    def parse_nodes(self, text: str) -> list[Node]:
        """
        텍스트를 분석하고 Node 리스트로 반환합니다.

        Args:
            text: 분석할 텍스트

        Returns:
            Node 객체 리스트

        Raises:
            RuntimeError: 파싱 실패 시
        """
        ...

    def parse_to_dict(self, text: str) -> dict:
        """
        텍스트를 분석하고 딕셔너리로 반환합니다.

        Args:
            text: 분석할 텍스트

        Returns:
            {"text": str, "nodes": [{"surface": str, "feature": str, ...}]}

        Raises:
            RuntimeError: 파싱 실패 시
        """
        ...

    def parse_nbest(self, text: str, n: int = 3) -> list[str]:
        """
        N-best 결과를 반환합니다.

        Args:
            text: 분석할 텍스트
            n: 반환할 후보 개수

        Returns:
            N개의 분석 결과 문자열 리스트

        Raises:
            RuntimeError: 파싱 실패 시
        """
        ...

Node 클래스

분석된 형태소 노드를 나타내는 클래스입니다.

class Node:
    """형태소 분석 노드"""

    @property
    def surface(self) -> str:
        """표면형 (실제 텍스트)"""
        ...

    @property
    def feature(self) -> str:
        """품사 및 의미 정보"""
        ...

    @property
    def pos(self) -> str:
        """품사 태그"""
        ...

    @property
    def start(self) -> int:
        """시작 위치 (바이트 단위)"""
        ...

    @property
    def length(self) -> int:
        """길이 (바이트 단위)"""
        ...

    @property
    def cost(self) -> int:
        """비용"""
        ...

    @property
    def base_form(self) -> str | None:
        """기본형 (원형)"""
        ...

    @property
    def reading(self) -> str | None:
        """읽기 정보"""
        ...

    def to_dict(self) -> dict:
        """딕셔너리로 변환"""
        ...

사용 예제

기본 사용

from mecab_ko import Tagger

tagger = Tagger()

# 텍스트 분석
text = "아버지가방에들어가신다"
result = tagger.parse(text)
print(result)

출력:

아버지    NNG,*,F,아버지,*,*,*,*
가        JKS,*,F,가,*,*,*,*
방        NNG,*,T,방,*,*,*,*
에        JKB,*,F,에,*,*,*,*
들어가    VV,*,F,들어가,*,*,*,*
시        EP,*,F,시,*,*,*,*
ㄴ다      EF,*,F,ㄴ다,*,*,*,*
EOS

Node 단위 처리

from mecab_ko import Tagger

tagger = Tagger()
nodes = tagger.parse_nodes("형태소 분석을 시작합니다")

for node in nodes:
    print(f"표면형: {node.surface}")
    print(f"품사: {node.pos}")
    if node.base_form:
        print(f"기본형: {node.base_form}")
    print("---")

사전 형식 출력

from mecab_ko import Tagger

tagger = Tagger()
result_dict = tagger.parse_to_dict("안녕하세요")

print(result_dict["text"])  # 원본 텍스트
for node in result_dict["nodes"]:
    print(node["surface"], node["pos"])

사용자 사전 사용

from mecab_ko import Tagger

tagger = Tagger(user_dict="user.csv")
result = tagger.parse("딥러닝과 머신러닝")
print(result)

user.csv 파일:

딥러닝,NNG,-1000,딥러닝
머신러닝,NNG,-1000,머신러닝

Wakati 출력 (형태소만)

from mecab_ko import Tagger

tagger = Tagger(output_format="wakati")
result = tagger.parse("형태소만 추출합니다")
print(result)  # "형태소 만 추출 하 ㅂ니다"

JSON 출력

from mecab_ko import Tagger
import json

tagger = Tagger(output_format="json")
result = tagger.parse("JSON 형식으로 출력")
data = json.loads(result)
print(json.dumps(data, ensure_ascii=False, indent=2))

N-best 분석

from mecab_ko import Tagger

tagger = Tagger()
candidates = tagger.parse_nbest("아버지가방에들어가신다", n=3)

for i, candidate in enumerate(candidates, 1):
    print(f"=== 후보 {i} ===")
    print(candidate)

대용량 파일 처리

from mecab_ko import Tagger

tagger = Tagger()

with open("large_file.txt", "r", encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if line:
            result = tagger.parse(line)
            # 결과 처리
            print(result)

멀티프로세싱

from mecab_ko import Tagger
from multiprocessing import Pool
import os

def analyze_text(text):
    # 각 프로세스마다 별도의 Tagger 생성
    tagger = Tagger()
    return tagger.parse(text)

if __name__ == "__main__":
    texts = [
        "첫 번째 문장",
        "두 번째 문장",
        "세 번째 문장",
    ]

    with Pool(processes=os.cpu_count()) as pool:
        results = pool.map(analyze_text, texts)

    for text, result in zip(texts, results):
        print(f"Input: {text}")
        print(f"Result: {result}\n")

데이터프레임과 통합

import pandas as pd
from mecab_ko import Tagger

tagger = Tagger()

df = pd.DataFrame({
    "text": ["첫 번째 문장", "두 번째 문장", "세 번째 문장"]
})

# 형태소 분석 결과 추가
df["morphs"] = df["text"].apply(
    lambda x: [node.surface for node in tagger.parse_nodes(x)]
)

# 품사 태그 추가
df["pos_tags"] = df["text"].apply(
    lambda x: [node.pos for node in tagger.parse_nodes(x)]
)

print(df)

Flask 웹 서버

from flask import Flask, request, jsonify
from mecab_ko import Tagger

app = Flask(__name__)
tagger = Tagger()  # 전역으로 한 번만 생성

@app.route("/analyze", methods=["POST"])
def analyze():
    data = request.get_json()
    text = data.get("text", "")

    if not text:
        return jsonify({"error": "No text provided"}), 400

    result = tagger.parse_to_dict(text)
    return jsonify(result)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

사용 예:

curl -X POST http://localhost:5000/analyze \
  -H "Content-Type: application/json" \
  -d '{"text": "안녕하세요"}'

동사/형용사 추출

from mecab_ko import Tagger

tagger = Tagger()
text = "저는 오늘 학교에 가서 공부를 했습니다."
nodes = tagger.parse_nodes(text)

# 동사(VV, VA) 추출
verbs = [node.surface for node in nodes if node.pos in ["VV", "VA"]]
print("동사/형용사:", verbs)
# 출력: ['가', '하']

# 명사(NNG, NNP) 추출
nouns = [node.surface for node in nodes if node.pos in ["NNG", "NNP"]]
print("명사:", nouns)
# 출력: ['오늘', '학교', '공부']

타입 힌트

Python 3.9+ 에서 타입 힌트를 사용할 수 있습니다:

from mecab_ko import Tagger, Node
from typing import List, Dict, Optional

def analyze_sentences(sentences: List[str]) -> List[List[Node]]:
    tagger: Tagger = Tagger()
    results: List[List[Node]] = []

    for sentence in sentences:
        nodes: List[Node] = tagger.parse_nodes(sentence)
        results.append(nodes)

    return results

성능 최적화

Tagger 재사용

# Bad - 매번 생성
for text in texts:
    tagger = Tagger()  # 비효율적
    result = tagger.parse(text)

# Good - 재사용
tagger = Tagger()  # 한 번만 생성
for text in texts:
    result = tagger.parse(text)

배치 처리

대용량 데이터는 배치로 처리:

def batch_analyze(texts, batch_size=1000):
    tagger = Tagger()
    results = []

    for i in range(0, len(texts), batch_size):
        batch = texts[i:i+batch_size]
        batch_results = [tagger.parse(text) for text in batch]
        results.extend(batch_results)

    return results

예외 처리

from mecab_ko import Tagger

tagger = Tagger()

try:
    result = tagger.parse("분석할 텍스트")
    print(result)
except RuntimeError as e:
    print(f"분석 실패: {e}")

관련 문서

Node.js 바인딩

MeCab-Ko는 Neon을 사용하여 Node.js 바인딩을 제공합니다.

설치

npm install mecab-ko
# or
yarn add mecab-ko
# or
pnpm add mecab-ko

빠른 시작

const { Tagger } = require('mecab-ko');

// Tagger 생성
const tagger = new Tagger();

// 문장 분석
const result = tagger.parse('안녕하세요');
console.log(result);

// 노드 단위 처리
const nodes = tagger.parseToNodes('형태소 분석');
nodes.forEach(node => {
  console.log(`${node.surface}\t${node.pos}\t${node.feature}`);
});

TypeScript:

import { Tagger, Node, TaggerConfig } from 'mecab-ko';

const tagger = new Tagger();
const nodes: Node[] = tagger.parseToNodes('안녕하세요');

API 레퍼런스

Tagger 클래스

형태소 분석기의 메인 클래스입니다.

class Tagger {
  /**
   * 새로운 Tagger를 생성합니다.
   * @param config - Tagger 설정
   */
  constructor(config?: TaggerConfig);

  /**
   * 텍스트를 분석하고 문자열로 반환합니다.
   * @param text - 분석할 텍스트
   * @returns 분석 결과 문자열
   */
  parse(text: string): string;

  /**
   * 텍스트를 분석하고 Node 배열로 반환합니다.
   * @param text - 분석할 텍스트
   * @returns Node 객체 배열
   */
  parseToNodes(text: string): Node[];

  /**
   * 텍스트를 분석하고 객체로 반환합니다.
   * @param text - 분석할 텍스트
   * @returns 분석 결과 객체
   */
  parseToObject(text: string): ParseResult;

  /**
   * N-best 결과를 반환합니다.
   * @param text - 분석할 텍스트
   * @param n - 반환할 후보 개수
   * @returns N개의 분석 결과 문자열 배열
   */
  parseNBest(text: string, n?: number): string[];

  /**
   * 비동기로 텍스트를 분석합니다.
   * @param text - 분석할 텍스트
   * @returns Promise<분석 결과 문자열>
   */
  parseAsync(text: string): Promise<string>;

  /**
   * 비동기로 Node 배열을 반환합니다.
   * @param text - 분석할 텍스트
   * @returns Promise<Node 객체 배열>
   */
  parseToNodesAsync(text: string): Promise<Node[]>;
}

TaggerConfig 인터페이스

interface TaggerConfig {
  /** 사전 디렉토리 경로 */
  dictDir?: string;

  /** 사용자 사전 파일 경로 */
  userDict?: string;

  /** 출력 포맷 ("mecab" | "wakati" | "json" | "csv") */
  outputFormat?: string;

  /** 띄어쓰기 패널티 (기본값: -1000) */
  spacePenalty?: number;

  /** 부분 처리 활성화 */
  partial?: boolean;

  /** 전부 출력 활성화 */
  allMorphs?: boolean;

  /** N-best 개수 */
  nbest?: number;

  /** theta 값 (N-best용) */
  theta?: number;
}

Node 인터페이스

interface Node {
  /** 표면형 (실제 텍스트) */
  surface: string;

  /** 품사 및 의미 정보 */
  feature: string;

  /** 품사 태그 */
  pos: string;

  /** 시작 위치 (바이트 단위) */
  start: number;

  /** 길이 (바이트 단위) */
  length: number;

  /** 비용 */
  cost: number;

  /** 기본형 (원형) */
  baseForm?: string;

  /** 읽기 정보 */
  reading?: string;
}

ParseResult 인터페이스

interface ParseResult {
  /** 원본 텍스트 */
  text: string;

  /** 분석된 노드 배열 */
  nodes: Node[];
}

사용 예제

기본 사용 (CommonJS)

const { Tagger } = require('mecab-ko');

const tagger = new Tagger();
const text = '아버지가방에들어가신다';
const result = tagger.parse(text);

console.log(result);

기본 사용 (ES Modules)

import { Tagger } from 'mecab-ko';

const tagger = new Tagger();
const result = tagger.parse('형태소 분석을 시작합니다');
console.log(result);

TypeScript

import { Tagger, Node, TaggerConfig } from 'mecab-ko';

const config: TaggerConfig = {
  outputFormat: 'json',
  spacePenalty: -1000,
};

const tagger = new Tagger(config);
const nodes: Node[] = tagger.parseToNodes('안녕하세요');

nodes.forEach((node: Node) => {
  console.log(`표면형: ${node.surface}`);
  console.log(`품사: ${node.pos}`);
  if (node.baseForm) {
    console.log(`기본형: ${node.baseForm}`);
  }
  console.log('---');
});

Node 단위 처리

const { Tagger } = require('mecab-ko');

const tagger = new Tagger();
const nodes = tagger.parseToNodes('형태소 분석을 시작합니다');

for (const node of nodes) {
  console.log(`표면형: ${node.surface}`);
  console.log(`품사: ${node.pos}`);
  console.log(`특성: ${node.feature}`);
  console.log('---');
}

객체 형식 출력

const { Tagger } = require('mecab-ko');

const tagger = new Tagger();
const result = tagger.parseToObject('안녕하세요');

console.log('원본 텍스트:', result.text);
console.log('노드 개수:', result.nodes.length);

result.nodes.forEach((node, index) => {
  console.log(`${index + 1}. ${node.surface} (${node.pos})`);
});

사용자 사전 사용

const { Tagger } = require('mecab-ko');

const tagger = new Tagger({
  userDict: './user.csv',
});

const result = tagger.parse('딥러닝과 머신러닝');
console.log(result);

user.csv:

딥러닝,NNG,-1000,딥러닝
머신러닝,NNG,-1000,머신러닝

Wakati 출력 (형태소만)

const { Tagger } = require('mecab-ko');

const tagger = new Tagger({ outputFormat: 'wakati' });
const result = tagger.parse('형태소만 추출합니다');
console.log(result); // "형태소 만 추출 하 ㅂ니다"

JSON 출력

const { Tagger } = require('mecab-ko');

const tagger = new Tagger({ outputFormat: 'json' });
const result = tagger.parse('JSON 형식으로 출력');
const data = JSON.parse(result);
console.log(JSON.stringify(data, null, 2));

N-best 분석

const { Tagger } = require('mecab-ko');

const tagger = new Tagger();
const candidates = tagger.parseNBest('아버지가방에들어가신다', 3);

candidates.forEach((candidate, index) => {
  console.log(`=== 후보 ${index + 1} ===`);
  console.log(candidate);
});

비동기 처리

const { Tagger } = require('mecab-ko');

const tagger = new Tagger();

async function analyze() {
  try {
    const result = await tagger.parseAsync('비동기 처리 예제');
    console.log(result);

    const nodes = await tagger.parseToNodesAsync('또 다른 문장');
    console.log(nodes);
  } catch (error) {
    console.error('분석 실패:', error);
  }
}

analyze();

Promise 체이닝

const { Tagger } = require('mecab-ko');

const tagger = new Tagger();

tagger.parseAsync('첫 번째 문장')
  .then(result => {
    console.log('결과 1:', result);
    return tagger.parseAsync('두 번째 문장');
  })
  .then(result => {
    console.log('결과 2:', result);
  })
  .catch(error => {
    console.error('오류:', error);
  });

병렬 처리

const { Tagger } = require('mecab-ko');

const tagger = new Tagger();
const texts = [
  '첫 번째 문장',
  '두 번째 문장',
  '세 번째 문장',
];

Promise.all(texts.map(text => tagger.parseAsync(text)))
  .then(results => {
    results.forEach((result, index) => {
      console.log(`결과 ${index + 1}:`, result);
    });
  })
  .catch(error => {
    console.error('오류:', error);
  });

Express 서버

const express = require('express');
const { Tagger } = require('mecab-ko');

const app = express();
const tagger = new Tagger();

app.use(express.json());

app.post('/analyze', async (req, res) => {
  try {
    const { text } = req.body;

    if (!text) {
      return res.status(400).json({ error: 'No text provided' });
    }

    const result = await tagger.parseToNodesAsync(text);
    res.json({ text, nodes: result });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

사용 예:

curl -X POST http://localhost:3000/analyze \
  -H "Content-Type: application/json" \
  -d '{"text": "안녕하세요"}'

파일 처리

const fs = require('fs');
const readline = require('readline');
const { Tagger } = require('mecab-ko');

const tagger = new Tagger();

async function processFile(filename) {
  const fileStream = fs.createReadStream(filename);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
  });

  for await (const line of rl) {
    if (line.trim()) {
      const result = tagger.parse(line);
      console.log(result);
    }
  }
}

processFile('large_file.txt')
  .catch(console.error);

스트림 처리

const { Tagger } = require('mecab-ko');
const { Transform } = require('stream');

const tagger = new Tagger();

class MecabTransform extends Transform {
  constructor(options) {
    super(options);
  }

  _transform(chunk, encoding, callback) {
    try {
      const text = chunk.toString();
      const result = tagger.parse(text);
      this.push(result + '\n');
      callback();
    } catch (error) {
      callback(error);
    }
  }
}

// 사용
process.stdin
  .pipe(new MecabTransform())
  .pipe(process.stdout);

명사/동사 추출

const { Tagger } = require('mecab-ko');

const tagger = new Tagger();
const text = '저는 오늘 학교에 가서 공부를 했습니다.';
const nodes = tagger.parseToNodes(text);

// 명사 추출
const nouns = nodes
  .filter(node => ['NNG', 'NNP'].includes(node.pos))
  .map(node => node.surface);

console.log('명사:', nouns);
// 출력: ['오늘', '학교', '공부']

// 동사 추출
const verbs = nodes
  .filter(node => ['VV', 'VA'].includes(node.pos))
  .map(node => node.surface);

console.log('동사:', verbs);
// 출력: ['가', '하']

Next.js API Route

// pages/api/analyze.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Tagger } from 'mecab-ko';

const tagger = new Tagger();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { text } = req.body;

  if (!text) {
    return res.status(400).json({ error: 'No text provided' });
  }

  try {
    const nodes = tagger.parseToNodes(text);
    res.status(200).json({ text, nodes });
  } catch (error) {
    res.status(500).json({ error: 'Analysis failed' });
  }
}

성능 최적화

Tagger 재사용

// Bad - 매번 생성
for (const text of texts) {
  const tagger = new Tagger(); // 비효율적
  const result = tagger.parse(text);
}

// Good - 재사용
const tagger = new Tagger(); // 한 번만 생성
for (const text of texts) {
  const result = tagger.parse(text);
}

비동기 배치 처리

const { Tagger } = require('mecab-ko');

async function batchAnalyze(texts, batchSize = 100) {
  const tagger = new Tagger();
  const results = [];

  for (let i = 0; i < texts.length; i += batchSize) {
    const batch = texts.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(text => tagger.parseAsync(text))
    );
    results.push(...batchResults);
  }

  return results;
}

에러 처리

const { Tagger } = require('mecab-ko');

try {
  const tagger = new Tagger({
    dictDir: '/invalid/path',
  });
} catch (error) {
  console.error('Tagger 생성 실패:', error.message);
}

const tagger = new Tagger();

try {
  const result = tagger.parse('분석할 텍스트');
  console.log(result);
} catch (error) {
  console.error('분석 실패:', error.message);
}

타입 정의 파일

TypeScript 사용 시 타입 정의가 자동으로 제공됩니다:

import { Tagger, Node, TaggerConfig, ParseResult } from 'mecab-ko';

관련 문서

WASM 바인딩

MeCab-Ko는 WebAssembly(WASM)를 통해 브라우저와 Deno 환경에서 실행할 수 있습니다.

설치

npm/yarn/pnpm

npm install @mecab-ko/wasm
# or
yarn add @mecab-ko/wasm
# or
pnpm add @mecab-ko/wasm

CDN

<script type="module">
  import init, { Tagger } from 'https://cdn.jsdelivr.net/npm/@mecab-ko/wasm/mecab_ko_wasm.js';

  await init();
  const tagger = new Tagger();
  console.log(tagger.parse('안녕하세요'));
</script>

빠른 시작

브라우저 (ES Modules)

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>MeCab-Ko WASM Demo</title>
</head>
<body>
  <textarea id="input" placeholder="분석할 텍스트 입력"></textarea>
  <button id="analyze">분석</button>
  <pre id="output"></pre>

  <script type="module">
    import init, { Tagger } from './node_modules/@mecab-ko/wasm/mecab_ko_wasm.js';

    // WASM 초기화
    await init();

    // Tagger 생성
    const tagger = new Tagger();

    // 분석 버튼 이벤트
    document.getElementById('analyze').addEventListener('click', () => {
      const text = document.getElementById('input').value;
      const result = tagger.parse(text);
      document.getElementById('output').textContent = result;
    });
  </script>
</body>
</html>

Webpack/Vite

import init, { Tagger } from '@mecab-ko/wasm';

// WASM 초기화
await init();

// Tagger 생성 및 사용
const tagger = new Tagger();
const result = tagger.parse('형태소 분석');
console.log(result);

Deno

import init, { Tagger } from 'https://deno.land/x/mecab_ko_wasm/mod.ts';

await init();

const tagger = new Tagger();
console.log(tagger.parse('안녕하세요'));

API 레퍼런스

init() 함수

WASM 모듈을 초기화합니다. 반드시 Tagger를 사용하기 전에 호출해야 합니다.

/**
 * WASM 모듈을 초기화합니다.
 * @param module_or_path - WASM 모듈 또는 경로 (선택)
 * @returns Promise<void>
 */
async function init(module_or_path?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;

Tagger 클래스

class Tagger {
  /**
   * 새로운 Tagger를 생성합니다.
   * @param config - Tagger 설정 (선택)
   */
  constructor(config?: TaggerConfig);

  /**
   * 텍스트를 분석하고 문자열로 반환합니다.
   * @param text - 분석할 텍스트
   * @returns 분석 결과 문자열
   */
  parse(text: string): string;

  /**
   * 텍스트를 분석하고 Node 배열로 반환합니다.
   * @param text - 분석할 텍스트
   * @returns Node 객체 배열
   */
  parseToNodes(text: string): Node[];

  /**
   * 텍스트를 분석하고 객체로 반환합니다.
   * @param text - 분석할 텍스트
   * @returns 분석 결과 객체
   */
  parseToObject(text: string): ParseResult;

  /**
   * 리소스를 해제합니다.
   */
  free(): void;
}

TaggerConfig 인터페이스

interface TaggerConfig {
  /** 출력 포맷 ("mecab" | "wakati" | "json" | "csv") */
  outputFormat?: string;

  /** 띄어쓰기 패널티 (기본값: -1000) */
  spacePenalty?: number;

  /** 부분 처리 활성화 */
  partial?: boolean;

  /** 전부 출력 활성화 */
  allMorphs?: boolean;
}

Node 인터페이스

interface Node {
  /** 표면형 (실제 텍스트) */
  surface: string;

  /** 품사 및 의미 정보 */
  feature: string;

  /** 품사 태그 */
  pos: string;

  /** 시작 위치 (바이트 단위) */
  start: number;

  /** 길이 (바이트 단위) */
  length: number;

  /** 비용 */
  cost: number;
}

사용 예제

React

import React, { useEffect, useState } from 'react';
import init, { Tagger } from '@mecab-ko/wasm';

function MecabAnalyzer() {
  const [tagger, setTagger] = useState<Tagger | null>(null);
  const [input, setInput] = useState('');
  const [result, setResult] = useState('');

  useEffect(() => {
    // WASM 초기화
    init().then(() => {
      setTagger(new Tagger());
    });

    // 클린업
    return () => {
      if (tagger) {
        tagger.free();
      }
    };
  }, []);

  const handleAnalyze = () => {
    if (tagger && input) {
      const parsed = tagger.parse(input);
      setResult(parsed);
    }
  };

  return (
    <div>
      <textarea
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="분석할 텍스트 입력"
      />
      <button onClick={handleAnalyze} disabled={!tagger}>
        분석
      </button>
      <pre>{result}</pre>
    </div>
  );
}

export default MecabAnalyzer;

Vue 3

<template>
  <div>
    <textarea v-model="input" placeholder="분석할 텍스트 입력"></textarea>
    <button @click="analyze" :disabled="!tagger">분석</button>
    <pre>{{ result }}</pre>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import init, { Tagger } from '@mecab-ko/wasm';

const tagger = ref<Tagger | null>(null);
const input = ref('');
const result = ref('');

onMounted(async () => {
  await init();
  tagger.value = new Tagger();
});

onUnmounted(() => {
  if (tagger.value) {
    tagger.value.free();
  }
});

const analyze = () => {
  if (tagger.value && input.value) {
    result.value = tagger.value.parse(input.value);
  }
};
</script>

Svelte

<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import init, { Tagger } from '@mecab-ko/wasm';

  let tagger: Tagger | null = null;
  let input = '';
  let result = '';

  onMount(async () => {
    await init();
    tagger = new Tagger();
  });

  onDestroy(() => {
    if (tagger) {
      tagger.free();
    }
  });

  function analyze() {
    if (tagger && input) {
      result = tagger.parse(input);
    }
  }
</script>

<textarea bind:value={input} placeholder="분석할 텍스트 입력" />
<button on:click={analyze} disabled={!tagger}>분석</button>
<pre>{result}</pre>

Web Worker

// worker.js
import init, { Tagger } from '@mecab-ko/wasm';

let tagger = null;

self.onmessage = async (event) => {
  const { type, text } = event.data;

  if (type === 'init') {
    await init();
    tagger = new Tagger();
    self.postMessage({ type: 'ready' });
  } else if (type === 'parse') {
    if (tagger) {
      const result = tagger.parse(text);
      self.postMessage({ type: 'result', result });
    }
  }
};
// main.js
const worker = new Worker('worker.js', { type: 'module' });

worker.onmessage = (event) => {
  const { type, result } = event.data;

  if (type === 'ready') {
    console.log('Worker ready');
    worker.postMessage({ type: 'parse', text: '안녕하세요' });
  } else if (type === 'result') {
    console.log('결과:', result);
  }
};

worker.postMessage({ type: 'init' });

Node 단위 처리

import init, { Tagger } from '@mecab-ko/wasm';

await init();

const tagger = new Tagger();
const nodes = tagger.parseToNodes('형태소 분석을 시작합니다');

nodes.forEach(node => {
  console.log(`표면형: ${node.surface}`);
  console.log(`품사: ${node.pos}`);
  console.log(`특성: ${node.feature}`);
  console.log('---');
});

tagger.free();

JSON 출력

import init, { Tagger } from '@mecab-ko/wasm';

await init();

const tagger = new Tagger({ outputFormat: 'json' });
const result = tagger.parseToObject('JSON 형식으로 출력');

console.log('원본 텍스트:', result.text);
console.log('노드 개수:', result.nodes.length);

result.nodes.forEach((node, index) => {
  console.log(`${index + 1}. ${node.surface} (${node.pos})`);
});

tagger.free();

명사 추출

import init, { Tagger } from '@mecab-ko/wasm';

await init();

const tagger = new Tagger();
const text = '저는 오늘 학교에 가서 공부를 했습니다.';
const nodes = tagger.parseToNodes(text);

const nouns = nodes
  .filter(node => ['NNG', 'NNP'].includes(node.pos))
  .map(node => node.surface);

console.log('명사:', nouns);
// 출력: ['오늘', '학교', '공부']

tagger.free();

실시간 입력 분석

import init, { Tagger } from '@mecab-ko/wasm';

await init();

const tagger = new Tagger();
const input = document.getElementById('input') as HTMLTextAreaElement;
const output = document.getElementById('output') as HTMLPreElement;

let debounceTimer: number;

input.addEventListener('input', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    const text = input.value;
    if (text) {
      const result = tagger.parse(text);
      output.textContent = result;
    } else {
      output.textContent = '';
    }
  }, 300);
});

// 페이지 언로드 시 정리
window.addEventListener('beforeunload', () => {
  tagger.free();
});

IndexedDB에 결과 저장

import init, { Tagger } from '@mecab-ko/wasm';

await init();

const tagger = new Tagger();

// IndexedDB 열기
const dbPromise = indexedDB.open('MecabDB', 1);

dbPromise.onupgradeneeded = (event) => {
  const db = (event.target as IDBOpenDBRequest).result;
  if (!db.objectStoreNames.contains('analyses')) {
    db.createObjectStore('analyses', { keyPath: 'id', autoIncrement: true });
  }
};

dbPromise.onsuccess = (event) => {
  const db = (event.target as IDBOpenDBRequest).result;

  function saveAnalysis(text: string) {
    const result = tagger.parseToObject(text);

    const transaction = db.transaction(['analyses'], 'readwrite');
    const store = transaction.objectStore('analyses');

    store.add({
      text: result.text,
      nodes: result.nodes,
      timestamp: Date.now(),
    });
  }

  saveAnalysis('저장할 텍스트');
};

Service Worker 캐싱

// service-worker.js
import init, { Tagger } from '@mecab-ko/wasm';

const CACHE_NAME = 'mecab-cache-v1';
let tagger = null;

self.addEventListener('install', async (event) => {
  event.waitUntil(
    (async () => {
      await init();
      tagger = new Tagger();
    })()
  );
});

self.addEventListener('message', (event) => {
  const { type, text, id } = event.data;

  if (type === 'parse' && tagger) {
    const result = tagger.parse(text);
    event.ports[0].postMessage({ id, result });
  }
});

번들 크기 최적화

Tree Shaking

// 필요한 것만 import
import init, { Tagger } from '@mecab-ko/wasm';

// Tagger만 사용
await init();
const tagger = new Tagger();

코드 스플리팅 (Webpack)

// 동적 import로 lazy loading
const loadMecab = async () => {
  const { default: init, Tagger } = await import('@mecab-ko/wasm');
  await init();
  return new Tagger();
};

// 필요할 때만 로드
button.addEventListener('click', async () => {
  const tagger = await loadMecab();
  const result = tagger.parse(input.value);
  console.log(result);
});

Vite 최적화

// vite.config.js
export default {
  optimizeDeps: {
    exclude: ['@mecab-ko/wasm'],
  },
  build: {
    target: 'esnext',
  },
};

메모리 관리

WASM 메모리는 자동으로 관리되지만, 명시적으로 해제할 수 있습니다:

const tagger = new Tagger();

// 사용
const result = tagger.parse('텍스트');

// 사용 완료 후 메모리 해제
tagger.free();

React/Vue 등에서 클린업:

useEffect(() => {
  let tagger: Tagger | null = null;

  init().then(() => {
    tagger = new Tagger();
  });

  return () => {
    if (tagger) {
      tagger.free();
    }
  };
}, []);

성능 고려사항

초기화 비용

WASM 초기화는 비용이 높으므로 앱 시작 시 한 번만 수행:

// Good - 앱 시작 시 한 번
const tagger = await initTagger();

// Bad - 사용할 때마다
button.addEventListener('click', async () => {
  const tagger = await initTagger(); // 비효율적
});

Tagger 재사용

// Good - 싱글톤 패턴
let globalTagger: Tagger | null = null;

async function getTagger() {
  if (!globalTagger) {
    await init();
    globalTagger = new Tagger();
  }
  return globalTagger;
}

// Bad - 매번 생성
async function analyze(text: string) {
  const tagger = new Tagger(); // 비효율적
  return tagger.parse(text);
}

Web Worker 활용

무거운 작업은 Web Worker에서 처리:

// 메인 스레드를 블록하지 않음
const worker = new Worker('mecab-worker.js', { type: 'module' });
worker.postMessage({ text: largeText });

브라우저 호환성

  • Chrome 57+
  • Firefox 52+
  • Safari 11+
  • Edge 16+

관련 문서

사전 빌드 가이드

MeCab-Ko는 CSV 형식의 사전 소스 파일에서 바이너리 사전을 빌드하는 도구를 제공합니다.

개요

사전 빌드 프로세스:

  1. CSV 사전 파일 준비
  2. 특성 정의 파일 작성
  3. 사전 빌더 실행
  4. 바이너리 사전 생성

사전 파일 구조

CSV 사전 형식

표면형,좌문맥ID,우문맥ID,비용,품사,의미정보1,의미정보2,...

예시:

가,1788,3544,3775,JKS,*,F,가,*,*,*,*
가게,1781,3536,2876,NNG,*,F,가게,*,*,*,*
학교,1781,3536,-1723,NNG,장소,F,학교,Compound,*,*,학교

필드 설명

필드설명필수
표면형사전 단어O
좌문맥ID좌측 문맥 ID (0-65535)O
우문맥ID우측 문맥 ID (0-65535)O
비용단어 비용 (낮을수록 선호)O
품사품사 태그 (NNG, VV 등)O
의미정보1-7추가 의미 정보X

특성 정의 파일 (feature.def)

# 품사 태그 정의
NNG    일반 명사
NNP    고유 명사
VV     동사
VA     형용사
...

CLI 도구로 빌드

기본 빌드

mecab-ko-dict-build \
  --input ./dict-src \
  --output ./dict-bin \
  --charset utf-8

옵션

mecab-ko-dict-build [OPTIONS]

Options:
  -i, --input <DIR>        입력 사전 디렉토리
  -o, --output <DIR>       출력 디렉토리
  -c, --charset <CHARSET>  문자 인코딩 [기본값: utf-8]
  -d, --dictionary <TYPE>  사전 타입 [mecab-ko-dic, ipa, uni]
  -f, --feature <FILE>     특성 정의 파일
  -m, --matrix <FILE>      연접 비용 행렬 파일
  -w, --wakati             Wakati 모드
  -h, --help               도움말 출력
  -V, --version            버전 정보

Rust API로 빌드

기본 사용

use mecab_ko_dict::builder::{DictBuilder, BuildConfig};
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = BuildConfig {
        input_dir: Path::new("./dict-src").to_path_buf(),
        output_dir: Path::new("./dict-bin").to_path_buf(),
        charset: "utf-8".to_string(),
        ..Default::default()
    };

    let builder = DictBuilder::new(config)?;
    builder.build()?;

    println!("사전 빌드 완료!");
    Ok(())
}

고급 설정

#![allow(unused)]
fn main() {
use mecab_ko_dict::builder::{DictBuilder, BuildConfig, DictType};

let config = BuildConfig {
    input_dir: Path::new("./dict-src").to_path_buf(),
    output_dir: Path::new("./dict-bin").to_path_buf(),
    charset: "utf-8".to_string(),
    dict_type: DictType::MecabKoDic,
    wakati: false,
    user_dict: Some(Path::new("./user.csv").to_path_buf()),
    matrix_file: Some(Path::new("./matrix.def").to_path_buf()),
    feature_file: Some(Path::new("./feature.def").to_path_buf()),
    compression: true,
    verbose: true,
};

let builder = DictBuilder::new(config)?;
builder.build()?;
}

사전 소스 디렉토리 구조

dict-src/
├── lex.csv              # 메인 사전 파일
├── matrix.def           # 연접 비용 행렬
├── feature.def          # 특성 정의
├── char.def             # 문자 정의
├── unk.def              # 미등록어 정의
└── rewrite.def          # 재작성 규칙 (선택)

lex.csv

주요 사전 엔트리:

# 명사
학교,1781,3536,-1723,NNG,장소,F,학교,*,*,*,*
컴퓨터,1781,3536,-2154,NNG,*,F,컴퓨터,Compound,*,*,컴퓨터

# 동사
가다,1788,3544,4500,VV,*,F,가,*,*,*,*
먹다,1788,3544,5200,VV,*,F,먹,*,*,*,*

# 조사
이,1789,3545,2500,JKS,*,F,이,*,*,*,*
가,1788,3544,3775,JKS,*,F,가,*,*,*,*

matrix.def

연접 비용 행렬:

# 크기 정의
1316 1316

# 좌문맥ID 우문맥ID 비용
0 0 0
0 1 100
0 2 200
...

char.def

문자 타입 정의:

# 타입명 문자범위 invoke group length
DEFAULT 0 1 0
SPACE   0x0020 0 1 0
HANGUL  0xAC00..0xD7A3 0 2 0
ALPHA   a..z 1 1 0
ALPHA   A..Z 1 1 0
DIGIT   0..9 1 1 0

unk.def

미등록어 처리:

DEFAULT,1,2,3000,UNK,*,*,*,*,*,*,*
HANGUL,1781,3536,5000,NNG,*,*,*,*,*,*,*
ALPHA,1,2,3500,SL,*,*,*,*,*,*,*
DIGIT,1,2,3500,SN,*,*,*,*,*,*,*

사용자 사전 빌드

CSV 형식

# 표면형,품사,비용,기본형
딥러닝,NNG,-1000,딥러닝
머신러닝,NNG,-1000,머신러닝
트랜스포머,NNG,-1000,트랜스포머

간단한 형식으로 빌드

mecab-ko-dict-build \
  --input user.csv \
  --output user-dict.bin \
  --user-dict

프로그래밍 방식

#![allow(unused)]
fn main() {
use mecab_ko_dict::builder::UserDictBuilder;

let builder = UserDictBuilder::new();
builder
    .add_entry("딥러닝", "NNG", -1000, Some("딥러닝"))?
    .add_entry("머신러닝", "NNG", -1000, Some("머신러닝"))?
    .add_entry("GPT", "SL", -1000, None)?
    .build_to_file("user-dict.bin")?;
}

최적화

압축

바이너리 사전 압축:

#![allow(unused)]
fn main() {
let config = BuildConfig {
    compression: true,
    compression_level: 6, // 1-9
    ..Default::default()
};
}

메모리 매핑

대용량 사전은 메모리 매핑 사용:

#![allow(unused)]
fn main() {
let config = BuildConfig {
    use_mmap: true,
    ..Default::default()
};
}

증분 빌드

변경된 파일만 재빌드:

#![allow(unused)]
fn main() {
let builder = DictBuilder::new(config)?;
builder.build_incremental()?; // 증분 빌드
}

사전 검증

빌드 후 검증

mecab-ko-dict-validate \
  --dict-dir ./dict-bin \
  --check-all

프로그래밍 검증

#![allow(unused)]
fn main() {
use mecab_ko_dict::validator::DictValidator;

let validator = DictValidator::new("./dict-bin")?;

// 기본 검증
validator.validate_basic()?;

// 완전 검증
validator.validate_full()?;

// 통계 출력
let stats = validator.statistics();
println!("총 엔트리: {}", stats.total_entries);
println!("총 노드: {}", stats.total_nodes);
}

벤치마킹

빌드된 사전의 성능 측정:

mecab-ko-dict-bench \
  --dict-dir ./dict-bin \
  --corpus ./test-corpus.txt

결과:

사전 로딩 시간: 45ms
평균 분석 속도: 2.3MB/s
메모리 사용량: 125MB

사전 병합

여러 사전 소스 병합:

#![allow(unused)]
fn main() {
use mecab_ko_dict::builder::DictMerger;

let merger = DictMerger::new();
merger
    .add_source("./mecab-ko-dic")?
    .add_source("./user-dict")?
    .add_source("./domain-dict")?
    .merge_to("./merged-dict")?;
}

고급: 연접 비용 학습

말뭉치에서 학습

mecab-ko-cost-train \
  --corpus ./sejong-corpus.txt \
  --output ./matrix.def \
  --iterations 100

CRF 기반 학습

#![allow(unused)]
fn main() {
use mecab_ko_dict::trainer::CostTrainer;

let trainer = CostTrainer::new();
trainer
    .load_corpus("./corpus.txt")?
    .train(100)?
    .save_matrix("./matrix.def")?;
}

문제 해결

빌드 오류

1. "Invalid CSV format"

# CSV 유효성 검사
mecab-ko-dict-validate --csv ./lex.csv

2. "Matrix dimension mismatch"

matrix.def의 차원이 일치하지 않음:

# 자동으로 matrix.def 생성
mecab-ko-dict-build --auto-matrix

3. "Out of memory"

대용량 사전의 경우:

# 스트리밍 모드로 빌드
mecab-ko-dict-build --streaming --chunk-size 10000

성능 최적화

빌드 시간 단축

# 병렬 빌드
mecab-ko-dict-build --parallel --jobs 8

사전 크기 축소

# 미사용 엔트리 제거
mecab-ko-dict-build --prune --min-cost -10000

예제: mecab-ko-dic 빌드

# 1. 소스 다운로드
git clone https://github.com/hephaex/mecab-ko-dic.git
cd mecab-ko-dic

# 2. 빌드
mecab-ko-dict-build \
  --input . \
  --output ./build \
  --charset utf-8 \
  --dictionary mecab-ko-dic \
  --parallel

# 3. 검증
mecab-ko-dict-validate --dict-dir ./build

# 4. 설치
sudo cp -r ./build /usr/local/lib/mecab/dic/mecab-ko-dic

참고 자료

성능 튜닝

MeCab-Ko의 성능을 최적화하는 방법을 소개합니다.

벤치마크 기준

테스트 환경:

  • CPU: AMD Ryzen 9 5950X (16 cores)
  • RAM: 64GB DDR4-3200
  • OS: Ubuntu 22.04 LTS
  • Rust: 1.75.0

기준 성능:

  • 단일 스레드: ~15MB/s
  • 멀티 스레드 (16 cores): ~180MB/s
  • 메모리 사용량: ~120MB (사전 로딩 포함)

Tagger 설정 최적화

띄어쓰기 패널티 조정

#![allow(unused)]
fn main() {
use mecab_ko::{Tagger, TaggerConfig};

let mut config = TaggerConfig::default();

// 높은 패널티 = 더 많은 띄어쓰기 (빠름, 정확도 낮음)
config.space_penalty = -500;

// 낮은 패널티 = 적은 띄어쓰기 (느림, 정확도 높음)
config.space_penalty = -2000;

// 기본값: -1000 (균형)
config.space_penalty = -1000;
}

성능 영향:

  • -500: 속도 +20%, 정확도 -5%
  • -1000: 기준
  • -2000: 속도 -15%, 정확도 +3%

최대 그룹화 크기

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();

// 작은 크기 = 빠름, 긴 복합어 처리 약함
config.max_grouping_size = 12;

// 큰 크기 = 느림, 긴 복합어 처리 강함
config.max_grouping_size = 48;

// 기본값: 24 (권장)
config.max_grouping_size = 24;
}

부분 처리 모드

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();

// 부분 처리 비활성화 (기본값, 더 빠름)
config.partial = false;

// 부분 처리 활성화 (미등록어 처리 강화, 느림)
config.partial = true;
}

Tagger 재사용

잘못된 사용

#![allow(unused)]
fn main() {
// Bad: 매번 Tagger 생성 (매우 느림)
for text in texts.iter() {
    let tagger = Tagger::new(TaggerConfig::default())?; // 비효율적!
    let result = tagger.parse(text)?;
}
}

성능: ~0.5MB/s

올바른 사용

#![allow(unused)]
fn main() {
// Good: Tagger 재사용
let tagger = Tagger::new(TaggerConfig::default())?;
for text in texts.iter() {
    let result = tagger.parse(text)?;
}
}

성능: ~15MB/s (30배 빠름)

병렬 처리

Rayon을 사용한 병렬화

#![allow(unused)]
fn main() {
use rayon::prelude::*;
use std::sync::Arc;

let tagger = Arc::new(Tagger::new(TaggerConfig::default())?);

let results: Vec<_> = texts
    .par_iter()
    .map(|text| {
        let tagger = Arc::clone(&tagger);
        tagger.parse(text)
    })
    .collect::<Result<_, _>>()?;
}

성능 (16 cores): ~180MB/s (12배 빠름)

수동 스레드 풀

#![allow(unused)]
fn main() {
use std::thread;
use std::sync::mpsc;

fn parallel_analyze(texts: Vec<String>, num_threads: usize) -> Vec<String> {
    let chunk_size = texts.len() / num_threads;
    let (tx, rx) = mpsc::channel();

    for chunk in texts.chunks(chunk_size) {
        let tx = tx.clone();
        let chunk = chunk.to_vec();

        thread::spawn(move || {
            let tagger = Tagger::new(TaggerConfig::default()).unwrap();
            for text in chunk {
                let result = tagger.parse(&text).unwrap();
                tx.send(result).unwrap();
            }
        });
    }

    drop(tx);
    rx.iter().collect()
}
}

배치 처리

내장 배치 API

#![allow(unused)]
fn main() {
let tagger = Tagger::new(TaggerConfig::default())?;

// 배치 처리 (더 효율적)
let results = tagger.parse_batch(&texts)?;
}

성능: +15% vs 개별 처리

커스텀 배치 크기

#![allow(unused)]
fn main() {
fn process_in_batches(
    tagger: &Tagger,
    texts: &[String],
    batch_size: usize,
) -> Result<Vec<String>, Error> {
    texts
        .chunks(batch_size)
        .flat_map(|batch| tagger.parse_batch(batch))
        .collect()
}

// 최적 배치 크기: 100-1000
let results = process_in_batches(&tagger, &texts, 500)?;
}

메모리 최적화

사전 메모리 매핑

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();
config.use_mmap = true; // 메모리 사용량 감소

let tagger = Tagger::new(config)?;
}

메모리 사용량:

  • 일반 로딩: ~120MB
  • mmap: ~20MB (6배 감소)

성능 영향: -5% (메모리 절약 우선시)

사전 압축

사전 빌드 시 압축 활성화:

mecab-ko-dict-build --compression --compression-level 6

사전 크기:

  • 비압축: ~250MB
  • 압축 (level 6): ~85MB (3배 감소)

로딩 시간:

  • 비압축: ~50ms
  • 압축: ~80ms

String Interning

반복되는 문자열을 인터닝:

#![allow(unused)]
fn main() {
use string_cache::DefaultAtom as Atom;

let mut seen = HashMap::new();

for node in nodes {
    let surface = seen
        .entry(node.surface.clone())
        .or_insert_with(|| Atom::from(node.surface.clone()));
}
}

메모리 절약: ~30% (대용량 데이터)

I/O 최적화

버퍼링

#![allow(unused)]
fn main() {
use std::io::{BufRead, BufReader};
use std::fs::File;

let file = File::open("large_file.txt")?;
let reader = BufReader::with_capacity(1024 * 1024, file); // 1MB 버퍼

for line in reader.lines() {
    let line = line?;
    let result = tagger.parse(&line)?;
}
}

성능: +40% vs 버퍼 없음

비동기 I/O (Tokio)

use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = File::open("large_file.txt").await?;
    let reader = BufReader::new(file);
    let mut lines = reader.lines();

    while let Some(line) = lines.next_line().await? {
        let result = tagger.parse(&line)?;
        // 처리
    }

    Ok(())
}

출력 포맷 최적화

Wakati 모드

품사 정보가 불필요한 경우:

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();
config.output_format = OutputFormat::Wakati; // 빠름

let tagger = Tagger::new(config)?;
}

성능: +25% vs MeCab 포맷

커스텀 포맷

필요한 정보만 출력:

#![allow(unused)]
fn main() {
config.output_format = OutputFormat::Custom("%m\n".to_string());
}

성능: +30% vs 전체 feature 출력

프로파일링

CPU 프로파일링

# perf를 사용한 프로파일링
cargo build --release
perf record --call-graph dwarf ./target/release/mecab-ko large_file.txt
perf report

메모리 프로파일링

# heaptrack 사용
heaptrack ./target/release/mecab-ko large_file.txt
heaptrack_gui heaptrack.mecab-ko.*.gz

Flamegraph

cargo install flamegraph
cargo flamegraph --bin mecab-ko -- large_file.txt

컴파일 최적화

Release 프로필

Cargo.toml:

[profile.release]
opt-level = 3              # 최대 최적화
lto = "fat"                # Link-Time Optimization
codegen-units = 1          # 단일 코드 생성 유닛
panic = "abort"            # panic 시 abort (작은 바이너리)
strip = true               # 디버그 심볼 제거

바이너리 크기: 15MB → 8MB 성능: +10%

타겟 CPU 최적화

# 현재 CPU에 최적화
RUSTFLAGS="-C target-cpu=native" cargo build --release

성능: +5-15% (CPU 의존적)

PGO (Profile-Guided Optimization)

# 1. 계측 빌드
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release

# 2. 대표 워크로드 실행
./target/release/mecab-ko large_corpus.txt

# 3. PGO 빌드
llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --release

성능: +15-20%

벤치마크

Criterion 벤치마크

#![allow(unused)]
fn main() {
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use mecab_ko::{Tagger, TaggerConfig};

fn bench_parse(c: &mut Criterion) {
    let tagger = Tagger::new(TaggerConfig::default()).unwrap();
    let text = "형태소 분석 벤치마크 테스트입니다.";

    c.bench_function("parse", |b| {
        b.iter(|| tagger.parse(black_box(text)))
    });
}

criterion_group!(benches, bench_parse);
criterion_main!(benches);
}

실행:

cargo bench

처리량 측정

#![allow(unused)]
fn main() {
use std::time::Instant;

let tagger = Tagger::new(TaggerConfig::default())?;
let text = std::fs::read_to_string("large_corpus.txt")?;
let size = text.len();

let start = Instant::now();
tagger.parse(&text)?;
let elapsed = start.elapsed();

let throughput = size as f64 / elapsed.as_secs_f64() / 1_000_000.0;
println!("처리량: {:.2} MB/s", throughput);
}

실전 최적화 사례

웹 서버 최적화

use actix_web::{web, App, HttpServer};
use std::sync::Arc;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 전역 Tagger (Arc로 공유)
    let tagger = Arc::new(Tagger::new(TaggerConfig::default()).unwrap());

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(tagger.clone()))
            .route("/analyze", web::post().to(analyze))
    })
    .workers(16) // CPU 코어 수
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

처리량: ~10,000 req/s

스트리밍 처리

#![allow(unused)]
fn main() {
use tokio::io::AsyncBufReadExt;

async fn stream_analyze(reader: impl AsyncBufRead + Unpin) {
    let tagger = Tagger::new(TaggerConfig::default()).unwrap();
    let mut lines = reader.lines();

    while let Some(line) = lines.next_line().await.unwrap() {
        if !line.trim().is_empty() {
            tokio::task::spawn_blocking({
                let tagger = tagger.clone();
                let line = line.clone();
                move || tagger.parse(&line)
            })
            .await
            .unwrap()
            .unwrap();
        }
    }
}
}

성능 체크리스트

  • Tagger 재사용 (매번 생성하지 않기)
  • 병렬 처리 활용 (Rayon, 스레드 풀)
  • 배치 처리 사용
  • 메모리 매핑 활성화 (대용량 사전)
  • 적절한 출력 포맷 선택
  • Release 프로필 최적화
  • 버퍼링 I/O 사용
  • 불필요한 feature 비활성화
  • PGO 적용 (중요 워크로드)
  • 프로파일링으로 병목 지점 확인

성능 비교

구현속도 (MB/s)메모리 (MB)
MeCab (C++)18150
MeCab-Ko (Rust)15120
Lindera12180
Kiwi22200

참고 자료

Elasticsearch 통합

MeCab-Ko를 Elasticsearch의 형태소 분석기로 사용하는 방법을 소개합니다.

개요

Elasticsearch는 검색 엔진으로, 한국어 검색을 위해 형태소 분석기가 필요합니다. MeCab-Ko는 Elasticsearch 플러그인 형태로 통합할 수 있습니다.

설치

플러그인 설치

# Elasticsearch 플러그인 설치
bin/elasticsearch-plugin install \
  https://github.com/hephaex/elasticsearch-analysis-mecab-ko/releases/download/v8.11.0/elasticsearch-analysis-mecab-ko-8.11.0.zip

버전 호환성:

ElasticsearchPlugin Version
8.11.x8.11.0
8.10.x8.10.0
7.17.x7.17.0

수동 빌드

# 소스 다운로드
git clone https://github.com/hephaex/elasticsearch-analysis-mecab-ko.git
cd elasticsearch-analysis-mecab-ko

# 빌드
./gradlew clean build

# 설치
bin/elasticsearch-plugin install file:///path/to/plugin.zip

사전 설치

# 사전 디렉토리 생성
sudo mkdir -p /usr/share/elasticsearch/config/mecab-ko-dic

# 사전 다운로드 및 압축 해제
wget https://github.com/hephaex/mecab-ko-dic/releases/latest/download/mecab-ko-dic.tar.gz
tar xzf mecab-ko-dic.tar.gz -C /usr/share/elasticsearch/config/mecab-ko-dic

설정

Analyzer 정의

PUT /my_index
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "mecab_ko_tokenizer": {
          "type": "mecab_ko",
          "dict_path": "/usr/share/elasticsearch/config/mecab-ko-dic",
          "user_dict_path": "/usr/share/elasticsearch/config/user-dic.csv"
        }
      },
      "analyzer": {
        "mecab_analyzer": {
          "type": "custom",
          "tokenizer": "mecab_ko_tokenizer"
        }
      }
    }
  }
}

Tokenizer 옵션

{
  "tokenizer": {
    "mecab_ko_tokenizer": {
      "type": "mecab_ko",
      "dict_path": "/path/to/dict",
      "user_dict_path": "/path/to/user-dict.csv",
      "output_format": "mecab",
      "space_penalty": -1000,
      "compound_noun_min_length": 2,
      "decompound": true,
      "pos_filter": ["NNG", "NNP", "VV", "VA"],
      "max_unk_length": 24
    }
  }
}

옵션 설명:

옵션기본값설명
dict_path-사전 디렉토리 경로
user_dict_path-사용자 사전 파일 경로
output_formatmecab출력 포맷
space_penalty-1000띄어쓰기 패널티
compound_noun_min_length2복합명사 최소 길이
decompoundfalse복합명사 분해
pos_filter-품사 필터 (배열)
max_unk_length24미등록어 최대 길이

사용 예제

기본 분석

POST /my_index/_analyze
{
  "analyzer": "mecab_analyzer",
  "text": "형태소 분석을 시작합니다"
}

응답:

{
  "tokens": [
    {
      "token": "형태소",
      "start_offset": 0,
      "end_offset": 3,
      "type": "NNG",
      "position": 0
    },
    {
      "token": "분석",
      "start_offset": 4,
      "end_offset": 6,
      "type": "NNG",
      "position": 1
    },
    {
      "token": "을",
      "start_offset": 6,
      "end_offset": 7,
      "type": "JKO",
      "position": 2
    },
    {
      "token": "시작",
      "start_offset": 8,
      "end_offset": 10,
      "type": "NNG",
      "position": 3
    },
    {
      "token": "하",
      "start_offset": 10,
      "end_offset": 11,
      "type": "XSV",
      "position": 4
    },
    {
      "token": "ㅂ니다",
      "start_offset": 11,
      "end_offset": 14,
      "type": "EF",
      "position": 5
    }
  ]
}

품사 필터링

명사만 추출:

PUT /my_index
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "mecab_ko_noun": {
          "type": "mecab_ko",
          "pos_filter": ["NNG", "NNP", "NNB"]
        }
      },
      "analyzer": {
        "noun_analyzer": {
          "type": "custom",
          "tokenizer": "mecab_ko_noun"
        }
      }
    }
  }
}

테스트:

POST /my_index/_analyze
{
  "analyzer": "noun_analyzer",
  "text": "저는 오늘 학교에 갑니다"
}

결과: ["오늘", "학교"]

복합명사 분해

PUT /my_index
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "mecab_ko_decompound": {
          "type": "mecab_ko",
          "decompound": true,
          "compound_noun_min_length": 2
        }
      },
      "analyzer": {
        "decompound_analyzer": {
          "type": "custom",
          "tokenizer": "mecab_ko_decompound"
        }
      }
    }
  }
}

테스트:

POST /my_index/_analyze
{
  "analyzer": "decompound_analyzer",
  "text": "한국어형태소분석기"
}

결과: ["한국어", "한국", "어", "형태소", "형태", "소", "분석기", "분석", "기"]

인덱싱 및 검색

인덱스 생성

PUT /documents
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "mecab_ko_tokenizer": {
          "type": "mecab_ko"
        }
      },
      "analyzer": {
        "korean_analyzer": {
          "type": "custom",
          "tokenizer": "mecab_ko_tokenizer",
          "filter": ["lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "korean_analyzer"
      },
      "content": {
        "type": "text",
        "analyzer": "korean_analyzer"
      }
    }
  }
}

문서 인덱싱

POST /documents/_doc/1
{
  "title": "Elasticsearch 한국어 검색",
  "content": "MeCab-Ko를 사용한 형태소 분석 예제입니다."
}

POST /documents/_doc/2
{
  "title": "형태소 분석기 비교",
  "content": "여러 한국어 형태소 분석기를 비교합니다."
}

검색

GET /documents/_search
{
  "query": {
    "match": {
      "content": "형태소 분석"
    }
  }
}

하이라이팅

GET /documents/_search
{
  "query": {
    "match": {
      "content": "형태소"
    }
  },
  "highlight": {
    "fields": {
      "content": {}
    }
  }
}

고급 설정

멀티 필드

다양한 분석기를 동시에 사용:

PUT /multi_field_index
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "korean_analyzer",
        "fields": {
          "ngram": {
            "type": "text",
            "analyzer": "ngram_analyzer"
          },
          "keyword": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

동의어 처리

PUT /synonym_index
{
  "settings": {
    "analysis": {
      "filter": {
        "korean_synonym": {
          "type": "synonym",
          "synonyms": [
            "컴퓨터, PC, 피씨",
            "휴대폰, 핸드폰, 모바일"
          ]
        }
      },
      "analyzer": {
        "korean_synonym_analyzer": {
          "type": "custom",
          "tokenizer": "mecab_ko_tokenizer",
          "filter": ["lowercase", "korean_synonym"]
        }
      }
    }
  }
}

사용자 사전

user-dic.csv:

# 표면형,품사,비용,기본형
딥러닝,NNG,-1000,딥러닝
머신러닝,NNG,-1000,머신러닝
트랜스포머,NNG,-1000,트랜스포머
GPT,SL,-1000,GPT

설정:

{
  "tokenizer": {
    "mecab_ko_custom": {
      "type": "mecab_ko",
      "user_dict_path": "/usr/share/elasticsearch/config/user-dic.csv"
    }
  }
}

성능 최적화

인덱스 샤드 설정

PUT /optimized_index
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "refresh_interval": "30s",
    "analysis": {
      "tokenizer": {
        "mecab_ko_tokenizer": {
          "type": "mecab_ko"
        }
      }
    }
  }
}

필터 캐싱

GET /documents/_search
{
  "query": {
    "bool": {
      "filter": {
        "term": {
          "status": "published"
        }
      },
      "must": {
        "match": {
          "content": "검색어"
        }
      }
    }
  }
}

벌크 인덱싱

curl -X POST "localhost:9200/documents/_bulk" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @bulk_data.ndjson

bulk_data.ndjson:

{"index":{"_id":"1"}}
{"title":"제목1","content":"내용1"}
{"index":{"_id":"2"}}
{"title":"제목2","content":"내용2"}

모니터링

분석기 성능 확인

GET /_nodes/stats/indices/indexing

쿼리 성능 프로파일링

GET /documents/_search
{
  "profile": true,
  "query": {
    "match": {
      "content": "검색어"
    }
  }
}

실전 예제

블로그 검색

PUT /blog
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "mecab_ko_tokenizer": {
          "type": "mecab_ko",
          "pos_filter": ["NNG", "NNP", "VV", "VA", "SL", "SH"]
        }
      },
      "analyzer": {
        "blog_analyzer": {
          "type": "custom",
          "tokenizer": "mecab_ko_tokenizer",
          "filter": ["lowercase", "trim"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "blog_analyzer",
        "boost": 2.0
      },
      "content": {
        "type": "text",
        "analyzer": "blog_analyzer"
      },
      "tags": {
        "type": "keyword"
      },
      "published_date": {
        "type": "date"
      }
    }
  }
}

검색:

GET /blog/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": {
              "query": "형태소 분석",
              "boost": 2
            }
          }
        },
        {
          "match": {
            "content": "형태소 분석"
          }
        }
      ],
      "filter": {
        "range": {
          "published_date": {
            "gte": "2024-01-01"
          }
        }
      }
    }
  },
  "highlight": {
    "fields": {
      "title": {},
      "content": {}
    }
  },
  "sort": [
    "_score",
    {"published_date": "desc"}
  ]
}

전자상거래 상품 검색

PUT /products
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "mecab_ko_product": {
          "type": "mecab_ko",
          "decompound": true,
          "pos_filter": ["NNG", "NNP", "SL", "SN"]
        }
      },
      "analyzer": {
        "product_analyzer": {
          "type": "custom",
          "tokenizer": "mecab_ko_product",
          "filter": ["lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "product_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "description": {
        "type": "text",
        "analyzer": "product_analyzer"
      },
      "price": {
        "type": "float"
      },
      "category": {
        "type": "keyword"
      }
    }
  }
}

문제 해결

플러그인 로딩 실패

# 로그 확인
tail -f /var/log/elasticsearch/elasticsearch.log

# 플러그인 목록 확인
bin/elasticsearch-plugin list

사전을 찾을 수 없음

# 사전 경로 확인
ls -la /usr/share/elasticsearch/config/mecab-ko-dic

# 권한 확인
chown -R elasticsearch:elasticsearch /usr/share/elasticsearch/config/mecab-ko-dic

성능 저하

# 분석기 캐시 통계 확인
GET /_nodes/stats/indices/query_cache

상세 문서

더 자세한 내용은 다음 문서를 참조하세요:

참고 자료

커스텀 분석기

MeCab-Ko의 내부 동작을 이해하고 커스텀 분석기를 작성하는 방법을 소개합니다.

분석 파이프라인

MeCab-Ko의 형태소 분석은 다음 단계로 진행됩니다:

입력 텍스트
    ↓
문자 정규화 (Character Normalization)
    ↓
래티스 구축 (Lattice Construction)
    ↓
최적 경로 탐색 (Viterbi Algorithm)
    ↓
후처리 (Post-processing)
    ↓
출력

커스텀 Tokenizer

기본 구조

#![allow(unused)]
fn main() {
use mecab_ko_core::{Lattice, Tokenizer, Token};

pub struct CustomTokenizer {
    dict: Dictionary,
    config: Config,
}

impl Tokenizer for CustomTokenizer {
    fn tokenize(&self, text: &str) -> Result<Vec<Token>, Error> {
        // 1. 래티스 생성
        let mut lattice = Lattice::new(text);

        // 2. 사전 검색 및 노드 추가
        self.build_lattice(&mut lattice)?;

        // 3. 최적 경로 탐색
        let path = self.viterbi(&lattice)?;

        // 4. 토큰 변환
        Ok(self.path_to_tokens(path))
    }
}
}

래티스 빌더

#![allow(unused)]
fn main() {
impl CustomTokenizer {
    fn build_lattice(&self, lattice: &mut Lattice) -> Result<(), Error> {
        let text = lattice.text();
        let len = text.len();

        for pos in 0..len {
            // 사전에서 검색
            let entries = self.dict.lookup(&text[pos..])?;

            for entry in entries {
                // 노드 추가
                lattice.add_node(Node {
                    surface: entry.surface.clone(),
                    pos: entry.pos,
                    left_id: entry.left_id,
                    right_id: entry.right_id,
                    cost: entry.cost,
                    start: pos,
                    length: entry.surface.len(),
                });
            }

            // 미등록어 처리
            if entries.is_empty() {
                self.add_unknown_node(lattice, pos)?;
            }
        }

        Ok(())
    }
}
}

Viterbi 알고리즘

#![allow(unused)]
fn main() {
impl CustomTokenizer {
    fn viterbi(&self, lattice: &Lattice) -> Result<Vec<usize>, Error> {
        let nodes = lattice.nodes();
        let mut best_cost = vec![i32::MAX; nodes.len()];
        let mut best_path = vec![None; nodes.len()];

        best_cost[0] = 0; // BOS

        for i in 1..nodes.len() {
            let node = &nodes[i];

            for j in 0..i {
                let prev = &nodes[j];

                if prev.end() != node.start {
                    continue;
                }

                // 연접 비용
                let conn_cost = self.dict.connection_cost(
                    prev.right_id,
                    node.left_id,
                );

                let total_cost = best_cost[j]
                    .saturating_add(conn_cost)
                    .saturating_add(node.cost);

                if total_cost < best_cost[i] {
                    best_cost[i] = total_cost;
                    best_path[i] = Some(j);
                }
            }
        }

        // 역추적
        self.backtrack(&best_path)
    }

    fn backtrack(&self, path: &[Option<usize>]) -> Result<Vec<usize>, Error> {
        let mut result = Vec::new();
        let mut current = path.len() - 1; // EOS

        while let Some(prev) = path[current] {
            result.push(prev);
            current = prev;
        }

        result.reverse();
        Ok(result)
    }
}
}

커스텀 필터

복합명사 분해기

#![allow(unused)]
fn main() {
pub struct CompoundNounDecomposer {
    min_length: usize,
}

impl CompoundNounDecomposer {
    pub fn decompose(&self, tokens: Vec<Token>) -> Vec<Token> {
        let mut result = Vec::new();

        for token in tokens {
            if token.pos == "NNG" && token.surface.chars().count() >= self.min_length {
                // 복합명사 분해
                result.extend(self.split_compound(&token));
            } else {
                result.push(token);
            }
        }

        result
    }

    fn split_compound(&self, token: &Token) -> Vec<Token> {
        // 사전 기반 분해 로직
        let mut parts = Vec::new();
        let surface = &token.surface;

        // 예: "형태소분석기" -> ["형태소", "분석기"]
        // 실제 구현은 사전 기반으로 수행

        parts
    }
}
}

품사 필터

#![allow(unused)]
fn main() {
pub struct PosFilter {
    allowed_pos: HashSet<String>,
}

impl PosFilter {
    pub fn filter(&self, tokens: Vec<Token>) -> Vec<Token> {
        tokens
            .into_iter()
            .filter(|token| self.allowed_pos.contains(&token.pos))
            .collect()
    }
}

// 사용
let filter = PosFilter {
    allowed_pos: ["NNG", "NNP", "VV", "VA"].iter().map(|s| s.to_string()).collect(),
};

let filtered = filter.filter(tokens);
}

불용어 필터

#![allow(unused)]
fn main() {
pub struct StopwordFilter {
    stopwords: HashSet<String>,
}

impl StopwordFilter {
    pub fn from_file(path: &Path) -> Result<Self, Error> {
        let content = std::fs::read_to_string(path)?;
        let stopwords = content
            .lines()
            .map(|line| line.trim().to_string())
            .filter(|line| !line.is_empty() && !line.starts_with('#'))
            .collect();

        Ok(Self { stopwords })
    }

    pub fn filter(&self, tokens: Vec<Token>) -> Vec<Token> {
        tokens
            .into_iter()
            .filter(|token| !self.stopwords.contains(&token.surface))
            .collect()
    }
}
}

분석기 조합

파이프라인 구성

#![allow(unused)]
fn main() {
pub struct AnalyzerPipeline {
    tokenizer: Box<dyn Tokenizer>,
    filters: Vec<Box<dyn TokenFilter>>,
}

impl AnalyzerPipeline {
    pub fn new(tokenizer: Box<dyn Tokenizer>) -> Self {
        Self {
            tokenizer,
            filters: Vec::new(),
        }
    }

    pub fn add_filter(mut self, filter: Box<dyn TokenFilter>) -> Self {
        self.filters.push(filter);
        self
    }

    pub fn analyze(&self, text: &str) -> Result<Vec<Token>, Error> {
        let mut tokens = self.tokenizer.tokenize(text)?;

        for filter in &self.filters {
            tokens = filter.apply(tokens);
        }

        Ok(tokens)
    }
}

// 사용
let pipeline = AnalyzerPipeline::new(Box::new(MecabTokenizer::new()?))
    .add_filter(Box::new(PosFilter::new(vec!["NNG", "NNP"])))
    .add_filter(Box::new(StopwordFilter::from_file("stopwords.txt")?))
    .add_filter(Box::new(LowercaseFilter::new()));

let tokens = pipeline.analyze("분석할 텍스트")?;
}

커스텀 비용 함수

띄어쓰기 가중치

#![allow(unused)]
fn main() {
pub struct SpaceCostCalculator {
    base_penalty: i32,
    position_weight: f32,
}

impl SpaceCostCalculator {
    pub fn calculate(&self, node: &Node, context: &Context) -> i32 {
        let mut cost = node.cost;

        // 띄어쓰기 패널티
        if context.has_space_before(node) {
            cost += self.base_penalty;

            // 위치 기반 가중치
            let position_factor = context.position() as f32 / context.total_length() as f32;
            cost += (self.base_penalty as f32 * position_factor * self.position_weight) as i32;
        }

        cost
    }
}
}

도메인 특화 비용

#![allow(unused)]
fn main() {
pub struct DomainCostAdjuster {
    domain_terms: HashMap<String, i32>,
}

impl DomainCostAdjuster {
    pub fn adjust_cost(&self, node: &mut Node) {
        if let Some(&adjustment) = self.domain_terms.get(&node.surface) {
            node.cost += adjustment;
        }
    }
}

// 사용
let adjuster = DomainCostAdjuster {
    domain_terms: [
        ("딥러닝".to_string(), -2000),   // 선호
        ("머신러닝".to_string(), -2000),
        ("AI".to_string(), -2000),
    ].iter().cloned().collect(),
};
}

N-best 분석기

N-best 경로 탐색

#![allow(unused)]
fn main() {
pub struct NBestAnalyzer {
    tokenizer: MecabTokenizer,
    n: usize,
    theta: f32,
}

impl NBestAnalyzer {
    pub fn analyze(&self, text: &str) -> Result<Vec<Vec<Token>>, Error> {
        let lattice = self.tokenizer.build_lattice(text)?;
        let paths = self.find_nbest_paths(&lattice)?;

        Ok(paths
            .into_iter()
            .map(|path| self.path_to_tokens(path))
            .collect())
    }

    fn find_nbest_paths(&self, lattice: &Lattice) -> Result<Vec<Vec<usize>>, Error> {
        // A* 알고리즘 또는 Forward-DP + Backward-A* 사용
        let mut candidates = Vec::new();
        let best_cost = self.find_best_cost(lattice)?;

        // theta 범위 내의 경로만 수집
        self.collect_paths(lattice, best_cost, &mut candidates)?;

        // 상위 N개 선택
        candidates.sort_by_key(|path| path.cost);
        Ok(candidates.into_iter().take(self.n).map(|p| p.nodes).collect())
    }
}
}

실전 예제

의료 도메인 분석기

#![allow(unused)]
fn main() {
pub struct MedicalAnalyzer {
    base_tokenizer: MecabTokenizer,
    medical_dict: HashMap<String, MedicalTerm>,
}

impl MedicalAnalyzer {
    pub fn analyze(&self, text: &str) -> Result<Vec<MedicalToken>, Error> {
        // 1. 기본 형태소 분석
        let tokens = self.base_tokenizer.tokenize(text)?;

        // 2. 의료 용어 인식
        let medical_tokens = self.recognize_medical_terms(&tokens)?;

        // 3. 의료 용어 정규화
        let normalized = self.normalize_medical_terms(medical_tokens)?;

        Ok(normalized)
    }

    fn recognize_medical_terms(&self, tokens: &[Token]) -> Result<Vec<MedicalToken>, Error> {
        let mut result = Vec::new();
        let mut i = 0;

        while i < tokens.len() {
            // 연속된 토큰을 결합하여 의료 용어 검색
            let mut matched = false;

            for len in (1..=5).rev() {
                if i + len > tokens.len() {
                    continue;
                }

                let phrase = tokens[i..i+len]
                    .iter()
                    .map(|t| t.surface.as_str())
                    .collect::<Vec<_>>()
                    .join("");

                if let Some(term) = self.medical_dict.get(&phrase) {
                    result.push(MedicalToken {
                        surface: phrase,
                        term_type: term.term_type.clone(),
                        standard_code: term.code.clone(),
                    });
                    i += len;
                    matched = true;
                    break;
                }
            }

            if !matched {
                result.push(MedicalToken::from_token(&tokens[i]));
                i += 1;
            }
        }

        Ok(result)
    }
}
}

법률 문서 분석기

#![allow(unused)]
fn main() {
pub struct LegalAnalyzer {
    tokenizer: MecabTokenizer,
    legal_patterns: Vec<Regex>,
}

impl LegalAnalyzer {
    pub fn analyze(&self, text: &str) -> Result<LegalDocument, Error> {
        let tokens = self.tokenizer.tokenize(text)?;

        Ok(LegalDocument {
            tokens,
            articles: self.extract_articles(text)?,
            clauses: self.extract_clauses(text)?,
            references: self.extract_references(text)?,
        })
    }

    fn extract_articles(&self, text: &str) -> Result<Vec<Article>, Error> {
        // "제1조", "제2조" 등 추출
        let pattern = Regex::new(r"제(\d+)조")?;
        let articles = pattern
            .captures_iter(text)
            .map(|cap| Article {
                number: cap[1].parse().unwrap(),
                content: self.extract_article_content(&cap),
            })
            .collect();

        Ok(articles)
    }
}
}

성능 최적화

캐싱

#![allow(unused)]
fn main() {
use lru::LruCache;

pub struct CachedAnalyzer {
    analyzer: MecabTokenizer,
    cache: Arc<Mutex<LruCache<String, Vec<Token>>>>,
}

impl CachedAnalyzer {
    pub fn analyze(&self, text: &str) -> Result<Vec<Token>, Error> {
        // 캐시 확인
        {
            let mut cache = self.cache.lock().unwrap();
            if let Some(cached) = cache.get(text) {
                return Ok(cached.clone());
            }
        }

        // 분석
        let tokens = self.analyzer.tokenize(text)?;

        // 캐시 저장
        {
            let mut cache = self.cache.lock().unwrap();
            cache.put(text.to_string(), tokens.clone());
        }

        Ok(tokens)
    }
}
}

테스트

단위 테스트

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_custom_tokenizer() {
        let tokenizer = CustomTokenizer::new().unwrap();
        let tokens = tokenizer.tokenize("테스트 문장").unwrap();

        assert_eq!(tokens.len(), 2);
        assert_eq!(tokens[0].surface, "테스트");
        assert_eq!(tokens[1].surface, "문장");
    }

    #[test]
    fn test_pos_filter() {
        let filter = PosFilter::new(vec!["NNG"]);
        let tokens = vec![
            Token::new("테스트", "NNG"),
            Token::new("하", "VV"),
            Token::new("다", "EF"),
        ];

        let filtered = filter.filter(tokens);
        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].surface, "테스트");
    }
}
}

참고 자료

Performance Dashboard

MeCab-Ko의 실시간 성능 벤치마크 대시보드입니다.

최신 벤치마크 결과

Version: loading...

Last Updated: loading...

Commit: loading...

Throughput (처리량)

Latency (지연 시간)

버전별 성능 추이


🎉 v0.5.0: 100% Token Accuracy 달성!

지표
Token Accuracy100.0%
Sentence Accuracy100.0%
F1 Score1.000
테스트 문장500개

KPI 목표 및 현황

지표목표v0.1.0v0.4.0v0.5.0상태
Token Accuracy95%+29.6%81.0%100.0%✅ PASS
Throughput200K ops/sec182K245K263K✅ PASS
Cold Start< 200ms120ms86ms86ms✅ PASS
Memory< 150MB215MB145MB145MB✅ PASS

v0.5.0은 정확도 100%를 달성하면서도 성능을 유지하고 있습니다.

벤치마크 환경

항목
OSUbuntu 22.04 (GitHub Actions)
CPUAMD EPYC 7763 (2 cores)
Memory7 GB
Rust1.75+

측정 항목 설명

Throughput (처리량)

  • tokenize_short: 10자 미만 짧은 문장 분석 속도
  • tokenize_medium: 50자 내외 중간 문장 분석 속도
  • tokenize_long: 200자 이상 긴 문장 분석 속도

Latency (지연 시간)

  • cold_start: 사전 로딩 포함 첫 번째 분석까지 시간
  • batch_100: 100개 문장 배치 처리 시간

CI/CD 통합

벤치마크는 다음 상황에서 자동 실행됩니다:

  • Push to main: 벤치마크 실행 및 대시보드 업데이트
  • Pull Request: 기준 브랜치와 비교하여 회귀 감지
  • Manual trigger: 전체 벤치마크 실행

성능 회귀 감지

PR에서 10% 이상 성능 저하가 감지되면 자동으로 경고가 표시됩니다.

⚠️ Performance Regression Detected!
The following benchmarks are >10% slower:
- tokenize_short: +15.2%

벤치마크 가이드

MeCab-Ko의 벤치마크를 실행하고 성능을 측정하는 방법을 안내합니다.

목차

  1. 벤치마크 환경
  2. 벤치마크 실행
  3. 벤치마크 종류
  4. 결과 분석
  5. CI 통합
  6. 커스텀 벤치마크

벤치마크 환경

시스템 요구사항

항목최소권장
CPU2 cores4+ cores
RAM4 GB8+ GB
Rust1.70+1.75+
사전mini-dictfull-dict

환경 변수 설정

# 사전 경로 (선택)
export MECAB_KO_DIC_DIR=/path/to/mecab-ko-dic

# 전체 사전 벤치마크 활성화
export MECAB_KO_FULL_DICT=1

# 벤치마크 반복 횟수
export BENCHMARK_SAMPLES=100

벤치마크 실행

모든 벤치마크 실행

cd rust/crates/benchmarks
cargo bench

특정 벤치마크 실행

# 토크나이저 벤치마크만
cargo bench --bench tokenizer_bench

# 배치 처리 벤치마크만
cargo bench --bench batch_bench

# 특정 테스트만
cargo bench -- "tokenize_short"

빠른 테스트 모드

# 샘플 수 줄이기
cargo bench -- --sample-size 10

# 워밍업 시간 줄이기
cargo bench -- --warm-up-time 1

결과 저장

# JSON 결과 저장
cargo bench -- --save-baseline v0.2.0

# 이전 결과와 비교
cargo bench -- --baseline v0.1.1

벤치마크 종류

1. tokenizer_bench

기본 토큰화 성능 측정

#![allow(unused)]
fn main() {
// 짧은 텍스트 (5자)
tokenize_short: "안녕하세요"

// 중간 텍스트 (50자)
tokenize_medium: "오늘 날씨가 좋아서 공원에서 산책을 했습니다..."

// 긴 텍스트 (200자)
tokenize_long: "자연어 처리는 인공지능 분야에서..."
}
지표설명
time평균 실행 시간
throughput초당 처리량 (ops/sec)
std_dev표준 편차

2. batch_bench

배치 처리 성능

#![allow(unused)]
fn main() {
// 배치 크기별 테스트
batch_100: 100개 문장
batch_1000: 1000개 문장
batch_5000: 5000개 문장
}
지표설명
total_time전체 처리 시간
avg_per_text문장당 평균 시간
throughputMB/s 처리량

3. memory_bench

메모리 사용량 측정

#![allow(unused)]
fn main() {
// 측정 항목
memory_allocation: 토큰화 시 메모리 할당
memory_reuse: 재사용 효율성
memory_pressure: 메모리 압력 테스트
}

4. cold_start_bench

초기화 시간 측정

#![allow(unused)]
fn main() {
// 측정 항목
dict_load: 사전 로딩 시간
first_tokenize: 첫 번째 토큰화
warm_tokenize: 워밍업 후 토큰화
}

5. viterbi_bench

Viterbi 알고리즘 성능

#![allow(unused)]
fn main() {
// 측정 항목
viterbi_search: 최적 경로 탐색
lattice_build: 래티스 구축
path_backtrack: 경로 역추적
}

6. trie_bench

Double-Array Trie 검색 성능

#![allow(unused)]
fn main() {
// 측정 항목
exact_match: 정확히 일치 검색
prefix_search: 접두사 검색
batch_lookup: 배치 검색
}

7. matrix_bench

연접 비용 매트릭스 검색

#![allow(unused)]
fn main() {
// 측정 항목
single_lookup: 단일 조회
batch_lookup: 배치 조회
cache_hit: 캐시 히트율
}

결과 분석

HTML 리포트

# 벤치마크 실행 후 리포트 열기
cargo bench
open target/criterion/report/index.html

리포트 구성

target/criterion/
├── report/
│   └── index.html          # 전체 개요
├── tokenize_short/
│   ├── report/index.html   # 개별 벤치마크 상세
│   └── base/               # 기준선 데이터
└── ...

통계 해석

지표의미
Mean평균 실행 시간
Std. Dev.표준 편차 (낮을수록 안정적)
Median중앙값 (이상치 영향 적음)
MAD중앙값 절대 편차

회귀 감지

Performance has regressed:
  tokenize_short time: [5.2 us 5.3 us 5.4 us]
                       change: [+8.2% +10.1% +12.3%] (p = 0.00 < 0.05)
  • p < 0.05: 통계적으로 유의미한 변화
  • change > 10%: 주의 필요
  • change > 20%: 회귀 가능성 높음

CI 통합

GitHub Actions 워크플로우

name: Benchmark

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Rust
        uses: dtolnay/rust-action@stable

      - name: Run benchmarks
        run: |
          cd rust/crates/benchmarks
          cargo bench -- --save-baseline current

      - name: Compare with main (PR only)
        if: github.event_name == 'pull_request'
        run: |
          git fetch origin main
          git checkout origin/main -- target/criterion
          cargo bench -- --baseline main

      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: benchmark-results
          path: target/criterion/

회귀 임계값 설정

# .github/workflows/benchmark.yml
env:
  BENCHMARK_THRESHOLD_PASS: 5      # 5% 미만: PASS
  BENCHMARK_THRESHOLD_WARN: 10     # 5-10%: WARNING
  BENCHMARK_THRESHOLD_FAIL: 20     # 10% 초과: FAIL

PR 코멘트 생성

- name: Post benchmark comment
  uses: actions/github-script@v7
  with:
    script: |
      const fs = require('fs');
      const results = JSON.parse(fs.readFileSync('benchmark-comparison.json'));

      let comment = '## Benchmark Results\n\n';
      comment += '| Benchmark | Before | After | Change |\n';
      comment += '|-----------|--------|-------|--------|\n';

      for (const [name, data] of Object.entries(results)) {
        const change = ((data.after - data.before) / data.before * 100).toFixed(1);
        const emoji = change > 10 ? '🔴' : change > 5 ? '🟡' : '🟢';
        comment += `| ${name} | ${data.before}us | ${data.after}us | ${emoji} ${change}% |\n`;
      }

      github.rest.issues.createComment({
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: context.issue.number,
        body: comment
      });

커스텀 벤치마크

Criterion 벤치마크 작성

#![allow(unused)]
fn main() {
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use mecab_ko::Tokenizer;

fn custom_benchmark(c: &mut Criterion) {
    let tokenizer = Tokenizer::new().expect("Failed to create tokenizer");

    // 단일 벤치마크
    c.bench_function("my_custom_bench", |b| {
        b.iter(|| {
            tokenizer.tokenize(black_box("테스트 문장"))
        })
    });

    // 파라미터화된 벤치마크
    let texts = vec![
        ("short", "안녕"),
        ("medium", "오늘 날씨가 좋습니다"),
        ("long", "자연어 처리는 인공지능의 핵심 기술입니다"),
    ];

    let mut group = c.benchmark_group("text_length");
    for (name, text) in texts {
        group.bench_with_input(
            BenchmarkId::new("tokenize", name),
            &text,
            |b, text| {
                b.iter(|| tokenizer.tokenize(black_box(text)))
            },
        );
    }
    group.finish();
}

criterion_group!(benches, custom_benchmark);
criterion_main!(benches);
}

처리량 측정

#![allow(unused)]
fn main() {
use criterion::{Criterion, Throughput};

fn throughput_benchmark(c: &mut Criterion) {
    let tokenizer = Tokenizer::new().unwrap();
    let text = "벤치마크 테스트용 긴 텍스트...".repeat(100);

    let mut group = c.benchmark_group("throughput");
    group.throughput(Throughput::Bytes(text.len() as u64));

    group.bench_function("large_text", |b| {
        b.iter(|| tokenizer.tokenize(black_box(&text)))
    });

    group.finish();
}
}

메모리 측정

#![allow(unused)]
fn main() {
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;

static ALLOCATED: AtomicUsize = AtomicUsize::new(0);

struct CountingAllocator;

unsafe impl GlobalAlloc for CountingAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        ALLOCATED.fetch_add(layout.size(), Ordering::SeqCst);
        System.alloc(layout)
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        ALLOCATED.fetch_sub(layout.size(), Ordering::SeqCst);
        System.dealloc(ptr, layout)
    }
}

fn memory_benchmark(c: &mut Criterion) {
    let tokenizer = Tokenizer::new().unwrap();

    c.bench_function("memory_usage", |b| {
        b.iter_custom(|iters| {
            let start = ALLOCATED.load(Ordering::SeqCst);

            for _ in 0..iters {
                let _ = tokenizer.tokenize("테스트");
            }

            let end = ALLOCATED.load(Ordering::SeqCst);
            std::time::Duration::from_nanos(
                ((end - start) / iters as usize) as u64
            )
        })
    });
}
}

성능 최적화 팁

벤치마크 시 주의사항

  1. 릴리스 빌드 사용

    cargo bench  # 기본적으로 --release
    
  2. 시스템 부하 최소화

    • 다른 프로그램 종료
    • 백그라운드 프로세스 최소화
  3. 충분한 워밍업

    #![allow(unused)]
    fn main() {
    criterion_group! {
        name = benches;
        config = Criterion::default()
            .warm_up_time(std::time::Duration::from_secs(5))
            .measurement_time(std::time::Duration::from_secs(10));
        targets = my_benchmark
    }
    }
  4. 통계적 유의성 확인

    • 최소 100회 이상 측정
    • p-value < 0.05 확인

일반적인 성능 문제

문제원인해결책
Cold start 느림사전 로딩mmap 사용
메모리 과다 사용캐싱 없음LRU 캐시 적용
배치 처리 느림직렬 처리Rayon 병렬화
긴 텍스트 느림O(n^2) 알고리즘청크 분할

벤치마크 결과 예시

v0.2.0 기준선

tokenize_short    time: [3.8 us 3.9 us 4.0 us]
tokenize_medium   time: [42.1 us 43.2 us 44.5 us]
tokenize_long     time: [138.2 us 141.5 us 145.1 us]

batch_100         time: [2.42 ms 2.48 ms 2.55 ms]
batch_1000        time: [26.8 ms 27.4 ms 28.1 ms]

cold_start        time: [128.5 ms 132.1 ms 136.2 ms]

KPI 목표

지표목표현재상태
Throughput200K ops/sec238KPASS
Cold Start< 200ms132msPASS
Memory< 150MB145MBPASS

참고 자료

품사 태그

MeCab-Ko는 세종 품사 태그 체계를 기반으로 하며, mecab-ko-dic의 확장 태그를 포함합니다.

품사 태그 개요

대분류

대분류설명태그 예시
체언명사류NNG, NNP, NNB, NR, NP
용언동사, 형용사류VV, VA, VX, VCP, VCN
관형사관형사MM
부사부사류MAG, MAJ
감탄사감탄사IC
조사조사류JKS, JKC, JKG, JKO, JKB, JKV, JKQ, JX, JC
어미어미류EP, EF, EC, ETN, ETM
접사접두사, 접미사XPN, XSN, XSV, XSA
어근어근XR
부호문장부호, 외국어 등SF, SP, SS, SE, SO, SW, SL, SH, SN

체언 (명사류)

NNG - 일반 명사

일반적인 사물이나 개념을 나타내는 명사

사과, 컴퓨터, 사랑, 행복, 분석

예시:

오늘 날씨가 좋습니다
     ↓
오늘/NNG  날씨/NNG  가/JKS  좋/VA  습니다/EF

NNP - 고유 명사

특정 사물이나 사람의 이름

서울, 한국, 김철수, 삼성, 앤트로픽

예시:

서울에서 회의가 있습니다
  ↓
서울/NNP  에서/JKB  회의/NNG  가/JKS  있/VA  습니다/EF

NNB - 의존 명사

단독으로 쓰이지 못하고 관형어 뒤에 오는 명사

것, 수, 줄, 바, 뿐, 만큼, 데

예시:

할 수 있다
  ↓
하/VV  ㄹ/ETM  수/NNB  있/VA  다/EF

NR - 수사

숫자를 나타내는 말

하나, 둘, 셋, 첫째, 열

NP - 대명사

명사를 대신하는 말

나, 너, 우리, 이것, 저것, 그것

용언 (동사/형용사류)

VV - 동사

동작이나 작용을 나타내는 말

가다, 먹다, 하다, 분석하다

예시:

밥을 먹는다
  ↓
밥/NNG  을/JKO  먹/VV  는다/EF

VA - 형용사

상태나 성질을 나타내는 말

크다, 작다, 아름답다, 빠르다

VX - 보조 용언

본용언 뒤에서 의미를 보충

-아/어 보다, -아/어 주다, -아/어 가다

VCP - 긍정 지정사

이다

VCN - 부정 지정사

아니다

수식언

MM - 관형사

체언을 꾸미는 말

새, 헌, 옛, 이, 그, 저, 모든

MAG - 일반 부사

용언이나 문장을 꾸미는 말

매우, 아주, 잘, 빨리, 천천히

MAJ - 접속 부사

문장이나 단어를 연결하는 부사

그러나, 그리고, 하지만, 그래서

독립언

IC - 감탄사

감정이나 의지를 나타내는 말

아, 어머, 네, 아니요

조사

JKS - 주격 조사

이, 가

JKC - 보격 조사

이, 가 (서술격 조사 '이다' 앞)

JKG - 관형격 조사

JKO - 목적격 조사

을, 를

JKB - 부사격 조사

에, 에서, 로, 으로, 와, 과

JKV - 호격 조사

아, 야, 여, 이여

JKQ - 인용격 조사

라고, 고

JX - 보조사

은, 는, 도, 만, 까지, 조차, 부터

JC - 접속 조사

와, 과, 하고, 이랑

어미

EP - 선어말 어미

-시-, -었-, -겠-

EF - 종결 어미

-다, -습니다, -ㅂ니다, -어요

EC - 연결 어미

-고, -면, -어서, -지만

ETN - 명사형 전성 어미

-ㅁ, -기

ETM - 관형형 전성 어미

-ㄴ, -는, -ㄹ

접사

XPN - 체언 접두사

풋-, 늦-, 햇-

XSN - 명사 파생 접미사

-님, -적, -화

XSV - 동사 파생 접미사

-하-, -되-, -시키-

XSA - 형용사 파생 접미사

-롭-, -스럽-

XR - 어근

단독으로 쓰이지 못하는 형태소

부호

SF - 마침표, 물음표, 느낌표

. ? !

SP - 쉼표, 가운뎃점, 콜론, 빗금

, · : /

SS - 따옴표, 괄호, 줄표

" ' ( ) [ ] -

SE - 줄임표

...

SO - 붙임표 (물결, 숨김 등)

~

SW - 기타 기호

SL - 외국어

API, GPU, AI

SH - 한자

韓國, 人工

SN - 숫자

123, 456

복합 태그

분석 결과에서 +로 연결된 복합 태그가 나타날 수 있습니다:

세요 -> EP+EF (선어말어미 + 종결어미)

태그 필터링

명사 필터

#![allow(unused)]
fn main() {
let nouns: Vec<_> = tokens.iter()
    .filter(|t| t.pos.starts_with("NN"))
    .collect();
}

동사/형용사 필터

#![allow(unused)]
fn main() {
let verbs: Vec<_> = tokens.iter()
    .filter(|t| t.pos.starts_with("VV") || t.pos.starts_with("VA"))
    .collect();
}

내용어(실질 형태소) 필터

#![allow(unused)]
fn main() {
let content_words: Vec<_> = tokens.iter()
    .filter(|t| {
        t.pos.starts_with("NN") ||  // 명사
        t.pos.starts_with("VV") ||  // 동사
        t.pos.starts_with("VA") ||  // 형용사
        t.pos.starts_with("MA")     // 부사
    })
    .collect();
}

다른 시스템과의 매핑

MeCab-KoKiwi설명
NNGNNG일반 명사
NNPNNP고유 명사
VVVV동사
VAVA형용사
JKSJKS주격 조사

참고 자료

MeCab-Ko-Dic 사전 포맷 분석 v2.0

문서 버전: 2.1 작성일: 2026-01-05 대상: mecab-ko-dic (https://bitbucket.org/eunjeon/mecab-ko-dic) 구현체: mecab-ko-dict Rust crate

이 문서는 DIC-001 이슈의 산출물로, mecab-ko-dic의 구조와 데이터 포맷을 완전히 분석하고 문서화합니다. Rust 구현체(mecab-ko-dict crate)의 데이터 구조와 API도 함께 설명합니다.


목차

  1. 저장소 구조
  2. CSV 사전 파일 구조
  3. 품사 태그 체계
  4. 정의 파일 분석
  5. 바이너리 사전 포맷
  6. 빌드 프로세스
  7. Rust 구현체 데이터 구조

1. 저장소 구조

mecab-ko-dic/
├── seed/                    # 원본 CSV 사전 및 정의 파일
│   ├── *.csv               # 품사별 단어 사전 (33개 파일)
│   ├── char.def            # 문자 카테고리 정의
│   ├── unk.def             # 미등록어 처리 정의
│   ├── feature.def         # CRF 피처 템플릿
│   ├── rewrite.def         # 피처 재작성 규칙
│   ├── pos-id.def          # 품사 ID 매핑
│   ├── dicrc               # 사전 설정 파일
│   ├── build.sh            # 빌드 스크립트
│   └── corpus/             # 학습용 말뭉치
│       └── eunjeon_corpus.txt
├── final/                  # 최종 배포용 사전
│   ├── Makefile.am         # Automake 설정
│   ├── configure.ac        # Autoconf 설정
│   ├── autogen.sh          # 자동 빌드 스크립트
│   ├── tools/              # 유틸리티 스크립트
│   │   └── add-userdic.sh  # 사용자 사전 추가
│   └── user-dic/           # 사용자 사전 CSV
└── utils/                  # Python 유틸리티

2. CSV 사전 파일 구조

2.1 파일 목록 (총 33개)

카테고리파일품사크기
명사NNG.csv일반 명사12.4 MB
NNP.csv고유 명사173 KB
NNB.csv의존 명사4.4 KB
NNBC.csv단위 의존 명사26 KB
NP.csv대명사12 KB
NR.csv수사18 KB
용언VV.csv동사290 KB
VA.csv형용사94 KB
VX.csv보조 용언4 KB
VCP.csv긍정 지정사 (이다)288 B
VCN.csv부정 지정사 (아니다)258 B
수식언MAG.csv일반 부사711 KB
MAJ.csv접속 부사10 KB
MM.csv관형사20 KB
조사J.csv조사 (JKS, JKO 등)14 KB
어미EC.csv연결 어미96 KB
EF.csv종결 어미70 KB
EP.csv선어말 어미1.6 KB
ETM.csv관형형 전성 어미5 KB
ETN.csv명사형 전성 어미456 B
접사/어근XPN.csv접두사2.4 KB
XSN.csv명사파생 접미사3.8 KB
XSA.csv형용사파생 접미사642 B
XSV.csv동사파생 접미사738 B
XR.csv어근129 KB
기타IC.csv감탄사50 KB
Inflect.csv활용형3.6 MB
Group.csv복합어/그룹228 KB
Hanja.csv한자어4.6 MB
Foreign.csv외래어530 KB
Wikipedia.csv위키피디아1.8 MB
CoinedWord.csv신조어5.7 KB
NorthKorea.csv북한어108 B

2.2 CSV 컬럼 구조 (12 컬럼, TSV 형식)

표층형	좌문맥ID	우문맥ID	비용	품사	의미분류	종성	읽기	타입	첫품사	끝품사	분석결과
#필드명설명예시
1surface표층형 (실제 단어)가건물
2left_id좌측 문맥 ID0
3right_id우측 문맥 ID0
4cost단어 비용0
5pos품사 태그NNG
6semantic_class의미 분류*
7jongseong종성 유무 (T/F)T
8reading읽기/기본형가건물
9type타입Compound, Inflect, *
10first_pos첫 형태소 품사*
11last_pos끝 형태소 품사*
12expression형태소 분해가/NNG/*+건물/NNG/*

2.3 종성(받침) 플래그

7번 컬럼의 T/F 값은 한국어 조사 연결에 중요:

  • T: 종성 있음 (예: 책, 강) → "은", "을" 연결
  • F: 종성 없음 (예: 나, 사과) → "는", "를" 연결

2.4 복합어/활용형 표현

12번 컬럼의 형태소 분해 형식:

형태소/품사/속성+형태소/품사/속성+...

예시:

# 복합어 (Compound)
세종시,0,0,0,NNP,지명,F,세종시,Compound,*,*,세종/NNP/지명+시/NNG/*

# 활용형 (Inflect)
위한,0,0,0,VV+ETM,*,T,위한,Inflect,VV,ETM,위하/VV/*+ᆫ/ETM/*
입니다,0,0,0,VCP+EF,*,F,입니다,Inflect,VCP,EF,이/VCP/*+ᄇ니다/EF/*

3. 품사 태그 체계

3.1 체언 (명사류)

태그명칭설명예시
NNG일반 명사General Noun사과, 컴퓨터
NNP고유 명사Proper Noun서울, 삼성
NNB의존 명사Dependent Noun것, 수, 바
NNBC단위 의존 명사Counter Noun개, 명, 원
NP대명사Pronoun나, 너, 그것
NR수사Numeral하나, 둘, 첫째

3.2 용언 (동사/형용사류)

태그명칭설명예시
VV동사Verb가다, 먹다
VA형용사Adjective예쁘다, 크다
VX보조 용언Auxiliary Verb있다, 하다
VCP긍정 지정사Positive Copula이다
VCN부정 지정사Negative Copula아니다

3.3 수식언

태그명칭설명예시
MM관형사Determiner이, 그, 새
MAG일반 부사Adverb매우, 아주
MAJ접속 부사Conjunctive Adverb그러나, 그리고

3.4 관계언 (조사)

태그명칭설명예시
JKS주격 조사Nominative이/가
JKC보격 조사Complementizer이/가
JKG관형격 조사Genitive
JKO목적격 조사Accusative을/를
JKB부사격 조사Adverbial에, 에서, 로
JKV호격 조사Vocative아/야
JKQ인용격 조사Quotative라고, 고
JX보조사Auxiliary은/는, 도, 만
JC접속 조사Conjunctive와/과, 하고

3.5 어미

태그명칭설명예시
EP선어말 어미Pre-Final Ending시, 았/었
EF종결 어미Final Ending다, 요, 니까
EC연결 어미Connective Ending고, 면, 어서
ETN명사형 전성 어미Nominal Ending기, 음
ETM관형형 전성 어미Adnominal Endingㄴ, 는, ㄹ

3.6 접사/어근

태그명칭설명예시
XPN체언 접두사Noun Prefix풋-, 헛-
XSN명사파생 접미사Noun Suffix-님, -질
XSV동사파생 접미사Verb Suffix-하다, -되다
XSA형용사파생 접미사Adj Suffix-스럽다, -롭다
XR어근Root깨끗, 착하

3.7 기호/특수

태그명칭설명
SF마침표/물음표/느낌표. ? !
SE줄임표
SSO여는 괄호( [ {
SSC닫는 괄호) ] }
SC쉼표/콜론/빗금, : /
SY기타 기호@ # $
SL외국어English, 日本語
SH한자韓國
SN숫자123, 45.6
SP공백(space)

4. 정의 파일 분석

4.1 char.def (문자 카테고리 정의)

형식

CATEGORY_NAME  INVOKE  GROUP  LENGTH
0xHHHH..0xJJJJ CATEGORY [CATEGORY2...]

mecab-ko-dic 카테고리

카테고리INVOKEGROUPLENGTH설명
DEFAULT010기본
SPACE010공백
HANGUL012한글
HANJA001한자
ALPHA110알파벳
NUMERIC110숫자
SYMBOL110기호
HANJANUMERIC110한자 숫자

속성 설명

속성의미
INVOKE0사전에 있으면 미등록어 처리 생략
INVOKE1항상 미등록어 후보도 생성
GROUP0그룹핑 비활성화
GROUP1동일 카테고리 문자 그룹핑
LENGTHn1~n 길이의 미등록어 후보 생성

한글 유니코드 범위

0xAC00..0xD7A3  HANGUL     # 한글 음절 (가~힣, 11,172자)
0x1100..0x11FF  HANGUL     # 한글 자모
0x3130..0x318F  HANGUL     # 한글 호환 자모 (ㄱ~ㅎ, ㅏ~ㅣ)

4.2 unk.def (미등록어 정의)

형식

CATEGORY,left_id,right_id,cost,POS,semantic,jongseong,reading,type,first,last,expr

mecab-ko-dic 미등록어 매핑

카테고리품사설명
DEFAULTSY기본 → 기호
SPACESP공백
HANGULUNKNOWN한글 미등록어
HANJASH한자
ALPHASL외국어
NUMERICSN숫자
SYMBOLSY기호
HIRAGANASL외국어
KATAKANASL외국어

4.3 matrix.def (연접 비용 행렬)

형식

<left_size> <right_size>
<right_id> <left_id> <cost>
...

비용 의미

비용 범위의미
음수 (예: -16,124)자연스러운 연결 (선호)
0중립
양수 (예: 5,824)부자연스러운 연결 (비선호)

Viterbi 비용 계산

총 비용 = matrix[lNode.rcAttr + lsize * rNode.lcAttr] + rNode.wcost + space_penalty

4.4 dicrc (사전 설정)

cost-factor = 800                          # 비용 스케일 팩터
bos-feature = BOS/EOS,*,*,*,*,*,*,*        # 문장 시작/끝 피처
eval-size = 4                              # 평가할 피처 수
config-charset = UTF-8                     # 문자셋

# 한국어 특화: 공백 페널티
left-space-penalty-factor = 100,3000,120,6000,172,3000,183,3000,...

5. 바이너리 사전 포맷

5.1 생성 파일 목록

파일설명생성 도구
sys.dic시스템 사전 (DA Trie + 토큰)mecab-dict-index
unk.dic미등록어 사전mecab-dict-index
matrix.bin연접 비용 행렬mecab-dict-index
char.bin문자 카테고리 맵mecab-dict-index
model.binCRF 모델 (선택)mecab-dict-index

5.2 sys.dic 바이너리 구조

[헤더 - 40바이트]
├── magic (4B)           # 매직 넘버
├── version (4B)         # 사전 버전
├── type (4B)            # SYS=0, UNK=1, USR=2
├── lexsize (4B)         # 어휘 항목 수
├── lsize (4B)           # 좌문맥 크기
├── rsize (4B)           # 우문맥 크기
├── dsize (4B)           # Double-Array 크기
├── tsize (4B)           # 토큰 배열 크기
├── fsize (4B)           # 피처 문자열 크기
├── dummy (4B)           # 예약
└── charset[32]          # 문자셋

[Double-Array Trie]
└── Darts 라이브러리 형식

[토큰 배열]
└── Token[] {
    lcAttr: u16      # 좌문맥 ID
    rcAttr: u16      # 우문맥 ID
    posid: u16       # 품사 ID
    wcost: i16       # 단어 비용
    feature: u32     # 피처 오프셋
    compound: u32    # 복합어 정보
}

[피처 문자열]
└── NULL 종료 문자열들

5.3 matrix.bin 바이너리 구조

[헤더]
├── lsize (2B)           # 좌측 크기
└── rsize (2B)           # 우측 크기

[연접 비용 행렬]
└── short[lsize * rsize] # 비용 값 배열

5.4 char.bin 바이너리 구조

[헤더]
└── category_count (4B)  # 카테고리 개수

[카테고리 이름]
└── char[32] * N         # 각 카테고리 이름

[CharInfo 테이블]
└── CharInfo[0xFFFF] {   # 모든 UCS-2 코드포인트
    type: 18 bits        # 카테고리 비트마스크
    default_type: 8 bits # 기본 카테고리 ID
    length: 4 bits       # LENGTH 값
    group: 1 bit         # GROUP 플래그
    invoke: 1 bit        # INVOKE 플래그
}

6. 빌드 프로세스

6.1 빌드 파이프라인

[seed/*.csv] ──────────────────────────────────────────────┐
      │                                                     │
      ├── mecab-dict-index -p  ───→ [left-id.def, right-id.def]
      │                                                     │
[corpus.txt] ── mecab-cost-train ──→ [model.bin]           │
      │                                                     │
      └── mecab-dict-gen ─────────→ [*.csv with costs]     │
                                          │                 │
                                          ▼                 │
                               [비용 조정 스크립트]          │
                                          │                 │
                                          ▼                 ▼
                               mecab-dict-index ───→ [sys.dic, unk.dic,
                                                      matrix.bin, char.bin]

6.2 빌드 스크립트 (seed/build.sh)

# 1단계: 품사 ID 할당
$DICT_INDEX -p -d . -c UTF-8 -t UTF-8 -f UTF-8

# 2단계: CRF 모델 학습
$COST_TRAIN -p ${cpu_count} -c 1.0 ${corpus_file} ${model_file}

# 3단계: 사전 생성 (비용 자동 할당)
$DICT_GEN -o ../final -m $model_file

# 4단계: 수동 비용 조정
./change_word_cost.sh
./change_connection_cost.sh

# 5단계: 바이너리 컴파일
cd ../final && ./configure && make

6.3 도구 목록

도구용도
mecab-dict-index사전 컴파일
mecab-dict-gen비용 자동 생성
mecab-cost-trainCRF 모델 학습
autoconf/automake빌드 시스템

6.4 사용자 사전 추가

# final/tools/add-userdic.sh
$DICT_INDEX \
    -m ${DIC_PATH}/model.def \
    -d ${DIC_PATH} \
    -u ${DIC_PATH}/user-custom.dic \
    -f utf-8 -t utf-8 \
    -a user-dic/custom.csv

부록: 한국어 특화 기능

A. mecab-ko의 left-space-penalty

띄어쓰기 앞의 형태소에 페널티를 부여하여 한국어 띄어쓰기 특성 반영:

품사ID 120 (조사) → 좌측 공백 시 6000 비용 추가
품사ID 172 (어미) → 좌측 공백 시 3000 비용 추가

B. 종성 기반 조사 연결

선행 종성조사 형태
T (받침 있음)은, 을, 이, 과
F (받침 없음)는, 를, 가, 와

C. 수동 연접 비용 조정 예시

# change_connection_cost.txt
JX,*,T,는|JKO,*,을 10000     # "는을" 방지
JX,*,T,은|JX,*,은 10000      # "은은" 방지
JKG,*,F,의|BOS/EOS,*,* 10000 # "의"로 시작 방지

7. Rust 구현체 데이터 구조

7.1 Entry 구조체

mecab-ko-dict crate에서 정의한 사전 엔트리 구조:

#![allow(unused)]
fn main() {
/// 사전 엔트리
/// 파일 위치: rust/crates/mecab-ko-dict/src/lib.rs
#[derive(Debug, Clone, PartialEq)]
pub struct Entry {
    /// 표면형 - 사전에 등록된 단어
    pub surface: String,

    /// 좌문맥 ID (left context ID)
    /// 연접 비용 계산 시 현재 노드의 좌측 ID로 사용
    pub left_id: u16,

    /// 우문맥 ID (right context ID)
    /// 연접 비용 계산 시 이전 노드의 우측 ID로 사용
    pub right_id: u16,

    /// 비용 (word cost)
    /// 단어 생성 비용, 낮을수록 우선 선택
    pub cost: i16,

    /// 품사 정보 (feature string)
    /// CSV의 5~12번째 필드를 콤마로 연결한 문자열
    /// 형식: "품사태그,의미부류,종성유무,읽기,타입,첫품사,끝품사,분석결과"
    pub feature: String,
}
}

feature 필드 상세

feature 필드는 8개의 서브필드로 구성됩니다:

인덱스필드명설명예시
0품사태그품사 정보NNG, VV+EC
1의미부류의미 분류*, 지명
2종성유무받침 여부T, F
3읽기발음/원형가방
4타입엔트리 타입*, Compound, Inflect, Preanalysis
5첫품사복합 형태소 시작 품사*, VV
6끝품사복합 형태소 종료 품사*, EC
7분석결과형태소 분해가깝/VA/*+아/EC/*

feature 예시

# 일반명사
NNG,*,T,가방,*,*,*,*

# 복합어 (Compound)
NNG,*,F,가가대소,Compound,*,*,가가/NNG/*+대소/NNG/*

# 활용형 (Inflect)
VA+EC,*,F,가까와,Inflect,VA,EC,가깝/VA/*+아/EC/*

# 고유명사 (지명)
NNP,지명,F,세종시,Compound,*,*,세종/NNP/지명+시/NNG/*

7.2 UserEntry 구조체

사용자 정의 사전용 엔트리:

#![allow(unused)]
fn main() {
/// 사용자 사전 엔트리
/// 파일 위치: rust/crates/mecab-ko-dict/src/user_dict.rs
#[derive(Debug, Clone, PartialEq)]
pub struct UserEntry {
    /// 표면형
    pub surface: String,
    /// 좌문맥 ID
    pub left_id: u16,
    /// 우문맥 ID
    pub right_id: u16,
    /// 비용 (낮을수록 우선)
    pub cost: i16,
    /// 품사 태그
    pub pos: String,
    /// 읽기 (발음)
    pub reading: Option<String>,
    /// 원형 (기본형)
    pub lemma: Option<String>,
}
}

7.3 Matrix 구조체

연접 비용 행렬 구현:

#![allow(unused)]
fn main() {
/// 밀집 연접 비용 행렬 (Dense Matrix)
/// 파일 위치: rust/crates/mecab-ko-dict/src/matrix.rs
#[derive(Debug, Clone)]
pub struct DenseMatrix {
    /// 좌문맥 크기
    lsize: usize,
    /// 우문맥 크기
    rsize: usize,
    /// 비용 배열 (row-major: costs[right_id + lsize * left_id])
    costs: Vec<i16>,
}
}

Matrix 인터페이스

#![allow(unused)]
fn main() {
/// 연접 비용 행렬 인터페이스
pub trait Matrix {
    /// 연접 비용 조회
    /// right_id: 이전 노드의 우문맥 ID
    /// left_id: 현재 노드의 좌문맥 ID
    fn get(&self, right_id: u16, left_id: u16) -> i32;

    /// 좌문맥 크기
    fn left_size(&self) -> usize;

    /// 우문맥 크기
    fn right_size(&self) -> usize;
}
}

사용 예시

#![allow(unused)]
fn main() {
use mecab_ko_dict::matrix::{DenseMatrix, Matrix, MatrixLoader};

// 텍스트 파일에서 로드
let matrix = DenseMatrix::from_def_file("matrix.def")?;

// 바이너리 파일에서 로드
let matrix = DenseMatrix::from_bin_file("matrix.bin")?;

// 자동 포맷 감지 로드
let matrix = MatrixLoader::load("matrix.def")?;

// 연접 비용 조회
let cost = matrix.get(right_id, left_id);
println!("Connection cost: {}", cost);
}

7.4 CharCategoryDef 구조체

문자 카테고리 정의:

#![allow(unused)]
fn main() {
/// 문자 카테고리 정의
/// 파일 위치: rust/crates/mecab-ko-core/src/unknown.rs
#[derive(Debug, Clone)]
pub struct CharCategoryDef {
    /// 카테고리 이름 (예: "HANGUL", "ALPHA")
    pub name: String,
    /// 카테고리 ID (0-255)
    pub id: CategoryId,
    /// INVOKE 플래그: 항상 미등록어 후보 생성 여부
    pub invoke: bool,
    /// GROUP 플래그: 동일 카테고리 문자 그룹핑 여부
    pub group: bool,
    /// LENGTH: 미등록어 후보 최대 길이 (0이면 제한 없음)
    pub length: usize,
}
}

7.5 UnknownDef 구조체

미등록어 정의:

#![allow(unused)]
fn main() {
/// 미등록어 정의
/// 파일 위치: rust/crates/mecab-ko-core/src/unknown.rs
#[derive(Debug, Clone)]
pub struct UnknownDef {
    /// 적용 카테고리 ID
    pub category_id: CategoryId,
    /// 좌문맥 ID
    pub left_id: u16,
    /// 우문맥 ID
    pub right_id: u16,
    /// 단어 비용
    pub cost: i16,
    /// 품사 태그
    pub pos: String,
    /// 피처 문자열 (품사 정보 전체)
    pub feature: String,
}
}

7.6 Trie 구조체

사전 검색용 Double-Array Trie:

#![allow(unused)]
fn main() {
/// Double-Array Trie
/// 파일 위치: rust/crates/mecab-ko-dict/src/trie.rs
pub struct Trie<'a> {
    /// 내부 Double-Array (yada 라이브러리)
    da: DoubleArray<Cow<'a, [u8]>>,
}

impl<'a> Trie<'a> {
    /// 정확히 일치하는 키 검색
    pub fn exact_match(&self, key: &str) -> Option<u32>;

    /// 공통 접두사 검색 (형태소 후보 탐색)
    pub fn common_prefix_search<'b>(
        &'b self,
        text: &'b str,
    ) -> impl Iterator<Item = (u32, usize)> + 'b;
}
}

7.7 Matrix 타입 비교

타입설명메모리사용 시나리오
DenseMatrix전체 행렬을 메모리에 저장O(lsize * rsize * 2)일반적 사용
SparseMatrixHashMap으로 희소 엔트리만 저장O(n * 10) (n=엔트리 수)대부분이 기본값인 경우
MmapMatrix메모리 맵 방식디스크에서 직접 접근대용량 사전, 멀티프로세스

7.8 바이너리 포맷 상세

matrix.bin 바이너리 포맷 (Rust 구현체)

[헤더 - 4바이트]
├── lsize (u16, little-endian)   # 좌문맥 크기
└── rsize (u16, little-endian)   # 우문맥 크기

[데이터]
└── costs[lsize * rsize] (i16, little-endian)  # 비용 배열

접근 공식:

index = right_id + lsize * left_id
cost = costs[index]

Trie 바이너리 포맷

yada 라이브러리의 Double-Array Trie 직렬화 형식을 사용합니다. zstd 압축 지원 (*.zst 확장자).


8. API 사용 예제

8.1 사전 엔트리 생성

#![allow(unused)]
fn main() {
use mecab_ko_dict::Entry;

let entry = Entry {
    surface: "안녕".to_string(),
    left_id: 1,
    right_id: 1,
    cost: 100,
    feature: "NNG,*,T,안녕,*,*,*,*".to_string(),
};
}

8.2 사용자 사전 사용

#![allow(unused)]
fn main() {
use mecab_ko_dict::user_dict::{UserDictionary, UserDictionaryBuilder};

// 빌더 패턴
let dict = UserDictionaryBuilder::new()
    .default_cost(-1000)
    .add("딥러닝", "NNG")
    .add_with_cost("머신러닝", "NNG", -500)
    .add_full("챗GPT", "NNP", -1000, Some("챗지피티"))
    .build();

// 직접 추가
let mut dict = UserDictionary::new();
dict.add_entry("클로드", "NNP", Some(-1000), None);

// CSV 파일에서 로드
dict.load_from_csv("user-dict.csv")?;

// 검색
let entries = dict.lookup("딥러닝");
}

8.3 연접 비용 행렬 사용

#![allow(unused)]
fn main() {
use mecab_ko_dict::matrix::{DenseMatrix, Matrix};

// 로드
let matrix = DenseMatrix::from_def_file("matrix.def")?;

// "나는" = "나/NP" + "는/JX" 연접 비용 계산
// NP의 right_id가 154, JX의 left_id가 120이라고 가정
let connection_cost = matrix.get(154, 120);

// 총 비용 계산
let total_cost = prev_node_cost + connection_cost + current_word_cost;
}

8.4 미등록어 처리

#![allow(unused)]
fn main() {
use mecab_ko_core::unknown::UnknownHandler;

let handler = UnknownHandler::korean_default();

// 미등록어 후보 생성
let candidates = handler.generate_candidates("테스트ABC", 0, false);

for candidate in candidates {
    println!("{}: {} (cost: {})",
        candidate.surface,
        candidate.pos,
        candidate.cost
    );
}
}

참고 자료


변경 이력

버전날짜변경 내용
v2.12026-01-05Rust 구현체 Entry/Matrix/Trie 구조체 문서 추가
v2.02026-01-04사전 포맷 완전 분석 및 문서화
v1.0-초기 mecab-ko-dic 포맷

바이너리 사전 포맷 v3.0 설계 명세서

문서 버전: 3.0 작성일: 2026-01-05 이슈: DIC-010 바이너리 사전 포맷 v3.0 설계 상태: Draft


목차

  1. 개요
  2. 설계 목표
  3. 파일 구조
  4. 공통 헤더 구조
  5. Double-Array Trie 바이너리 포맷
  6. Connection Matrix 바이너리 포맷
  7. 엔트리 데이터 포맷
  8. 압축 지원
  9. 메모리 매핑 지원
  10. 버전 관리 메커니즘
  11. 사용자 사전 포맷
  12. 구현 참조

1. 개요

1.1 문서 목적

이 문서는 MeCab-Ko Rust 구현을 위한 바이너리 사전 포맷 v3.0을 정의합니다. 기존 MeCab 바이너리 포맷과의 차이점을 명시하고, Rust 에코시스템에 최적화된 새로운 포맷을 설계합니다.

1.2 기존 포맷과의 비교

항목MeCab 기존 포맷v3.0 신규 포맷
Trie 구현Darts (C++)yada (Rust)
직렬화커스텀 바이너리bincode + 옵션
압축미지원Zstd 지원
메모리 매핑부분 지원완전 지원 (memmap2)
바이트 순서플랫폼 의존Little-Endian 고정
버전 관리단순 버전 번호Semantic Versioning + 호환성 체크

1.3 관련 코드 파일

  • /home/mare/mecab-ko/rust/crates/mecab-ko-dict/src/trie.rs - Double-Array Trie 구현
  • /home/mare/mecab-ko/rust/crates/mecab-ko-dict/src/matrix.rs - Connection Matrix 구현
  • /home/mare/mecab-ko/rust/crates/mecab-ko-dict/src/user_dict.rs - 사용자 사전 구현
  • /home/mare/mecab-ko/rust/crates/mecab-ko-dict/src/lib.rs - 라이브러리 인터페이스

2. 설계 목표

2.1 핵심 목표

  1. 빠른 로딩: 메모리 매핑을 통한 지연 로딩 지원
  2. 효율적 압축: Zstd 압축으로 디스크 공간 절약 (30-50% 크기 감소 목표)
  3. 메모리 효율성: Zero-copy 접근 가능한 구조
  4. 하위 호환성: 버전 업그레이드 시 graceful degradation
  5. 크로스 플랫폼: Little-Endian 고정으로 플랫폼 독립성 보장

2.2 비기능 요구사항

요구사항목표
사전 로딩 시간 (mmap)< 10ms
사전 로딩 시간 (전체)< 100ms
압축률 (Zstd level 3)30-50%
메모리 오버헤드< 5%

3. 파일 구조

3.1 사전 디렉토리 레이아웃

dict/
├── mecab-ko-dict.meta       # 메타데이터 (JSON)
├── sys.dic                  # 시스템 사전 (Trie + 엔트리)
├── sys.dic.zst              # 압축된 시스템 사전 (선택)
├── matrix.bin               # 연접 비용 행렬
├── matrix.bin.zst           # 압축된 연접 비용 행렬 (선택)
├── char.bin                 # 문자 카테고리 정의
├── unk.bin                  # 미등록어 정의
└── user/                    # 사용자 사전 디렉토리
    ├── user.dic             # 사용자 사전
    └── *.csv                # 사용자 사전 CSV 원본

3.2 파일별 역할

파일용도필수압축 가능
mecab-ko-dict.meta사전 메타정보OX
sys.dic시스템 사전OO
matrix.bin연접 비용 행렬OO
char.bin문자 카테고리OX
unk.bin미등록어 정의OX
user.dic사용자 사전XO

4. 공통 헤더 구조

4.1 파일 헤더 (모든 바이너리 파일 공통)

┌────────────────────────────────────────────────────┐
│                   File Header (64 bytes)            │
├────────────────────────────────────────────────────┤
│ Offset │ Size │ Type    │ Field          │ Value   │
├────────┼──────┼─────────┼────────────────┼─────────┤
│ 0x00   │ 4    │ [u8; 4] │ magic          │ "MKOD" │
│ 0x04   │ 1    │ u8      │ version_major  │ 3       │
│ 0x05   │ 1    │ u8      │ version_minor  │ 0       │
│ 0x06   │ 1    │ u8      │ version_patch  │ 0       │
│ 0x07   │ 1    │ u8      │ flags          │ 비트맵   │
│ 0x08   │ 4    │ u32     │ file_type      │ 파일 타입│
│ 0x0C   │ 4    │ u32     │ checksum       │ CRC32   │
│ 0x10   │ 8    │ u64     │ data_size      │ 데이터크기│
│ 0x18   │ 8    │ u64     │ entry_count    │ 엔트리 수│
│ 0x20   │ 32   │ [u8;32] │ reserved       │ 예약     │
└────────────────────────────────────────────────────┘

4.2 Magic Number

"MKOD" = MeCab-Ko Dictionary (0x4D 0x4B 0x4F 0x44)

4.3 Flags 비트맵

Bit 0: 압축 여부 (0: 미압축, 1: 압축)
Bit 1: 압축 알고리즘 (0: Zstd, 1: 예약)
Bit 2: Endianness (0: Little, 1: Big) - 현재 항상 0
Bit 3: 메모리 맵 최적화 (0: 미적용, 1: 적용)
Bit 4-7: 예약

4.4 File Type 코드

코드타입설명
0x01SYS_DICT시스템 사전
0x02USER_DICT사용자 사전
0x03MATRIX연접 비용 행렬
0x04CHAR_DEF문자 카테고리
0x05UNK_DEF미등록어 정의

4.5 Rust 구조체 정의

#![allow(unused)]
fn main() {
/// 파일 헤더 (64바이트)
#[repr(C, packed)]
pub struct FileHeader {
    /// 매직 넘버 "MKOD"
    pub magic: [u8; 4],
    /// 버전 (major.minor.patch)
    pub version_major: u8,
    pub version_minor: u8,
    pub version_patch: u8,
    /// 플래그 비트맵
    pub flags: u8,
    /// 파일 타입
    pub file_type: u32,
    /// 체크섬 (CRC32)
    pub checksum: u32,
    /// 데이터 크기 (헤더 제외)
    pub data_size: u64,
    /// 엔트리 수
    pub entry_count: u64,
    /// 예약 공간
    pub reserved: [u8; 32],
}
}

5. Double-Array Trie 바이너리 포맷

5.1 개요

Double-Array Trie는 yada 라이브러리를 사용하여 구현됩니다. 이 섹션은 yada가 생성하는 바이너리 포맷과 이를 사전 파일에 통합하는 방법을 정의합니다.

5.2 yada 라이브러리 바이너리 포맷

yada 라이브러리는 다음과 같은 내부 바이너리 구조를 사용합니다:

┌──────────────────────────────────────────────────────────┐
│                 yada Double-Array 구조                    │
├──────────────────────────────────────────────────────────┤
│ [u32 array]                                               │
│ ├── base[0], check[0]  - interleaved                     │
│ ├── base[1], check[1]                                    │
│ ├── ...                                                   │
│ └── base[n], check[n]                                    │
└──────────────────────────────────────────────────────────┘

5.3 Trie 파일 구조 (sys.dic)

┌────────────────────────────────────────────────────────────┐
│                    sys.dic 파일 구조                        │
├────────────────────────────────────────────────────────────┤
│ [File Header]           64 bytes                           │
├────────────────────────────────────────────────────────────┤
│ [Trie Section Header]   16 bytes                           │
│ ├── trie_size: u64      Trie 바이트 크기                    │
│ └── entry_offset: u64   엔트리 섹션 오프셋                   │
├────────────────────────────────────────────────────────────┤
│ [Trie Data]             Variable                           │
│ └── yada Double-Array 바이너리                              │
├────────────────────────────────────────────────────────────┤
│ [Entry Section Header]  16 bytes                           │
│ ├── entry_count: u64    엔트리 수                           │
│ └── feature_offset: u64 피처 문자열 오프셋                   │
├────────────────────────────────────────────────────────────┤
│ [Entry Array]           Variable                           │
│ └── Entry[] (고정 크기)                                     │
├────────────────────────────────────────────────────────────┤
│ [Feature Strings]       Variable                           │
│ └── NULL 종료 UTF-8 문자열들                                │
└────────────────────────────────────────────────────────────┘

5.4 Trie 빌드 및 로드 API

현재 구현된 API (trie.rs 기반):

#![allow(unused)]
fn main() {
/// Trie 빌드
pub fn build(entries: &[(&str, u32)]) -> Result<Vec<u8>> {
    // 정렬된 키-값 쌍에서 Trie 빌드
    DoubleArrayBuilder::build(&keyset)
}

/// Trie 로드 (메모리에서)
pub fn new(bytes: &'a [u8]) -> Self {
    Self {
        da: DoubleArray::new(Cow::Borrowed(bytes)),
    }
}

/// 파일에서 로드
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Trie<'static>> {
    let bytes = std::fs::read(path)?;
    Ok(Self::from_vec(bytes))
}

/// 압축 파일에서 로드 (Zstd)
pub fn from_compressed_file<P: AsRef<Path>>(path: P) -> Result<Trie<'static>> {
    let file = std::fs::File::open(path)?;
    let mut decoder = zstd::Decoder::new(file)?;
    let mut bytes = Vec::new();
    decoder.read_to_end(&mut bytes)?;
    Ok(Self::from_vec(bytes))
}
}

5.5 Trie 검색 API

#![allow(unused)]
fn main() {
/// 정확히 일치하는 키 검색
pub fn exact_match(&self, key: &str) -> Option<u32>;

/// 공통 접두사 검색 (형태소 분석의 핵심)
pub fn common_prefix_search<'b>(&'b self, text: &'b str)
    -> impl Iterator<Item = (u32, usize)> + 'b;

/// 특정 위치에서 공통 접두사 검색
pub fn common_prefix_search_at(&self, text: &str, start_byte: usize)
    -> Vec<(u32, usize)>;
}

6. Connection Matrix 바이너리 포맷

6.1 개요

연접 비용 행렬은 형태소 간 연결 비용을 저장합니다. v3.0에서는 세 가지 저장 전략을 지원합니다:

  1. DenseMatrix: 모든 값을 메모리에 저장 (기본)
  2. SparseMatrix: 희소 행렬 최적화
  3. MmapMatrix: 메모리 맵 기반 접근

6.2 Dense Matrix 바이너리 포맷

┌────────────────────────────────────────────────────────────┐
│                   matrix.bin 파일 구조                      │
├────────────────────────────────────────────────────────────┤
│ [File Header]           64 bytes (생략 가능)                │
├────────────────────────────────────────────────────────────┤
│ [Matrix Header]         4 bytes                            │
│ ├── lsize: u16          좌문맥 크기 (Little-Endian)         │
│ └── rsize: u16          우문맥 크기 (Little-Endian)         │
├────────────────────────────────────────────────────────────┤
│ [Cost Array]            lsize * rsize * 2 bytes            │
│ └── costs[i]: i16       연접 비용 (Little-Endian)           │
│     인덱스: right_id + lsize * left_id                      │
└────────────────────────────────────────────────────────────┘

6.3 비용 배열 접근

#![allow(unused)]
fn main() {
/// 비용 조회 (row-major order)
fn get(&self, right_id: u16, left_id: u16) -> i32 {
    let index = right_id as usize + self.lsize * left_id as usize;
    self.costs[index] as i32
}
}

6.4 현재 구현 (matrix.rs)

#![allow(unused)]
fn main() {
/// 밀집 연접 비용 행렬
pub struct DenseMatrix {
    lsize: usize,
    rsize: usize,
    costs: Vec<i16>,
}

/// 바이너리 직렬화
pub fn to_bin_bytes(&self) -> Vec<u8> {
    let mut buf = Vec::with_capacity(MATRIX_HEADER_SIZE + self.costs.len() * 2);
    buf.write_u16::<LittleEndian>(self.lsize as u16).ok();
    buf.write_u16::<LittleEndian>(self.rsize as u16).ok();
    for &cost in &self.costs {
        buf.write_i16::<LittleEndian>(cost).ok();
    }
    buf
}

/// 바이너리 역직렬화
pub fn from_bin_bytes(data: &[u8]) -> Result<Self> {
    let mut cursor = io::Cursor::new(data);
    let lsize = cursor.read_u16::<LittleEndian>()? as usize;
    let rsize = cursor.read_u16::<LittleEndian>()? as usize;
    // ... costs 읽기
}
}

6.5 메모리 맵 행렬

#![allow(unused)]
fn main() {
/// 메모리 맵 연접 비용 행렬
pub struct MmapMatrix {
    lsize: usize,
    rsize: usize,
    mmap: memmap2::Mmap,
}

impl MmapMatrix {
    /// 파일에서 메모리 맵으로 로드
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let file = std::fs::File::open(path)?;
        let mmap = unsafe { memmap2::Mmap::map(&file)? };
        // 헤더 읽기...
    }

    /// 비용 조회 (직접 mmap 접근)
    fn get(&self, right_id: u16, left_id: u16) -> i32 {
        let offset = MATRIX_HEADER_SIZE
            + (right_id as usize + self.lsize * left_id as usize) * 2;
        let bytes = [self.mmap[offset], self.mmap[offset + 1]];
        i16::from_le_bytes(bytes) as i32
    }
}
}

6.6 희소 행렬 (선택적 최적화)

희소 행렬은 대부분의 값이 기본값인 경우 메모리를 절약합니다:

#![allow(unused)]
fn main() {
pub struct SparseMatrix {
    lsize: usize,
    rsize: usize,
    default_cost: i16,
    entries: HashMap<usize, i16>,  // 비기본 값만 저장
}
}

희소도 기준: 기본값이 아닌 엔트리가 10% 미만일 때 권장

6.7 Matrix Trait

모든 행렬 구현이 따라야 하는 인터페이스:

#![allow(unused)]
fn main() {
pub trait Matrix {
    /// 연접 비용 조회
    fn get(&self, right_id: u16, left_id: u16) -> i32;

    /// 좌문맥 크기
    fn left_size(&self) -> usize;

    /// 우문맥 크기
    fn right_size(&self) -> usize;

    /// 전체 엔트리 수
    fn entry_count(&self) -> usize {
        self.left_size() * self.right_size()
    }
}
}

6.8 비용 값 범위

범위의미
i16::MIN (-32768)매우 자연스러운 연결
음수자연스러운 연결 (선호)
0중립
양수부자연스러운 연결 (비선호)
i16::MAX (32767)연결 불가능

7. 엔트리 데이터 포맷

7.1 사전 엔트리 구조

#![allow(unused)]
fn main() {
/// 사전 엔트리 (고정 크기 16바이트)
#[repr(C, packed)]
pub struct BinaryEntry {
    /// 좌문맥 ID
    pub left_id: u16,
    /// 우문맥 ID
    pub right_id: u16,
    /// 단어 비용
    pub cost: i16,
    /// 플래그 (종성 유무 등)
    pub flags: u16,
    /// 피처 문자열 오프셋
    pub feature_offset: u32,
    /// 피처 문자열 길이
    pub feature_length: u16,
    /// 예약
    pub reserved: u16,
}
}

7.2 플래그 비트맵

Bit 0: 종성 유무 (0: F, 1: T)
Bit 1-2: 타입 (00: 일반, 01: Compound, 10: Inflect, 11: 예약)
Bit 3-15: 예약

7.3 피처 문자열 포맷

품사,의미분류,종성,읽기,타입,첫품사,끝품사,분석결과

예시:

NNG,인사,T,안녕,*,*,*,*
VV+ETM,*,T,위한,Inflect,VV,ETM,위하/VV/*+ᆫ/ETM/*

8. 압축 지원

8.1 지원 압축 알고리즘

알고리즘플래그 값압축률속도권장 용도
Zstd0매우 높음빠름기본 권장
미압축-없음최고메모리 맵

8.2 Zstd 압축 설정

#![allow(unused)]
fn main() {
/// 압축 레벨 가이드라인
/// Level 1-3: 빠른 압축 (배포용)
/// Level 6-9: 균형 (일반용)
/// Level 19-22: 최대 압축 (아카이브용)

const DEFAULT_COMPRESSION_LEVEL: i32 = 3;

/// 압축 저장
pub fn save_to_compressed_file<P: AsRef<Path>>(
    bytes: &[u8],
    path: P,
    level: i32,
) -> Result<()> {
    let file = std::fs::File::create(path)?;
    let mut encoder = zstd::Encoder::new(file, level)?;
    encoder.write_all(bytes)?;
    encoder.finish()?;
    Ok(())
}
}

8.3 압축 파일 확장자 규칙

원본 파일압축 파일
sys.dicsys.dic.zst
matrix.binmatrix.bin.zst
user.dicuser.dic.zst

8.4 로딩 전략

압축 파일 존재 확인
    ↓
[압축 파일 있음] → Zstd 압축 해제 → 메모리 로드
    ↓
[압축 파일 없음] → 원본 파일 로드
    ↓
[메모리 맵 요청] → Mmap 생성 (압축 미지원)

9. 메모리 매핑 지원

9.1 개요

메모리 매핑(mmap)은 대용량 사전을 효율적으로 로드하는 방법입니다. 파일을 가상 메모리에 매핑하여 실제 접근 시에만 물리 메모리를 사용합니다.

9.2 memmap2 라이브러리 사용

#![allow(unused)]
fn main() {
use memmap2::Mmap;

/// 메모리 맵 생성
pub fn create_mmap(path: &Path) -> Result<Mmap> {
    let file = std::fs::File::open(path)?;
    // SAFETY: 파일이 외부에서 수정되지 않는다고 가정
    let mmap = unsafe { Mmap::map(&file)? };
    Ok(mmap)
}
}

9.3 메모리 맵 최적화 파일 구조

메모리 맵을 효율적으로 사용하려면 다음 조건을 만족해야 합니다:

  1. 정렬: 데이터 섹션이 페이지 경계(4KB)에 정렬
  2. 미압축: 압축된 파일은 mmap 불가
  3. 고정 크기: 가변 길이 데이터는 별도 섹션에 분리

9.4 MmapMatrix 구현 예시

#![allow(unused)]
fn main() {
impl MmapMatrix {
    /// 비용 오프셋 계산
    #[inline]
    fn offset(&self, right_id: u16, left_id: u16) -> usize {
        MATRIX_HEADER_SIZE
            + (right_id as usize + self.lsize * left_id as usize) * 2
    }

    /// 비용 조회 (zero-copy)
    fn get(&self, right_id: u16, left_id: u16) -> i32 {
        let offset = self.offset(right_id, left_id);
        if offset + 2 <= self.mmap.len() {
            let bytes = [self.mmap[offset], self.mmap[offset + 1]];
            i16::from_le_bytes(bytes) as i32
        } else {
            INVALID_CONNECTION_COST
        }
    }
}
}

9.5 사용 권장 사항

시나리오권장 로딩 방식
서버 (다중 프로세스)MmapMatrix (메모리 공유)
CLI (단일 실행)DenseMatrix (전체 로드)
임베디드 (메모리 제한)MmapMatrix + 압축 해제 캐시
WebAssemblyDenseMatrix (mmap 미지원)

10. 버전 관리 메커니즘

10.1 Semantic Versioning

Major.Minor.Patch
  │     │     │
  │     │     └── 버그 수정 (하위 호환)
  │     └────── 기능 추가 (하위 호환)
  └──────────── 호환성 변경 (하위 비호환 가능)

10.2 호환성 규칙

버전 변경읽기 가능쓰기 가능
Major 증가X (마이그레이션 필요)X
Minor 증가O (기존 필드만)O (새 필드 무시)
Patch 증가OO

10.3 버전 검증 코드

#![allow(unused)]
fn main() {
const CURRENT_VERSION: (u8, u8, u8) = (3, 0, 0);

pub fn validate_version(header: &FileHeader) -> Result<()> {
    let file_version = (
        header.version_major,
        header.version_minor,
        header.version_patch,
    );

    // Major 버전 불일치 시 에러
    if file_version.0 != CURRENT_VERSION.0 {
        return Err(DictError::Version {
            expected: format!("{}.x.x", CURRENT_VERSION.0),
            found: format!("{}.{}.{}",
                file_version.0, file_version.1, file_version.2),
        });
    }

    // Minor 버전이 더 높으면 경고
    if file_version.1 > CURRENT_VERSION.1 {
        log::warn!("Dictionary version is newer than library");
    }

    Ok(())
}
}

10.4 마이그레이션 도구

버전 업그레이드가 필요한 경우를 위한 CLI 도구:

# v2.x → v3.0 마이그레이션
mecab-ko-dict migrate --input old_dict/ --output new_dict/ --target-version 3.0

11. 사용자 사전 포맷

11.1 CSV 포맷 (입력)

# 사용자 정의 사전
# 표면형,품사,비용,읽기
형태소분석,NNG,-1000,형태소분석
딥러닝,NNG,-500,
챗GPT,NNP,-1000,챗지피티

11.2 바이너리 포맷 (컴파일 후)

사용자 사전도 시스템 사전과 동일한 구조를 사용합니다:

┌────────────────────────────────────────┐
│ [File Header]    file_type = USER_DICT │
├────────────────────────────────────────┤
│ [Trie Data]                            │
├────────────────────────────────────────┤
│ [Entry Array]                          │
├────────────────────────────────────────┤
│ [Feature Strings]                      │
└────────────────────────────────────────┘

11.3 UserDictionary 구현 (user_dict.rs)

#![allow(unused)]
fn main() {
/// 사용자 사전 엔트리
pub struct UserEntry {
    pub surface: String,
    pub left_id: u16,
    pub right_id: u16,
    pub cost: i16,
    pub pos: String,
    pub reading: Option<String>,
    pub lemma: Option<String>,
}

/// 사용자 사전
pub struct UserDictionary {
    entries: Vec<UserEntry>,
    surface_map: HashMap<String, Vec<usize>>,
    trie_cache: Option<Vec<u8>>,
    default_cost: i16,  // 기본값: -1000
}

impl UserDictionary {
    /// CSV에서 로드
    pub fn load_from_csv<P: AsRef<Path>>(&mut self, path: P) -> Result<&mut Self>;

    /// Trie 빌드
    pub fn build_trie(&mut self) -> Result<&[u8]>;

    /// 검색
    pub fn lookup(&self, surface: &str) -> Vec<&UserEntry>;
}
}

11.4 사용자 사전 우선순위

검색 순서:
1. 사용자 사전 (낮은 비용 우선)
2. 시스템 사전

비용 가이드라인:
- -2000 ~ -1000: 최우선 (신조어, 고유명사)
- -999 ~ -500: 높은 우선순위
- -499 ~ 0: 일반 우선순위
- 양수: 낮은 우선순위

12. 구현 참조

12.1 의존성 (Cargo.toml)

[dependencies]
yada = "0.5"           # Double-Array Trie
byteorder = "1.5"      # 바이트 순서 처리
memmap2 = "0.9"        # 메모리 매핑
zstd = "0.13"          # 압축

12.2 전체 로딩 예시

#![allow(unused)]
fn main() {
use mecab_ko_dict::{
    Trie, DenseMatrix, MmapMatrix, MatrixLoader, ConnectionMatrix
};

// 시스템 사전 로드
let trie = if use_mmap {
    Trie::from_file("dict/sys.dic")?
} else if compressed {
    Trie::from_compressed_file("dict/sys.dic.zst")?
} else {
    Trie::from_file("dict/sys.dic")?
};

// 연접 비용 행렬 로드
let matrix: ConnectionMatrix = if use_mmap {
    ConnectionMatrix::from_mmap_file("dict/matrix.bin")?
} else {
    ConnectionMatrix::load("dict/matrix.bin")?  // 자동 포맷 감지
};

// 비용 조회
let cost = matrix.get(right_id, left_id);
}

12.3 파일 목록 및 크기 예상

파일미압축 크기Zstd 압축압축률
sys.dic~50 MB~25 MB50%
matrix.bin~6 MB~2 MB67%
char.bin~130 KBN/A-
unk.bin~10 KBN/A-
합계~56 MB~27 MB52%

부록

A. 체크섬 계산

#![allow(unused)]
fn main() {
use crc32fast::Hasher;

pub fn calculate_checksum(data: &[u8]) -> u32 {
    let mut hasher = Hasher::new();
    hasher.update(data);
    hasher.finalize()
}
}

B. 에러 타입

#![allow(unused)]
fn main() {
/// 사전 에러 타입
pub enum DictError {
    /// IO 에러
    Io(std::io::Error),

    /// 포맷 에러
    Format(String),

    /// 버전 불일치
    Version { expected: String, found: String },

    /// 체크섬 불일치
    Checksum { expected: u32, found: u32 },
}
}

C. 향후 확장 계획

  1. v3.1: 증분 업데이트 지원
  2. v3.2: 원격 사전 로딩 (HTTP)
  3. v4.0: FST(Finite State Transducer) 기반 Trie

참고 자료


문서 버전: 3.0 최종 수정: 2026-01-05

MeCab-Ko Rust 프로젝트 구조

문서 버전: 1.0 작성일: 2026-01-04 이슈: RST-002 프로젝트 구조 및 Cargo workspace 설계


목차

  1. 개요
  2. Crate 구조
  3. 의존성 그래프
  4. Crate 상세
  5. 빌드 및 테스트
  6. 개발 로드맵

1. 개요

1.1 설계 원칙

원칙설명
모듈화기능별 독립 crate 분리
최소 의존성각 crate는 필요한 의존성만 포함
안전성unsafe 코드 금지, unwrap()/expect() 라이브러리에서 금지
문서화모든 public API에 rustdoc 필수
테스트유닛 테스트 + 통합 테스트

1.2 하이브리드 접근법

RST-001 분석 결과에 따라 하이브리드 접근법 채택:

┌─────────────────────────────────────────────────────────────┐
│                     신규 개발                                │
├─────────────────────────────────────────────────────────────┤
│  • mecab-ko-hangul (완료) - 한글 자모 처리                   │
│  • mecab-ko-core - 띄어쓰기 패널티, Viterbi, Lattice         │
│  • mecab-ko-dict - 사전 로딩, 연접 비용                       │
│  • mecab-ko-dict-builder - CSV → 바이너리 변환               │
├─────────────────────────────────────────────────────────────┤
│                     Lindera 참조                             │
├─────────────────────────────────────────────────────────────┤
│  • Double Array Trie 알고리즘 (yada 라이브러리)               │
│  • Viterbi 구현 패턴                                         │
│  • 에러 처리 패턴 (thiserror)                                │
│  • 필터 시스템 설계                                           │
└─────────────────────────────────────────────────────────────┘

2. Crate 구조

2.1 디렉토리 레이아웃

rust/
├── Cargo.toml                    # Workspace 정의
├── README.md
└── crates/
    ├── mecab-ko/                 # 📦 Facade crate (통합 인터페이스)
    │   ├── Cargo.toml
    │   └── src/
    │       └── lib.rs
    │
    ├── mecab-ko-core/            # 📦 핵심 엔진
    │   ├── Cargo.toml
    │   └── src/
    │       ├── lib.rs
    │       └── pos_tag.rs        # 품사 태그 정의
    │
    ├── mecab-ko-dict/            # 📦 사전 관리
    │   ├── Cargo.toml
    │   └── src/
    │       └── lib.rs
    │
    ├── mecab-ko-dict-builder/    # 📦 사전 빌더
    │   ├── Cargo.toml
    │   └── src/
    │       ├── lib.rs
    │       └── main.rs           # CLI 바이너리
    │
    ├── mecab-ko-hangul/          # 📦 한글 유틸리티 (완료)
    │   ├── Cargo.toml
    │   └── src/
    │       └── lib.rs
    │
    └── mecab-ko-cli/             # 📦 CLI 도구
        ├── Cargo.toml
        └── src/
            └── main.rs

2.2 Crate 요약

Crate타입상태설명
mecab-kolib스텁Facade - 모든 기능 재export
mecab-ko-corelib스텁Lattice, Viterbi, Tokenizer
mecab-ko-dictlib스텁사전 로딩, 검색, 연접 비용
mecab-ko-dict-builderlib+bin스텁CSV → 바이너리 변환
mecab-ko-hangullib완료한글 자모 처리
mecab-ko-clibin스텁명령줄 도구

3. 의존성 그래프

                    ┌──────────────────┐
                    │   mecab-ko-cli   │
                    │   (바이너리)      │
                    └────────┬─────────┘
                             │
                    ┌────────▼─────────┐
                    │     mecab-ko     │
                    │   (facade lib)   │
                    └────────┬─────────┘
                             │
         ┌───────────────────┼───────────────────┐
         │                   │                   │
         ▼                   ▼                   ▼
┌────────────────┐  ┌────────────────┐  ┌────────────────┐
│ mecab-ko-core  │  │ mecab-ko-dict  │  │mecab-ko-hangul │
│  (핵심 엔진)    │  │   (사전)        │  │  (한글 처리)    │
└───────┬────────┘  └───────┬────────┘  └────────────────┘
        │                   │                   ▲
        │                   │                   │
        └───────────────────┴───────────────────┘
                            │
              ┌─────────────┴─────────────┐
              │  mecab-ko-dict-builder    │
              │    (사전 빌드 도구)         │
              └───────────────────────────┘

4. Crate 상세

4.1 mecab-ko-hangul (완료)

한글 자모 처리 유틸리티.

#![allow(unused)]
fn main() {
// 주요 기능
pub fn decompose(c: char) -> Option<(char, char, Option<char>)>;
pub fn compose(cho: char, jung: char, jong: Option<char>) -> Option<char>;
pub fn is_hangul_syllable(c: char) -> bool;
pub fn has_jongseong(c: char) -> Option<bool>;
pub fn classify_char(c: char) -> CharType;
}

의존성: 없음 (zero dependencies)

4.2 mecab-ko-core

핵심 형태소 분석 엔진.

#![allow(unused)]
fn main() {
// 주요 타입
pub struct Tokenizer { ... }
pub struct Token { ... }
pub struct Lattice { ... }
pub enum PosTag { ... }  // 41개 품사 태그

// 주요 기능
impl Tokenizer {
    pub fn new() -> Result<Self>;
    pub fn tokenize(&self, text: &str) -> Vec<Token>;
    pub fn wakati(&self, text: &str) -> Vec<String>;
    pub fn nouns(&self, text: &str) -> Vec<String>;
}
}

핵심 구현 예정:

  • space_penalty.rs - 띄어쓰기 패널티 (left-space-penalty-factor)
  • viterbi.rs - Viterbi 알고리즘
  • lattice.rs - Lattice 구조체

의존성: mecab-ko-hangul, mecab-ko-dict, thiserror, serde

4.3 mecab-ko-dict

사전 관리 라이브러리.

#![allow(unused)]
fn main() {
// 주요 타입
pub struct Entry { ... }
pub trait Dictionary {
    fn lookup(&self, surface: &str) -> Vec<Entry>;
    fn get_connection_cost(&self, left_id: u16, right_id: u16) -> i16;
}

// 구현체
pub struct MmapDictionary { ... }  // Memory-mapped 사전
}

핵심 구현 예정:

  • Double Array Trie (yada 라이브러리 활용)
  • 연접 비용 매트릭스
  • 미등록어 처리 (char.def, unk.def)

의존성: mecab-ko-hangul, fst, yada, memmap2, zstd, byteorder

4.4 mecab-ko-dict-builder

사전 빌드 도구.

#![allow(unused)]
fn main() {
// 주요 타입
pub struct DictionaryBuilder { ... }
pub struct BuildConfig { ... }
pub struct CsvEntry { ... }  // 12컬럼 CSV

// CLI 사용법
// mecab-ko-dict-builder --input ./mecab-ko-dic --output dict.bin
}

핵심 구현 예정:

  • CSV 파싱 (33개 파일)
  • Double Array Trie 빌드
  • matrix.def 변환
  • char.def, unk.def 변환
  • Zstd 압축

의존성: mecab-ko-hangul, mecab-ko-dict, csv, yada, rkyv, zstd, clap, indicatif

4.5 mecab-ko

Facade crate - 사용자 친화적 인터페이스.

#![allow(unused)]
fn main() {
// 모든 기능을 하나로 통합
pub use mecab_ko_core::{Tokenizer, Token, PosTag};
pub use mecab_ko_hangul::{decompose, compose, is_hangul};
pub use mecab_ko_dict::{Dictionary, Entry};

// 간편 사용
use mecab_ko::Tokenizer;
let tok = Tokenizer::new()?;
let tokens = tok.tokenize("안녕하세요");
}

의존성: mecab-ko-core, mecab-ko-dict, mecab-ko-hangul

4.6 mecab-ko-cli

명령줄 도구.

# 기본 사용법
mecab-ko "안녕하세요"

# 분리만 (wakati)
mecab-ko -O wakati "안녕하세요"

# JSON 출력
mecab-ko -O json "안녕하세요"

# 사전 지정
mecab-ko -d /path/to/dict "안녕하세요"

의존성: mecab-ko-core, clap, anyhow, serde_json


5. 빌드 및 테스트

5.1 빌드 명령어

# 전체 빌드
cd rust
cargo build

# 릴리스 빌드
cargo build --release

# 특정 crate 빌드
cargo build -p mecab-ko-hangul

# 문서 생성
cargo doc --open

5.2 테스트

# 전체 테스트
cargo test

# 특정 crate 테스트
cargo test -p mecab-ko-hangul

# 문서 테스트
cargo test --doc

5.3 린트 및 포맷팅

# Clippy 검사
cargo clippy --all-targets

# 포맷팅
cargo fmt

# 포맷팅 확인
cargo fmt --check

5.4 Workspace 설정

# Cargo.toml
[workspace]
resolver = "2"
members = [
    "crates/mecab-ko",
    "crates/mecab-ko-core",
    "crates/mecab-ko-dict",
    "crates/mecab-ko-dict-builder",
    "crates/mecab-ko-hangul",
    "crates/mecab-ko-cli",
]

[workspace.lints.rust]
unsafe_code = "deny"
missing_docs = "warn"

[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"

6. 개발 로드맵

Phase 1: 코어 구현 (현재)

Sprint 1-2:
├── ✅ DIC-001: mecab-ko-dic 소스 분석
├── ✅ DIC-002: 품사 태그 체계 검토 (pos_tag.rs)
├── ✅ RST-001: Lindera 코드베이스 분석
├── ✅ RST-002: 프로젝트 구조 설계
└── 🔄 RST-003: Lattice 구조체 구현

Sprint 3-4:
├── □ RST-004: Viterbi 알고리즘 구현
├── □ RST-005: 띄어쓰기 패널티 구현
└── □ RST-006: 사전 로더 구현

Phase 2: 사전 빌더

Sprint 5-6:
├── □ DIC-003: CSV 파서 구현
├── □ DIC-004: Double Array Trie 빌더
└── □ DIC-005: 바이너리 포맷 정의

Phase 3: 통합 및 최적화

Sprint 7-8:
├── □ RST-010: CLI 완성
├── □ RST-011: 벤치마크 구현
└── □ RST-012: 성능 최적화

Phase 4: 바인딩

Sprint 9-10:
├── □ BND-001: Python 바인딩 (PyO3)
└── □ BND-002: C FFI

부록: 주요 의존성

의존성버전용도
thiserror1.0에러 정의
anyhow1.0에러 전파
serde1.0직렬화
fst0.4FST 자료구조
yada0.5Double Array Trie
memmap20.9Memory-mapped IO
zstd0.13압축
rkyv0.8Zero-copy 직렬화
csv1.3CSV 파싱
clap4.4CLI 파싱
criterion0.5벤치마크
pyo30.20Python 바인딩

참고 자료

MeCab-Ko-Dic 빌드 프로세스 가이드

문서 버전: 1.0 작성일: 2026-01-04 대상: mecab-ko-dic 사전 빌드 시스템

이 문서는 DIC-001 이슈의 산출물로, mecab-ko-dic의 빌드 프로세스를 상세히 문서화합니다.


목차

  1. 빌드 환경 준비
  2. 빌드 파이프라인 개요
  3. 단계별 빌드 프로세스
  4. 핵심 도구 분석
  5. 비용 조정 시스템
  6. 사용자 사전 빌드
  7. Rust 재구현 고려사항

1. 빌드 환경 준비

1.1 필수 도구

# MeCab 코어 도구
mecab-dict-index    # 사전 컴파일러
mecab-dict-gen      # 사전 생성기
mecab-cost-train    # CRF 학습기

# 빌드 시스템
autoconf            # configure 스크립트 생성
automake            # Makefile 생성
make                # 빌드 실행

1.2 디렉토리 구조

mecab-ko-dic/
├── seed/           # 입력: CSV 원본 + 정의 파일
│   ├── *.csv
│   ├── *.def
│   ├── dicrc
│   ├── build.sh
│   └── corpus/
└── final/          # 출력: 바이너리 사전
    ├── *.dic
    ├── *.bin
    ├── *.def
    └── dicrc

2. 빌드 파이프라인 개요

2.1 전체 흐름도

┌─────────────────────────────────────────────────────────────────┐
│  Phase 1: 초기화                                                │
│  ───────────────                                                │
│  seed/*.csv + *.def ──→ mecab-dict-index -p                    │
│                              │                                  │
│                              ▼                                  │
│                    left-id.def, right-id.def                   │
├─────────────────────────────────────────────────────────────────┤
│  Phase 2: CRF 학습                                              │
│  ───────────────                                                │
│  corpus/eunjeon_corpus.txt ──→ mecab-cost-train                │
│                                      │                          │
│                                      ▼                          │
│                                 model.bin                       │
├─────────────────────────────────────────────────────────────────┤
│  Phase 3: 사전 생성                                             │
│  ───────────────                                                │
│  *.csv + model.bin ──→ mecab-dict-gen                          │
│                              │                                  │
│                              ▼                                  │
│                    비용이 할당된 CSV 파일들                     │
├─────────────────────────────────────────────────────────────────┤
│  Phase 4: 비용 조정                                             │
│  ───────────────                                                │
│  change_word_cost.sh ──→ 단어 비용 수동 조정                   │
│  change_connection_cost.sh ──→ 연접 비용 수동 조정             │
├─────────────────────────────────────────────────────────────────┤
│  Phase 5: 바이너리 컴파일                                       │
│  ─────────────────                                              │
│  final/*.csv ──→ mecab-dict-index                              │
│                       │                                         │
│                       ▼                                         │
│    sys.dic, unk.dic, matrix.bin, char.bin                      │
└─────────────────────────────────────────────────────────────────┘

2.2 입출력 파일 매핑

입력처리출력
*.csv사전 컴파일sys.dic
unk.def미등록어 컴파일unk.dic
char.def문자 속성 컴파일char.bin
matrix.def연접 행렬 컴파일matrix.bin
feature.def모델 컴파일model.bin

3. 단계별 빌드 프로세스

3.1 Phase 1: 품사 ID 할당

# seed/build.sh 일부
DICT_INDEX=/usr/local/libexec/mecab/mecab-dict-index

# 품사 ID 자동 생성 (-p 옵션)
$DICT_INDEX -p -d . -c UTF-8 -t UTF-8 -f UTF-8

생성 파일:

  • left-id.def: 좌측 문맥 ID 매핑
  • right-id.def: 우측 문맥 ID 매핑

left-id.def 형식:

BOS/EOS,*,*,*,*,*,*,* 0
NNG,*,*,*,*,*,*,* 150
VV,*,*,*,*,*,*,* 173
JKO,*,*,*,*,*,*,* 120
...

3.2 Phase 2: CRF 모델 학습

COST_TRAIN=/usr/local/libexec/mecab/mecab-cost-train
corpus_file="corpus/eunjeon_corpus.txt"
model_file="model.def"

# CRF 학습 실행
$COST_TRAIN -p ${cpu_count} -c 1.0 ${corpus_file} ${model_file}

코퍼스 형식 (eunjeon_corpus.txt):

안녕	NNG,인사,T,안녕,*,*,*,*
하	XSV,*,F,하,*,*,*,*
세요	EP+EF,*,F,세요,Inflect,EP,EF,시/EP/*+어요/EF/*
EOS
나	NP,*,F,나,*,*,*,*
는	JX,*,T,는,*,*,*,*
...

학습 파라미터:

  • -p N: 병렬 처리 스레드 수
  • -c 1.0: CRF 정규화 계수

3.3 Phase 3: 사전 생성

DICT_GEN=/usr/local/libexec/mecab/mecab-dict-gen

# 모델 기반 비용 자동 할당
$DICT_GEN -o ../final -m $model_file

처리 내용:

  1. CSV 파일의 left_id, right_id, cost 컬럼 채우기
  2. CRF 모델 기반 최적 비용 계산
  3. matrix.def 생성 (연접 비용 행렬)

3.4 Phase 4: 비용 수동 조정

단어 비용 조정

# change_word_cost.sh
while read line; do
    surface=$(echo $line | cut -d',' -f1)
    pos=$(echo $line | cut -d',' -f2)
    new_cost=$(echo $line | cut -d',' -f3)

    # CSV 파일에서 해당 단어의 비용 수정
    sed -i "s/^${surface},.*,${pos},/${surface},0,0,${new_cost},${pos},/" *.csv
done < change_word_cost.txt

change_word_cost.txt 예시:

은,JX,-1000       # "은" 보조사 비용 낮춤
를,JKO,-500       # "를" 목적격 조사

연접 비용 조정

# change_connection_cost.sh
# matrix.def 에서 특정 품사 조합의 비용 수정

change_connection_cost.txt 예시:

JX,*,T,는|JKO,*,을 10000     # "는을" 연접 억제
JX,*,T,은|JX,*,은 10000      # "은은" 연접 억제
JKG,*,F,의|BOS/EOS,*,* 10000 # 문장 시작 "의" 억제

3.5 Phase 5: 바이너리 컴파일

cd ../final

# Autotools 빌드
./autogen.sh    # configure 스크립트 생성
./configure     # 빌드 설정
make            # 바이너리 생성
make install    # 설치 (선택)

생성되는 바이너리:

파일크기 (대략)설명
sys.dic40-50 MB시스템 사전 (DA Trie)
unk.dic수 KB미등록어 사전
matrix.bin수 MB연접 비용 행렬
char.bin수백 KB문자 속성 테이블

4. 핵심 도구 분석

4.1 mecab-dict-index

소스: mecab-ko/src/dictionary_compiler.cpp

주요 옵션:

-d DIR      입력 사전 디렉토리
-o DIR      출력 디렉토리
-f CHARSET  입력 CSV 문자셋 (UTF-8)
-t CHARSET  출력 바이너리 문자셋 (UTF-8)
-p          품사 ID 할당 모드
-s          시스템 사전 빌드
-u FILE     사용자 사전 생성
-m FILE     모델 파일 (비용 자동 추정)
-U          미등록어 사전 빌드
-C          문자 카테고리 빌드

내부 처리 흐름:

// dictionary_compiler.cpp
int DictionaryCompiler::run(int argc, char **argv) {
    // 1. 문자 카테고리 컴파일
    CharProperty::compile(char_def, unk_def, char_bin);

    // 2. 미등록어 사전 컴파일
    Dictionary::compile(param, tmp, unk_dic);

    // 3. 시스템 사전 컴파일
    Dictionary::compile(param, dic_files, sys_dic);

    // 4. 연접 행렬 컴파일
    Connector::compile(matrix_def, matrix_bin);
}

4.2 Double-Array Trie (Darts)

소스: mecab-ko/src/darts.h

사전 검색에 사용되는 고속 Trie 구조:

// 공통 접두사 검색
size_t commonPrefixSearch(const char *key,
                          result_type *result,
                          size_t result_len,
                          size_t len) const;

// 정확 매칭 검색
int exactMatchSearch(const char *key, size_t len) const;

특징:

  • O(n) 검색 복잡도 (n = 키 길이)
  • 메모리 효율적 압축
  • 한글 UTF-8 지원

4.3 연접 비용 행렬 (Connector)

소스: mecab-ko/src/connector.cpp

int Connector::cost(const Node *lNode, const Node *rNode) const {
    // 행렬 인덱스 계산: rcAttr + lsize * lcAttr
    int base_cost = matrix_[lNode->rcAttr + lsize_ * rNode->lcAttr];

    // 단어 비용 추가
    int word_cost = rNode->wcost;

    // mecab-ko 특화: 공백 페널티
    int space_penalty = get_space_penalty_cost(rNode);

    return base_cost + word_cost + space_penalty;
}

5. 비용 조정 시스템

5.1 비용 구성 요소

총 비용 = 연접 비용 + 단어 비용 + 공백 페널티
          (matrix)   (wcost)    (left-space-penalty)

5.2 비용 스케일링

dicrc 설정:

cost-factor = 800

CRF 학습된 raw 비용에 800을 곱하여 정수화:

final_cost = raw_cost * 800

5.3 공백 페널티 (mecab-ko 특화)

dicrc 설정:

left-space-penalty-factor = 100,3000,120,6000,172,3000,183,3000

형식: 품사ID1,페널티1,품사ID2,페널티2,...

품사 ID품사페널티의미
120JKO (목적격 조사)6000조사 앞 공백 억제
172EC (연결 어미)3000어미 앞 공백 억제

6. 사용자 사전 빌드

6.1 CSV 형식

# user-dic/custom.csv
카카오뱅크,,,0,NNP,기업,F,카카오뱅크,Compound,*,*,카카오/NNP/*+뱅크/NNG/*
테슬라,,,0,NNP,기업,F,테슬라,*,*,*,*
GPT-4,,,0,SL,*,*,GPT-4,*,*,*,*

6.2 빌드 명령

# final/tools/add-userdic.sh
DICT_INDEX=/usr/local/libexec/mecab/mecab-dict-index
DIC_PATH=/usr/local/lib/mecab/dic/mecab-ko-dic

$DICT_INDEX \
    -m ${DIC_PATH}/model.def \    # 비용 자동 추정
    -d ${DIC_PATH} \              # 시스템 사전 참조
    -u ${DIC_PATH}/user-custom.dic \  # 출력 파일
    -f utf-8 -t utf-8 \
    -a user-dic/custom.csv        # 입력 CSV

6.3 사전 우선순위

1. 사용자 사전 (user-*.dic)
2. 시스템 사전 (sys.dic)
3. 미등록어 사전 (unk.dic)

7. Rust 재구현 고려사항

7.1 필요한 Crate

기능C++Rust 대안
Double-Array Triedartsyada, daachorse
CRF 학습crfppcrfsuite-rs, 자체 구현
바이너리 직렬화자체 포맷bincode, rkyv
문자셋 변환iconvencoding_rs

7.2 바이너리 호환성

옵션 1: 기존 포맷 호환

  • 장점: 기존 mecab-ko-dic 그대로 사용 가능
  • 단점: 레거시 포맷 제약

옵션 2: 새 포맷 설계 (v3.0)

  • 장점: Rust 최적화, 현대적 압축
  • 단점: 사전 재빌드 필요

7.3 핵심 구현 모듈

#![allow(unused)]
fn main() {
// 제안 구조
crates/
├── mecab-ko-dict/
│   ├── src/
│   │   ├── loader.rs      // 사전 로더
│   │   ├── builder.rs     // 사전 빌더
│   │   ├── trie.rs        // Double-Array Trie
│   │   ├── matrix.rs      // 연접 행렬
│   │   ├── char_prop.rs   // 문자 속성
│   │   └── format.rs      // 바이너리 포맷
│   └── Cargo.toml
}

7.4 빌드 도구 재구현

#![allow(unused)]
fn main() {
// mecab-ko-dict-tools (CLI)
pub enum Command {
    /// 품사 ID 할당
    AssignPosId { input_dir: PathBuf },

    /// 사전 컴파일
    Compile {
        input_dir: PathBuf,
        output_dir: PathBuf,
        charset: String,
    },

    /// 사용자 사전 추가
    AddUserDict {
        sys_dic: PathBuf,
        user_csv: PathBuf,
        output: PathBuf,
    },
}
}

부록: 빌드 트러블슈팅

A. 일반적인 오류

오류원인해결
unknown POS tagCSV 품사 태그 오류pos-id.def 확인
charset mismatch인코딩 불일치-f UTF-8 -t UTF-8 확인
matrix size mismatchID 범위 초과left/right-id.def 재생성

B. 성능 최적화

# 병렬 빌드
make -j$(nproc)

# CRF 학습 병렬화
mecab-cost-train -p $(nproc) ...

참고 자료

컨트리뷰션 가이드

MeCab-Ko 프로젝트에 기여하는 방법을 안내합니다.

시작하기

1. 저장소 포크

# GitHub에서 Fork 버튼 클릭 후
git clone https://github.com/YOUR_USERNAME/mecab-ko.git
cd mecab-ko

2. 개발 환경 설정

# Rust 설치 (1.75.0 이상)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 빌드
cd rust
cargo build

# 테스트
cargo test

3. 브랜치 생성

git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix

코딩 규칙

Rust 스타일

#![allow(unused)]
fn main() {
// Good
pub fn parse_text(text: &str) -> Result<Vec<Token>, Error> {
    // 구현
}

// Bad
pub fn ParseText(Text: &str) -> Result<Vec<Token>, Error> {
    // 구현
}
}

네이밍 컨벤션

  • 함수/메서드: snake_case
  • 타입/구조체: PascalCase
  • 상수: SCREAMING_SNAKE_CASE
  • 모듈: snake_case

문서화

모든 public API에 rustdoc 주석 필수:

#![allow(unused)]
fn main() {
/// 텍스트를 형태소 분석합니다.
///
/// # Arguments
///
/// * `text` - 분석할 텍스트
///
/// # Returns
///
/// 분석된 토큰 리스트
///
/// # Errors
///
/// 사전을 찾을 수 없거나 파싱 오류 시 에러 반환
///
/// # Examples
///
/// ```
/// use mecab_ko::Tagger;
///
/// let tagger = Tagger::new(Default::default())?;
/// let result = tagger.parse("안녕하세요")?;
/// ```
pub fn parse(&self, text: &str) -> Result<Vec<Token>, Error> {
    // 구현
}
}

에러 처리

#![allow(unused)]
fn main() {
// Good - Result 반환
pub fn load_dictionary(path: &Path) -> Result<Dictionary, Error> {
    let file = File::open(path)?;
    // ...
}

// Bad - panic 사용
pub fn load_dictionary(path: &Path) -> Dictionary {
    let file = File::open(path).unwrap(); // 금지!
    // ...
}
}

라이브러리 코드에서 unwrap(), expect() 금지!

Unsafe 최소화

#![allow(unused)]
fn main() {
// 꼭 필요한 경우만 사용
unsafe {
    // SAFETY: 이유 설명 필수
}
}

테스트

단위 테스트

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_basic() {
        let tagger = Tagger::new(Default::default()).unwrap();
        let result = tagger.parse("테스트").unwrap();
        assert!(!result.is_empty());
    }

    #[test]
    fn test_parse_empty() {
        let tagger = Tagger::new(Default::default()).unwrap();
        let result = tagger.parse("").unwrap();
        assert!(result.is_empty());
    }
}
}

통합 테스트

#![allow(unused)]
fn main() {
// tests/integration_test.rs
use mecab_ko::Tagger;

#[test]
fn test_full_workflow() {
    let tagger = Tagger::new(Default::default()).unwrap();
    let text = "형태소 분석을 테스트합니다.";
    let result = tagger.parse(text).unwrap();

    // 검증
    assert!(result.iter().any(|t| t.surface == "형태소"));
}
}

벤치마크

#![allow(unused)]
fn main() {
// benches/parse_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn bench_parse(c: &mut Criterion) {
    let tagger = Tagger::new(Default::default()).unwrap();

    c.bench_function("parse Korean text", |b| {
        b.iter(|| {
            tagger.parse(black_box("형태소 분석 벤치마크"))
        })
    });
}

criterion_group!(benches, bench_parse);
criterion_main!(benches);
}

커밋 메시지

포맷

<type>(<scope>): <subject>

<body>

<footer>

Type

  • feat: 새로운 기능
  • fix: 버그 수정
  • docs: 문서 변경
  • style: 코드 포맷팅
  • refactor: 리팩토링
  • test: 테스트 추가/수정
  • chore: 빌드/설정 변경
  • perf: 성능 개선

예시

feat(tagger): N-best 분석 기능 추가

N-best 경로 탐색 알고리즘을 구현하여 여러 후보 결과를 반환할 수
있도록 개선했습니다.

- NBestAnalyzer 구조체 추가
- Viterbi 알고리즘 N-best 버전 구현
- 테스트 케이스 추가

Closes #123

Pull Request

체크리스트

PR 생성 전 확인:

  • 코드가 빌드됨 (cargo build)
  • 모든 테스트 통과 (cargo test)
  • Clippy 경고 없음 (cargo clippy)
  • 포맷팅 적용 (cargo fmt)
  • 문서 업데이트
  • CHANGELOG.md 업데이트
  • 테스트 추가/수정

PR 템플릿

## 변경 사항

<!-- 무엇을 변경했는지 설명 -->

## 동기

<!-- 왜 이 변경이 필요한지 설명 -->

## 테스트

<!-- 어떻게 테스트했는지 설명 -->

## 스크린샷 (해당되는 경우)

## 체크리스트

- [ ] 코드 빌드 확인
- [ ] 테스트 통과 확인
- [ ] 문서 업데이트
- [ ] CHANGELOG.md 업데이트

CI/CD

GitHub Actions

PR 생성 시 자동 실행:

  • Rust 빌드 (stable, beta, nightly)
  • 테스트 실행
  • Clippy 린트
  • 코드 포맷 확인
  • 문서 빌드

로컬 확인

# 전체 CI 체크
./scripts/ci-check.sh

이슈 보고

버그 리포트

## 버그 설명

<!-- 버그에 대한 명확한 설명 -->

## 재현 방법

1. ...
2. ...
3. ...

## 예상 동작

<!-- 어떻게 동작해야 하는지 -->

## 실제 동작

<!-- 실제로 어떻게 동작하는지 -->

## 환경

- OS: [e.g., Ubuntu 22.04]
- Rust 버전: [e.g., 1.75.0]
- MeCab-Ko 버전: [e.g., 0.1.0]

## 추가 정보

<!-- 스크린샷, 로그 등 -->

기능 제안

## 기능 설명

<!-- 제안하는 기능에 대한 명확한 설명 -->

## 동기

<!-- 왜 이 기능이 필요한지 -->

## 사용 예시

```rust
// 코드 예시

대안


## 릴리스 프로세스

### 버전 관리

Semantic Versioning 사용:

- MAJOR: 호환되지 않는 API 변경
- MINOR: 하위 호환되는 기능 추가
- PATCH: 하위 호환되는 버그 수정

### 릴리스 체크리스트

1. [ ] CHANGELOG.md 업데이트
2. [ ] Cargo.toml 버전 업데이트
3. [ ] 문서 버전 업데이트
4. [ ] 태그 생성 (`git tag v0.1.0`)
5. [ ] GitHub Release 생성
6. [ ] crates.io 배포 (`cargo publish`)

## 코드 리뷰

### 리뷰어 가이드

- 코드 스타일 확인
- 테스트 커버리지 확인
- 성능 영향 평가
- 문서화 확인
- 에러 처리 확인

### 리뷰이 가이드

- 피드백에 열린 자세
- 변경 이유 명확히 설명
- 요청된 수정사항 반영
- 토론이 필요한 경우 이슈 생성

## 라이선스

기여한 코드는 프로젝트의 Apache 2.0 또는 MIT 라이선스를 따릅니다.

## 행동 강령

- 존중하는 태도
- 건설적인 피드백
- 포용적인 언어 사용
- 협력적인 자세

## 연락처

- GitHub Issues: 버그 리포트, 기능 제안
- GitHub Discussions: 일반적인 질문, 토론
- Email: hephaex@gmail.com

## 참고 자료

- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)
- [Cargo Book](https://doc.rust-lang.org/cargo/)
- [The Rust Book](https://doc.rust-lang.org/book/)

FAQ

자주 묻는 질문과 답변을 모았습니다.

일반

MeCab-Ko와 은전한닢(mecab-ko)의 차이점은?

항목은전한닢 (mecab-ko)MeCab-Ko (이 프로젝트)
언어C++Rust
메모리 안전성unsafe 코드 포함unsafe 없음
유지보수2018년 이후 중단활발히 개발 중
크로스 플랫폼빌드 복잡Cargo로 간편 빌드
WASM 지원불가지원 예정

Kiwi나 Lindera와의 차이점은?

  • Kiwi: C++ 기반, 독자적인 알고리즘, 높은 정확도
  • Lindera: Rust 기반, 일본어 중심, 한국어 부분 지원
  • MeCab-Ko: Rust 기반, 한국어 특화, mecab-ko 호환

라이선스는?

Apache 2.0 또는 MIT 중 선택하여 사용할 수 있습니다. 상업적 사용이 가능합니다.

설치

Rust가 설치되어 있지 않아도 사용할 수 있나요?

CLI 도구의 경우, 미리 빌드된 바이너리를 사용하면 Rust 없이도 사용 가능합니다. (배포 예정)

라이브러리로 사용하려면 Rust가 필요합니다.

Windows에서 빌드 오류가 발생합니다

Visual Studio Build Tools가 설치되어 있는지 확인하세요:

# Install via rustup
rustup default stable-msvc

macOS에서 xcrun: error 오류

Xcode Command Line Tools를 설치하세요:

xcode-select --install

사용법

분석 결과가 이상합니다

  1. 사전 경로 확인: 올바른 사전이 로드되었는지 확인
  2. 사용자 사전: 도메인 특화 단어는 사용자 사전에 추가
  3. 인코딩: 입력 텍스트가 UTF-8인지 확인
# Check dictionary info
mecab-ko dict

# Use with verbose output
mecab-ko -O dump "문제되는 텍스트"

신조어가 제대로 분석되지 않습니다

사용자 사전에 추가하세요:

# user.csv
새로운단어,NNG,-1000,
mecab-ko --user-dic user.csv "새로운단어를 분석합니다"

메모리 사용량이 높습니다

사전 로딩 시 약 150-200MB의 메모리가 필요합니다. 이는 정상적인 동작입니다.

메모리를 줄이려면:

  • 필요한 기능만 포함한 경량 사전 사용 (개발 중)
  • 서버 환경에서 싱글톤 패턴으로 토크나이저 공유

처리 속도가 느립니다

  1. 릴리스 빌드 사용:

    cargo build --release
    
  2. 배치 처리: 한 번에 여러 문장 처리

  3. 토크나이저 재사용: 매번 생성하지 않기

#![allow(unused)]
fn main() {
// Good: Reuse tokenizer
let tokenizer = Tokenizer::new()?;
for text in texts {
    let tokens = tokenizer.tokenize(text);
}

// Bad: Create new tokenizer each time
for text in texts {
    let tokenizer = Tokenizer::new()?;  // Slow!
    let tokens = tokenizer.tokenize(text);
}
}

사용자 사전

CSV 파일 형식이 맞는지 확인하려면?

# Check if file is valid UTF-8
file user.csv

# Test loading
mecab-ko -q --user-dic user.csv "테스트" 2>&1

우선순위 조정은 어떻게 하나요?

비용(cost) 값을 조정합니다. 낮을수록 높은 우선순위:

최우선단어,NNG,-2000,
높은우선순위,NNG,-1000,
보통,NNG,-500,

동음이의어 처리는?

같은 표면형에 다른 품사를 등록할 수 있습니다:

# '배'의 여러 의미
배,NNG,-1000,    # 과일
배,NNG,-1000,    # 신체 부위
배,NNG,-1000,    # 선박

분석기는 문맥에 따라 적절한 분석을 선택합니다.

프로그래밍

병렬 처리가 가능한가요?

TokenizerSend + Sync를 구현하므로 스레드 간 공유가 가능합니다:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use rayon::prelude::*;

let tokenizer = Arc::new(Tokenizer::new()?);

let results: Vec<_> = texts
    .par_iter()
    .map(|text| tokenizer.tokenize(text))
    .collect();
}

async/await와 함께 사용할 수 있나요?

현재 토크나이저는 동기 API만 제공합니다. 비동기 환경에서는 spawn_blocking 사용:

#![allow(unused)]
fn main() {
use tokio::task::spawn_blocking;

let tokens = spawn_blocking(move || {
    tokenizer.tokenize(text)
}).await?;
}

Python에서 사용할 수 있나요?

Python 바인딩이 제공됩니다 (PyPI 배포 준비 중):

from mecab_ko import Mecab

mecab = Mecab()
tokens = mecab.pos("안녕하세요")
print(tokens)
# [('안녕', 'NNG'), ('하', 'XSV'), ('세요', 'EP+EF')]

또는 subprocess로 CLI 호출:

import subprocess
import json

result = subprocess.run(
    ["mecab-ko", "-O", "json", "안녕하세요"],
    capture_output=True,
    text=True
)
tokens = json.loads(result.stdout)

웹 브라우저에서 사용할 수 있나요?

WASM 바인딩이 제공됩니다:

import init, { Mecab } from 'mecab-ko-wasm';

await init();
const mecab = new Mecab();
const tokens = mecab.tokenize("안녕하세요");
console.log(tokens);

오류 해결

Dictionary error: IO error

사전 파일 경로가 잘못되었거나 파일이 없습니다:

# Check if path exists
ls -la /path/to/dict

# Use explicit path
mecab-ko -d /correct/path/to/dict "테스트"

Invalid dictionary format

사전 파일이 손상되었거나 버전이 맞지 않습니다:

  1. 사전 재다운로드
  2. 바이너리 사전 버전 확인
  3. 소스에서 사전 재빌드

Version mismatch

사전 버전과 프로그램 버전이 맞지 않습니다:

# Check versions
mecab-ko version
mecab-ko dict /path/to/dict

호환되는 버전의 사전을 사용하세요.

Failed to initialize tokenizer

  1. 기본 사전이 포함된 빌드인지 확인
  2. 환경 변수 확인: MECAB_KO_DIC_PATH
  3. 명시적 사전 경로 지정

기여

버그를 발견했습니다

GitHub Issues에 보고해 주세요:

  • https://github.com/hephaex/mecab-ko/issues

포함할 정보:

  • MeCab-Ko 버전
  • 운영체제
  • 재현 단계
  • 입력 텍스트 (가능한 경우)
  • 예상 결과 vs 실제 결과

기능 제안을 하고 싶습니다

GitHub Issues에 Feature Request로 등록해 주세요.

코드 기여는 어떻게 하나요?

  1. 저장소 Fork
  2. Feature 브랜치 생성
  3. 변경 사항 커밋
  4. Pull Request 제출

자세한 내용은 CONTRIBUTING.md를 참조하세요.

추가 질문

이 FAQ에서 답을 찾지 못했다면:

  1. GitHub Discussions: https://github.com/hephaex/mecab-ko/discussions
  2. 이슈 등록: https://github.com/hephaex/mecab-ko/issues
  3. 문서 검색: https://docs.rs/mecab-ko

변경 이력

모든 주요 변경 사항을 기록합니다.

이 프로젝트는 Semantic Versioning을 따릅니다.

0.4.0 - 2026-03-05 🎉 crates.io 정식 배포

추가됨 (Added)

crates.io 배포

  • 6개 크레이트 정식 배포 완료
    • mecab-ko (파사드 크레이트)
    • mecab-ko-hangul (한글 유틸리티)
    • mecab-ko-dict (사전 로더)
    • mecab-ko-core (핵심 엔진)
    • mecab-ko-dict-builder (사전 빌드 도구)
    • mecab-ko-dict-validator (사전 검증 도구)

세종 코퍼스 호환 모드 강화 (mecab-ko-core)

  • apply_decomposition_corrections(): 오분석 패턴 보정
  • apply_token_merges(): 잘못 분해된 토큰 병합
  • apply_lexicon_overrides(): 고빈도 어휘 매핑
  • apply_context_corrections(): 컨텍스트 기반 품사 보정
  • EP (선어말어미) 패턴 확장: 과거, 추측, 높임, 회상
  • EC (연결어미) 패턴 확장: 이유, 조건, 시간, 양보
  • ETM/ETN 패턴 확장: 관형형, 명사형

고유명사 사전 확장

  • ~200개 고유명사 추가
    • 도시: 안양, 안산, 파주, 김해, 창원, 청주, 전주, 포항, 원주
    • 서울 구/동: 강남, 서초, 송파, 명동, 홍대, 이태원, 잠실
    • 국가: 멕시코, 네덜란드, 싱가포르, 말레이시아 등 30+
    • 기업/브랜드: 틱톡, 넷플릭스, 테슬라, 쿠팡, 배달의민족
    • 대학: 서울대, 연세대, 고려대, 카이스트, 포스텍
    • 유명인: 이순신, 세종대왕, 손흥민, 방탄소년단, 블랙핑크

신조어 사전 v3.0

  • 511개 신조어 (123 → 511, +315%)
  • AI/ML: Claude, Gemini, Midjourney, RAG, AGI
  • 소셜미디어: Threads, Bluesky, Shorts, 크리에이터
  • MZ세대: 갓생, 무지출, 킹받다, 레게노
  • 경제: HBM, 밈주식, DSR
  • 기술: Rust, Kubernetes, Docker

평가 데이터셋 확장

  • 160 → 300문장 확장
  • 추가 카테고리: 일상대화, 뉴스, 기술, 신조어, 불규칙, 복합

CI 자동 정확도 측정

  • dict-build.yml에 accuracy-test job 추가
  • 기준선 대비 회귀 탐지
  • 정확도 이력 JSON 아티팩트 (90일 보관)
  • GitHub Step Summary 통합

변경됨 (Changed)

  • 모든 크레이트 v0.4.0 버전 업그레이드
  • 정확도 기준선 업데이트: 23.8% (300문장, 세종 모드)

측정 결과 (300문장 데이터셋, 세종 모드)

지표v0.3.x (160문장)v0.4.0 (300문장)
Token Accuracy29.6%23.8%
Sentence Accuracy14.4%7.3%
F1 Score0.2950.229

Note: 확장된 데이터셋에는 복잡한 문장, 신조어, 불규칙 활용이 포함되어 절대값이 낮아짐

성능 (회귀 없음)

  • 처리 속도: 6,250 문장/초
  • 처리량: 3.0-3.7M chars/sec
  • v0.3.0 대비 회귀 없음

0.3.1 - 2026-03-04

추가됨 (Added)

세종 코퍼스 호환 모드 (mecab-ko-core)

  • SejongToken: 세종 코퍼스 형식 토큰 구조체
  • SejongConverter: 복합 태그 분리 변환기
  • EndingRule: 어미 분리 규칙 (VV+EF, VA+EF, EC, ETM 등)
  • 지원 어미 패턴:
    • 다, 아/어, 았/었 (과거형)
    • ㄴ/은, ㄹ (관형형)
    • 고, 니다/습니다 (연결/종결)
  • is_compound_tag(): 복합 품사 태그 감지
  • split_compound_tag(): 복합 태그 분리
  • format_sejong(): 세종 형식 문자열 출력

CLI 개선 (mecab-ko-cli)

  • evaluate --sejong: 세종 호환 모드로 정확도 평가
  • evaluate_dataset_sejong(): 세종 모드 평가 함수

사전 현대화 계획

  • mecab-ko-dic v3.0 현대화 계획 문서
  • 목표: 816K → 1M+ 엔트리
  • Phase 1-4 로드맵 (Sprint 20-26)

측정 결과

지표기존세종 모드개선
Token Accuracy15.2%16.8%+1.6%p
Sentence Accuracy8.1%10.0%+1.9%p
F1 Score0.1650.183+0.018

0.3.0 - 2026-03-03

추가됨 (Added)

K-best 경로 탐색 (mecab-ko-core)

  • ImprovedNbestSearcher: K-best Viterbi 알고리즘
  • NbestPath, NbestResult 구조체
  • 각 노드에서 K개의 최선 후보 유지
  • 이터레이터 지원 (iter(), IntoIterator)

사용자 정의 분석 모드 (mecab-ko-core)

  • AnalysisMode enum: Full, NounsOnly, VerbsOnly 등 10가지 모드
  • PosFilter: 품사 필터링 (접두사/정확 매칭)
  • AnalyzerConfig: 분석 설정 조합
  • 편의 함수: extract_nouns(), extract_verbs()

Lattice 시각화 (mecab-ko-core)

  • LatticeViz: 시각화 도구
  • DOT, HTML, Text, JSON 출력 포맷
  • d3-graphviz 기반 인터랙티브 뷰어

토큰화 캐싱 (mecab-ko-core)

  • TokenCache: LRU 캐시 (스레드 안전)
  • CachingTokenizer<T>: 토크나이저 래퍼
  • 캐시 히트/미스 통계

스트리밍 API 개선 (mecab-ko-core)

  • ProgressStreamingTokenizer: 진행률 콜백
  • LargeFileProcessor: 대용량 파일 처리
  • 스마트 문장 경계 청킹
  • 오버랩 청킹 지원

npm 배포

  • mecab-ko-wasm v0.3.0 npm에서 설치 가능

0.2.0 - 2026-03-02

추가됨 (Added)

사전 동기화 (mecab-ko-dict-sync)

  • OpenDictClient: 국립국어원 API 연동 클라이언트
  • DictConverter: NIKL에서 MeCab-Ko 형식으로 변환
  • 30+ 품사 태그 매핑 (명사->NNG, 고유명사->NNP, 동사->VV 등)
  • 빈도 기반 비용 계산 (high=0, medium=500, low=1000)
  • UserEntry::to_csv_line(): MeCab-Ko 호환 CSV 출력

CLI 개선 (mecab-ko-cli)

  • sync 서브커맨드: 사전 동기화
    • --source opendict: NIKL OpenDict API
    • --query: 검색어
    • --api-key: API 키
    • --output: CSV 출력 파일
  • evaluate 서브커맨드: 정확도 평가
    • Token/Sentence/POS Accuracy 측정
    • Precision/Recall/F1 계산
    • 품사별 정확도 리포트
  • --benchmark N: 성능 측정 옵션
  • --stats: 분석 통계 옵션
  • REPL 7가지 출력 포맷 전환

사용자 사전 개선 (mecab-ko-dict)

  • validate(): 항목 검증
  • stats(): 사전 통계
  • remove_duplicates(): 중복 제거
  • remove_surface(): 표면형으로 삭제
  • estimate_pos(): 자동 품사 추정
  • check_csv_duplicates(): CSV 검증
  • check_system_conflicts(): 시스템 사전 충돌 검사

사전 품질 검증 (mecab-ko-dict-validator)

  • 품사 태그 분포 분석
  • 비용 값 분포 분석 (평균/중앙값/표준편차)
  • 이상치 탐지 (3-sigma, IQR)
  • 일관성 검사 강화
  • --analyze, --fix CLI 플래그

Unknown 단어 처리 개선 (mecab-ko-core)

  • WordPattern 열거형: Plain, ProperNoun, CamelCase, HangulAlphaMix, NumberUnit, Emoji
  • 패턴별 비용 조정
  • 품사 태그 추정 개선

복합명사 분해 개선

  • 종성 패턴 분석 알고리즘 개선
  • 접미사 자동 감지: 들, 님, 분, 꾼
  • 접두사 자동 감지: 신, 구, 총, 부, 전, 후
  • Character offset 정확도 개선

커뮤니티 기여

  • CONTRIBUTING.md: 신조어 추가 가이드
  • 4개 이슈 템플릿
  • CODE_OF_CONDUCT.md (Contributor Covenant 2.0)
  • PR 템플릿

신조어 사전

  • 123개 신조어 (2018-2024)
  • data/user-dict/neologisms.csv

변경됨 (Changed)

  • 최소 Rust 버전: 1.75
  • Node.js 22 지원
  • Python 3.13 지원

수정됨 (Fixed)

  • WASM 빌드 (wasm-opt = false)
  • dict-sync 크레이트 Clippy 경고

문서

  • 마이그레이션 가이드
  • NIKL API 조사 문서
  • mecab-ko-dic 현대화 계획

0.1.1 - 2026-03-01

추가됨 (Added)

  • crates.io 발행 (6개 크레이트)
  • GitHub Releases 자동화
  • 성능 회귀 탐지 CI
  • mdBook 문서 사이트
  • Docker 이미지 (GHCR)
  • Chart.js 성능 대시보드

수정됨 (Fixed)

  • LazyEntries, mmap 메모리 최적화
  • WASM zstd-sys 이슈 (optional feature)

0.1.0 - 2026-03-01

추가됨 (Added)

핵심 라이브러리 (mecab-ko-core)

  • Viterbi 알고리즘 구현
  • Lattice 구조
  • 토크나이저
  • Zero-cost 추상화

한글 유틸리티 (mecab-ko-hangul)

  • 자모 분리/결합
  • 음절 처리
  • 문자 유형 분류
  • 종성 판별

사전 관리 (mecab-ko-dict)

  • Double-Array Trie
  • 연접 비용 매트릭스
  • 사용자 사전 (CSV)
  • 메모리 매핑

CLI (mecab-ko-cli)

  • 다양한 출력 포맷
  • N-best 출력
  • 배치 처리

바인딩

  • Python (PyO3, KoNLPy 호환)
  • Node.js (napi-rs)
  • WASM (wasm-bindgen)

Elasticsearch (mecab-ko-elasticsearch)

  • Nori 호환 분석기
  • 토큰 필터
  • Decompound 모드

CI/CD

  • GitHub Actions 워크플로우
  • 벤치마크 자동화
  • 문서 배포

레거시 버전 (C/C++ 구현)

[0.9.2] (mecab-0.996)

  • dicdir 값 수정
  • Java SWIG 메모리 누수 수정

[0.9.1] (mecab-0.996)

  • 새 사전 항목 추가 버그 수정
  • 자동 문맥 ID 조회

[0.9.0] (mecab-0.996)

  • MeCab 0.996 기반
  • 좌측 공백 연접 비용 증가 기능

로드맵

단기 계획 (v0.4.x - v0.5.0)

  • crates.io 정식 배포 ✅
  • mecab-ko-dic v3.0 (100만+ 엔트리)
  • 정확도 40%+ 달성 (사전 품질 개선)
  • PyPI 배포
  • 신조어 자동 수집 파이프라인 실행

중기 계획 (v0.5.0)

  • 정확도 70%+ 달성
  • OpenSearch 플러그인
  • 실시간 사전 업데이트

장기 계획 (v1.0.0)

  • Production-ready Elasticsearch 플러그인
  • 정확도 90%+ 달성
  • PyPI 정식 배포

버전 정책

버전 번호

  • MAJOR: 하위 호환성이 깨지는 API 변경
  • MINOR: 하위 호환성을 유지하는 기능 추가
  • PATCH: 하위 호환성을 유지하는 버그 수정

지원 정책

  • 최신 버전만 지원
  • 보안 문제는 이전 MINOR 버전까지 패치 제공

기여자

프로젝트에 기여해 주신 모든 분들께 감사드립니다.

  • hephaex (@hephaex) - 프로젝트 리더

기여하고 싶으시다면 GitHub에서 참여해 주세요.


마이그레이션 가이드

기존 MeCab-Ko C++ 버전에서 Rust 버전으로 마이그레이션하는 방법을 안내합니다.

C++ API에서 Rust API로

Tagger 생성

C++ (기존)

MeCab::Tagger *tagger = MeCab::createTagger("");

Rust (신규)

#![allow(unused)]
fn main() {
let tagger = Tagger::new(TaggerConfig::default())?;
}

분석 실행

C++ (기존)

const char* result = tagger->parse("안녕하세요");

Rust (신규)

#![allow(unused)]
fn main() {
let result = tagger.parse("안녕하세요")?;
}

Node 순회

C++ (기존)

for (const MeCab::Node *node = tagger->parseToNode(text);
     node;
     node = node->next) {
    std::cout << node->surface << std::endl;
}

Rust (신규)

#![allow(unused)]
fn main() {
let result = tagger.parse("텍스트")?;
for node in result.iter() {
    println!("{}", node.surface);
}
}

리소스 정리

C++ (기존)

delete tagger;

Rust (신규)

#![allow(unused)]
fn main() {
// 자동으로 Drop 트레잇이 처리
// 명시적으로 drop(tagger); 가능하지만 불필요
}

Python 바인딩 마이그레이션

mecab-python에서 mecab-ko로

mecab-python (기존)

import MeCab

tagger = MeCab.Tagger()
result = tagger.parse("안녕하세요")

mecab-ko (신규)

from mecab_ko import Tagger

tagger = Tagger()
result = tagger.parse("안녕하세요")

대부분의 API가 호환되므로 import만 변경하면 됩니다.

설정 파일 마이그레이션

mecabrc 파일

C++ (기존)

dicdir = /usr/local/lib/mecab/dic/mecab-ko-dic
userdic = /path/to/user.dic

Rust (신규)

#![allow(unused)]
fn main() {
let config = TaggerConfig {
    dict_dir: Some(PathBuf::from("/usr/local/lib/mecab/dic/mecab-ko-dic")),
    user_dict: Some(PathBuf::from("/path/to/user.csv")),
    ..Default::default()
};
}

CLI 도구 마이그레이션

명령어 옵션

대부분의 옵션이 호환됩니다:

C++Rust설명
-d DIR-d DIR사전 디렉토리
-u FILE--user-dic FILE사용자 사전
-O FORMAT-o FORMAT출력 포맷
-N NUM--nbest NUMN-best 개수

사전 포맷 호환성

Rust 버전은 기존 C++ 사전 파일을 그대로 사용할 수 있습니다.

# 기존 mecab-ko-dic 사용
mecab-ko -d /usr/local/lib/mecab/dic/mecab-ko-dic "텍스트"

성능 차이

일반적으로 Rust 버전이 C++ 버전과 유사하거나 약간 더 빠릅니다:

  • 단일 스레드: C++ 대비 95-105%
  • 멀티 스레드: C++ 대비 110-130% (Rayon 사용 시)
  • 메모리 사용: C++ 대비 80-90%

호환성 문제

1. 출력 포맷 차이

일부 edge case에서 출력 포맷이 약간 다를 수 있습니다. 대부분의 경우 동일합니다.

2. 사용자 사전 포맷

CSV 포맷이 조금 다릅니다:

C++ (기존)

표면형,좌문맥ID,우문맥ID,비용,품사,...

Rust (신규, 간소화 지원)

표면형,품사,비용,기본형

둘 다 지원하므로 기존 파일도 사용 가능합니다.

빌드 시스템 마이그레이션

Makefile에서 Cargo로

Makefile (기존)

mecab: main.o
    g++ -o mecab main.o -lmecab

Cargo.toml (신규)

[dependencies]
mecab-ko = "0.1"

Docker 이미지 마이그레이션

Dockerfile (C++ 기존)

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y mecab mecab-ko-dic

Dockerfile (Rust 신규)

FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM ubuntu:22.04
COPY --from=builder /app/target/release/mecab-ko /usr/local/bin/

단계별 마이그레이션

1단계: 병렬 실행

기존 C++ 버전과 Rust 버전을 동시에 실행하여 결과 비교:

# C++ 버전
echo "테스트" | mecab-ko-old > output_cpp.txt

# Rust 버전
echo "테스트" | mecab-ko > output_rust.txt

# 비교
diff output_cpp.txt output_rust.txt

2단계: 테스트 환경에서 검증

# 기존 시스템 테스트
import MeCab_old
tagger_old = MeCab_old.Tagger()

# 새 시스템 테스트
from mecab_ko import Tagger
tagger_new = Tagger()

# 결과 비교
test_texts = ["문장1", "문장2", "문장3"]
for text in test_texts:
    result_old = tagger_old.parse(text)
    result_new = tagger_new.parse(text)
    assert result_old == result_new

3단계: 점진적 전환

  1. 새로운 기능부터 Rust 버전 사용
  2. 읽기 전용 작업을 Rust로 전환
  3. 중요하지 않은 서비스 전환
  4. 메인 서비스 전환

체크리스트

  • 의존성 확인 (Rust 1.70.0 이상)
  • 사전 파일 호환성 확인
  • API 변경사항 파악
  • 테스트 케이스 작성
  • 성능 벤치마크
  • 롤백 계획 수립
  • 모니터링 설정
  • 문서 업데이트

문제 해결

"사전을 찾을 수 없음" 오류

# 사전 경로 확인
mecab-ko -d /path/to/dict --version

# 환경 변수 설정
export MECAB_DICT_DIR=/usr/local/lib/mecab/dic/mecab-ko-dic

"성능 저하" 문제

#![allow(unused)]
fn main() {
// Release 빌드 사용
cargo build --release

// 병렬 처리 활성화
use rayon::prelude::*;
results = texts.par_iter().map(|t| tagger.parse(t)).collect();
}

지원

마이그레이션 관련 질문:

  • GitHub Issues
  • GitHub Discussions