๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ
- Kotlin: 2.2.20
๐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๋ณด
Repository: github.com/Jwhyee/profanity-filter
์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ Java, Kotlin, Spring Boot ๊ธฐ๋ฐ์์ ๋ชจ๋ ์ ์์ ์ผ๋ก ๋์ํฉ๋๋ค. ์ด ํฌ์คํ ์์๋ Kotlin ๊ธฐ๋ฐ์ผ๋ก ์ค๋ช ํ์ง๋ง, README.md์๋ Java ์ฝ๋๋ ์ถ๊ฐ๋์ด ์์ต๋๋ค. Gradle(Kotlin, Groovy), Maven ๊ฐ๊ฐ์ ์ค์ ๋ฐฉ๋ฒ ๋ํ README.md์์ ํ์ธํ ์ ์์ต๋๋ค.
์์ง ๋ถ์กฑํ ์ ์ด ๋ง์ง๋ง, ์ปค๋ฎค๋ํฐ์ ํผ๋๋ฐฑ์ ๋ฐ์ผ๋ฉฐ ์ ์ง์ ์ผ๋ก ๊ฐ์ ํด๋๊ฐ ์์ ์ ๋๋ค. ๊ด์ฌ ์์ผ์ ๋ถ๋ค์ ๊ธฐ์ฌ(PR)์ ์๊ฒฌ(Issue) ๋ชจ๋ ํ์ํฉ๋๋ค!
๐ฏ ๊ฐ๋ฐ ๋ฐฐ๊ฒฝ
์ต๊ทผ ํ ์ด ํ๋ก์ ํธ์์ ์ธ๊ธฐ ๊ฒ์์ด ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๋ ์ค, '๋๊ตฐ๊ฐ๊ฐ ์๋์ ์ผ๋ก ๋น์์ด๋ฅผ ๊ฒ์ํ์ฌ ์ธ๊ธฐ ๊ฒ์์ด์ ์ฌ๋ฆฌ๋ ์ํฉ์ด ์๊ธฐ์ง ์์๊น?'๋ผ๋ ์๊ฐ์ด ๋ค์๋ค. ๊ทธ๋์ ๋น์์ด๋ฅผ ํํฐ๋งํ๋ ๊ธฐ๋ฅ์ ๋ง๋ค๋ ค๊ณ ์ด๊ฒ์ ๊ฒ ์ฐพ์๋ณด๋, ๋๋ถ๋ถ ๋ฌธ์์ด ๋งค์นญ ๋ฐ ์ ๊ท ํํ์ ๊ธฐ๋ฐ์ ์ฝ๋๊ฐ ๋ง์๋ค.
๊ทธ๋ฐ ์ฝ๋๋ ๋์์ง๋ ์์ง๋ง, ํ๊ตญ์ด ํน์ฑ์ "์1๋ฐ", "์ ๋ฐ" ๊ฐ์ ๋ณ์น ์ฐํ ํํ์ด ๋ง์ ๋จ์ ๋งค์นญ์ผ๋ก๋ ํ๊ณ๊ฐ ์์๋ค. ๋ํ "์๋ฐ์ "์ด๋ผ๋ ๋จ์ด๋ ์์ด ์๋์ง๋ง ๋น์์ด๊ฐ ํฌํจ๋์ด ์์ด, ๋๋ถ๋ถ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ ์์ผ๋ก ์ธ์ํ๊ธฐ์ ์ง์ ๋ง๋๋ ๊ฒ ๋ง๊ฒ ๋ค๊ณ ํ๋จํ๋ค.
์ด๋ป๊ฒ ๊ฐ๋ฐํ ์ง ์ฐพ์๋ณด๋ ์ค, ์์ ์ Trie ์๊ณ ๋ฆฌ์ฆ์ ๊ณต๋ถํ๋ฉฐ ๋ณด์๋ ์ฐ์ํํ์ ๋ค ๊ธฐ์ ๋ธ๋ก๊ทธ์ ๊ธ์ ๋ค์ ๋ณด๊ฒ ๋์๋๋ฐ, ๋ฑ ๋ด๊ฐ ์ํ๋ ๊ตฌ์กฐ์๊ณ ์ด ๊ธ์ ๊ธฐ๋ฐ์ผ๋ก ๋ง๋ค๋ฉด ์ํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
๐ ํต์ฌ ์๊ณ ๋ฆฌ์ฆ
Trie (ํธ๋ผ์ด)
Trie๋ ๋ฌธ์์ด ์งํฉ์ ํธ๋ฆฌ ํํ๋ก ์ ์ฅํ๋ ์๋ฃ๊ตฌ์กฐ๋ค. ์๋ฅผ ๋ค์ด ["์๋ฐ", "์จ๋ฐ", "์๋ฐ"]๋ผ๋ ๋จ์ด๋ฅผ Trie์ ๋ฃ๋๋ค๊ณ ๊ฐ์ ํด๋ณด์.
(root)
โโ ์
โ โโ ๋ฐ [END]
โ โโ ๋ฐ [END]
โโ ์จ
โโ ๋ฐ [END]
๊ฐ ๋
ธ๋๋ ํ๋์ ๋ฌธ์๋ฅผ ๋ํ๋ด๋ฉฐ, ๋ฃจํธ์์ ํน์ ๋
ธ๋๊น์ง์ ๊ฒฝ๋ก๊ฐ ํ๋์ ๋ฌธ์์ด์ ํ์ฑํ๋ค. [END] ํ์๋ ํด๋น ์์น์์ ํ๋์ ๋จ์ด๊ฐ ์์ฑ๋จ์ ์๋ฏธํ๋ค.
์๊ฐ ๋ณต์ก๋
L: ๋ชจ๋ ๊ธ์น์ด ๊ธธ์ด์ ํฉ, N: ์ ๋ ฅ ๋ฌธ์์ด์ ๊ธธ์ด, K: ๊ธ์น์ด ๊ฐ์
- ์ฝ์ : O(L)
- ๊ฒ์: O(N × K)
Trie์ ํ๊ณ
Trie๋ ํ ๋ฒ์ ํ๋์ ํจํด๋ง ๊ฒ์ํ ์ ์๋ค. ์์ฒ ๊ฐ์ ๋น์์ด๋ฅผ ๋์์ ์ฐพ์ผ๋ ค๋ฉด ๊ฐ ํจํด์ ๊ฐ๋ณ์ ์ผ๋ก ๊ฒ์ฌํด์ผ ํ๋ฏ๋ก ๋นํจ์จ์ ์ด๋ค.
์๋ฅผ ๋ค์ด ๋ค์ ์ํฉ์ ๋ณด์:
private val FILTER_WORDS = arrayOf(
"์๋
ํ์ธ์",
"์๋
ํ์ธ์ฌ",
"์๋
ํ์ธ์ "
)
private const val INPUT = "์๋
ํ์ธ์ฉ"
fun check(trie: Trie) {
FILTER_WORDS.forEach { trie.insert(it) }
val isFiltered = trie.contains(INPUT)
}
์ด ๊ฒฝ์ฐ Trie ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ๋ค.
(root)
โโ ์
โโ ๋
โโ ํ
โโ ์ธ
โโ ์ [END]
โโ ์ฌ [END]
โโ ์ [END]
ํจ์จ์ ์ผ๋ก ๋์ํ๋ ค๋ฉด "์๋ ํ์ธ"๊น์ง ์งํํ ํ '์ธ'์ ์์ ๋ ธ๋๋ค๋ง ํ์ธํ๋ฉด ๋์ง๋ง, ์ผ๋ฐ์ ์ธ Trie ๊ฒ์์ ๊ฐ ํจํด์ ์ฒ์๋ถํฐ ๋๊น์ง ๊ฐ๋ณ์ ์ผ๋ก ๋งค์นญํ๋ค.
์ฆ, "์๋ ํ์ธ์ฉ"์ด๋ผ๋ ์ ๋ ฅ์ ๋ํด์ ๋ค์๊ณผ ๊ฐ์ด ๊ฒ์ฌํ๋ ๊ฒ์ด๋ค.
- "์๋ ํ์ธ์" ๊ฒ์ฌ → ๋งค์นญ ์คํจ
- ์ฒ์๋ถํฐ ๋ค์ "์๋ ํ์ธ์ฌ" ๊ฒ์ฌ → ๋งค์นญ ์คํจ
- ์ฒ์๋ถํฐ ๋ค์ "์๋ ํ์ธ์ " ๊ฒ์ฌ → ๋งค์นญ ์คํจ
์ด์ฒ๋ผ ๊ณตํต ์ ๋์ฌ๋ฅผ ๋ฐ๋ณตํด์ ํ์ํ๋ ๋นํจ์จ์ด ๋ฐ์ํ๋ค. ์ด๊ฒ์ด Trie๋ง์ผ๋ก๋ ๋ค์ค ํจํด ๋งค์นญ์ด ๋๋ฆฐ ์ด์ ๋ค.
Aho-Corasick (์ํธ-์ฝ๋ผ์)
Aho-Corasick์ Trie์ ์คํจ ํจ์(Failure Function)๋ฅผ ์ถ๊ฐํ์ฌ ์ฌ๋ฌ ํจํด์ ๋์์ O(N) ์๊ฐ์ ๊ฒ์ํ ์ ์๊ฒ ๋ง๋ ์๊ณ ๋ฆฌ์ฆ์ด๋ค. ๊ธฐ๋ณธ์ ์ธ ๋์ ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ๋ค.
- Goto ํจ์: ๊ธฐ๋ณธ Trie ๊ตฌ์กฐ๋ก, ๋ค์ ๋ฌธ์๋ก ์ด๋
- Failure ํจ์: ํ์ฌ ์ํ์์ ๋งค์นญ ์คํจ ์ ๋๋์๊ฐ ์ํ
- Output ํจ์: ํ์ฌ ์์น์์ ๋ฐ๊ฒฌ๋ ๋ชจ๋ ํจํด
๋์ ์์
Trie์ ๊ฐ์ ์์๋ฅผ Aho-Corasick์ผ๋ก ์ฒ๋ฆฌํด๋ณด์.
ํจํด: ["์๋
ํ์ธ์", "์๋
ํ์ธ์ฌ", "์๋
ํ์ธ์ "]
์
๋ ฅ: "์๋
ํ์ธ์ฉ"
1๋จ๊ณ: Trie ๊ตฌ์กฐ (Goto ํจ์)
(root)
โโ ์
โโ ๋
โโ ํ
โโ ์ธ
โโ ์ [END]
โโ ์ฌ [END]
โโ ์ [END]
2๋จ๊ณ: Failure ๋งํฌ ๊ตฌ์ถ
(root)
โโ ์
โโ ๋
--fail--> (root)
โโ ํ --fail--> (root)
โโ ์ธ --fail--> (root)
โโ ์ [END] --fail--> (root)
โโ ์ฌ [END] --fail--> (root)
โโ ์ [END] --fail--> (root)
3๋จ๊ณ: ๊ฒ์ ๊ณผ์
์
๋ ฅ: "์๋
ํ์ธ์ฉ"
1. '์' → goto(root, '์') = state_์
2. '๋
' → goto(state_์, '๋
') = state_๋
3. 'ํ' → goto(state_๋
, 'ํ') = state_ํ
4. '์ธ' → goto(state_ํ, '์ธ') = state_์ธ
5. '์ฉ' → goto(state_์ธ, '์ฉ') = FAIL
→ failure(state_์ธ) = root
→ goto(root, '์ฉ') = FAIL
→ ๋งค์นญ ์คํจ
Trie vs Aho-Corasick ๋น๊ต
Trie
- "์๋ ํ์ธ์" ์ ์ฒด ๊ฒ์ฌ (5๊ธ์)
- "์๋ ํ์ธ์ฌ" ์ ์ฒด ๊ฒ์ฌ (5๊ธ์)
- "์๋ ํ์ธ์ " ์ ์ฒด ๊ฒ์ฌ (5๊ธ์)
- ์ด 15๊ธ์ ๋น๊ต
Aho-Corasick
- "์๋ ํ์ธ์ฉ" ํ ๋ฒ๋ง ์ํ (5๊ธ์)
- ์ด 5๊ธ์ ๋น๊ต
์ด์ฒ๋ผ Aho-Corasick์ ์ ๋ ฅ ๋ฌธ์์ด์ ๋จ ํ ๋ฒ๋ง ์ํํ๋ฉด์ ๋ชจ๋ ํจํด์ ๋์์ ์ฐพ์ ์ ์๋ค. ์ด๊ฒ์ด ๋น์์ด ํํฐ๋ง์ฒ๋ผ ์์ฒ ๊ฐ์ ํจํด์ ์ฐพ์์ผ ํ๋ ๊ฒฝ์ฐ ์๋์ ์ผ๋ก ๋น ๋ฅธ ์ด์ ๋ค.
โ๏ธ ์ ์ฉ ๋ฐฉ๋ฒ
Gradle ์ค์
2026๋ 1์ 14์ผ : Maven Central์๋ ์ฌ๋ฆฌ์ง ์์๊ธฐ ๋๋ฌธ์ Jitpack์ ์ค์ ํด์ฃผ์ด์ผ ํฉ๋๋ค.
settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
build.gradle.kts
dependencies {
implementation("com.github.Jwhyee:profanity-filter:1.0.0")
}
Spring ํตํฉ ์์
Configuration
@Configuration
class FilterConfig {
@Bean
fun profanityValidator(): ProfanityValidator {
val banTrie = ProfanityTrie.create(
customWords = listOf("์ปค์คํ
๋น์์ด"),
excludeWords = listOf("์ ์ธํ ๋น์์ด")
)
return ProfanityValidator(banTrie, setOf("์๋ฐ์ ", "๊ฐ๋ฐ"))
}
}
Service (์์ฑ์ ์ฃผ์ )
@Service
class CommentService(
private val validator: ProfanityValidator
) {
fun createComment(content: String) {
try {
validator.validate(content)
// ์ ์ ์ฒ๋ฆฌ
} catch (e: ProfanityDetectedException) {
throw IllegalArgumentException(
"๋ถ์ ์ ํ ๋จ์ด ๊ฐ์ง: ${e.detectedWords}"
)
}
}
}
POJO ์์
class ContentFilter {
private val validator = ProfanityValidator(
ProfanityTrie.create(),
setOf("์๋ฐ์ ")
)
fun filter(text: String): Boolean {
return try {
validator.validate(text)
true
} catch (e: ProfanityDetectedException) {
false
}
}
}
๐ค ํ๊ณ
์์ ์ Trie๋ฅผ ๊ณต๋ถํ๋ฉด์ "์ด๊ฒ ์ค์ ๋ก ์ด๋์ ์ฐ์ผ๊น?"๋ผ๋ ์๋ฌธ์ด ์์๋๋ฐ, ๋น์์ด ํํฐ๋ง์ด๋ผ๋ ์ค์ ๋ฌธ์ ๋ฅผ ๋ง๋ ๊ณต๋ถํ ๊ฒ์ ์ง์ ๋ง๋ค์ด๋ณผ ์ ์์ด ์ฌ๋ฏธ์์๋ค. ์์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ง๋ง Trie ์๊ณ ๋ฆฌ์ฆ์ ๋ค์ ๊ณต๋ถํ ์ ์์๊ณ , ์ค๊ณ์ ์ต์ ํ ๋ฑ์ ๊ณผ์ ์ ๊ฒฝํํ ์ ์์ด ๋ ์ฌ๋ฏธ์์๋ ๊ฒ ๊ฐ๋ค. ํ์ง๋ง ์์ง ๋ถ์กฑํ ์ ์ด ๋ง์ ๊ฒ ๊ฐ๋ค. ์๊ฐ๋๋ ๊ฒ๋ค์ Issue์ ํ๋์ฉ ์ถ๊ฐํ๊ณ ์ฒ๋ฆฌํ๋ฉด์ ์ ๋ฐ์ดํธ๋ฅผ ํด๋๊ฐ์ผ๊ฒ ๋ค.
'Devlog > Project' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [AI] Kotlin Spring + Google AI Studio๋ก Github ์ฝ๋ ๋ฆฌ๋ทฐ ๋ด ๋ง๋ค๊ธฐ (0) | 2025.10.30 |
|---|---|
| [AI] LM Studio์ n8n์ผ๋ก Gitlab ์ฝ๋ ๋ฆฌ๋ทฐ ๋ด ๋ง๋ค๊ธฐ (0) | 2025.10.21 |