TIL - 20260929

juni·3일 전

TIL

목록 보기
465/467

0929 운영 자동화/AI 워크플로우 심화 (23/N): Job Scheduling, Cron, 분산 실행과 Missed Job 복구


✅ 1. 자동화가 늘어나면 결국 ‘시간’에 의해 실행되는 작업이 생긴다

지금까지는 주로:

HTTP 요청
Webhook
Queue Event
Git Push

처럼 어떤 사건이 발생했을 때 자동화가 시작되는 구조를 다뤘다.

하지만 실제 운영에서는 정해진 시간마다 실행해야 하는 작업도 많다.

예:

매일 오전 9시
일일 통계 생성

매일 자정
오래된 임시 데이터 정리

매시간
UNKNOWN Job Reconciliation

5분마다
Provider Health 확인

매주 월요일
주간 운영 보고서 생성

매일 새벽
백업 검증

이런 작업을 Scheduled Job이라고 볼 수 있다.


✅ 2. 가장 단순한 방법은 Cron이다

예:

0 9 * * *

는 매일 오전 9시에 실행한다.

NestJS에서도 Scheduler를 이용해:

@Cron('0 9 * * *')
async handleDailyReport() {
  // ...
}

같은 형태로 만들 수 있다.

작은 프로젝트에서는 이 정도로도 충분해 보인다.

하지만 운영 환경에서는 문제가 생긴다.


✅ 3. 서버가 한 대일 때는 잘 되다가 서버가 두 대가 되면 문제가 생긴다

예를 들어 Production Instance가 두 개라고 하자.

Server A
Server B

두 서버 모두 같은 Cron 코드를 가지고 있다.

오전 9시가 되면:

Server A
→ Daily Report 실행

Server B
→ Daily Report 실행

결과:

같은 보고서 2개 생성

될 수 있다.


✅ 4. Scheduler도 분산 환경을 고려해야 한다

핵심 질문은:

여러 서버가 같은 시간을 감지했을 때 누가 실제 작업을 실행할 것인가?

다.

이를 해결하지 않으면 다음과 같은 일이 생길 수 있다.

알림톡 중복 발송

Export 중복 생성

통계 중복 집계

Notion 보고서 중복 업로드

정리 작업 동시 실행

✅ 5. Scheduler와 Job 실행을 분리하는 것이 좋다

좋은 구조는:

Scheduler
↓
Job 생성
↓
Queue
↓
Worker

이다.

Scheduler가 실제 무거운 작업까지 직접 하지 않는다.

예:

09:00

Scheduler
↓
DailyReportJob 생성

Queue
↓
Worker 실행

✅ 6. Scheduler의 책임

Scheduler는 가능한 한 단순하게 유지한다.

지금 실행해야 하는 Job이 있는가?

있다면 Job을 등록한다.

정도다.

실제:

DB 조회

Report 생성

Notion 업로드

파일 생성

은 Worker가 담당한다.


✅ 7. 이렇게 분리하는 이유

Scheduler 안에서 직접 모든 작업을 하면:

실행 시간이 길어짐

실패 Retry 어려움

중복 실행 제어 어려움

서버 재시작 시 상태 추적 어려움

이 생긴다.

Queue Job으로 만들면 기존에 다뤘던:

Retry

Idempotency

Heartbeat

Lease

Reconciliation

DLQ

구조를 그대로 재사용할 수 있다.


✅ 8. Scheduled Job도 Idempotency가 필요하다

예:

2026-09-29 일일 보고서 생성

이라는 작업이 두 번 실행되었다고 하자.

다음 Key를 만들 수 있다.

daily-report:2026-09-29

이 Key가 이미 존재하면 두 번째 실행은 막는다.


✅ 9. Schedule Instance라는 개념

Cron 정의와 실제 실행을 구분하면 좋다.

예:

Schedule

daily-report
매일 09:00

그리고 실제 실행:

Schedule Run

daily-report
2026-09-29 09:00

이다.


✅ 10. Schedule과 ScheduleRun

예:

Schedule
정책

ScheduleRun
실제 실행 기록

이다.

한 Schedule에는 시간이 지날수록 여러 Run이 생긴다.

daily-report

├─ 09/27 Run
├─ 09/28 Run
└─ 09/29 Run

✅ 11. Schedule 모델 예시

interface Schedule {
  id: string;

  name: string;

  cronExpression: string;

  timezone: string;

  enabled: boolean;

  jobType: string;
}

✅ 12. ScheduleRun 모델

interface ScheduleRun {
  id: string;

  scheduleId: string;

  scheduledFor: Date;

  status:
    | 'PENDING'
    | 'QUEUED'
    | 'RUNNING'
    | 'SUCCESS'
    | 'FAILED'
    | 'MISSED'
    | 'SKIPPED';

  jobId?: string;
}

✅ 13. scheduledFor가 중요하다

Job이 실제로:

09:03

에 실행됐어도 원래 예정 시간이:

09:00

이면:

scheduledFor = 09:00

을 기록한다.

그래야 이 Run이 어떤 예약 실행인지 알 수 있다.


✅ 14. 실행 시간과 예정 시간을 구분한다

예:

scheduledFor
09:00

startedAt
09:03

completedAt
09:04

이렇게 보면:

Scheduler Delay = 3분

도 측정할 수 있다.


✅ 15. Scheduled Job Idempotency Key

예:

schedule:{scheduleId}:{scheduledFor}

처럼 만들 수 있다.

예:

schedule:daily-report:2026-09-29T09:00

이 값에 UNIQUE를 걸면 동일 예약 실행 중복 생성을 막을 수 있다.


✅ 16. 분산 Scheduler에서 가장 단순한 해결책

DB Unique Constraint를 사용하는 것이다.

서버 A와 B가 동시에:

09:00 Run 생성

을 시도한다고 하자.

DB에는:

(scheduleId, scheduledFor)
UNIQUE

를 둔다.

결과:

Server A
INSERT 성공

Server B
UNIQUE 충돌
→ Skip

이 된다.


✅ 17. 이것이 Distributed Lock보다 단순할 때가 많다

무조건 Redis Lock 같은 것을 먼저 넣을 필요는 없다.

단순히:

이 시간의 Run Record가 존재하는가?

만 보장하면 된다면 DB UNIQUE로 충분하다.


✅ 18. Prisma 모델 예시

model ScheduleRun {
  id           String   @id @default(cuid())

  scheduleId   String
  scheduledFor DateTime

  status       String

  jobId        String?

  startedAt    DateTime?
  completedAt  DateTime?

  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt

  @@unique([scheduleId, scheduledFor])
  @@index([status, scheduledFor])
}

✅ 19. Scheduler 흐름

예:

09:00

Server A
Server B

둘 다 Due Schedule 발견

↓

ScheduleRun INSERT 시도

↓

A 성공
B Unique Conflict

↓

A만 Queue Job 생성

이다.


✅ 20. Lock을 사용할 수도 있다

더 복잡한 Scheduler에서는:

Scheduler Leader

하나만 실행하게 만들 수도 있다.

예:

distributed lock

을 잡은 서버만 Scheduling을 수행한다.

하지만 현재 규모에서는 DB Run Record 방식이 더 단순할 가능성이 높다.


✅ 21. Leader Election은 언제 필요한가?

예:

수천 개 Schedule

매초 Scheduling

복잡한 Calendar Rule

대규모 분산 시스템

정도라면 Leader Election을 고려할 수 있다.

현재 프로젝트에서는 과하다.


✅ 22. Scheduled Job과 Worker Job ID는 다르다

예:

ScheduleRun
run_123

과:

Queue Job
job_837

은 역할이 다르다.

ScheduleRun은:

왜, 언제 실행될 예정이었는가?

를 기록한다.

Queue Job은:

실제로 어떤 실행 작업을 처리하는가?

를 나타낸다.


✅ 23. ScheduleRun에서 Queue Job을 연결한다

예:

ScheduleRun
run_123

scheduledFor
09:00

↓

Queue Job
job_837

이렇게 연결한다.


✅ 24. Scheduler가 Queue 등록 전에 죽는 상황

까다로운 케이스다.

ScheduleRun INSERT 성공

↓

서버 Crash

↓

Queue Job 생성 못 함

DB에는:

PENDING

Run이 남는다.


✅ 25. Reconciliation으로 복구한다

예:

ScheduleRun
PENDING

createdAt
10분 전

인데 Job이 없다면 Reconciler가:

Queue Job 생성

↓

QUEUED

로 복구할 수 있다.


✅ 26. Scheduler에도 UNKNOWN 비슷한 상태가 생긴다

예:

Queue Publish 요청

↓

Connection Timeout

실제로 Queue에 들어갔는지 알 수 없다.

