[LIBRARY] Java, Kotlin ๊ธฐ๋ฐ˜ Spring ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋น„์†์–ด ํ•„ํ„ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

  • 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 ๊ฒ€์ƒ‰์€ ๊ฐ ํŒจํ„ด์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๊ฐœ๋ณ„์ ์œผ๋กœ ๋งค์นญํ•œ๋‹ค.

 

์ฆ‰, "์•ˆ๋…•ํ•˜์„ธ์šฉ"์ด๋ผ๋Š” ์ž…๋ ฅ์— ๋Œ€ํ•ด์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฒ€์‚ฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

  1. "์•ˆ๋…•ํ•˜์„ธ์š”" ๊ฒ€์‚ฌ → ๋งค์นญ ์‹คํŒจ
  2. ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ "์•ˆ๋…•ํ•˜์„ธ์—ฌ" ๊ฒ€์‚ฌ → ๋งค์นญ ์‹คํŒจ
  3. ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ "์•ˆ๋…•ํ•˜์„ธ์œ " ๊ฒ€์‚ฌ → ๋งค์นญ ์‹คํŒจ

์ด์ฒ˜๋Ÿผ ๊ณตํ†ต ์ ‘๋‘์‚ฌ๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ํƒ์ƒ‰ํ•˜๋Š” ๋น„ํšจ์œจ์ด ๋ฐœ์ƒํ•œ๋‹ค. ์ด๊ฒƒ์ด Trie๋งŒ์œผ๋กœ๋Š” ๋‹ค์ค‘ ํŒจํ„ด ๋งค์นญ์ด ๋А๋ฆฐ ์ด์œ ๋‹ค.

Aho-Corasick (์•„ํ˜ธ-์ฝ”๋ผ์‹)

Aho-Corasick์€ Trie์— ์‹คํŒจ ํ•จ์ˆ˜(Failure Function)๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์—ฌ๋Ÿฌ ํŒจํ„ด์„ ๋™์‹œ์— O(N) ์‹œ๊ฐ„์— ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“  ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด๋‹ค. ๊ธฐ๋ณธ์ ์ธ ๋™์ž‘ ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. Goto ํ•จ์ˆ˜: ๊ธฐ๋ณธ Trie ๊ตฌ์กฐ๋กœ, ๋‹ค์Œ ๋ฌธ์ž๋กœ ์ด๋™
  2. Failure ํ•จ์ˆ˜: ํ˜„์žฌ ์ƒํƒœ์—์„œ ๋งค์นญ ์‹คํŒจ ์‹œ ๋˜๋Œ์•„๊ฐˆ ์ƒํƒœ
  3. 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์— ํ•˜๋‚˜์”ฉ ์ถ”๊ฐ€ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๋ฉด์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•ด๋‚˜๊ฐ€์•ผ๊ฒ ๋‹ค.