지난주(P5-1)에 @Cron을 EventBridge로 옮기긴 했는데, 방식이 rate(1 minute)로 scheduler-worker를 1분마다 깨워서 전체 채널을 DB에서 훑는 거였다. 채널이 늘어날수록 이 폴링 비용이 커진다. 채널마다 개별 EventBridge 규칙을 붙여서, 그 채널의 업로드 시각에만 Lambda가 호출되는 구조로 바꾸기로 했다.
규칙을 생성/삭제하려면 어떤 규칙이 이 채널 것인지 알아야 한다.
model Channel {
uploadSchedule String?
schedulerEnabled Boolean @default(false)
schedulerCategory String @default("top")
eventBridgeRuleArn String?
...
}
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 표현식으로 등록)가 대신 해준다.
스케줄러를 켜거나 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 규칙 없이도 충분하다"고 적어놨는데, 사실 폴링은 이미 위에서 없앤 뒤였다. 저장 실패를 막는 게 급해서 일단 응급처치부터 한 거고, 이 상태로는 스케줄러가 아예 안 돈다.
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 처리(이전 상태 롤백, 저장 상태 텍스트/색상)는 앞 글에서 다뤘다. 원인은 이거였다.