이때 무작정 다시 Publish하면 중복 Job이 생길 수 있다.


✅ 27. Job에도 ScheduleRun ID를 넣는다

예:

{
  "scheduleRunId": "run_123",
  "idempotencyKey": "schedule:daily-report:2026-09-29T09:00"
}

Queue에서 중복 전달되더라도 Worker가 같은 Run인지 알 수 있다.


✅ 28. Scheduler는 At-Least-Once를 기본 가정으로 생각한다

실제 분산 시스템에서는:

정확히 한 번

실행을 완벽히 보장하기 어렵다.

현실적인 접근은:

한 번 이상 전달될 수 있음

↓

실제 Side Effect는
한 번만 발생하도록 설계

하는 것이다.


✅ 29. Exactly Once보다 Effectively Once

개념적으로:

Exactly Once

를 완벽하게 구현하기보다:

At-Least-Once Delivery
+
Idempotency
=
Effectively Once

에 가까운 구조를 만든다.


✅ 30. Cron Job에도 실패 유형이 다르다

예:

Scheduler 자체 실패

Job Queue 등록 실패

Worker 실패

외부 API 실패

DB 실패

를 구분해야 한다.


✅ 31. Scheduler Failure

예:

09:00 Scheduler 프로세스 다운

이면 아예 Run이 생성되지 않을 수 있다.

이게 Missed Job이다.


✅ 32. Missed Job이란?

원래:

09:00

실행돼야 하는 Job이 있었는데 서버가 내려가 있었다고 하자.

서버가:

09:30

복구됐다.

단순 Cron이라면 09:00 Job은 영원히 사라질 수 있다.

이게 Missed Job 문제다.


✅ 33. 모든 Missed Job을 다시 실행해야 하는 것은 아니다

예:

매분 시스템 상태 확인

Job이 30분 동안 놓쳤다고:

30개 Run

을 복구할 필요는 없을 수 있다.

반면:

일일 정산

은 놓치면 반드시 복구해야 한다.


✅ 34. Misfire Policy

Schedule마다 놓친 실행을 어떻게 처리할지 정책을 둔다.

예:

SKIP

RUN_ONCE

CATCH_UP_ALL

MANUAL

✅ 35. SKIP

놓친 작업은 그냥 건너뛴다.

예:

1분마다 Health Snapshot

서버가 20분 꺼졌다고 과거 20개 Snapshot을 다시 만들 필요는 없다.


✅ 36. RUN_ONCE

놓친 작업이 여러 개여도 복구 후 한 번만 실행한다.

예:

Cache Refresh

이다.

과거 Refresh를 전부 실행할 필요 없이 현재 상태 한 번만 갱신하면 된다.


✅ 37. CATCH_UP_ALL

놓친 모든 작업을 실행한다.

예:

시간대별 정산

날짜별 데이터 집계

에서 각 시간의 결과가 모두 필요할 수 있다.


✅ 38. MANUAL

영향이 큰 작업은 자동 복구하지 않는다.

예:

대량 고객 메시지 발송

금전 정산

데이터 삭제

놓쳤다면 운영자가 확인 후 실행한다.


✅ 39. Schedule마다 Misfire Policy가 달라야 한다

예:

작업Policy
Health CheckSKIP
Cache RefreshRUN_ONCE
일일 통계CATCH_UP_ALL
대량 알림 발송MANUAL

이런 식이다.


✅ 40. Grace Period

예정 시간에서 너무 오래 지난 작업은 실행 가치가 없을 수도 있다.

예:

오전 9시 안내 메시지

를 오후 6시에 보내면 오히려 문제가 된다.

그래서:

misfireGracePeriod

를 둘 수 있다.


✅ 41. 예시

Scheduled
09:00

Grace
30분

현재
09:20
→ 실행 가능
현재
11:00
→ SKIP / MANUAL

같이 판단한다.


✅ 42. 업무별 Freshness가 다르다

예:

일일 리포트
2시간 늦어도 의미 있음
실시간 고객 안내
2시간 늦으면 의미 없음

이 차이를 Schedule Policy에 반영해야 한다.


✅ 43. Timezone은 반드시 명시한다

Cron에서 매우 자주 실수하는 부분이다.

09:00

만 저장하면 어느 시간대인지 모호하다.

따라서:

timezone = Asia/Seoul

같이 명시한다.


✅ 44. 서버 Timezone에 의존하지 않는다

개발 서버는:

Asia/Seoul

인데 Production은:

UTC

일 수 있다.

Cron이 서버 로컬 시간을 기준으로 하면 9시간 차이가 날 수 있다.


✅ 45. DB에는 UTC 저장 + Schedule에는 Timezone 보존

일반적으로:

DB Timestamp
UTC

로 저장하되,

Schedule Definition에는:

timezone
Asia/Seoul

을 유지하는 방식이 좋다.


✅ 46. DST도 고려해야 할 수 있다

한국은 현재 DST가 없지만 해외 사용자나 호주 워홀 이후 개인 자동화를 만들 경우에는 달라질 수 있다.

예:

Australia/Sydney

는 계절에 따라 UTC Offset이 바뀔 수 있다.

따라서:

UTC+10

처럼 Offset만 고정하기보다 Timezone 이름을 사용하는 것이 안전하다.


✅ 47. Cron Expression을 사람이 이해하기 어렵다

예:

0 0 9 * * 1-5

만 저장하면 운영자가 즉시 이해하기 어렵다.

관리자 화면에서:

평일 오전 9시

같은 설명도 같이 보여주는 것이 좋다.


✅ 48. Schedule 활성화/비활성화

운영 중 특정 Cron을 잠시 멈출 필요가 있다.

예:

enabled = false

를 지원한다.

코드를 삭제하거나 서버를 재배포해서 끄는 방식은 비효율적이다.


✅ 49. Schedule Kill Switch

특히 위험한 Scheduled Job은 즉시 끌 수 있어야 한다.

예:

대량 알림톡

자동 데이터 정리

AI 자동 작업

이다.


✅ 50. Schedule 변경도 Configuration Change다

예:

매일 09:00
→ 매시간

으로 잘못 바꾸면 작업량이 24배가 된다.

따라서 Schedule 변경도 Audit 대상이다.


✅ 51. Schedule Audit Log

예:

Action
SCHEDULE_UPDATED

Schedule
daily-report

Before
0 9 * * *

After
0 * * * *

Actor
admin_1

처럼 남긴다.


✅ 52. Cron 변경은 생각보다 위험하다

예:

0 * * * *

와:

* * * * *

는 큰 차이다.

한 글자 실수로:

시간당 1회
→ 분당 1회

가 될 수 있다.


✅ 53. Schedule Validation

Cron 변경 시:

Expression 유효성

다음 실행 시간

예상 하루 실행 횟수

최소 실행 간격

을 보여주면 좋다.


✅ 54. 너무 짧은 주기를 차단할 수 있다

예:

최소 1분

또는 업무에 따라:

최소 5분

같은 정책을 둘 수 있다.

잘못된 Cron으로 폭주하는 것을 방지한다.


✅ 55. Next Run Preview

관리자에서 Cron을 설정할 때:

다음 실행:

2026-09-30 09:00
2026-10-01 09:00
2026-10-02 09:00

처럼 보여주면 실수를 크게 줄일 수 있다.


✅ 56. Schedule 변경에도 Version을 둘 수 있다

예:

daily-report

version 7

변경 후:

version 8

로 증가시킨다.


✅ 57. Optimistic Lock도 적용 가능

두 관리자가 같은 Schedule을 수정할 경우:

version = 7

을 조건으로 Update한다.

먼저 수정한 사람이 version 8로 바꾸면 다른 Update는 실패한다.


✅ 58. Schedule Run 상태 머신

예:

PENDING
↓
QUEUED
↓
RUNNING
↓
SUCCESS

실패:

RUNNING
↓
FAILED
↓
RETRY_PENDING

놓친 경우:

MISSED

정책상 실행하지 않을 경우:

SKIPPED

✅ 59. Run과 Attempt를 구분한다

예:

09:00 Daily Report

는 ScheduleRun 하나다.

실패해서 세 번 Retry했다고:

Run 3개

가 되는 것은 아니다.

Run 1개
└─ Attempt 1
└─ Attempt 2
└─ Attempt 3

이다.


✅ 60. ScheduleRun ID는 그대로 유지한다

Retry할 때도:

scheduleRunId = run_123

은 유지한다.

그래야 동일 예약 작업의 재시도라는 것을 알 수 있다.


✅ 61. Retry로 다음 Schedule과 겹칠 수 있다

예:

Hourly Job

10:00 Run
실패

Retry가 계속돼:

11:00

까지 이어졌다.

