[#1] 말조심: 데이터베이스 구축

Nyam·2025년 11월 30일

말조심

목록 보기
2/4
post-thumbnail

MalJosim 개발일지 - 욕설 필터링 API 구축기

프로젝트 개요

MalJosim은 RAG(Retrieval-Augmented Generation)와 LLM을 활용한 욕설 필터링 API 서비스입니다.
단순 키워드 매칭을 넘어서 컨텍스트를 이해하고 더 정확한 필터링을 제공하는 것이 목표입니다.

기술 스택

  • 프레임워크: NestJS 10.x
  • 데이터베이스: PostgreSQL (Prisma ORM)
  • 캐시: Redis (ioredis)
  • AI/ML: LangChain, OpenAI, ChromaDB(예정)
  • 언어: TypeScript

구현 완료된 기능

1. 프로젝트 초기 설정

  • NestJS 프로젝트 생성 및 기본 모듈 구조 설정
  • Prisma를 통한 PostgreSQL 연결 설정
  • Redis 연결 및 캐시 모듈 구성
  • 환경 변수 관리 (ConfigModule)

2. 데이터베이스 스키마 설계

BadWord 테이블

  • 글로벌 금칙어를 관리하는 마스터 테이블
  • 주요 필드:
    • word: 원본 금칙어
    • normalizedWord: 정규화된 형태 (자모 합치기, leetspeak 변환 등)
    • severity: 심각도 레벨 (LOW, MEDIUM, HIGH, CRITICAL)
    • category: 카테고리 (PROFANITY, HATE_SPEECH, SEXUAL, VIOLENCE, SPAM, OTHER)
    • isActive: 활성화 여부 (소프트 삭제)
    • aliases: 별칭/변형 리스트

ClientBadWord 테이블

  • 클라이언트별 금칙어 정책 오버라이드
  • 멀티 테넌트 환경에서 클라이언트별 맞춤 정책 적용 가능
  • overrideSeverity를 통한 클라이언트별 심각도 재정의

History 테이블

  • 감사 로그를 위한 변경 이력 추적
  • CREATE, UPDATE, DELETE 액션 기록
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
}

enum Severity {
  LOW
  MEDIUM
  HIGH
  CRITICAL
}

enum Category {
  PROFANITY
  HATE_SPEECH
  SEXUAL
  VIOLENCE
  SPAM
  OTHER
}

enum HistoryAction {
  CREATE
  UPDATE
  DELETE
}

model BadWord {
  id             String    @id @default(uuid())
  word           String    @db.VarChar(100)
  normalizedWord String    @db.VarChar(100)
  severity       Severity  @default(MEDIUM)
  category       Category  @default(OTHER)
  isActive       Boolean   @default(true)
  aliases        String[]  @default([])
  createdAt      DateTime  @default(now())
  updatedAt      DateTime  @updatedAt

  // Relations
  clientBadWords ClientBadWord[]

  // Indexes
  @@index([normalizedWord])
  @@index([isActive])
  @@index([category])
  @@unique([word])
}

model ClientBadWord {
  clientId         String    @db.VarChar(100)
  wordId           String
  overrideSeverity Severity?
  isActive         Boolean   @default(true)
  createdAt        DateTime  @default(now())
  updatedAt        DateTime  @updatedAt

  // Relations
  badWord BadWord @relation(fields: [wordId], references: [id], onDelete: Cascade)

  // Composite Primary Key
  @@id([clientId, wordId])
  @@index([clientId])
  @@index([wordId])
}

model History {
  id        String       @id @default(uuid())
  tableName String       @db.VarChar(50)
  recordId  String
  action    HistoryAction
  userId    String?      @db.VarChar(100)
  createdAt DateTime     @default(now())

  @@index([tableName, recordId])
  @@index([createdAt])
}

3. BadWord CRUD API 구현

엔드포인트

  • POST /bad-words: 금칙어 생성
  • GET /bad-words: 금칙어 목록 조회 (페이지네이션, 필터링 지원)
  • GET /bad-words/:id: 특정 금칙어 조회
  • PATCH /bad-words/:id: 금칙어 수정
  • DELETE /bad-words/:id: 금칙어 삭제 (소프트 삭제)

주요 기능

  • 중복 체크: word 필드에 unique 제약 조건으로 중복 방지
  • 유효성 검증: class-validator를 통한 DTO 검증
  • 필터링: 카테고리, 심각도, 활성화 여부, 검색어로 필터링 가능
  • 페이지네이션: page, limit 파라미터 지원

DTO 구조

  • CreateBadWordDto: 생성용 DTO
  • UpdateBadWordDto: 수정용 DTO (부분 업데이트 지원)
  • QueryBadWordDto: 조회용 쿼리 파라미터 DTO
  • BadWordResponseDto: 응답용 DTO

4. Redis 캐싱 전략

Write-Through 패턴 구현

  • 데이터베이스에 쓰기 작업 시 Redis 캐시도 동시에 업데이트
  • 캐시와 DB의 일관성 보장

캐시 구조

  • 글로벌 금칙어 Set: bad_words:global - 모든 활성 금칙어의 normalizedWord 저장
  • 정규화 단어 매핑: bad_words:normalized:{normalizedWord} - 정규화된 단어 → 원본 단어 정보
  • 단어 상세 정보: bad_words:detail:{id} - 단어의 전체 정보
  • 클라이언트별 금칙어: bad_words:client:{clientId} - 클라이언트별 활성 금칙어

초기화 전략

  • 애플리케이션 시작 시 (onModuleInit) PostgreSQL에서 활성 금칙어를 로드하여 Redis에 저장
  • 서버 재시작 시에도 캐시가 자동으로 동기화

캐시 메서드

  • loadGlobalBadWords(): 글로벌 금칙어 로드
  • loadClientBadWords(clientId): 클라이언트별 금칙어 로드
  • isBadWord(normalizedWord): 글로벌 금칙어 여부 확인
  • isClientBadWord(clientId, normalizedWord): 클라이언트별 금칙어 여부 확인
  • addBadWord(): 금칙어 추가 (Write-through)
  • updateBadWord(): 금칙어 업데이트 (Write-through)
  • removeBadWord(): 금칙어 삭제 (Write-through)

5. Health Check 모듈

서비스의 상태를 모니터링하기 위한 헬스 체크 엔드포인트 구현:

  • 서비스 상태 확인
  • PostgreSQL 연결 상태 확인
  • Redis 연결 상태 확인

6. 데이터베이스 모듈

  • PrismaService 구현
  • PostgreSQL 연결 풀 관리
  • PrismaPg 어댑터를 통한 연결 최적화
  • 모듈 생명주기 관리 (onModuleInit, onModuleDestroy)

아키텍처 특징

1. 모듈화된 구조

src/
├── app.module.ts          # 루트 모듈
├── bad-word/              # 금칙어 관리 모듈
│   ├── bad-word.controller.ts
│   ├── bad-word.service.ts
│   ├── bad-word.module.ts
│   └── dto/               # Data Transfer Objects
├── cache/                 # 캐시 모듈
│   ├── cache.service.ts
│   ├── cache.module.ts
│   └── cache-keys.ts     # Redis 키 관리
├── database/              # 데이터베이스 모듈
│   ├── prisma.service.ts
│   └── database.module.ts
└── health/                # 헬스 체크 모듈
    ├── health.controller.ts
    └── health.service.ts

2. DTO 패턴

  • 요청/응답 데이터의 타입 안정성 보장
  • 유효성 검증 로직 포함
  • Prisma 타입과의 변환 메서드 제공 (buildCreateData(), buildUpdateData())

3. 에러 처리

  • NestJS의 내장 예외 클래스 활용
  • NotFoundException: 리소스를 찾을 수 없을 때
  • ConflictException: 중복 데이터 생성 시도 시

성능 최적화

1. 인덱싱 전략

  • normalizedWord 인덱스: 정규화된 단어 기준 빠른 조회
  • isActive 인덱스: 활성 금칙어만 필터링
  • category 인덱스: 카테고리별 통계/정책 조회
  • word unique 제약: 중복 방지 및 빠른 조회

2. Redis 파이프라인

  • 여러 Redis 명령을 한 번에 실행하여 네트워크 오버헤드 감소
  • loadGlobalBadWords()에서 대량 데이터 로드 시 활용

3. 병렬 쿼리

  • findAll()에서 Promise.all()을 사용하여 count와 데이터 조회를 병렬 처리

다음 단계 (TODO)

  1. 필터링 엔진 구현

    • 텍스트 정규화 로직 (자모 분리/합치기, leetspeak 변환 등)
    • 실제 필터링 API 엔드포인트 구현
  2. RAG + LLM 통합

    • ChromaDB를 활용한 벡터 DB 구축
    • LangChain을 통한 LLM 연동
    • 컨텍스트 기반 필터링 로직
  3. 클라이언트별 정책 API

    • ClientBadWord CRUD API
    • 클라이언트별 필터링 정책 관리
  4. 테스트 코드 작성

    • Unit 테스트
    • E2E 테스트
  5. API 문서화

    • Swagger/OpenAPI 연동
  6. 배포 및 인프라

    • Docker 컨테이너화
    • CI/CD 파이프라인 구축

배운 점 & 고민한 점

1. Write-Through vs Write-Back

  • Write-Through 패턴을 선택한 이유: 데이터 일관성이 필터링 서비스에서 매우 중요하기 때문
  • 트레이드오프: 쓰기 성능은 약간 느려지지만, 읽기 성능과 일관성이 보장됨

2. 정규화된 단어 관리

  • 다양한 변형(예: "씨발", "씨8", "cibal")을 하나의 normalizedWord로 통합
  • Redis Set을 활용한 O(1) 조회 성능

3. 소프트 삭제

  • isActive 플래그를 통한 소프트 삭제 구현
  • 데이터 복구 가능성과 이력 추적을 위해 물리적 삭제 대신 선택

4. 멀티 테넌트 설계

  • ClientBadWord 테이블을 통한 클라이언트별 정책 오버라이드
  • 글로벌 정책과 클라이언트별 정책의 우선순위 관리 필요

마무리

현재까지 기본적인 CRUD API와 캐싱 인프라를 구축했습니다.
다음 단계로 실제 필터링 엔진과 AI 모델 연동을 진행할 예정입니다.

프로젝트 저장소: [GitHub 링크]
기술 문서: docs/rdb-schema.md 참고


profile
Backend Developer

0개의 댓글