word 컬럼에 인덱스가 없으면 검색 시 성능 저하가 올 수 있음. word 컬럼에 UNIQUE INDEX를 걸어주기?
Redis 활용: 현재 Redis를 쓰고 있으니까, 이 리스트를 Redis Set(SADD curse_words ...)에 저장해두고 SISMEMBER로 체크하면 훨씬 빠름
그러나 SISMEMBER 는 메세지에서 단어를 추출해서 이 단어가 Redis Set에 존재하는지를 판별해야함. 그러려면 공백을 기준으로 tokenizing을 해서 넘겨줘야하는데 그럼 비속어와 다른 단어가 붙어있을 경우를 판별하지 못함. 따로 별도 tokenizing 규칙들을 여러개 추가하거나 접두/접미 탐지가 추가로 필요함.
→ 오히려 더 복잡하고 단어가 추가될 때마다 해당 단어의 조합에 따른 규칙들을 추가해야함. 공수 필요.
따라서 전체 비속어 리스트를 가져와서 정규식 패턴 매칭으로 판별하는 방식을 사용.
하단의 성능 및 최적화 부분 참고.
바보가 들어간 바보자식)도 잡히도록 전체 비속어 리스트를 가져와서 패턴 매칭 방식기존 방식
const curseWords = await this.getWordsFromRedis();
if (curseWords.length === 0) {
return { sanitized: message, hasCurse: false };
}
let sanitized = message;
let hasCurse = false;
for (const word of curseWords) {
if (!word) continue;
// 대소문자 구분 없이 전체에서 순수한 리터럴 텍스트로 word를 찾기 위한 패턴을 만듦
const pattern = new RegExp(word.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi');
if (!pattern.test(sanitized)) continue;
hasCurse = true;
const mask = '*'.repeat(word.length);
sanitized = sanitized.replace(pattern, mask);
}
...
private async getWordsFromRedis(): Promise<string[]> {
try {
const members = await this.redisClient.sMembers(this.REDIS_SET_KEY);
return members ?? [];
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
this.logger.error(`Redis에서 비속어 조회 실패: ${msg}`);
return [];
}
}
기존 방식은 메시지마다 Redis에서 sMembers로 전체 세트를 가져오므로, 트래픽이 많으면 네트워크 왕복과 직렬화 비용이 생김. 단어 수가 100 미만이면 체감은 적겠지만, QPS가 높으면 병목이 될 수 있다.
더 빠르게 하려면
new RegExp(...)를 한 번 만들고 재사용하면 per-request 패턴 생성도 줄일 수 있다.부분 일치 + 성능: 공백 없이 붙은 케이스까지 필요하면 현재처럼 전체 단어 리스트 기반으로 정규식을 돌려야 하니, 캐싱 + 사전 컴파일이 가장 효과적
규모가 커지면 Trie/Aho-Corasick 같은 다중 패턴 매칭을 메모리에서 돌리는 것도 방법
즉, 단어 수가 적다면 크게 문제 없을 가능성이 높지만, 안정적인 저지연을 위해 로드 시 캐싱 + 정규식 사전 컴파일로 Redis 왕복을 제거하는 게 가장 현실적인 최적화
sMembers는 네트워크 I/O가 쌓일 수 있다.
메모리 캐싱 + 정규식 사전 컴파일로 타협안을 적용