개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← YouTube Shorts 자동화

스케줄러 재설계 — 채널별 EventBridge 구조

2026년 7월 27일
schedulerEventBridgeAWSIAM

지난주(P5-1)에 @Cron을 EventBridge로 옮기긴 했는데, 방식이 rate(1 minute)로 scheduler-worker를 1분마다 깨워서 전체 채널을 DB에서 훑는 거였다. 채널이 늘어날수록 이 폴링 비용이 커진다. 채널마다 개별 EventBridge 규칙을 붙여서, 그 채널의 업로드 시각에만 Lambda가 호출되는 구조로 바꾸기로 했다.

Channel — eventBridgeRuleArn 필드 추가

규칙을 생성/삭제하려면 어떤 규칙이 이 채널 것인지 알아야 한다.

model Channel {
  uploadSchedule       String?
  schedulerEnabled     Boolean  @default(false)
  schedulerCategory    String   @default("top")
  eventBridgeRuleArn   String?
  ...
}

scheduler-worker — 폴링 제거, 이벤트 수신 방식으로

rate(1 minute) 트리거를 걷어내고, EventBridge가 넘겨주는 channelId로 그 채널 하나만 처리하는 핸들러로 바꿨다.

 functions:
   handler:
     handler: src/handler.ts
-    events:
-      - schedule:
-          rate: rate(1 minute)
-          enabled: true
export const handler: ScheduledHandler = async (event) => {
  const channelId = (event as unknown as { channelId?: string }).channelId;
  if (!channelId) {
    log.warn('channelId 없음 — 스킵');
    return;
  }

  const channel = await prisma.channel.findUnique({
    where: { id: channelId },
    select: { id: true, schedulerEnabled: true, isActive: true, schedulerCategory: true },
  });

  if (!channel?.schedulerEnabled || !channel.isActive) {
    log.info({ channelId }, '스케줄 스킵: 비활성 채널');
    return;
  }
  // ... 이하 진행 중인 Job 체크, 뉴스 수집, Job 생성은 기존과 동일
};

shouldRunNow로 cron 표현식을 직접 파싱해서 "지금이 실행 시각인가"를 판단하던 코드도 통째로 없어졌다. 이제 그 판단은 EventBridge 규칙 자체(cron 표현식으로 등록)가 대신 해준다.

API — 토글 시 규칙 자동 생성/삭제

스케줄러를 켜거나 cron을 바꾸면 API가 createChannelRule을 호출해서 EventBridge 규칙을 만들고, 끄면 deleteChannelRule로 지운다.

if (willBeEnabled && newCron && (cronChanged || enabledChanged)) {
  if (exists.eventBridgeRuleArn) await deleteChannelRule(id);
  data.eventBridgeRuleArn = await createChannelRule(id, newCron);
} else if (!willBeEnabled && enabledChanged) {
  if (exists.eventBridgeRuleArn) await deleteChannelRule(id);
  data.eventBridgeRuleArn = null;
}

저장이 깨졌다 — 일단 롤백

여기까지 배포하고 스케줄러 토글을 눌러봤더니 저장 자체가 실패했다. API가 EventBridge에 규칙을 생성하려는데 IAM 권한이 없어서 호출이 그대로 죽는 거였다. 사용자 입장에선 토글만 눌렀는데 저장이 안 되는 상황.

원인 파악에 시간을 더 쓰기보다, 일단 저장은 되게 만들어야 했다. EventBridge 호출 부분을 통째로 빼고 DB 업데이트만 하도록 롤백했다.

-import { createChannelRule, deleteChannelRule } from './eventbridge.js';
 ...
 async updateSchedule(id: string, dto: UpdateScheduleDto) {
-  const current = await this.repo.findSchedulerConfig(id);
-  if (!current) throw new NotFoundException('채널을 찾을 수 없습니다.');
-  ...
-  if (willBeEnabled && newCron && ...) {
-    data.eventBridgeRuleArn = await createChannelRule(id, newCron);
-  }
+  const exists = await this.repo.findSchedulerConfig(id);
+  if (!exists) throw new NotFoundException('채널을 찾을 수 없습니다.');
   return this.repo.updateSchedule(id, data);
 }

이 롤백 커밋 메시지에 "scheduler-worker가 rate(1 min) 폴링으로 DB를 직접 읽으므로 EventBridge 규칙 없이도 충분하다"고 적어놨는데, 사실 폴링은 이미 위에서 없앤 뒤였다. 저장 실패를 막는 게 급해서 일단 응급처치부터 한 거고, 이 상태로는 스케줄러가 아예 안 돈다.

IAM 권한 잡고 다시 붙임 (P6-1)

lambda:CreateEventSourceMapping, events:PutRule 계열 권한을 API의 실행 역할에 추가하고, EventBridge 호출을 다시 붙였다.

async updateSchedule(id: string, dto: UpdateScheduleDto) {
  const exists = await this.repo.findSchedulerConfig(id);
  if (!exists) throw new NotFoundException('채널을 찾을 수 없습니다.');

  const willBeEnabled = dto.schedulerEnabled ?? exists.schedulerEnabled;
  const newCron = dto.cronExpression ?? exists.uploadSchedule;
  const cronChanged = dto.cronExpression !== undefined && dto.cronExpression !== exists.uploadSchedule;
  const enabledChanged = dto.schedulerEnabled !== undefined && dto.schedulerEnabled !== exists.schedulerEnabled;

  if (willBeEnabled && newCron && (cronChanged || enabledChanged)) {
    if (exists.eventBridgeRuleArn) await deleteChannelRule(id);
    data.eventBridgeRuleArn = await createChannelRule(id, newCron);
  } else if (!willBeEnabled && enabledChanged) {
    if (exists.eventBridgeRuleArn) await deleteChannelRule(id);
    data.eventBridgeRuleArn = null;
  }

  return this.repo.updateSchedule(id, data);
}

토글 저장 자체가 실패하던 문제와 그 UI 처리(이전 상태 롤백, 저장 상태 텍스트/색상)는 앞 글에서 다뤘다. 원인은 이거였다.

← 이전 글웹 리팩터 — useYoutubeConnect 훅 분리
다음 글 →Analytics 동기화 — 일일 배치 자동화