동시에:

11:00 Run

도 시작할 수 있다.


✅ 62. Overlap Policy가 필요하다

같은 Schedule의 실행이 겹칠 때 정책을 정한다.

예:

ALLOW

SKIP_NEW

QUEUE_NEW

CANCEL_OLD

✅ 63. ALLOW

이전 Run이 끝나지 않아도 새 Run을 실행한다.

예:

독립적인 통계 Snapshot

등에서 가능하다.


✅ 64. SKIP_NEW

이전 Run이 아직 실행 중이면 새 Run을 건너뛴다.

예:

Cache Refresh

같은 작업에서 쓸 수 있다.


✅ 65. QUEUE_NEW

새 Run은 생성하지만 이전 Run이 끝난 뒤 실행한다.

예:

시간별 집계

처럼 순서대로 모두 처리해야 할 때 유용하다.


✅ 66. CANCEL_OLD는 조심해야 한다

새 Schedule이 시작했다고 기존 Job을 강제 종료하면 중간 Side Effect가 남을 수 있다.

따라서 Cancel Safe한 작업에서만 사용한다.


✅ 67. 현재 프로젝트에는 SKIP_NEW / QUEUE_NEW가 실용적이다

예:

Reconciliation
→ SKIP_NEW

이전 Reconciliation이 아직 돌고 있다면 새 작업을 굳이 또 돌릴 필요가 없을 수 있다.

반면:

시간별 리포트
→ QUEUE_NEW

처럼 모든 기간 데이터가 필요한 경우 순차 처리한다.


✅ 68. Max Runtime

Job이 비정상적으로 오래 실행될 수도 있다.

예:

평소 2분

현재 2시간

이라면 문제가 있다.

Schedule 정의에:

maxRuntime

을 둘 수 있다.


✅ 69. Max Runtime 초과

예:

RUNNING
2시간 초과

↓

STALLED 또는 UNKNOWN

으로 처리하고 Reconciliation을 시작한다.


✅ 70. 단 Timeout과 강제 종료는 구분한다

DB Query가 중간에 실행 중일 수도 있고 외부 API Side Effect가 발생했을 수도 있다.

단순히 프로세스를 죽이고 재실행하면 위험하다.

0916~0917의:

UNKNOWN
→ Reconcile

원칙을 그대로 사용한다.


✅ 71. Job Heartbeat

긴 Scheduled Job이라면:

heartbeatAt

을 주기적으로 갱신한다.

예:

Export
대량 집계
AI 분석

등이다.


✅ 72. Lease

Worker가 Run을 가져갔다면:

lockedBy

leaseUntil

을 관리할 수 있다.

Lease가 살아 있는 동안 다른 Worker는 가져가지 않는다.


✅ 73. 서버 재시작 후 Job Recovery

예:

03:00
Daily aggregation 시작

03:05
서버 재시작

03:10
서버 복구

Scheduler만 다시 켜서는 안 된다.

기존:

RUNNING

상태의 Job도 검사해야 한다.


✅ 74. Startup Reconciliation

애플리케이션 시작 시:

오래된 RUNNING

PENDING

UNKNOWN

Run을 검사할 수 있다.

다만 모든 것을 앱 시작 Blocking으로 하지 않는 편이 좋다.

별도 Recovery Worker로 보내는 것이 안전하다.


✅ 75. Schedule Reconciliation

주기적으로 다음을 검사한다.

실행 예정이었는데 Run 없음

PENDING인데 Job 없음

QUEUED인데 Queue Job 없음

RUNNING인데 Heartbeat 없음

각각 복구한다.


✅ 76. ‘Run이 아예 없는’ Missed Job을 찾는 방법

예:

Schedule
매일 09:00

Last Successful Run
09/28

현재
09/30 10:00

라면:

09/29 09:00
09/30 09:00

이 빠졌는지 계산한다.


✅ 77. 모든 과거 Schedule을 무한 검색하면 안 된다

Schedule이 몇 년 된 경우:

수십만 Run

을 계산할 수 있다.

따라서:

lastEvaluatedAt

이나:

nextRunAt

을 관리하는 편이 좋다.


✅ 78. nextRunAt

Schedule에:

nextRunAt

을 저장한다.

Scheduler는:

nextRunAt <= now

인 Schedule만 찾는다.


✅ 79. 실행 후 다음 시간 계산

예:

현재 nextRunAt
09/29 09:00

Run 생성 후:

다음
09/30 09:00

으로 업데이트한다.


✅ 80. nextRunAt 업데이트도 Transaction을 고려한다

예:

ScheduleRun 생성 성공

nextRunAt 업데이트 실패

하면 같은 시간을 다시 Scheduling할 수 있다.

하지만 ScheduleRun UNIQUE가 최종 방어선 역할을 한다.


✅ 81. Unique Constraint가 중요한 이유가 다시 나온다

애플리케이션 코드에서:

이미 Run 있나?

확인한 뒤 Insert하는 것만으로는 Race Condition이 생길 수 있다.

최종 방어는:

DB UNIQUE

로 둔다.


✅ 82. Schedule 변경 중 기존 Run 처리

예:

매일 09:00
→ 매일 10:00

으로 변경했다.

이미 생성된:

09:00 Run

을 어떻게 할지 정책이 필요하다.

보통 이미 생성된 Run은 유지하고 미래 실행부터 새 Schedule을 적용하는 편이 이해하기 쉽다.


✅ 83. 예약 시각 변경은 과거 Run을 수정하지 않는다

Run은 실행 당시의 기록이다.

Schedule Definition

이 바뀌었다고 과거:

scheduledFor

값을 수정하면 안 된다.


✅ 84. Schedule Snapshot

Run 생성 시 당시 Schedule 정보를 일부 저장할 수 있다.

예:

scheduleVersion

cronExpression

timezone

이다.

나중에 Schedule이 바뀌어도 당시 실행 조건을 확인할 수 있다.


✅ 85. Scheduled Job Payload Version

시간이 지나면서 Job Payload 구조도 바뀔 수 있다.

예:

v1
date

에서:

v2
date + carrier

로 바뀐다.

Queue에 오래 대기 중인 Job이 있다면 새 Worker와 호환되지 않을 수 있다.


✅ 86. Payload Version을 명시한다

예:

{
  "version": 2,
  "scheduleRunId": "run_123",
  "payload": {}
}

Worker가 지원하지 않는 Version이면 명확히 실패시킨다.


✅ 87. Schedule 실행도 Audit 대상이 될 수 있다

자동 실행은:

Actor
SYSTEM_SCHEDULER

로 남긴다.

예:

Action
DAILY_REPORT_TRIGGERED

ScheduleRun
run_123

✅ 88. 누가 수동으로 Run했는지도 구분한다

관리자가:

지금 실행

버튼을 누를 수 있다.

이 경우:

triggerType
MANUAL

로 기록한다.

자동이면:

SCHEDULED

이다.


✅ 89. Manual Run과 Scheduled Run은 같은 Worker를 사용한다

실행 엔진을 따로 만들 필요가 없다.

Scheduled Trigger
      ↓
    Queue
      ↓
    Worker

Manual Trigger
      ↓
    Queue
      ↓
    Worker

처럼 Trigger만 다르게 한다.


✅ 90. 수동 실행의 Idempotency

예:

일일 보고서 다시 생성

버튼을 연속으로 클릭할 수 있다.

그래서 수동 실행도:

Force New Run

Reuse Existing Run

정책을 명확히 한다.


✅ 91. ‘재실행’과 ‘새로운 실행’을 구분한다

예:

09/29 Daily Report

가 실패했다.

관리자가 Retry하면:

같은 ScheduleRun
새 Attempt

이다.

반면 수정된 설정으로 완전히 새 보고서를 만들고 싶다면:

Manual Run

을 새로 생성할 수 있다.


✅ 92. Backfill

과거 기간의 작업을 다시 실행하는 것을 Backfill이라고 볼 수 있다.

예:

9월 1일 ~ 9월 10일
통계 재집계

이다.


✅ 93. Backfill은 Cron Retry와 다르다

Retry:

기존 Run을 다시 시도

Backfill:

과거 기간에 대해 새로운 실행 생성

이다.


✅ 94. Backfill은 특히 부하 관리가 중요하다

예:

365일 통계

를 한꺼번에 Backfill하면:

DB Load 증가

Queue 폭주

API Rate Limit

이 생길 수 있다.


✅ 95. Backfill Concurrency 제한

예:

동시에 2개 날짜

만 처리한다.

또는:

Batch 7일

씩 진행한다.


✅ 96. Backfill은 Production 핵심 Traffic보다 낮은 Priority로

Queue Priority가 있다면:

고객 알림
HIGH

