티스토리 뷰

 금칙어 필터링 기능을 분석하고 리팩토링 해보자!(1)

지난 포스팅에서 나는 기존 팀원이 짜놓은 코드를 리팩토링 했다.

단순 라이브러리를 이해하고 리팩토링 한 것 뿐아니라, 파일로 읽던 금칙어 목록들을 DB로 이전시켰다.

 

📂 파일로 금칙어를 읽어 처리하는데의 문제점

Sub module 아래의 금칙어 파일

파일로 금칙어를 읽으면 몇가지 불편한 문제점이 발생했다.

 

[문제점 1] 금칙어 목록을 Continuous Delivery 과정에서 jar 파일 뿐 아니라 금칙어 파일도 S3에 같이 저장해줘야 했다. 그래야 application 실행 시점에 메모리에 읽을 수 있으니깐.

[문제점 2] 금칙어를 추가하거나, 삭제하려면 다시 배포를 해야한다. 그러면, 서브모듈 관리도 해야하고, 버전 관리에 금칙어 텍스트 파일 관리가 포함되게 된다.

따라서, 코드가 아닌 단순 목록 관리가 버전 관리에 포함되게 되는 문제가 발생한다.

[문제점 3] 금칙어에 대한 추가적인 요구사항이 발생했을때 확장성이 떨어진다. 예를들어, 사람들이 자주 입력하는 금칙어에 대한 통계정보가 필요한 경우 어떻게 기능추가를 할 수 있을까? 

그러면 어차피 DB로 테이블을 분리해야하기 때문에, 처음부터 파일이 아닌 DB로 분리하는게 맞다고 판단했다.

 

👍 이렇게 해결했어요!

따라서, 나는 DB로 금칙어 목록을 관리하는게 맞다고 판단했고, 다음과 같이 금칙어에 대한 변동사항이 생기면 서버를 껐다가 다시 키는게 아닌, api를 통해 동적으로 관리할 수 있도록 구조를 변경했다.

 

아래와 같은 4가지 api를 추가했다. 

추가한 금칙어 관리 api들

그리고 아래와 같이 문장에서 발견된 금칙어에 대해 사용횟수도 올려주어 통계정보로 사용할 수 있게 만들었다.

 

또한, 기존 파일로 관리되던 금칙어 목록을 Client에서 필요 할 수도 있기 때문에, 영향을 최소화하기 위해서 CSV로 금칙어에 대한 정보를 추출 할 수 있게 만들었고,

api 호출 시마다 CSV 파일을 만드는 것은 비효율적이며 서버에 많은 부하를 줄 수 있기 때문에 아래와 같이 CSV 파일을 만들어 S3에 저장하고, 금칙어에 대한 변경사항이 생긴 시간 (LastModifiedDate 컬럼 중 가장 최근 값)과 다운로드 시간을 비교해 변동사항이 없다면 기존 생성해 둔 CSV 경로를 메모리에 저장해두고 리턴하게 만들었다. (CSV 생성은 apache common csv 라이브러리를 이용했다)

 

그리고 아래와 같이 금칙어에 대한 변동사항이 있을때, Concurrent Radix Tree의 이점을 최대한 활용하여 trie또한 업데이트 시켜주었다.

Profanity Loader 클래스, Trie를 관리하는 역할을 한다.

그런데, 이 Concurrent Tree 라이브러리를 사용하면 아호코라식 알고리즘을 사용하지 못한다.

어떻게 하면 좋을까?

 

👀 아호코라식 알고리즘을 도입해야할까? 라이브러리들을 비교해보자!

Concurrent-Trees 라이브러리

기존에 사용하고 있던 ConcurrentInvertedRadixTree는 금칙어 개수에 상관없이(n) 입력받는 문자열 d의 길이 * log(키워드 평균길이)의 시간복잡도를 가진다.

https://github.com/robert-bor/aho-corasick

 

GitHub - robert-bor/aho-corasick: Java implementation of the Aho-Corasick algorithm for efficient string matching

Java implementation of the Aho-Corasick algorithm for efficient string matching - robert-bor/aho-corasick

github.com

반면, 아호 코라식 알고리즘을 제공하는 위 라이브러리를 확인해보면, 다음과 같은 말을 확인할 수 있다.

얼마나 많은 키워드가 주어지더라도, O(n)이다.

 

즉, 라이브러리를 비교해보면 시간복잡도 측면에 있어서는 아호코라식 알고리즘을 사용하는게 조금 더 나을 것으로 보인다.

 

하지만 위 라이브러리의 문제점은, 트라이 생성에 있는데, 아래와 같이 Trie 생성이 한번 생성되면 고정되고, 변경할 수가 없다.

따라서 동적으로 금칙어를 추가/삭제할 때 Trie에 추가하는게 아닌 Trie 자체를 다시 만들어야 한다.

그러면 Trie를 금칙어 추가 / 삭제 기능이 일어날때마다 Trie를 다시 만들어줘야하고, 그런 삭제 / 변경이 빈번할 경우 동시성 문제도 고려해야하고, 매번 다시 메모리에 생성하는데 비용이 발생한다.

 

가장 크게 아호코라식 알고리즘을 제공하는 라이브러리를 사용하는 것을 멈춘 이유는 금칙어는 상대적으로 긴 단어가 없다. 길어도 5글자가 대부분이라 시간복잡도는 거의 차이가 나지 않을 것으로 보인다.

따라서 기존 라이브러리로도 충분한 성능을 낼 것으로 기대되었다.

 

✨ 성능 개선 결과

성능을 측정하기 위해서 Spring에서 제공하는 StopWatch를 local에서만 사용할 수 있도록 AOP를 이용한 어노테이션으로 만들어서 메서드 시간 측정을 진행했다.

 

전역에서 금칙어 필터링을 DTO에서 처리하기 위해서 Custom annotation으로 만들어 검증했는데 해당 `isValid()` 메서드 stopwatch를 붙혀 결과를 측정했다.

 

DB에서 가지고와 List를 모두 순회할 경우

글자 500자
1000자 (비속어 포함 x)
비속어 포함 1000자
5000자
금칙어 1000개, 글자수 5000자 기준, List 사용
금칙어 1000개, 글자수 5000자 기준, List 사용

메모리에 있는 Trie를 사용해 순회할 경우

글자 500자
1000자 (비속어 포함 x)
1000자 (비속어 포함 x)
비속어 포함 1000자
5천자, 비속어 포함
5천자, 비속어 포함
금칙어 1000개, 글자수 5000자 기준 (Trie) 사용
금칙어 1000개, 글자수 5000자 기준 (Trie) 사용
금칙어 1000개, 글자수 5000자 기준 (Trie) 사용

 

위 결과를 보면 Trie를 사용할 경우 응답 시간이 약 2.5배 이상 단축되는 것을 확인할 수 있었다

 

 

Reference

https://techblog.woowahan.com/15764/

 

고르곤졸라는 되지만 고르곤 졸라는 안 돼! 배달의민족에서 금칙어를 관리하는 방법 | 우아한형

{{item.name}} 안녕하세요! 셀러시스템팀에서 서버 개발을 하고 있는 김예빈이라고 합니다. 배달의민족에는 금칙어를 관리하는 "통합금칙어시스템"이라는 것이 있습니다. 금칙어란? 법 혹은 규칙으

techblog.woowahan.com

 

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday