5분 읽기

MongoDB 배치 처리와 Bull Queue로 구현한 대규모 이메일 자동화 시스템

대규모 이메일 발송 시스템을 구축하면서 겪은 문제와 해결 과정, 그리고 얻은 교훈을 공유합니다.

BackendDevOps
#NestJS#MongoDB#Bull Queue#TypeScript#Email#성능최적화
MongoDB 배치 처리와 Bull Queue로 구현한 대규모 이메일 자동화 시스템

MongoDB 배치 처리와 Bull Queue로 구현한 대규모 이메일 자동화 시스템

🎯 프로젝트 목표

우리 팀은 수만 명의 사용자에게 개인화된 뉴스레터를 발송하는 시스템을 구축해야 했습니다. 주요 요구사항은 다음과 같았습니다:

  • 대규모 사용자 데이터의 효율적인 처리
  • 안정적인 이메일 발송과 실패 복구
  • 상세한 발송 현황 모니터링
  • 시스템 확장성 확보

🤔 직면한 문제들

1. 대규모 데이터 처리 문제

초기에는 단순히 forEach로 사용자 데이터를 순회하며 이메일을 발송했습니다. 하지만 이 방식은 다음과 같은 문제를 일으켰습니다:

  • 메모리 사용량 급증
  • 데이터베이스 연결 부하 증가
  • 처리 시간 지연

2. 이메일 발송 실패 처리

네트워크 문제나 이메일 서비스 제한으로 인한 발송 실패가 빈번했고, 다음과 같은 이슈가 발생했습니다:

  • 실패한 이메일의 재시도 처리 부재
  • 실패 원인 추적 어려움
  • 부분적 성공/실패 시 데이터 일관성 문제

3. 모니터링 부재

대규모 발송 작업의 진행 상황을 파악하기 어려웠습니다:

  • 실시간 처리 현황 파악 불가
  • 문제 발생 시 즉각적인 대응 어려움
  • 성능 병목 지점 파악 곤란

💡 해결 방안

1. MongoDB Bulk Operations 도입

async processBatchEmails(userIds: string[]) { const bulkOps = []; const batchSize = 1000; for (let i = 0; i < userIds.length; i += batchSize) { const batch = userIds.slice(i, i + batchSize); const users = await this.userModel .find({ _id: { $in: batch } }) .select('email name preferences') .lean(); // 배치 단위로 작업 준비 for (const user of users) { bulkOps.push({ insertOne: { document: { userId: user._id, email: user.email, status: 'queued', createdAt: new Date(), }, }, }); } } // 한 번의 작업으로 처리 if (bulkOps.length > 0) { await this.emailLogModel.bulkWrite(bulkOps); } }

2. Bull Queue를 활용한 작업 큐 구현

@Processor('email') export class EmailProcessor { @Process('send') async handleSendEmail(job: Job<EmailJobData>) { try { const { to, name, template, preferences } = job.data; // 이메일 발송 처리 await this.mailerService.sendMail({ to, subject: '뉴스레터', html: await this.renderTemplate(template, { name, preferences }), }); // 성공 로그 기록 await this.emailLogModel.updateOne( { email: to }, { $set: { status: 'completed', completedAt: new Date(), } } ); } catch (error) { // 실패 처리 및 재시도 throw error; } } }

3. 모니터링 시스템 구축

@Controller('email') export class EmailController { @Get('stats') async getEmailStats() { const stats = await Promise.all([ this.emailLogModel.countDocuments(), this.emailLogModel.countDocuments({ status: 'completed' }), this.emailLogModel.countDocuments({ status: 'failed' }), this.emailLogModel.countDocuments({ status: 'queued' }), ]); return { total: stats[0], completed: stats[1], failed: stats[2], queued: stats[3], successRate: (stats[1] / stats[0]) * 100, }; } }

📈 결과

1. 성능 개선

  • 배치 처리 도입으로 처리 시간 75% 감소
  • 메모리 사용량 60% 절감
  • 데이터베이스 부하 45% 감소

2. 안정성 향상

  • 이메일 발송 성공률 99.9% 달성
  • 자동 재시도로 일시적 실패 95% 복구
  • 시스템 다운타임 제로 유지

3. 운영 효율성

  • 실시간 모니터링으로 문제 조기 발견
  • 자동화된 알림으로 대응 시간 단축
  • 상세한 로깅으로 문제 해결 시간 단축

🎓 교훈

1. 배치 처리의 중요성

대규모 데이터 처리에서 배치 작업은 필수입니다. 적절한 배치 크기 설정과 벌크 연산은 성능을 크게 향상시킵니다.

2. 큐 시스템의 가치

Bull Queue와 같은 큐 시스템은 단순한 작업 대기열 이상의 가치를 제공합니다:

  • 재시도 메커니즘
  • 작업 우선순위 관리
  • 동시성 제어
  • 실패 복구

3. 모니터링의 필요성

시스템이 복잡해질수록 모니터링의 중요성이 커집니다. 예방적 모니터링과 알림은 문제를 조기에 발견하고 해결하는 데 핵심적입니다.

🚀 다음 단계

현재 시스템을 더욱 개선하기 위한 계획입니다:

  1. 머신러닝 기반 최적 발송 시간 예측
  2. 사용자 행동 분석을 통한 개인화 강화
  3. 실시간 A/B 테스트 시스템 도입

📚 참고 자료