Backfill
LOW

로 두는 것이 좋다.


✅ 97. Schedule Priority

Scheduled Job에도 우선순위를 둘 수 있다.

예:

CRITICAL
주문 Reconciliation

HIGH
알림톡 Recovery

NORMAL
일일 Report

LOW
과거 통계 Backfill

✅ 98. Scheduled Job이 정상 API 자원을 빼앗으면 안 된다

예:

새벽 대량 Export

↓
DB CPU 100%

↓
고객 주문 API 느려짐

이면 Scheduler 설계가 잘못된 것이다.


✅ 99. 실행 시간을 비혼잡 시간으로 잡는 이유

대량 작업은:

새벽

에 배치하는 경우가 많다.

하지만 단순히 새벽이라는 이유만으로 안전한 것은 아니다.

다른 Batch가 같은 시간에 몰릴 수 있다.


✅ 100. Thundering Herd

예:

00:00

Daily Report
Backup
Cleanup
Statistics
Export

모두 동시에 시작하면 부하가 폭증한다.

이를 피해야 한다.


✅ 101. Schedule 분산

예:

00:05
Cleanup

00:15
Statistics

00:30
Report

01:00
Backup Verification

처럼 분산시킨다.


✅ 102. Jitter를 Scheduled Job에도 적용 가능하다

정확한 시간이 중요하지 않은 작업이라면:

09:00 ± 5분

처럼 약간 분산할 수 있다.

여러 Instance/서비스가 동시에 외부 API를 때리는 것을 줄일 수 있다.


✅ 103. 단 정확한 비즈니스 시간은 Jitter를 쓰면 안 된다

예:

오전 9시 정각 발송

이 요구사항이라면 Random Delay를 함부로 추가하면 안 된다.


✅ 104. Schedule Dependency

어떤 Job이 다른 Job 이후에 실행되어야 할 수 있다.

예:

Daily Data Aggregation

↓

Daily Report

단순히:

03:00 집계

03:30 보고서

로 시간만 벌려놓는 것은 불안하다.


✅ 105. 시간보다 Dependency를 명시하는 것이 낫다

예:

AggregationJob SUCCESS

↓

ReportJob 생성

처럼 Event/Workflow로 연결한다.


✅ 106. Cron은 ‘시작점’에만 사용한다

좋은 구조:

03:00 Cron

↓

Aggregation Workflow 시작

↓

집계 완료 Event

↓

Report 생성

↓

업로드

이다.

모든 Step을 각각 다른 Cron으로 만드는 것보다 안전하다.


✅ 107. Scheduled Workflow

하나의 Cron이 여러 Step을 실행할 수도 있다.

예:

DailyReportWorkflow

1. Git Activity 수집

2. 작업 내용 분석

3. Markdown 생성

4. Notion Upload

5. 완료 기록

✅ 108. Workflow Step 상태

예:

COLLECT
SUCCESS

GENERATE
SUCCESS

NOTION_UPLOAD
FAILED

이면 전체를 처음부터 다시 실행하지 않고 Upload부터 Resume할 수 있다.


✅ 109. 현재 Local LLM Work Report와 잘 맞는다

예:

매일 오후 7시 30분

↓

Git Activity 수집

↓

Ollama Report 생성

↓

Markdown 저장

↓

Notion Upload

를 Schedule Workflow로 만들 수 있다.


✅ 110. Report Workflow의 Idempotency

예:

project
platform

date
2026-09-29

라면:

daily-report:platform:2026-09-29

같은 Key를 사용한다.


✅ 111. 중복 파일 방지

파일명도:

2026-09-29-platform-daily-summary.md

처럼 Logical Key와 연결하면 좋다.

이미 존재한다면:

overwrite

version 생성

skip

정책을 정한다.


✅ 112. Notion 업로드도 중복 방지

같은 보고서를 Retry하다가 Notion 페이지가 두 개 생성될 수 있다.

그래서:

notionPageId

를 저장하거나:

externalId
daily-report:platform:2026-09-29

같은 값을 사용한다.


✅ 113. Scheduler와 AI Workflow

AI 자동화는 실행 시간이 길고 실패 가능성이 높기 때문에 Scheduler 안에서 직접 LLM을 호출하지 않는 편이 좋다.

Scheduler
↓
AITask 생성
↓
AI Worker

로 분리한다.


✅ 114. AI Scheduled Task 예시

매주 월요일 오전 8시

↓

지난주 Git Commit 수집

↓

AI 주간 업무 요약

↓

Notion 저장

✅ 115. AI Schedule에도 비용/자원 Budget이 필요하다

예:

매시간
전체 Repository 분석

같은 작업은 로컬 장비나 API 비용을 과도하게 사용할 수 있다.

Schedule을 설정할 때 예상 비용도 고려한다.


✅ 116. AI Scheduled Task Overlap

이전 AI Report가 아직 생성 중인데 다음 Schedule이 시작될 수 있다.

AI 작업에는 보통:

SKIP_NEW

또는:

QUEUE_NEW

가 더 안전하다.


✅ 117. AI Run Fingerprint와 연결

0916에서 다뤘던:

task
commitSha
promptVersion
contextHash
policyVersion

등을 Fingerprint로 사용해 같은 입력의 중복 실행을 막을 수 있다.


✅ 118. Schedule Config 변경 시 AI Prompt Version도 기록

예:

ScheduleRun
2026-09-29

promptVersion
v21

model
shn-coder

처럼 당시 실행 환경을 남긴다.


✅ 119. Scheduled Job Observability

최소한 다음 지표를 보면 좋다.

schedule_runs_total

schedule_run_success_total

schedule_run_failed_total

schedule_run_missed_total

schedule_run_duration

schedule_start_delay

✅ 120. Start Delay

startedAt - scheduledFor

이다.

예:

예정
09:00

시작
09:08

이면 8분이다.

Scheduler 또는 Queue가 밀리고 있다는 신호가 될 수 있다.


✅ 121. Success Rate

예:

지난 30일

Daily Report

예정 Run
30

성공
29

실패
1

성공률:

96.7%

이다.


✅ 122. Schedule Freshness SLO

예:

Daily Report

예정 시간 이후
30분 이내 완료

95%

같은 SLO도 만들 수 있다.


✅ 123. Missed Run Alert

예:

scheduledFor + gracePeriod

을 지났는데 Run이 없으면:

SCHEDULE_MISSED

Alert를 낸다.


✅ 124. Scheduler가 살아 있는지 감시하는 방법

Scheduler 자체 Health도 필요하다.

예:

scheduler heartbeat

을 주기적으로 기록한다.


✅ 125. Scheduler Heartbeat

예:

scheduler_instance
scheduler-a

lastHeartbeatAt
09:28

가 너무 오래되면 Scheduler가 죽었을 수 있다.


✅ 126. 하지만 Instance Health보다 더 중요한 것은 Run 생성 여부다

Scheduler 프로세스는 살아 있지만 Bug 때문에 Job을 생성하지 않을 수도 있다.

따라서:

Heartbeat 정상

만 믿으면 안 된다.


✅ 127. Business Schedule Invariant

예:

매일 오전 9시 Daily Report가 하나 있어야 한다.

자체를 검사한다.

즉:

오늘 09:00 Run 존재?

를 확인하는 것이 더 직접적이다.


✅ 128. Silent Scheduler Failure

가장 위험한 경우다.

Error 없음

프로세스 정상

그런데 Cron Trigger 실행 안 됨

이다.

그래서 Expected Run 검사가 필요하다.


✅ 129. Scheduler Reconciler

예:

매 10분

↓

최근 Due Schedule 확인

↓

ScheduleRun 존재 여부 검사

↓

없는 경우 Misfire Policy 적용

할 수 있다.


✅ 130. Reconciler 자체 Scheduler 문제

그런데 Reconciler도 Cron이면:

Reconciler도 안 돌 수 있음

이라는 문제가 있다.

그래서 핵심 시스템에서는 별도 외부 Scheduler나 Health Check가 도움이 될 수 있다.

현재 규모에서는 지나치게 복잡하게 갈 필요는 없다.


✅ 131. 지금은 애플리케이션 Scheduler + DB 상태 관리 정도로 충분하다

현재 프로젝트에서는:

NestJS Scheduler
+
PostgreSQL ScheduleRun
+
Queue Worker
+
Reconciliation

정도면 현실적인 구조다.


✅ 132. Cron을 무조건 외부 서비스로 옮길 필요는 없다

예:

AWS EventBridge Scheduler

같은 외부 스케줄러도 사용할 수 있지만 모든 Job에 필요하지는 않다.

애플리케이션이 항상 구동되고 관리할 Schedule이 많지 않다면 내부 Scheduler도 충분하다.


