BACK TO RESUME

Profanity Filter Library

GITHUB

2026.01

1인 개발 (기여도 100%)

Aho-Corasick 알고리즘 기반의 O(N) 비속어 탐색 엔진 및 라이브러리

KotlinAho-CorasickTrieJitpackGradleJUnit5

Technical Insights & Record

1. Why this project? (Motivation)

퇴사 후 동기들과 사이드 프로젝트를 진행하며 리뷰 기능에 비속어 필터링이 필요했습니다. 라이브러리 대신 직접 구현하면서 배우자는 판단으로 리서치를 시작했고, 우아한형제들 기술 블로그를 통해 기존 방식의 한계를 분석했습니다.

정규식은 금칙어가 늘수록 O(N×K)로 느려지고, '시1발' 같은 변칙 표현을 커버하려면 패턴이 기하급수적으로 복잡해지는 구조적 문제가 있었습니다.

단순 구현에 그치지 않고 JMH로 기존 방식과 직접 비교 측정하는 것까지 스스로 범위를 확장했습니다. 결과적으로 사이드 프로젝트는 무산됐지만 라이브러리는 끝까지 완성해 Jitpack에 배포했습니다.

2. Performance Results

* JMH(Java Microbenchmark Harness)를 사용하여 직접 측정한 성능 비교 수치입니다.

라이브러리

3.15 ms

단순 정규식

70.64 ms

퍼포먼스 향상

22.4x

시간복잡도

O(N)

Click to Enlarge
Regex vs Aho-Corasick Performance Comparison

3. Architecture & How It Works

Core Architecture

Trie 자료구조를 기반으로 Failure Function을 결합한 아호코라식 알고리즘을 핵심 엔진으로 채택했습니다. 매칭 실패 시 루트로 돌아가지 않고 매칭된 접두사의 가장 긴 접미사 노드로 즉시 이동하여 입력 문자열을 단 한 번만 순회(O(N))하도록 설계했습니다.

Deployment (Jitpack)

오픈소스 라이브러리로 배포하기 위해 Jitpack을 선택했습니다. GitHub 리포지토리의 태그 릴리즈로 버전을 관리하며, GradleMaven 환경에서 의존성 한 줄만 추가하면 바로 사용할 수 있도록 구성했습니다.

Aho-Corasick Algorithm & Trie DiagramClick to Enlarge
Aho-Corasick Algorithm Structure

4. Engineering Challenges

Problem 01: Normalization Index Mismatch

변칙 우회 방어 및 원본 인덱스 복원 알고리즘

정규화 과정에서 텍스트 길이가 바뀌면 탐지된 인덱스와 원본 인덱스가 어긋납니다. IntArray 매핑 테이블로 정규화 전후 위치를 추적해 마스킹 위치 오차를 잡았습니다.
kotlin
detectedBans.forEach { ban ->
    val isAllowed = detectedAllows.any { allow -> 
        overlaps(ban.start, ban.end, allow.start, allow.end) 
    }
    if (!isAllowed) {
        val originalStart = mapping.indexMap[ban.start]
        val originalEnd = mapping.indexMap[ban.end]

        for (i in originalStart..originalEnd) {
            resultChars[i] = maskChar // 비속어를 **로 마스킹
        }
    }
}
Problem 02: False Positives

정상 단어 보호를 위한 Interval Overlap 로직

금칙어 구간이 허용 단어 구간에 포함될 경우(예: '시발' ⊂ '시발점') 마스킹에서 제외해야 합니다. 두 구간의 겹침 여부를 판단하는 로직으로 이 문제를 처리했습니다.
kotlin
// Interval Overlap: 금칙어가 허용 단어 구간에 포함되면 무시
val remains = detectedBans.filterNot { ban ->
    detectedAllows.any { allow -> 
        overlaps(ban.start, ban.end, allow.start, allow.end)
    }
}

private fun overlaps(s1: Int, e1: Int, s2: Int, e2: Int) = s1 <= e2 && s2 <= e1

5. Lessons Learned

알고리즘 도입으로 실제로 얼마나 차이를 만드는지 JMH로 직접 측정해보기 전까지는 확신이 없었습니다. 수치로 확인하고 나서야 기술 선택이 서비스 품질과 직결된다는 걸 체감했습니다.

처음으로 다른 개발자가 쓸 라이브러리를 만들어보면서, 내가 편한 구조와 쓰는 사람이 편한 구조가 다르다는 걸 알았습니다. API 설계와 Jitpack 배포 과정이 생각보다 신경 쓸 게 많았지만, 이 과정이 재미있게 느껴졌습니다.