욕설 필터링 시스템을 설계할 때 다양한 방법론과 기술적인 선택지가 존재합니다. 저는 프로젝트를 진행하면서 욕설 필터링을 구현하기 위해 여러 접근 방식을 조사했고, 각각의 장단점을 비교해 보았습니다. 그 결과, 단순한 데이터베이스 기반 필터링부터 시작해, 정규 표현식을 활용한 필터링을 구현하는 방식을 선택했습니다.


1. 욕설 필터링의 일반적인 방법

욕설 필터링을 구현하는 방법은 크게 두 가지로 나눌 수 있습니다.

  1. 데이터베이스 기반 필터링
    • 욕설과 비속어를 데이터베이스에 저장하고, 입력된 텍스트를 이 데이터와 비교해 감지하는 방식입니다.
    • 구현이 간단하고 빠르게 결과를 도출할 수 있지만, 데이터베이스 크기에 따라 성능이 저하될 수 있습니다.
    • 방대한 욕설 데이터베이스가 필요하며, 새로운 욕설이나 변형된 표현에 대한 대응이 어렵습니다.
  2. 자연어 처리 기반 필터링
    • 머신러닝 또는 딥러닝 모델을 활용해 욕설을 감지하는 방식입니다.
    • 문맥을 이해하고 변형된 욕설도 감지할 수 있는 장점이 있지만, 구현이 복잡하며 모델 학습에 많은 데이터와 리소스가 필요합니다.

2. 데이터베이스 기반 필터링 적용

욕설 필터링을 가장 빠르게 구현할 수 있는 방법은 데이터베이스에 욕설 데이터를 저장하고 이를 비교하는 방식입니다.
초기에 단순한 구현을 위해 욕설 데이터베이스를 직접 만들어 활용했으며, 기본적으로 다음과 같은 과정을 통해 처리했습니다.

 

욕설 데이터 수집

  1. 기본 욕설 목록 추가:
    초기에는 간단한 욕설 단어를 데이터베이스에 직접 추가하여 테스트를 진행했습니다.
  2. 확장 가능한 욕설 데이터베이스 조사:
    이후 필터링의 정확도를 높이기 위해, 방대한 욕설 데이터베이스를 제공하는 자료들을 조사했습니다.
    대표적으로 아래와 같은 자료들이 있었습니다:
    • Technote:
      국내에서 자주 사용되는 욕설과 비속어를 포함한 방대한 데이터베이스를 제공.
    • ZDNet 기사:
      네이버가 제공하는 욕설 필터링 데이터에 대한 정보. 양이 방대하고 다양한 욕설 패턴을 포함하고 있었습니다.
      이를 통해 기존에 생각하지 못했던 다양한 욕설과 변형된 표현까지 감지할 수 있다는 점을 알게 되었습니다.
  3. 현실적 한계:
    그러나 이러한 방대한 데이터를 실제 프로젝트에 바로 적용하기에는 리소스와 성능 문제가 발생할 수 있어, 현재 프로젝트에서는 단순한 데이터베이스로 시작했습니다.

3. 정규 표현식을 활용한 욕설 필터링

욕설 데이터베이스 기반 필터링은 초기 구현 단계에서는 유용하지만, 성능 문제와 변형된 욕설(예: 띄어쓰기, 특수문자 사용)에 대한 대응이 부족하다는 한계가 있습니다. 이를 보완하기 위해 정규 표현식(Regex)을 활용한 필터링 로직을 구현해 보았습니다.

정규 표현식 적용 과정

  1. 욕설 패턴 정의:
    • 바보, 멍청이 같은 단순한 욕설을 포함한 기본 패턴 작성.
    • 욕설 단어 변형(예: 바ㅂㅗ, 멍청@이)을 감지하기 위해 다양한 정규 표현식을 작성.
  2. 구현 예시:
@Service
@Transactional
@RequiredArgsConstructor
public class ProfanityFilterService {

    private final ProfanityRepository profanityRepository;

    public List<String> getPatterns() {
        return profanityRepository.findAll().stream()
                .map(Profanity::getPattern)
                .toList();
    }


    public String filter(String text, List<String> patterns) {
        System.out.println("Original text: " + text);
        String filteredText = text;

        for (String pattern : patterns) {
            Pattern compiledPattern = Pattern.compile("(?i)" + pattern, Pattern.UNICODE_CASE);
            if (compiledPattern.matcher(text).find()) {
                filteredText = compiledPattern.matcher(filteredText).replaceAll("****");
            }
        }

        return filteredText;
    }
    
}

블러 처리 결과

 


4. 결론