✅ 133. 외부 Scheduler가 유용한 경우

예:

애플리케이션과 독립적으로 반드시 실행되어야 함

서버가 Scale-to-zero 가능

정확한 예약 Trigger가 중요

AWS 중심 Infrastructure

등이다.


✅ 134. 외부 Scheduler를 써도 Idempotency는 필요하다

외부 서비스가 Trigger한다고 중복 가능성이 0이 되는 것은 아니다.

같은 Event 재전달

Network Retry

를 고려해야 한다.


✅ 135. Scheduler는 Trigger일 뿐이다

가장 중요한 원칙이다.

Scheduler
=
실행을 시작시키는 장치

일 뿐이다.

업무 정확성은:

Idempotency

State

Transaction

Worker

Reconciliation

이 담당한다.


✅ 136. Scheduled Data Cleanup은 특히 위험하다

예:

90일 이상 임시 데이터 삭제

Cron을 만든다고 하자.

잘못된 날짜 계산 하나면 대량 데이터가 삭제될 수 있다.


✅ 137. Destructive Scheduled Job은 별도 보호

예:

Dry Run

삭제 예상 건수

Maximum Delete Limit

Approval

Audit

을 둘 수 있다.


✅ 138. Delete Limit

예:

평소 삭제
100~500건

인데 갑자기:

120,000건

이 조회됐다면 자동 실행하지 않는다.


✅ 139. Safety Threshold

예:

deleteCount > 5000

→ MANUAL_REQUIRED

로 처리한다.


✅ 140. Dry Run

실제 삭제 전:

대상 건수

가장 오래된 데이터

가장 최근 데이터

를 확인한다.


✅ 141. Cleanup Job도 Soft Delete 우선

가능하다면 즉시 Hard Delete보다는:

Soft Delete

↓

Retention

↓

Hard Delete

단계로 운영할 수 있다.

0819에서 다룬 데이터 보존 전략과 연결된다.


✅ 142. 백업 Job도 ‘백업 생성’만 보면 안 된다

예:

백업 성공

로그가 있다고 실제 복구 가능한 백업이라는 보장은 없다.


✅ 143. Backup Verification Schedule

별도의 Scheduled Job으로:

Backup 존재 확인

Size 확인

최근 생성 시간

Restore Test

를 수행할 수 있다.


✅ 144. Restore Test 자동화

Production 전체 복구를 매일 할 필요는 없지만 주기적으로 Test Environment에 복구해볼 수 있다.

Backup
↓
Test DB Restore
↓
Integrity Check

형태다.


✅ 145. Scheduler에서 Business Date를 명시한다

특히 일일 통계에서는:

실행 날짜

와:

집계 대상 날짜

가 다를 수 있다.

예:

09/30 01:00 실행

Business Date
09/29

이다.


✅ 146. now()에만 의존하지 않는다

Job 내부에서:

const date = new Date();

로 집계 날짜를 결정하면 Retry 시 날짜가 바뀔 수 있다.

예:

23:59 실행 시작
00:01 Retry

하면 다른 날짜를 집계할 수 있다.


✅ 147. ScheduleRun Payload에 Business Date 저장

예:

{
  "scheduledFor": "2026-09-30T01:00:00+09:00",
  "businessDate": "2026-09-29"
}

처럼 실행 시점에 고정한다.


✅ 148. Deterministic Job

같은 Run을 Retry해도 입력값이 바뀌지 않는 것이 좋다.

예:

Run ID

Business Date

Config Version

Prompt Version

등을 고정한다.


✅ 149. 단 실시간 데이터를 필요로 하는 Job은 Snapshot 정책을 정한다

예:

09:00 Report

가 실패 후 11:00에 Retry됐다.

11:00 최신 데이터를 써야 하는지:

09:00 기준 Snapshot

을 써야 하는지 업무에 따라 결정한다.


✅ 150. Schedule Input Snapshot

재현성이 중요한 Job이라면:

inputSnapshotId

를 저장할 수 있다.


✅ 151. AI Report에서도 중요하다

동일한 09/29 Report를 다시 생성했는데 Git Commit이 추가되면 결과가 달라질 수 있다.

원래 보고서를 재현하고 싶다면:

commit range

을 Run 생성 시 고정한다.


✅ 152. 예시

Daily Report Run

fromCommit
abc123

toCommit
def456

promptVersion
v21

로 저장한다.


✅ 153. Schedule Run History

관리자 화면에서:

Schedule

최근 실행

다음 실행

성공률

현재 상태

를 볼 수 있으면 운영성이 높아진다.


✅ 154. Schedule 목록 예시

작업주기최근 결과다음 실행
Daily Report매일 19:30성공09/30 19:30
Reconciliation10분성공09:40
Cleanup매일 03:00성공09/30 03:00
Weekly Report월 08:00성공10/05 08:00

✅ 155. Schedule Detail

예:

Daily Report

Status
ENABLED

Cron
30 19 * * *

Timezone
Asia/Seoul

Overlap
SKIP_NEW

Misfire
RUN_ONCE

Max Runtime
20m

Last Run
SUCCESS

Next Run
2026-09-30 19:30

✅ 156. 실패한 Run 화면

예:

Scheduled
19:30

Started
19:31

Failed
19:36

Error
NOTION_UPLOAD_FAILED

Attempts
3

Current
DEAD_LETTER

관리자가:

Retry

할 수 있다.


✅ 157. Retry 전 상태 검증

수동 Retry라고 무조건 처음부터 실행하지 않는다.

Workflow State를 보고:

Report 생성 완료

Notion Upload만 실패

라면 Upload부터 재개한다.


✅ 158. Schedule 삭제도 조심한다

Schedule을 삭제해도 과거 Run 기록은 남겨두는 편이 좋다.

따라서:

Hard Delete

보다는:

DISABLED / ARCHIVED

를 고려한다.


✅ 159. Schedule Definition과 History를 분리하는 이유

Schedule은 변경되거나 삭제될 수 있지만 과거 실행 기록은 운영 Audit에 필요하다.


✅ 160. Schedule 생성 권한

대량 발송 같은 Job을 아무 관리자나 만들 수 있으면 위험하다.

Schedule Type별로 권한을 제한한다.


✅ 161. AI Scheduled Task 생성도 제한

AI가 스스로:

매시간 이 작업 실행

Schedule을 만들게 두면 자동화가 폭증할 수 있다.

초기에는 AI가 Schedule을 제안하고 사람이 승인하는 것이 낫다.


✅ 162. AI가 만들 수 있는 제안

예:

최근 7일간 매일 수행한 수동 작업이 있습니다.

매일 19:30 Scheduled Job으로
자동화할 수 있습니다.

정도다.


✅ 163. AI가 Schedule 비용을 같이 계산하게 할 수 있다

예:

Daily
30 Run / month

Hourly
720 Run / month

처럼 실행량을 비교한다.

LLM API라면 예상 Token 비용도 함께 계산할 수 있다.


✅ 164. Schedule 폭주 방지

새 Schedule 생성 시:

Estimated Runs Per Day

를 계산한다.

예:

1440/day

가 나오면 Warning을 띄운다.


✅ 165. Global Scheduling Limit

예:

동시 Scheduled Worker
최대 5개

처럼 전체 제한을 둘 수 있다.


✅ 166. 업무별 Concurrency Limit

예:

Report
1

Export
2

Reconciliation
1

등이다.


✅ 167. Schedule Queue 분리

중요도에 따라:

automation-critical

automation-normal

automation-low

Queue를 나눌 수도 있다.

현재 규모에서는 너무 많이 나누지 않는 편이 낫다.


✅ 168. Scheduler 자체를 복잡하게 만들지 않는다

현재 규모에서 필요한 것은:

Cron Parser

Run Record

Unique Constraint

Queue

Worker

Reconciliation

정도다.

자체적으로 완전한 Airflow 같은 시스템을 만들 필요는 없다.


✅ 169. 언제 전문 Workflow/Scheduler 도구를 고려할까?

예:

Workflow 수백 개

복잡한 DAG

장시간 작업

많은 Backfill

여러 개발팀

복잡한 Dependency

정도라면 별도 도구를 고려한다.

현재는 과하다.


✅ 170. 지금 프로젝트의 추천 구조

NestJS Scheduler

↓

ScheduleService

↓

ScheduleRun DB

↓

Queue

↓

Worker

↓

Job / Workflow

↓

Audit + Metric

이다.


✅ 171. 추천 Schedule Policy 모델

interface SchedulePolicy {
  timezone: string;

  overlap:
    | 'ALLOW'
    | 'SKIP_NEW'
    | 'QUEUE_NEW';

