Spring

[Spring/Java] 정규식을 통한 비속어 필터링

테런 2023. 5. 6. 18:43
  • 비속어 필터링
비속어들은 DB에 저장되어 관리되거나, 비속어 필터 라이브러리가 있으니 확인해보길 바란다. DB로 관리하는 것과 라이브러리를 활용하는 것에는 각각 장단점이 있으니 방식은 비교해보고 결정해보길 바란다.

게시물 등록 시에 모든 비속어를 DB에서 매번 조회하고 비교하는 것은 비효율적이다.
Cache, Session, Singleton, In-memory 저장소(Redis 등)와 같은 방식으로 값을 미리 조회해서 사용하는 방식이 효율적이다.

이 글에서는 비속어들이 DB에 저장되어 관리되고, Redis를 사용하는 환경에서 비속어 필터링한다.

 

  • 비속어 DB로부터 Redis에 비속어 정규식 패턴 저장
public void saveSlangListToRedis() {
    // DB로부터 모든 비속어 조회 (약 600개 단어)
    List<String> slangList = slangRepository.findAll();

    StringBuilder stringBuilder = new StringBuilder();
    
    // 모든 비속어를 정규식 패턴으로 사용하기 위해 하나의 String으로 변환
    for (String slang : slangList) {
        // stringBuilder -> 비속어|비속어|비속어|..
        stringBuilder.append(slang).append('|');
    }

    // Redis
    ValueOperations<String, String> redis = redisTemplate.opsForValue();

    // slang이란 Key로 비속어 정규식 패턴 저장
    redis.set("slang", String.valueOf(stringBuilder));
}
비속어 DB가 업데이트될 때, Redis값도 함께 업데이트 해준다.

 

  • Redis에서 키값("slang")으로 조회해서 데이터 확인 -> 한글은 깨져서 들어가지만 상관없다.
> get slang
"\xeb\xb9\xa0\xea\xb5\xb4\xec\x9d\xb4|......"

 

  • 비속어 필터링 코드
public class SlangUtil {
    
    // 비속어 필터링
    public static String slangFilter(String text, String slangPattern, char maskChar) {
        Pattern pattern = Pattern.compile(slangPattern, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(text);
        StringBuilder stringBuilder = new StringBuilder();

        while (matcher.find()) {
            if (matcher.group().isEmpty()) continue;
            matcher.appendReplacement(stringBuilder, masking(matcher.group(), 0, matcher.group().length(), maskChar));
        }

        matcher.appendTail(stringBuilder);

        return tagFilter(stringBuilder.toString());
    }

    // Html 태그 필터링
    public static String tagFilter(String text) {
        Pattern pattern = Pattern.compile("<(\"[^\"]*\"|'[^']*'|[^'\">])*>");
        Matcher matcher = pattern.matcher(text);

        return matcher.replaceAll("");
    }

    // 비속어 마스킹
    public static String masking(String text, int startIndex, int endIndex, char maskChar) {
        if (startIndex > endIndex) {
            throw new ResponseStatusException(HttpStatus.OK, ErrorCodeType.MASKING_ERROR.name());
        }

        if (text.isEmpty()) {
            return "";
        }

        if (startIndex < 0) {
            startIndex = 0;
        }

        if (endIndex > text.length()) {
            endIndex = text.length();
        }

        int maskLength = endIndex - startIndex;

        if (maskLength == 0) {
            return text;
        }

        String sbMaskString = String.valueOf(maskChar).repeat(maskLength);

        return text.substring(0, startIndex)
                + sbMaskString
                + text.substring(startIndex + maskLength);
    }
}

 

  • 테스트
public void slangFilterTest() {
    // Redis
    ValueOperations<String, String> redis = redisTemplate.opsForValue();

    String text = "<b>이 ${비속어} 너 ${비속어} ${비속어}의 ${비속어}야!</b>";
    System.out.println(SlangUtil.slangFilter(text, redis.get("slang"), '*'));
    // 결과: 이 ** 너 ** **의 **야!
}

 

  • 결과
2000자(단어 500개 이상)가 넘는 글로 테스트해도 경과 시간은 52ms로 매우 빠르게 비속어 필터링이 된 것을 확인 할 수 있었다.