카테고리 없음

KOMORAN 오픈 소스를 활용한 한국어 키워드 추출

ehdbs7908 2025. 12. 4. 21:31

– 캡스톤 프로젝트 실전 적용 사례 –

 

캡스톤 프로젝트를 진행하면서 가장 어려웠던 부분 중 하나는

“긴 한국어 텍스트에서 핵심 키워드를 어떻게 추출할 것인가”였다.

 

특히 나는 음성(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을 어떻게 적용했는지를 설명했다.

 

이 구조는 챗봇, 회의록 자동 요약, 감정 분석 등
다양한 응용 서비스에서도 그대로 적용할 수 있다.