  misfire:
    | 'SKIP'
    | 'RUN_ONCE'
    | 'CATCH_UP_ALL'
    | 'MANUAL';

  gracePeriodMs?: number;

  maxRuntimeMs?: number;
}

✅ 172. Schedule Definition Prisma 예시

model Schedule {
  id             String   @id @default(cuid())

  name           String   @unique
  jobType        String

  cronExpression String
  timezone       String

  enabled        Boolean  @default(true)

  overlapPolicy  String
  misfirePolicy  String

  gracePeriodMs  Int?
  maxRuntimeMs   Int?

  nextRunAt      DateTime?

  version        Int      @default(1)

  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  runs           ScheduleRun[]
}

✅ 173. ScheduleRun Prisma 예시

model ScheduleRun {
  id             String   @id @default(cuid())

  scheduleId     String
  scheduledFor   DateTime

  triggerType    String

  status         String

  idempotencyKey String   @unique

  jobId          String?

  attemptCount   Int      @default(0)

  startedAt      DateTime?
  completedAt    DateTime?

  lastHeartbeatAt DateTime?

  schedule       Schedule @relation(
    fields: [scheduleId],
    references: [id]
  )

  @@unique([scheduleId, scheduledFor, triggerType])
  @@index([status, scheduledFor])
}

✅ 174. Scheduler 의사 코드

async function tick() {
  const dueSchedules =
    await scheduleRepository.findDue(
      new Date(),
    );

  for (const schedule of dueSchedules) {
    await createRunIfAbsent(schedule);
  }
}

✅ 175. Run 생성

async function createRunIfAbsent(
  schedule: Schedule,
) {
  const scheduledFor =
    schedule.nextRunAt;

  try {
    const run =
      await scheduleRunRepository.create({
        scheduleId: schedule.id,
        scheduledFor,
        idempotencyKey:
          `${schedule.id}:${scheduledFor.toISOString()}`,
      });

    await queue.add({
      scheduleRunId: run.id,
    });
  } catch (error) {
    if (isUniqueViolation(error)) {
      return;
    }

    throw error;
  }
}

✅ 176. 실제로는 Run 생성과 nextRunAt 업데이트를 같이 고려

가능하면 Transaction 안에서:

Run 생성

Next Run 계산

을 처리한다.

Queue Publish는 DB Transaction 밖일 수 있으므로 Reconciliation으로 보완한다.


✅ 177. Transactional Outbox를 사용할 수도 있다

더 강하게 가면:

ScheduleRun 생성

Outbox Event 생성

COMMIT

후 Outbox Worker가 Queue에 전달한다.

이러면:

DB 저장 성공
Queue 등록 실패

문제를 줄일 수 있다.


✅ 178. 하지만 현재는 과도하게 시작하지 않아도 된다

이미 Outbox를 쓰고 있다면 재사용하고,

아니라면:

PENDING ScheduleRun Reconciliation

만으로도 충분할 수 있다.


✅ 179. Missed Job Reconciler 의사 코드

async function reconcileMissedRuns() {
  const schedules =
    await scheduleRepository.findEnabled();

  for (const schedule of schedules) {
    const missed =
      calculateMissedRuns(schedule);

    await applyMisfirePolicy(
      schedule,
      missed,
    );
  }
}

✅ 180. Overlap 체크

예:

const activeRun =
  await runRepository.findActive(
    schedule.id,
  );

if (
  activeRun &&
  schedule.overlapPolicy === 'SKIP_NEW'
) {
  await markSkipped();
  return;
}

✅ 181. Queue New 정책

active Run 존재

↓

새 Run은 PENDING

↓

이전 Run 종료

↓

다음 Run Queue 등록

형태다.


✅ 182. Schedule 실행에서도 State Transition을 통제한다

예:

PENDING
→ SUCCESS

같은 잘못된 전이는 막는다.


✅ 183. Scheduled Job Error Code

예:

SCHEDULE_MISSED

SCHEDULE_OVERLAP

SCHEDULE_RUN_TIMEOUT

SCHEDULE_QUEUE_FAILED

SCHEDULE_INVALID_CRON

SCHEDULE_DISABLED

등을 정의할 수 있다.


✅ 184. Metric Label

Schedule 이름을 Label로 사용할 때도 개수가 너무 많아지지 않도록 주의한다.

현재 수십 개 수준이라면 크게 문제되지 않을 수 있다.


✅ 185. Alert 기준 예시

Critical Schedule
2회 연속 실패
Daily Report
예정 시간 + 1시간 미완료
Reconciliation
15분 이상 실행 없음

등이다.


✅ 186. 모든 Schedule 실패를 Critical Alert로 보내지 않는다

예:

주간 내부 AI Report 실패

는 P3일 수 있다.

반면:

주문 Reconciliation Scheduler 중단

은 훨씬 중요하다.


✅ 187. Schedule Criticality

예:

CRITICAL

HIGH

NORMAL

LOW

을 Schedule에 넣을 수도 있다.


✅ 188. Criticality에 따라 Alert가 달라진다

예:

CRITICAL
1회 Miss → Warning

2회 Miss → Critical
LOW
3회 실패 후 알림

처럼 운영한다.


✅ 189. Schedule 변경도 Release Gate와 연결 가능

위험한 Cron 변경이라면:

변경 Preview

실행 빈도 계산

중복 여부 확인

Approval

후 적용한다.


✅ 190. Cron 변경 Risk 예시

Before
1/day

After
1440/day

라면:

HIGH RISK

로 분류해야 한다.


✅ 191. Schedule Change Dry Run

새 Cron을 실제 적용하기 전에:

향후 24시간 예상 실행 시각

을 계산해볼 수 있다.


✅ 192. Schedule Import/Export

나중에 설정 백업을 위해:

name: daily-report
cron: "30 19 * * *"
timezone: Asia/Seoul
overlap: SKIP_NEW
misfire: RUN_ONCE

같은 Manifest로 관리할 수도 있다.


✅ 193. 하지만 Secret이나 개인 데이터는 Schedule Payload에 넣지 않는다

예:

전화번호 목록

API Key

JWT

를 Cron Definition에 넣는 것은 좋지 않다.

Schedule은 참조 ID만 가진다.


✅ 194. ScheduleRun Payload도 최소화

예:

{
  "businessDate": "2026-09-29",
  "projectId": "platform"
}

정도로 유지한다.


✅ 195. 대량 대상은 실행 시 조회

예:

고객 10,000명 ID

를 Queue Payload에 넣기보다:

campaignId

를 전달하고 Worker가 DB에서 조회한다.


✅ 196. Scheduled 대량 발송은 Chunking

예:

10,000건

을 하나의 Worker가 모두 처리하지 않는다.

Campaign Job

↓

Batch Job 1
500건

Batch Job 2
500건
...

으로 나눈다.


✅ 197. Batch 단위 Retry

500건 중 일부 실패해도 전체 10,000건을 처음부터 재실행하지 않는다.


✅ 198. 대량 Job에는 Progress가 필요하다

예:

Total
10,000

Processed
7,500

Success
7,420

Failed
80

를 확인할 수 있어야 한다.


✅ 199. ScheduleRun과 Child Job

예:

Campaign ScheduleRun

└─ Batch 1
└─ Batch 2
└─ Batch 3

형태로 연결할 수 있다.


✅ 200. Parent가 성공하는 기준

모든 Child Job이:

SUCCESS

또는 허용 가능한 실패

상태가 되어야 Parent ScheduleRun도 완료된다.


✅ 201. Partial Success

대량 작업에서는:

9,990 성공
10 실패

가 있을 수 있다.

상태를:

PARTIAL_SUCCESS

로 두는 것도 가능하다.


✅ 202. Schedule 보고서

예:

Daily Campaign

Scheduled
09:00

Completed
09:23

Success
9,990

Failed
10

Status
PARTIAL_SUCCESS

처럼 기록한다.


✅ 203. AI가 Scheduler 운영을 도울 수 있는 영역

AI는:

최근 실패 Schedule 요약

반복 실패 원인 분류

Cron 변경 위험도 설명

Missed Job 목록 정리

Runbook 추천

등에 활용하기 좋다.


✅ 204. AI가 Cron을 생성할 때도 검증이 필요하다

AI가:

매주 월요일 오전 9시

를 Cron으로 바꿨다고 무조건 저장하지 않는다.

Parser로 실제 다음 실행 시각을 계산한다.


✅ 205. Deterministic Validation이 우선이다

Cron 유효성은 AI에게 판단시킬 문제가 아니다.

Cron Parser

Timezone Library

Validation

으로 검사한다.

AI는 사람에게 설명하는 용도로 사용한다.


✅ 206. AI Scheduler Assistant 예시

요청:
매주 월요일 오전 9시에 주간 리포트를 생성

