블로그

SQS vs EventBridge — MSA 전환에서 이벤트 기반 아키텍처를 설계한 기준

모놀리식에서 MSA로 전환하면서 서비스 간 결합도를 제거하기 위해 AWS 이벤트 기반 아키텍처를 도입했습니다. SQS와 EventBridge를 어떤 기준으로 나눠 쓰는지, 멱등성과 DLQ를 어떻게 설계했는지 공유합니다.

AWSSQSEventBridgeMSACatenoid이벤트드리븐

참고: 이 글의 코드는 실제 업무에서 경험한 내용을 바탕으로 개념적으로 재작성한 것입니다. 실제 회사 코드와는 무관합니다.

이벤트 기반 아키텍처 구조

Command: SQS 완료 이벤트: EventBridge 3회 실패 동영상 업로드 서비스 트랜스코딩Lambda 라우팅 메타데이터 서비스 알림 서비스 채널 서비스 DLQ 운영자 알람

들어가며

Catenoid에서 Loomex 미디어 유통 관리 솔루션을 .NET 모놀리식에서 Node.js MSA로 전환하는 프로젝트를 담당했습니다.

MSA 전환에서 가장 큰 도전은 기능 분리 자체가 아닙니다. 서비스를 분리한 뒤 서비스들이 서로 어떻게 통신하는가의 문제입니다. 잘못 설계하면 분리는 됐는데 결합도는 그대로인 "분산 모놀리식"이 만들어집니다.


문제: 동기식 서비스 간 호출의 한계

모놀리식에서 서비스를 분리하고 HTTP API로 서로 호출하면, 표면적으로는 MSA처럼 보입니다. 하지만 실제로는 이런 문제들이 생깁니다.

[동영상 업로드 서비스]
    → HTTP 호출 → [트랜스코딩 서비스]
    → HTTP 호출 → [메타데이터 서비스]
    → HTTP 호출 → [알림 서비스]
  • 트랜스코딩 서비스가 응답하는 동안 업로드 서비스는 대기
  • 트랜스코딩 서비스가 다운되면 업로드 서비스도 실패
  • 새로운 후속 작업이 추가될 때마다 업로드 서비스 코드를 수정해야 함

이것은 HTTP 껍데기를 쓴 모놀리식입니다.


설계 기준: Command vs Event

이벤트 기반 아키텍처를 도입할 때 가장 먼저 정립한 기준은 "Command"와 "Event"의 구분이었습니다.

구분 정의 AWS 서비스
Command 특정 작업을 수행하라는 요청. 처리자가 1개 SQS (Queue)
Event 상태 변경이 발생했다는 사실. 처리자가 여럿일 수 있음 EventBridge (Bus)

SQS 사용 케이스: VOD 트랜스코딩 요청

동영상 업로드가 완료되면 트랜스코딩이 필요합니다. 트랜스코딩은 **특정 Lambda 함수에 위임하는 명령(Command)**입니다.

// 업로드 완료 후 트랜스코딩 요청 (Command → SQS)
await sqs.sendMessage({
  QueueUrl: TRANSCODING_QUEUE_URL,
  MessageBody: JSON.stringify({
    videoId: video.id,
    sourcePath: video.s3Path,
    targetFormats: ['mp4_720p', 'mp4_1080p']
  }),
  MessageGroupId: video.id  // FIFO Queue로 순서 보장
}).promise();

SQS를 사용하면 Lambda가 다운되어도 메시지는 큐에 보존됩니다. Lambda가 복구되면 자동으로 처리를 재개합니다.

EventBridge 사용 케이스: 인코딩 완료 이벤트

트랜스코딩이 완료되면 여러 서비스가 이 사실을 알아야 합니다:

  • 메타데이터 서비스: 동영상 정보 업데이트
  • 알림 서비스: 광고주에게 완료 알림
  • 채널 서비스: 관련 채널 상태 업데이트
// 트랜스코딩 완료 이벤트 발행 (Event → EventBridge)
await eventBridge.putEvents({
  Entries: [{
    Source: 'catenoid.transcoding',
    DetailType: 'TranscodingCompleted',
    Detail: JSON.stringify({
      videoId: video.id,
      status: 'COMPLETED',
      outputPaths: { mp4_720p: '...', mp4_1080p: '...' }
    }),
    EventBusName: 'catenoid-media-bus'
  }]
}).promise();

EventBridge 규칙으로 각 서비스가 독립적으로 이 이벤트를 구독합니다. 새로운 서비스가 추가되어도 트랜스코딩 서비스 코드를 건드리지 않아도 됩니다.


안정성 설계: DLQ와 멱등성

Dead Letter Queue (DLQ)

메시지 처리가 반복적으로 실패하면 그 메시지는 DLQ로 이동합니다. 이를 통해:

  • 처리 실패한 메시지를 잃지 않음
  • 실패 원인 분석 후 재처리 가능
  • 정상 메시지 처리를 방해하지 않음
// SQS 큐 생성 시 DLQ 설정
const queueAttributes = {
  RedrivePolicy: JSON.stringify({
    deadLetterTargetArn: DLQ_ARN,
    maxReceiveCount: '3'  // 3번 실패 후 DLQ로
  })
};

멱등성 (Idempotency)

SQS Standard Queue는 "최소 1회 전달(At-Least-Once Delivery)"을 보장합니다. 같은 메시지가 두 번 처리될 수 있다는 의미입니다.

// 멱등성 키로 중복 처리 방지
export const handler = async (event: SQSEvent) => {
  for (const record of event.Records) {
    const messageId = record.messageId;
    
    // 이미 처리한 메시지인지 확인
    const isProcessed = await idempotencyStore.exists(messageId);
    if (isProcessed) {
      console.log(`Skipping already processed: ${messageId}`);
      continue;
    }
    
    await processTranscoding(JSON.parse(record.body));
    
    // 처리 완료 기록
    await idempotencyStore.set(messageId, { processedAt: new Date() });
  }
};

추가: S3 자동 정리

라이브 스트리밍 중 생성된 임시 청크 파일과 썸네일은 스트림 종료 후 자동으로 삭제되어야 합니다.

// EventBridge 스케줄러로 주기적 정리
export const cleanupHandler = async () => {
  const expiredChannels = await db.channels
    .findMany({ where: { status: 'ENDED', endedAt: { lt: sevenDaysAgo } } });
  
  for (const channel of expiredChannels) {
    await s3.deleteObjects({
      Bucket: MEDIA_BUCKET,
      Delete: {
        Objects: await listChunkFiles(channel.id)
      }
    }).promise();
  }
};

결과

이벤트 기반 아키텍처 전환으로:

  • 서비스 간 결합도 제거: 트랜스코딩 서비스가 다운되어도 업로드 서비스는 정상 동작
  • 새 기능 추가 용이: EventBridge 규칙만 추가하면 기존 코드 변경 없이 새 서비스 연결 가능
  • 인프라 비용 효율화: 트랜스코딩 같은 무거운 작업은 Lambda로 분리해 필요할 때만 실행

마치며: 언제 이벤트 기반이 적합한가

이벤트 기반 아키텍처가 항상 정답은 아닙니다. 다음 경우에 특히 효과적입니다:

  1. 처리 시간이 긴 비동기 작업 (동영상 트랜스코딩, 이메일 발송 등)
  2. 여러 서비스가 동일 이벤트에 반응해야 하는 경우 (Pub/Sub 패턴)
  3. 처리 실패 시 재시도가 필요한 작업 (DLQ 활용)

반면 즉각적인 응답이 필요하거나(결제 확인 등) 처리 순서가 매우 중요한 경우에는 동기식 호출이 더 적합합니다.