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 호환 분석기
성능 지표
| 지표 | 목표 | 측정값 | 상태 |
|---|---|---|---|
| Throughput | 150K ops/sec | 238K | PASS |
| Cold Start | < 200ms | 132ms | PASS |
| Memory | < 150MB | 145MB | PASS |
프로젝트 구조
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-core | Lattice, 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-cli | mecab-ko 명령줄 도구 | v0.4.0 |
mecab-ko-elasticsearch | Nori 호환 분석기 | v0.4.0 |
mecab-ko-wasm | WebAssembly 바인딩 | v0.3.1 (npm) |
다른 프로젝트와의 비교
| 프로젝트 | 언어 | Throughput | Memory | 특징 |
|---|---|---|---|---|
| mecab-ko (원본) | C++ | 18 MB/s | ~80 MB | 원조, 유지보수 중단 |
| Kiwi | C++ | 22 MB/s | ~150 MB | 독자 모델, 높은 정확도 |
| Lindera | Rust | 12 MB/s | ~180 MB | 일본어 중심 |
| MeCab-Ko | Rust | 15 MB/s | ~145 MB | mecab-ko 호환, 순수 Rust |
v0.4.0 주요 변경사항
crates.io 정식 배포 🎉
- 6개 크레이트 crates.io 정식 배포 완료
cargo add mecab-ko로 손쉬운 설치- MIT/Apache 2.0 듀얼 라이선스
세종 코퍼스 호환 모드 강화
- 후처리 파이프라인 완성 (4단계)
apply_decomposition_corrections(): 오분석 패턴 보정apply_token_merges(): 잘못 분해된 토큰 병합apply_lexicon_overrides(): 고빈도 어휘 매핑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 | 사전 빌더 기능 포함 |
serde | JSON 직렬화 지원 |
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 |
| Windows | C:\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 default | MeCab 호환 포맷 |
| Wakati | -O wakati | 형태소만 공백 분리 |
| JSON | -O json | JSON 배열 |
| CSV | -O csv | CSV 포맷 |
| 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의 기본적인 사용법을 단계별로 배웁니다.
목차
환경 설정
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}")
토큰 정보 활용
토큰 구조체
각 토큰은 다음 정보를 포함합니다:
| 필드 | 타입 | 설명 |
|---|---|---|
surface | String | 표면형 (실제 텍스트) |
pos | String | 품사 태그 |
start | usize | 시작 위치 (바이트) |
end | usize | 끝 위치 (바이트) |
reading | Option | 읽기 정보 |
lemma | Option | 기본형 (원형) |
위치 정보 활용
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의 고급 기능을 활용하는 방법을 알아봅니다.
목차
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를 웹 애플리케이션에 통합하는 방법을 알아봅니다.
목차
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 엔드포인트
| Method | Path | Description |
|---|---|---|
| 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);
다음 단계
- 성능 튜닝: 서버 최적화
- Elasticsearch 통합: 검색 엔진 연동
- 벤치마크 가이드: 성능 측정
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 "형태소 분석"
사용 가능한 포맷:
| 포맷 | 설명 | 예시 출력 |
|---|---|---|
default | MeCab 기본 포맷 (기본값) | 안녕\tNNG |
wakati | 형태소만 공백 분리 | 안녕 하 세요 |
json | JSON 배열 | [{"surface":"안녕",...}] |
csv | CSV 헤더 포함 | 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 Score | F1 점수 |
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
}
]
필드 설명:
| 필드 | 타입 | 설명 |
|---|---|---|
surface | string | 표면형 |
pos | string | 품사 태그 |
start | number | 시작 바이트 위치 |
end | number | 끝 바이트 위치 |
reading | string? | 읽기 (있는 경우) |
lemma | string? | 원형 (있는 경우) |
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
문제 해결
엔트리가 적용되지 않음
- 비용이 너무 높은지 확인 (음수 값 사용)
- 품사 태그가 올바른지 확인
- 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는 다양한 출력 포맷을 지원합니다. 용도에 따라 적절한 포맷을 선택하세요.
포맷 비교
| 포맷 | 용도 | 파싱 용이성 | 가독성 |
|---|---|---|---|
| default | MeCab 호환 | 중간 | 좋음 |
| 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
}
]
필드 설명
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
surface | string | O | 표면형 (원문 그대로) |
pos | string | O | 품사 태그 |
start | number | O | 시작 바이트 위치 |
end | number | O | 끝 바이트 위치 (exclusive) |
reading | string | X | 읽기/발음 |
lemma | string | X | 원형/기본형 |
활용 예시
# 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 | 사전 빌더 기능 포함 |
python | Python 바인딩 포함 |
wasm | WASM 지원 포함 |
rayon | 병렬 처리 지원 |
serde | JSON 직렬화 지원 |
성능 최적화
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 형식의 사전 소스 파일에서 바이너리 사전을 빌드하는 도구를 제공합니다.
개요
사전 빌드 프로세스:
- CSV 사전 파일 준비
- 특성 정의 파일 작성
- 사전 빌더 실행
- 바이너리 사전 생성
사전 파일 구조
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++) | 18 | 150 |
| MeCab-Ko (Rust) | 15 | 120 |
| Lindera | 12 | 180 |
| Kiwi | 22 | 200 |
참고 자료
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
버전 호환성:
| Elasticsearch | Plugin Version |
|---|---|
| 8.11.x | 8.11.0 |
| 8.10.x | 8.10.0 |
| 7.17.x | 7.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_format | mecab | 출력 포맷 |
space_penalty | -1000 | 띄어쓰기 패널티 |
compound_noun_min_length | 2 | 복합명사 최소 길이 |
decompound | false | 복합명사 분해 |
pos_filter | - | 품사 필터 (배열) |
max_unk_length | 24 | 미등록어 최대 길이 |
사용 예제
기본 분석
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
상세 문서
더 자세한 내용은 다음 문서를 참조하세요:
- Elasticsearch / OpenSearch 통합 가이드 - 전체 설치 및 설정 가이드
- Nori 호환성 가이드 - Nori 플러그인과의 호환성 및 마이그레이션
- 예제 설정 파일 - 바로 사용 가능한 설정 템플릿
참고 자료
커스텀 분석기
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 Accuracy | 100.0% |
| Sentence Accuracy | 100.0% |
| F1 Score | 1.000 |
| 테스트 문장 | 500개 |
KPI 목표 및 현황
| 지표 | 목표 | v0.1.0 | v0.4.0 | v0.5.0 | 상태 |
|---|---|---|---|---|---|
| Token Accuracy | 95%+ | 29.6% | 81.0% | 100.0% | ✅ PASS |
| Throughput | 200K ops/sec | 182K | 245K | 263K | ✅ PASS |
| Cold Start | < 200ms | 120ms | 86ms | 86ms | ✅ PASS |
| Memory | < 150MB | 215MB | 145MB | 145MB | ✅ PASS |
v0.5.0은 정확도 100%를 달성하면서도 성능을 유지하고 있습니다.
벤치마크 환경
| 항목 | 값 |
|---|---|
| OS | Ubuntu 22.04 (GitHub Actions) |
| CPU | AMD EPYC 7763 (2 cores) |
| Memory | 7 GB |
| Rust | 1.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의 벤치마크를 실행하고 성능을 측정하는 방법을 안내합니다.
목차
벤치마크 환경
시스템 요구사항
| 항목 | 최소 | 권장 |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8+ GB |
| Rust | 1.70+ | 1.75+ |
| 사전 | mini-dict | full-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 | 문장당 평균 시간 |
| throughput | MB/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 ) }) }); } }
성능 최적화 팁
벤치마크 시 주의사항
-
릴리스 빌드 사용
cargo bench # 기본적으로 --release -
시스템 부하 최소화
- 다른 프로그램 종료
- 백그라운드 프로세스 최소화
-
충분한 워밍업
#![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 } } -
통계적 유의성 확인
- 최소 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 목표
| 지표 | 목표 | 현재 | 상태 |
|---|---|---|---|
| Throughput | 200K ops/sec | 238K | PASS |
| Cold Start | < 200ms | 132ms | PASS |
| Memory | < 150MB | 145MB | PASS |
참고 자료
품사 태그
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-Ko | Kiwi | 설명 |
|---|---|---|
| NNG | NNG | 일반 명사 |
| NNP | NNP | 고유 명사 |
| VV | VV | 동사 |
| VA | VA | 형용사 |
| JKS | JKS | 주격 조사 |
참고 자료
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. 저장소 구조
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 비용 품사 의미분류 종성 읽기 타입 첫품사 끝품사 분석결과
| # | 필드명 | 설명 | 예시 |
|---|---|---|---|
| 1 | surface | 표층형 (실제 단어) | 가건물 |
| 2 | left_id | 좌측 문맥 ID | 0 |
| 3 | right_id | 우측 문맥 ID | 0 |
| 4 | cost | 단어 비용 | 0 |
| 5 | pos | 품사 태그 | NNG |
| 6 | semantic_class | 의미 분류 | * |
| 7 | jongseong | 종성 유무 (T/F) | T |
| 8 | reading | 읽기/기본형 | 가건물 |
| 9 | type | 타입 | Compound, Inflect, * |
| 10 | first_pos | 첫 형태소 품사 | * |
| 11 | last_pos | 끝 형태소 품사 | * |
| 12 | expression | 형태소 분해 | 가/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 카테고리
| 카테고리 | INVOKE | GROUP | LENGTH | 설명 |
|---|---|---|---|---|
| DEFAULT | 0 | 1 | 0 | 기본 |
| SPACE | 0 | 1 | 0 | 공백 |
| HANGUL | 0 | 1 | 2 | 한글 |
| HANJA | 0 | 0 | 1 | 한자 |
| ALPHA | 1 | 1 | 0 | 알파벳 |
| NUMERIC | 1 | 1 | 0 | 숫자 |
| SYMBOL | 1 | 1 | 0 | 기호 |
| HANJANUMERIC | 1 | 1 | 0 | 한자 숫자 |
속성 설명
| 속성 | 값 | 의미 |
|---|---|---|
| INVOKE | 0 | 사전에 있으면 미등록어 처리 생략 |
| INVOKE | 1 | 항상 미등록어 후보도 생성 |
| GROUP | 0 | 그룹핑 비활성화 |
| GROUP | 1 | 동일 카테고리 문자 그룹핑 |
| LENGTH | n | 1~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 미등록어 매핑
| 카테고리 | 품사 | 설명 |
|---|---|---|
| DEFAULT | SY | 기본 → 기호 |
| SPACE | SP | 공백 |
| HANGUL | UNKNOWN | 한글 미등록어 |
| HANJA | SH | 한자 |
| ALPHA | SL | 외국어 |
| NUMERIC | SN | 숫자 |
| SYMBOL | SY | 기호 |
| HIRAGANA | SL | 외국어 |
| KATAKANA | SL | 외국어 |
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.bin | CRF 모델 (선택) | 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-train | CRF 모델 학습 |
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) | 일반적 사용 |
SparseMatrix | HashMap으로 희소 엔트리만 저장 | 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.1 | 2026-01-05 | Rust 구현체 Entry/Matrix/Trie 구조체 문서 추가 |
| v2.0 | 2026-01-04 | 사전 포맷 완전 분석 및 문서화 |
| v1.0 | - | 초기 mecab-ko-dic 포맷 |
바이너리 사전 포맷 v3.0 설계 명세서
문서 버전: 3.0 작성일: 2026-01-05 이슈: DIC-010 바이너리 사전 포맷 v3.0 설계 상태: Draft
목차
- 개요
- 설계 목표
- 파일 구조
- 공통 헤더 구조
- Double-Array Trie 바이너리 포맷
- Connection Matrix 바이너리 포맷
- 엔트리 데이터 포맷
- 압축 지원
- 메모리 매핑 지원
- 버전 관리 메커니즘
- 사용자 사전 포맷
- 구현 참조
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 핵심 목표
- 빠른 로딩: 메모리 매핑을 통한 지연 로딩 지원
- 효율적 압축: Zstd 압축으로 디스크 공간 절약 (30-50% 크기 감소 목표)
- 메모리 효율성: Zero-copy 접근 가능한 구조
- 하위 호환성: 버전 업그레이드 시 graceful degradation
- 크로스 플랫폼: 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 | 사전 메타정보 | O | X |
sys.dic | 시스템 사전 | O | O |
matrix.bin | 연접 비용 행렬 | O | O |
char.bin | 문자 카테고리 | O | X |
unk.bin | 미등록어 정의 | O | X |
user.dic | 사용자 사전 | X | O |
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 코드
| 코드 | 타입 | 설명 |
|---|---|---|
| 0x01 | SYS_DICT | 시스템 사전 |
| 0x02 | USER_DICT | 사용자 사전 |
| 0x03 | MATRIX | 연접 비용 행렬 |
| 0x04 | CHAR_DEF | 문자 카테고리 |
| 0x05 | UNK_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에서는 세 가지 저장 전략을 지원합니다:
- DenseMatrix: 모든 값을 메모리에 저장 (기본)
- SparseMatrix: 희소 행렬 최적화
- 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 지원 압축 알고리즘
| 알고리즘 | 플래그 값 | 압축률 | 속도 | 권장 용도 |
|---|---|---|---|---|
| Zstd | 0 | 매우 높음 | 빠름 | 기본 권장 |
| 미압축 | - | 없음 | 최고 | 메모리 맵 |
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.dic | sys.dic.zst |
matrix.bin | matrix.bin.zst |
user.dic | user.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 메모리 맵 최적화 파일 구조
메모리 맵을 효율적으로 사용하려면 다음 조건을 만족해야 합니다:
- 정렬: 데이터 섹션이 페이지 경계(4KB)에 정렬
- 미압축: 압축된 파일은 mmap 불가
- 고정 크기: 가변 길이 데이터는 별도 섹션에 분리
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 + 압축 해제 캐시 |
| WebAssembly | DenseMatrix (mmap 미지원) |
10. 버전 관리 메커니즘
10.1 Semantic Versioning
Major.Minor.Patch
│ │ │
│ │ └── 버그 수정 (하위 호환)
│ └────── 기능 추가 (하위 호환)
└──────────── 호환성 변경 (하위 비호환 가능)
10.2 호환성 규칙
| 버전 변경 | 읽기 가능 | 쓰기 가능 |
|---|---|---|
| Major 증가 | X (마이그레이션 필요) | X |
| Minor 증가 | O (기존 필드만) | O (새 필드 무시) |
| Patch 증가 | O | O |
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 MB | 50% |
| matrix.bin | ~6 MB | ~2 MB | 67% |
| char.bin | ~130 KB | N/A | - |
| unk.bin | ~10 KB | N/A | - |
| 합계 | ~56 MB | ~27 MB | 52% |
부록
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. 향후 확장 계획
- v3.1: 증분 업데이트 지원
- v3.2: 원격 사전 로딩 (HTTP)
- v4.0: FST(Finite State Transducer) 기반 Trie
참고 자료
- yada 라이브러리 - Double-Array Trie
- memmap2 - 메모리 매핑
- zstd - Zstandard 압축
- MeCab-Ko-Dic 포맷 v2 - 기존 포맷 분석
- Lindera 분석 - 참조 구현 분석
문서 버전: 3.0 최종 수정: 2026-01-05
MeCab-Ko Rust 프로젝트 구조
문서 버전: 1.0 작성일: 2026-01-04 이슈: RST-002 프로젝트 구조 및 Cargo workspace 설계
목차
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-ko | lib | 스텁 | Facade - 모든 기능 재export |
mecab-ko-core | lib | 스텁 | Lattice, Viterbi, Tokenizer |
mecab-ko-dict | lib | 스텁 | 사전 로딩, 검색, 연접 비용 |
mecab-ko-dict-builder | lib+bin | 스텁 | CSV → 바이너리 변환 |
mecab-ko-hangul | lib | 완료 | 한글 자모 처리 |
mecab-ko-cli | bin | 스텁 | 명령줄 도구 |
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
부록: 주요 의존성
| 의존성 | 버전 | 용도 |
|---|---|---|
| thiserror | 1.0 | 에러 정의 |
| anyhow | 1.0 | 에러 전파 |
| serde | 1.0 | 직렬화 |
| fst | 0.4 | FST 자료구조 |
| yada | 0.5 | Double Array Trie |
| memmap2 | 0.9 | Memory-mapped IO |
| zstd | 0.13 | 압축 |
| rkyv | 0.8 | Zero-copy 직렬화 |
| csv | 1.3 | CSV 파싱 |
| clap | 4.4 | CLI 파싱 |
| criterion | 0.5 | 벤치마크 |
| pyo3 | 0.20 | Python 바인딩 |
참고 자료
- docs/lindera-analysis.md - Lindera 분석 보고서
- docs/dictionary-format-v2.md - 사전 포맷 문서
- docs/pos-tag-mapping.md - 품사 태그 매핑
- docs/build-process.md - 빌드 프로세스
MeCab-Ko-Dic 빌드 프로세스 가이드
문서 버전: 1.0 작성일: 2026-01-04 대상: mecab-ko-dic 사전 빌드 시스템
이 문서는 DIC-001 이슈의 산출물로, mecab-ko-dic의 빌드 프로세스를 상세히 문서화합니다.
목차
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
처리 내용:
- CSV 파일의
left_id,right_id,cost컬럼 채우기 - CRF 모델 기반 최적 비용 계산
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.dic | 40-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 | 품사 | 페널티 | 의미 |
|---|---|---|---|
| 120 | JKO (목적격 조사) | 6000 | 조사 앞 공백 억제 |
| 172 | EC (연결 어미) | 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 Trie | darts | yada, daachorse |
| CRF 학습 | crfpp | crfsuite-rs, 자체 구현 |
| 바이너리 직렬화 | 자체 포맷 | bincode, rkyv |
| 문자셋 변환 | iconv | encoding_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 tag | CSV 품사 태그 오류 | pos-id.def 확인 |
charset mismatch | 인코딩 불일치 | -f UTF-8 -t UTF-8 확인 |
matrix size mismatch | ID 범위 초과 | 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
사용법
분석 결과가 이상합니다
- 사전 경로 확인: 올바른 사전이 로드되었는지 확인
- 사용자 사전: 도메인 특화 단어는 사용자 사전에 추가
- 인코딩: 입력 텍스트가 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의 메모리가 필요합니다. 이는 정상적인 동작입니다.
메모리를 줄이려면:
- 필요한 기능만 포함한 경량 사전 사용 (개발 중)
- 서버 환경에서 싱글톤 패턴으로 토크나이저 공유
처리 속도가 느립니다
-
릴리스 빌드 사용:
cargo build --release -
배치 처리: 한 번에 여러 문장 처리
-
토크나이저 재사용: 매번 생성하지 않기
#![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, # 선박
분석기는 문맥에 따라 적절한 분석을 선택합니다.
프로그래밍
병렬 처리가 가능한가요?
Tokenizer는 Send + 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
사전 파일이 손상되었거나 버전이 맞지 않습니다:
- 사전 재다운로드
- 바이너리 사전 버전 확인
- 소스에서 사전 재빌드
Version mismatch
사전 버전과 프로그램 버전이 맞지 않습니다:
# Check versions
mecab-ko version
mecab-ko dict /path/to/dict
호환되는 버전의 사전을 사용하세요.
Failed to initialize tokenizer
- 기본 사전이 포함된 빌드인지 확인
- 환경 변수 확인:
MECAB_KO_DIC_PATH - 명시적 사전 경로 지정
기여
버그를 발견했습니다
GitHub Issues에 보고해 주세요:
- https://github.com/hephaex/mecab-ko/issues
포함할 정보:
- MeCab-Ko 버전
- 운영체제
- 재현 단계
- 입력 텍스트 (가능한 경우)
- 예상 결과 vs 실제 결과
기능 제안을 하고 싶습니다
GitHub Issues에 Feature Request로 등록해 주세요.
코드 기여는 어떻게 하나요?
- 저장소 Fork
- Feature 브랜치 생성
- 변경 사항 커밋
- Pull Request 제출
자세한 내용은 CONTRIBUTING.md를 참조하세요.
추가 질문
이 FAQ에서 답을 찾지 못했다면:
- GitHub Discussions: https://github.com/hephaex/mecab-ko/discussions
- 이슈 등록: https://github.com/hephaex/mecab-ko/issues
- 문서 검색: 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 Accuracy | 29.6% | 23.8% |
| Sentence Accuracy | 14.4% | 7.3% |
| F1 Score | 0.295 | 0.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 Accuracy | 15.2% | 16.8% | +1.6%p |
| Sentence Accuracy | 8.1% | 10.0% | +1.9%p |
| F1 Score | 0.165 | 0.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)
AnalysisModeenum: 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-wasmv0.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,--fixCLI 플래그
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 NUM | N-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단계: 점진적 전환
- 새로운 기능부터 Rust 버전 사용
- 읽기 전용 작업을 Rust로 전환
- 중요하지 않은 서비스 전환
- 메인 서비스 전환
체크리스트
- 의존성 확인 (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