AI:
Schedule Draft 생성

↓

Parser:
Cron 검증

↓

Preview:
다음 5회 실행시간 표시

↓

Human Approval

↓

Schedule 저장

✅ 207. AI에게 무제한 Schedule 생성 권한을 주지 않는다

그렇지 않으면 잘못된 자동화가:

매분 실행

되거나 비용을 과도하게 사용할 수 있다.


✅ 208. Schedule Creation Policy

예:

최소 Interval

일일 최대 실행 횟수

허용 Job Type

허용 Environment

을 정책으로 둔다.


✅ 209. Production Schedule은 Approval

예:

Staging
자동 생성 가능

Production
Manual Approval

형태가 안전하다.


✅ 210. Local LLM 자동화에 적용한다면

현재 구조를 예로:

19:30

ScheduleRun 생성

↓

Git 변경사항 수집

↓

shn-coder 실행

↓

Markdown 생성

↓

Notion Upload

↓

Run SUCCESS

로 볼 수 있다.


✅ 211. 실행 중 Ollama가 꺼져 있다면

예:

MODEL_UNAVAILABLE

로 실패한다.

즉시 무한 Retry하지 말고:

Retryable

Backoff

최대 시도

정책을 둔다.


✅ 212. 다음 날 Report까지 밀리지 않게 한다

전날 Run이 아직 실패 상태라면 새 Run 처리 정책이 필요하다.

예:

각 날짜는 독립
→ ALLOW / QUEUE_NEW

가 가능하다.


✅ 213. 날짜별 Report는 CATCH_UP이 어울릴 수 있다

서버가 3일 꺼져 있었다면:

3일치 Report

를 생성하고 싶을 수 있다.

그렇다면:

misfire = CATCH_UP_ALL

이다.


✅ 214. 단 한꺼번에 LLM 3개를 동시에 돌릴 필요는 없다

overlap = QUEUE_NEW

concurrency = 1

로 순서대로 실행하면 된다.


✅ 215. Notion Upload 실패는 전체 LLM 재실행할 필요가 없다

Workflow Step:

Generate
SUCCESS

Upload
FAILED

이면 Upload Step만 Retry한다.

이것이 Schedule + Workflow + Resume 연결이다.


✅ 216. 구현 우선순위

현재 프로젝트에서는 다음 순서가 현실적이다.

1단계

Schedule Definition

Timezone

nextRunAt

2단계

ScheduleRun

DB Unique Constraint

Queue Job 연결

3단계

Retry

Overlap Policy

Misfire Policy

4단계

Heartbeat

Stale Recovery

Missed Job Reconciliation

5단계

Admin Schedule UI

Run History

Manual Retry

Backfill

✅ 217. 현재 가장 ROI 높은 기능 7개

1. ScheduleRun 기록

2. scheduleId + scheduledFor UNIQUE

3. Timezone 명시

4. Worker 분리

5. Overlap Policy

6. Misfire Policy

7. Missed Run 탐지

이 정도만 있어도 단순 Cron보다 훨씬 안전하다.


✅ 218. 당장 과한 것

현재 규모에서는:

직접 DAG Engine 개발

Leader Election Cluster

복잡한 Workflow DSL

대규모 Scheduler Platform

초단위 수십만 Schedule

까지 만들 필요는 없다.


✅ 219. Codex 구현 프롬프트

현재 NestJS + Prisma 기반 프로젝트에
안전한 Scheduled Job 실행 구조를 추가해줘.

목표는 Airflow 같은 복잡한 Workflow 시스템을 만드는 것이 아니라,
기존 Cron 기반 자동화를
중복 실행, 서버 재시작, Missed Job 상황에서도
안전하게 운영할 수 있도록 개선하는 것이다.

현재 Queue / Worker / Job 구조가 있다면 반드시 재사용하고,
새로운 Queue 시스템을 중복 도입하지 않는다.

1. Schedule 모델을 추가한다.

필드 예:
- id
- name
- jobType
- cronExpression
- timezone
- enabled
- overlapPolicy
- misfirePolicy
- gracePeriodMs
- maxRuntimeMs
- nextRunAt
- version
- createdAt
- updatedAt

2. overlapPolicy는 다음을 지원한다.

- ALLOW
- SKIP_NEW
- QUEUE_NEW

CANCEL_OLD는 현재는 구현하지 않는다.

3. misfirePolicy는 다음을 지원한다.

- SKIP
- RUN_ONCE
- CATCH_UP_ALL
- MANUAL

4. ScheduleRun 모델을 추가한다.

필드 예:
- id
- scheduleId
- scheduledFor
- triggerType
- status
- idempotencyKey
- jobId
- attemptCount
- startedAt
- completedAt
- lastHeartbeatAt

5. 동일 Scheduled Run이 중복 생성되지 않도록
DB Unique Constraint를 사용한다.

기준:
scheduleId + scheduledFor + triggerType

또는 동등한 안전한 구조를 사용한다.

6. Scheduler는 실제 Business Logic을 직접 실행하지 않는다.

Scheduler 역할:
- Due Schedule 찾기
- ScheduleRun 생성
- Queue Job 등록

실제 실행은 기존 Worker가 담당한다.

7. Queue Payload에는 최소 다음을 포함한다.

- scheduleRunId
- idempotencyKey

필요한 Business Input은
ScheduleRun 또는 DB에서 조회한다.

8. Scheduler Instance가 여러 개 떠도
같은 scheduledFor에 실제 Run이 한 번만 생성되어야 한다.

애플리케이션의 선조회만 믿지 말고
DB Unique Constraint를 최종 방어선으로 사용한다.

9. ScheduleRun 상태는 다음을 고려한다.

- PENDING
- QUEUED
- RUNNING
- SUCCESS
- PARTIAL_SUCCESS
- FAILED
- RETRY_PENDING
- MISSED
- SKIPPED
- MANUAL_REQUIRED

10. Retry 시 새 ScheduleRun을 만들지 않고
기존 Run의 Attempt를 증가시킨다.

11. 실행이 오래 걸리는 Job을 위해
heartbeat를 지원할 수 있는 구조를 만든다.

12. maxRuntime을 초과하고
heartbeat가 오래된 RUNNING Job은
stale 후보로 탐지할 수 있게 한다.

13. Stale Job은 무조건 처음부터 Retry하지 않고
기존 Reconciliation 구조가 있다면 연결한다.

14. Schedule 자체가 실행되지 않은
Missed Run을 탐지할 수 있는 구조를 만든다.

15. Missed Run 발견 시
Schedule의 misfirePolicy에 따라 처리한다.

SKIP
→ Run을 SKIPPED 처리

RUN_ONCE
→ 최신 Missed Run 한 건 실행

CATCH_UP_ALL
→ 누락 Run 모두 생성

MANUAL
→ MANUAL_REQUIRED 처리

16. gracePeriodMs가 있다면
예정 시간에서 너무 오래 지난 Run은
정책에 따라 자동 실행하지 않는다.

17. overlapPolicy를 적용한다.

SKIP_NEW:
기존 Active Run이 있으면 새 Run Skip

QUEUE_NEW:
Run은 생성하지만 이전 Run 완료 후 Queue 등록

ALLOW:
동시 실행 허용

18. 모든 Schedule은 timezone을 명시하게 한다.

서버의 로컬 Timezone에 의존하지 않는다.

19. DB Timestamp는 기존 프로젝트 정책에 맞춰
UTC 기준으로 관리한다.

20. Cron Expression 저장 시
다음 실행 시간을 계산해 nextRunAt을 관리한다.

21. Cron 수정 시 Validation을 수행한다.

- Cron 문법
- Timezone
- 예상 다음 실행 시간
- 지나치게 짧은 실행 주기

22. 관리자가 확인할 수 있도록
다음 실행 시각 Preview 기능을 만들 수 있게 한다.

23. Schedule 변경 시 Audit Log에 기록한다.

- actor
- before
- after
- reason
- version

24. Schedule 수정에는 Optimistic Lock을 적용할 수 있게 한다.

25. Manual Run을 지원하되
Scheduled Run과 동일한 Worker를 재사용한다.

triggerType:
- SCHEDULED
- MANUAL
- BACKFILL

26. Backfill은 과거 기간에 대한
새로운 Run 생성으로 처리한다.

대량 Backfill이 동시에 실행되지 않도록
Concurrency 제한을 고려한다.

27. Destructive Job을 자동 생성하지 않는다.

삭제 작업 등은
Safety Threshold / Manual Approval 구조와
연결 가능하도록 한다.

28. Schedule Metric을 기록할 수 있게 한다.

- run count
- success
- failed
- missed
- duration
- start delay

29. 테스트를 작성한다.