욕설 필터링은 데이터베이스 기반의 간단한 구현부터 시작해 점진적으로 고도화할 수 있습니다. 초기 단계에서는 정규 표현식을 활용해 간단하게 필터링 로직을 작성했으며, 이후 확장성을 고려해 다음과 같은 방향으로 발전시킬 계획입니다.

  1. 방대한 욕설 데이터를 포함하는 데이터베이스와 결합.
  2. 사용자 경험을 고려한 실시간 필터링 로직 추가.

욕설 필터링 시스템(Clean Bot)을 설계하면서 가장 큰 고민은 "필터링이 실시간으로 이루어져야 하는가?" 였습니다. 이를 결정하기 위해 다양한 요소를 고려했으며, 각각의 장단점을 아래와 같이 정리해보았습니다.


1. 실시간 필터링의 장단점

장점

  • 사용자 경험 개선: 욕설이 즉시 차단되어 플랫폼의 건전성을 유지하고 사용자 만족도를 높일 수 있습니다.
  • 문제 확산 방지: 부적절한 콘텐츠가 다른 사용자에게 노출되기 전에 차단됩니다.

단점

  • 성능 이슈: 대규모 트래픽 환경에서는 높은 처리 성능이 요구되며, 지연(Latency)이 발생할 가능성이 있습니다.
  • 비용 증가: 실시간 처리를 위해 추가적인 서버 자원과 기술적 구현이 필요합니다.
  • 오탐(False Positive): 과도한 필터링으로 부적절하지 않은 콘텐츠까지 차단될 위험이 있습니다.

2. 비실시간(배치) 필터링의 장단점

장점

  • 성능 최적화: 배치 처리로 서버 부하를 줄이고, 전체 시스템의 안정성을 유지할 수 있습니다.
  • 비용 절감: 실시간이 아니기 때문에 상대적으로 적은 자원으로 구현이 가능합니다.

단점

  • 지연된 대응: 욕설이 일정 시간 동안 노출될 가능성이 있습니다.
  • 사용자 불만: 부적절한 언어가 즉시 차단되지 않으면 관리 부족으로 느껴질 수 있습니다.

결론

일단 욕설 필터링은 배치 처리 방식으로 구현하여 기본적인 기능을 완성한 뒤, 이후 실시간성을 어떻게 도입할지 고민하는 방향으로 진행하고 있습니다.


배치 처리 코드 구현

아래는 욕설 필터링 시스템을 배치 처리로 구현한 코드입니다. Spring Batch를 활용하여 데이터베이스의 Posting 엔티티 중 필터링이 필요한 데이터를 주기적으로 처리합니다.

 
@Configuration
@RequiredArgsConstructor
public class ProfanityBatch {
	private final JobRepository jobRepository;
    private final PlatformTransactionManager platformTransactionManager;
    private final PostingRepository postingRepository;
    private final ProfanityFilterService profanityFilterService;
    
    @Bean
    public Job profanityJob() {
    	return new JobBuilder("profanityJob", jobRepository)
        	.start(profanityStep()) .build(); 
    }
    
    @Bean
    public Step profanityStep() { 
    	return new StepBuilder("profanityStep", jobRepository)
        	.<Posting, Posting>chunk(10, platformTransactionManager)
            .reader(profanityReader()) .processor(profanityProcessor())
            .writer(profanityWriter()) .build();
    }
    
    @Bean
    public RepositoryItemReader<Posting> profanityReader() {
    	return new RepositoryItemReaderBuilder<Posting>()
        	.name("profanityReader")
            .pageSize(10)
            .repository(postingRepository) 
            .methodName("findByIsCleanedFalse")
            .arguments(Collections.emptyList())
            .sorts(Map.of("id", Sort.Direction.ASC))
            .build(); 
    }
    
    @Bean
    public ItemProcessor<Posting, Posting> profanityProcessor() {
    	return posting -> {
			List<String> patterns = profanityFilterService.getPatterns();
            String filteredContent = profanityFilterService.filter(posting.getContent(), patterns);
            posting.updateCleanPosting(filteredContent);
            return posting;
        };
    }
            
    @Bean 
    public RepositoryItemWriter<Posting> profanityWriter() {
    	return new RepositoryItemWriterBuilder<Posting>() 
        .repository(postingRepository)
        .methodName("save")
        .build();
    } 
}

코드 설명

  1. Job과 Step 구성:
    • profanityJob: 배치 작업을 시작하는 엔트리포인트.
    • profanityStep: 필터링 작업의 단위, 데이터 읽기-처리-쓰기 구성.
  2. Reader:
    • 데이터베이스에서 필터링이 필요한 데이터(isCleaned = false)를 읽어옵니다.
  3. Processor:
    • ProfanityFilterService를 활용하여 콘텐츠에서 욕설을 필터링하고, 필터링된 콘텐츠로 업데이트합니다.
  4. Writer:
    • 필터링이 완료된 데이터를 다시 저장합니다.

+ Recent posts