– 캡스톤 프로젝트 실전 적용 사례 –
캡스톤 프로젝트를 진행하면서 가장 어려웠던 부분 중 하나는
“긴 한국어 텍스트에서 핵심 키워드를 어떻게 추출할 것인가”였다.
특히 나는 음성(STT) → 텍스트 변환 → 핵심 키워드 자동 추출이라는 기능이 필요했기 때문에
단순 문자열 검색 방식으로는 정확한 분석이 어렵다는 한계를 느꼈다.
그 과정에서 적용한 오픈소스가 바로 KOMORAN, 한국어 형태소 분석기이다.
이번 글에서는 내가 실제 캡스톤 프로젝트에서 KOMORAN을 활용해 키워드 추출 기능을 구현한 과정을 정리한다.
- KOMORAN이란?
KOMORAN(KOrean MORphological ANalyzer)은 한국어 문장을 형태소 단위로 분석하고
품사 태깅(POS Tagging)을 수행해주는 오픈 소스 NLP 도구이다.
한국어는 조사·어미·복합어가 많아 단순 토큰화로는 의미를 잡기 어려우므로
KOMORAN과 같은 형태소 분석기를 사용하면 명사·동사·형용사 등 의미 기반 처리가 가능하다.
- 내가 KOMORAN을 선택한 이유 (캡스톤 관점)
1. 한국어 분석 정확도가 높음
2. Java + Spring Boot 환경에 자연스럽게 연동됨
3. MIT 기반 오픈소스로 연구·개발 프로젝트에 부담 없이 적용 가능
4. 명사 추출 기능(getNouns())이 핵심 키워드 분석에 매우 적합함
내 내 프로젝트는 서버가 Spring Boot로 구축되어 있었기 때문에 KOMORAN의 Java 지원은 결정적인 장점이었다.
- 전체 처리 흐름
내가 구현한 키워드 추출 기능은 다음과 같은 구조로 이루어졌다.
STT 텍스트 입력
↓
KOMORAN 형태소 분석
↓
명사 추출(getNouns)
↓
한 글자 단어 / 불용어 제거
↓
TF(단순 빈도 기반) 분석
↓
상위 N개 키워드 선정
↓
문자열 또는 JSON 형태로 반환
이 과정을 모두 하나의 서비스로 묶어 프로젝트 어디서든 쉽게 호출할 수 있도록 설계했다.
- 실제 구현한 코드 (KeywordExtractorService)
아래는 내가 실제로 캡스톤 프로젝트에서 작성하여 사용한 코드 전체이다.
Spring Boot 기반으로 서비스 계층에서 KOMORAN을 초기화하고,
텍스트 입력에 따라 명사를 추출해 키워드를 계산하는 기능을 포함한다.
package com.example.innobyte.service;
import kr.co.shineware.nlp.komoran.constant.DEFAULT_MODEL;
import kr.co.shineware.nlp.komoran.core.Komoran;
import kr.co.shineware.nlp.komoran.model.KomoranResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
/**
* 텍스트에서 중요 키워드를 추출하기 위한 서비스
* Komoran 한국어 분석기를 사용해서 명사를 추출 + TF 기반으로 중요도를 계산
*/
@Slf4j
@Service
public class KeywordExtractorService {
private Komoran komoran;
private static final Set<String> KEYWORD_POS = Set.of("NNG", "NNP", "NNB");
private static final Set<String> STOPWORDS = Set.of(
"것", "수", "등", "때", "개", "점", "번", "명", "곳");
@PostConstruct
public void init() {
try {
log.info("Komoran 형태소 분석기 초기화 시작");
this.komoran = new Komoran(DEFAULT_MODEL.FULL);
log.info("Komoran 형태소 분석기 초기화 완료");
} catch (Exception e) {
log.error("Komoran 초기화 실패", e);
throw new RuntimeException("Komoran 초기화 실패", e);
}
}
public Map<String, Integer> extractKeywords(String text, int topN) {
if (text == null || text.trim().isEmpty()) {
log.warn("입력 텍스트가 비어있습니다.");
return Collections.emptyMap();
}
try {
log.debug("키워드 추출 시작 - 텍스트 길이: {} 글자", text.length());
KomoranResult result = komoran.analyze(text);
List<String> nouns = result.getNouns();
log.debug("추출된 명사 개수: {}", nouns.size());
Map<String, Integer> wordFrequency = calculateWordFrequency(nouns);
Map<String, Integer> topKeywords = getTopKeywords(wordFrequency, topN);
log.info("키워드 추출 완료 - 총 {}개 키워드", topKeywords.size());
return topKeywords;
} catch (Exception e) {
log.error("키워드 추출 중 오류 발생", e);
return Collections.emptyMap();
}
}
private Map<String, Integer> calculateWordFrequency(List<String> nouns) {
Map<String, Integer> wordFrequency = new HashMap<>();
for (String noun : nouns) {
if (noun.length() <= 1 || STOPWORDS.contains(noun)) {
continue;
}
wordFrequency.put(noun, wordFrequency.getOrDefault(noun, 0) + 1);
}
return wordFrequency;
}
private Map<String, Integer> getTopKeywords(Map<String, Integer> wordFrequency, int topN) {
return wordFrequency.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(topN)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new));
}
public String formatKeywords(Map<String, Integer> keywords) {
if (keywords == null || keywords.isEmpty()) {
return "추출된 키워드가 없습니다.";
}
StringBuilder sb = new StringBuilder("");
keywords.forEach((word, count) -> sb.append(String.format("%s(%d회), ", word, count)));
if (sb.length() > 10) {
sb.setLength(sb.length() - 2);
}
return sb.toString();
}
public String toJsonString(Map<String, Integer> keywords) {
if (keywords == null || keywords.isEmpty()) {
return "{\"keywords\": []}";
}
StringBuilder json = new StringBuilder("{\"keywords\": [");
keywords.forEach(
(word, count) -> json.append(String.format("{\"word\": \"%s\", \"count\": %d},", word, count)));
if (json.charAt(json.length() - 1) == ',') {
json.setLength(json.length() - 1);
}
json.append("]}");
return json.append("]}").toString();
}
public void addStopword(String word) {
if (word != null && !word.trim().isEmpty()) {
STOPWORDS.add(word.trim());
log.info("불용어 추가: {}", word);
}
}
}
- KOMORAN 적용 효과 (실제 프로젝트 기준)
1. STT 특성상 자주 등장하는 불필요 단어(것, 수, 등) 제거
→ 텍스트 분석 품질이 확실히 향상됨.
2. 긴 문장에서 핵심 주제를 자동으로 요약하는 데 활용
→ 키워드를 기반으로 요약·분류 기능 확장 가능.
3. 백엔드에서 자동 처리 → 프론트에서 손쉽게 시각화 가능
→ 차트, 워드클라우드 등에 바로 활용할 수 있음.
4. 오픈소스 기반 → 빠르게 적용 가능
→ 캡스톤처럼 시간 압박이 큰 프로젝트에서 매우 유리함.
- 마무리
KOMORAN은 한국어 기반 캡스톤 프로젝트에서
형태소 분석 + 명사 추출 + 키워드 분석을 빠르게 구현할 수 있는 강력한 도구다.
특히 음성 기반 서비스나 텍스트 분류·요약 시스템을 만든다면
KOMORAN을 활용한 키워드 추출은 정말 큰 도움이 된다.
이번 글에서는 내가 직접 구현한 실제 코드를 바탕으로
KOMORAN을 어떻게 적용했는지를 설명했다.
이 구조는 챗봇, 회의록 자동 요약, 감정 분석 등
다양한 응용 서비스에서도 그대로 적용할 수 있다.