필수 Scenario:

- 동일 Schedule 중복 Trigger
- 두 Scheduler 동시 Run 생성
- Unique Constraint에 의한 중복 방지
- Worker 실패 Retry
- SKIP_NEW
- QUEUE_NEW
- Missed Run + SKIP
- Missed Run + RUN_ONCE
- Missed Run + CATCH_UP_ALL
- Grace Period 초과
- 잘못된 Cron 거부
- Timezone 적용
- Stale RUNNING 탐지
- Schedule Optimistic Lock 충돌

30. 기존 코드 구조와 Queue 시스템을 먼저 분석하고,
없는 시스템을 새로 가정하지 말고
현재 프로젝트에 가장 단순한 형태로 통합해줘.

✅ 220. Local LLM Work Report용 Codex 프롬프트

현재 local-llm-work-report의 Daily Report 자동화를
Scheduled Workflow로 운영할 수 있도록 설계를 검토해줘.

목표:

매일 지정된 시간에
프로젝트별 Git 작업 내용을 수집하고
로컬 Ollama 모델을 이용해 Markdown Report를 생성한 뒤
필요한 경우 Notion에 업로드한다.

요구사항:

1. Schedule과 실제 Report Run을 분리한다.

2. 날짜별 Report Run의 Idempotency Key는
project + businessDate를 기준으로 한다.

예:
daily-report:platform:2026-09-29

3. 같은 날짜/프로젝트 Report가
Scheduler 중복 실행으로 두 개 생성되지 않게 한다.

4. Workflow를 Step 단위로 나눈다.

- COLLECT_GIT
- GENERATE_REPORT
- SAVE_MARKDOWN
- UPLOAD_NOTION

5. GENERATE_REPORT 성공 후
UPLOAD_NOTION만 실패했다면
LLM을 다시 실행하지 않고 Upload 단계부터 Resume한다.

6. 각 Run에 다음 정보를 기록한다.

- project
- businessDate
- commit range
- model
- promptVersion
- startedAt
- completedAt
- status

7. 서버나 Scheduler가 며칠 중단되었을 때
누락된 날짜 Report를 Backfill할 수 있게 한다.

8. Backfill은 동시에 여러 Ollama Run을 실행하지 않고
기본 concurrency=1로 순차 처리한다.

9. 이미 생성된 Markdown 파일이 있다면
무조건 덮어쓰지 말고
기존 Run과 Artifact를 확인한다.

10. Notion 중복 페이지가 생성되지 않도록
Report Run과 notionPageId를 연결한다.

11. Ollama가 실행되지 않은 경우
MODEL_UNAVAILABLE로 분류하고
제한된 Retry + Backoff를 사용한다.

12. Report 자동화가 실패해도
다른 프로젝트 작업이나 Git Repository 상태에는
Side Effect를 주지 않도록 한다.

13. Production 서비스와 별개의 개인 자동화이므로
과도한 Infrastructure를 추가하지 않고
현재 CLI / Markdown / Notion 구조를 최대한 유지한다.

현재 구현을 먼저 분석한 후
변경 범위를 최소화한 설계안과
필요한 코드 수정 목록을 제시해줘.

✅ 221. 실무 체크리스트

Schedule

  • Cron Expression이 명확한가?
  • Timezone을 지정했는가?
  • nextRunAt을 확인할 수 있는가?
  • Schedule을 Disable할 수 있는가?
  • 다음 실행 Preview가 있는가?

중복 방지

  • scheduleId + scheduledFor 기준이 있는가?
  • DB Unique Constraint가 있는가?
  • 여러 서버가 Trigger해도 안전한가?
  • Worker Side Effect도 Idempotent한가?

Missed Job

  • 서버가 중단됐을 때 놓친 Run을 찾을 수 있는가?
  • Misfire Policy가 있는가?
  • Grace Period가 있는가?
  • 놓친 모든 Job을 무조건 실행하지 않는가?

Overlap

  • 이전 Run이 끝나지 않은 경우 정책이 있는가?
  • ALLOW / SKIP_NEW / QUEUE_NEW를 구분하는가?
  • Retry가 다음 Run과 겹쳐도 안전한가?

Worker

  • Scheduler와 Worker가 분리되어 있는가?
  • Heartbeat가 필요한가?
  • Max Runtime이 있는가?
  • Stale Job을 복구할 수 있는가?

Backfill

  • Retry와 Backfill을 구분하는가?
  • 대량 Backfill Concurrency가 제한되어 있는가?
  • Production 핵심 작업보다 우선순위가 낮은가?

Data

  • Business Date를 명시하는가?
  • Retry 시 입력값이 변하지 않는가?
  • 필요한 경우 Input Snapshot을 유지하는가?

Observability

  • ScheduledFor를 기록하는가?
  • StartedAt을 기록하는가?
  • Start Delay를 측정하는가?
  • Missed Run을 Alert할 수 있는가?
  • Run History를 볼 수 있는가?

AI Automation

  • Scheduler 안에서 직접 LLM을 실행하지 않는가?
  • AI Worker와 분리했는가?
  • AI Run 중복 실행을 막는가?
  • Step Resume가 가능한가?
  • Model / Prompt Version을 기록하는가?

📌 요약

Scheduled Job은 단순히:

Cron 하나 등록

한다고 끝나는 문제가 아니다.

운영 환경에서는:

서버가 여러 대일 수 있고

서버가 예약 시간에 꺼져 있을 수 있고

Worker가 중간에 죽을 수 있고

같은 Trigger가 두 번 발생할 수 있고

이전 작업과 다음 작업이 겹칠 수 있다.

그래서 안전한 구조는:

Scheduler
↓
ScheduleRun
↓
Queue
↓
Worker
↓
Result

로 분리한다.

특히 중요한 것은:

Schedule
≠
ScheduleRun

이다.

Schedule은:

매일 09:00 실행

이라는 규칙이고,

ScheduleRun은:

2026-09-29 09:00 실행 건

이라는 실제 실행 기록이다.

동일 시간의 Run에는:

(scheduleId, scheduledFor)
UNIQUE

같은 DB 제약을 둬 여러 Scheduler가 동시에 실행돼도 실제 Run은 하나만 생성되도록 한다.

하지만 Run 자체가 한 번만 만들어졌다고 Side Effect까지 한 번이라는 보장은 없다.

따라서:

Schedule 중복 방지
+
Worker Idempotency

둘 다 필요하다.

또 하나 중요한 것이 Missed Job이다.

09:00 실행 예정

↓

서버 중단

↓

09:30 서버 복구

같은 상황에서 단순 Cron은 09:00 작업을 잃어버릴 수 있다.

그래서 Schedule별로:

SKIP

RUN_ONCE

CATCH_UP_ALL

MANUAL

같은 Misfire Policy를 정의한다.

또한 이전 Run이 아직 끝나지 않은 상태에서 다음 시간이 오면:

ALLOW

SKIP_NEW

QUEUE_NEW

같은 Overlap Policy가 필요하다.

현재 구조에서는 예를 들어:

Reconciliation
→ SKIP_NEW

Daily Report
→ QUEUE_NEW

독립 Snapshot
→ ALLOW

처럼 업무 특성에 맞춰 사용할 수 있다.

Scheduled Workflow에서도 기존에 배운 원칙은 그대로 이어진다.

Retry
+
Idempotency
+
Heartbeat
+
Lease
+
Reconciliation
+
Observability

이다.

특히 Local LLM Report 자동화처럼 여러 Step이 있는 경우:

Git 수집
↓
LLM 생성
↓
Markdown 저장
↓
Notion Upload

를 하나의 거대한 Cron 함수로 만들기보다 Workflow로 나누는 것이 좋다.

예를 들어 Notion 업로드만 실패했다면:

LLM 재실행

이 아니라:

UPLOAD_NOTION Step만 Retry

하면 된다.

그리고 날짜 기반 작업에서는 반드시:

현재 실행 시간

과:

Business Date

를 구분해야 한다.

Retry하는 시점이 다음 날이 되더라도 동일 Run이 다른 날짜 데이터를 처리해서는 안 된다.

결국 Scheduled Automation의 핵심은:

“정해진 시간에 함수를 실행하는 것”이 아니라, 특정 시점에 실행되어야 할 업무를 하나의 상태 있는 Run으로 기록하고, 중복·누락·실패·재시작 상황에서도 최종 결과를 일관되게 만드는 것

이다.

지금까지의 흐름에 Scheduler를 연결하면:

Time / Event
↓
Trigger
↓
Create Run
↓
Idempotency
↓
Queue
↓
Worker
↓
Observe
↓
Retry / Reconcile
↓
Recover
↓
Audit

가 된다.

0개의 댓글