처음에는 자동화가 단순하다.
주문 상태 변경
→ 알림톡 발송
하지만 기능이 늘어나면:
주문 상태 변경
↓
Audit Log
↓
NotificationJob 생성
↓
알림톡 발송
↓
외부 Provider 응답
↓
Webhook 수신
↓
고객 상태 업데이트
↓
관리자 알림
↓
통계 반영
처럼 여러 단계가 연결된다.
AI 자동화도 비슷하다.
Git 변경 수집
↓
LLM 분석
↓
Markdown 생성
↓
파일 저장
↓
Notion 업로드
↓
Git Commit
↓
Push
이걸 하나의 함수에서 끝까지 처리하면 운영이 어려워진다.
예를 들어:
Step 1
Git 수집
SUCCESS
Step 2
LLM Report 생성
SUCCESS
Step 3
Markdown 저장
SUCCESS
Step 4
Notion Upload
FAILED
라고 하자.
이때 전체 Workflow를:
FAILED
라고만 기록하면 정보가 부족하다.
이미 앞의 세 단계는 정상 완료됐다.
따라서 중요한 것은:
어디까지 성공했는가?
어디서 실패했는가?
다시 시작할 때 어디부터 시작해야 하는가?
다.
Workflow는 여러 작업을 하나의 업무 흐름으로 묶은 것이다.
예:
DailyReportWorkflow
안에:
COLLECT_GIT
GENERATE_REPORT
SAVE_MARKDOWN
UPLOAD_NOTION
이 존재한다.
각 Step은 독립적인 상태를 가진다.
Job은 보통:
하나의 실행 단위
다.
예:
Notion에 페이지 업로드
Workflow는:
여러 Job/Step의 실행 순서와 상태를 관리
한다.
예:
Report 생성
→ 파일 저장
→ Notion 업로드
이다.
Workflow의 다음 Step을 중앙에서 결정하는 방식이다.
예:
Workflow Orchestrator
↓
Step 1 실행
↓
성공 확인
↓
Step 2 실행
↓
성공 확인
↓
Step 3 실행
즉 Orchestrator가:
지금 어느 Step인가?
다음에는 무엇을 해야 하는가?
실패하면 어떻게 해야 하는가?
를 알고 있다.
분산 시스템에서는 흔히 두 방식을 구분한다.
중앙 Workflow가 순서를 관리한다.
OrderWorkflow
↓
Payment
↓
Notification
↓
Complete
각 서비스가 Event를 보고 다음 Event를 만든다.
ORDER_CREATED
↓
Payment Service
PAYMENT_COMPLETED
↓
Notification Service
NOTIFICATION_SENT
중앙 지휘자가 없다.
현재는 1인 개발 환경이고 서비스 규모도 상대적으로 단순하다.
Choreography를 과도하게 사용하면:
어떤 Event가 어떤 Event를 발생시키는지
전체 흐름을 어디서 봐야 하는지
실패 시 어느 서비스가 복구해야 하는지
가 복잡해질 수 있다.
따라서 중요한 업무 흐름은:
명시적인 Workflow Orchestrator
를 두는 것이 관리하기 쉽다.
예:
Order Status Changed
같은 Domain Event는 여전히 유용하다.
다만 중요한 장시간 업무를:
Event A
→ Event B
→ Event C
→ Event D
에만 맡기지 말고 Workflow의 현재 상태를 별도로 기록하는 것이다.
Workflow 정의와 실제 실행을 구분한다.
예:
Workflow Definition
DailyReportWorkflow
실제 실행:
Workflow Run
2026-09-30
platform
이다.
0929의 Schedule / ScheduleRun과 같은 사고방식이다.
Workflow Definition
=
실행 절차
WorkflowRun
=
실제 한 번의 업무 실행
예:
DailyReportWorkflow
├─ 09/28 Run
├─ 09/29 Run
└─ 09/30 Run
예:
PENDING
RUNNING
WAITING
SUCCESS
FAILED
COMPENSATING
COMPENSATED
MANUAL_REQUIRED
CANCELLED
정도로 둘 수 있다.
Workflow는 항상 CPU를 사용하며 실행되는 것이 아니다.
예:
알림톡 발송
↓
외부 Webhook 대기
처럼 외부 Event를 기다릴 수도 있다.
이때:
RUNNING
으로 계속 Worker를 점유할 필요가 없다.
WAITING
상태로 저장해두고 Webhook이 오면 다시 Resume한다.
예:
상품 신청
↓
외부 본인인증 대기
↓
최대 30분
이런 Workflow에서 Worker 하나가 30분 동안 기다리는 것은 비효율적이다.
대신:
Workflow 상태 저장
↓
WAITING_EXTERNAL_EVENT
↓
Worker 종료
후 Event가 들어오면 재개한다.
예:
interface WorkflowStep {
id: string;
workflowRunId: string;
name: string;
status:
| 'PENDING'
| 'RUNNING'
| 'SUCCESS'
| 'FAILED'
| 'WAITING'
| 'SKIPPED';
attemptCount: number;
}
너무 크게:
PROCESS_ORDER
하나로 만들면 실패 위치를 알기 어렵다.
반대로:
READ_FIELD_1
READ_FIELD_2
CHECK_VALUE
처럼 너무 작게 나누면 오케스트레이션이 복잡해진다.
업무 의미 단위로 나누는 것이 좋다.
VALIDATE_ORDER
UPDATE_ORDER_STATUS
CREATE_NOTIFICATION_JOB
WAIT_NOTIFICATION_RESULT
FINALIZE_ORDER
처럼 실제 업무 단계가 드러나야 한다.
각 Step이 완료될 때:
Checkpoint
를 남긴다.
예:
Step 1 SUCCESS
↓
DB 저장
↓
Step 2 실행
Worker가 Step 2 중간에 죽어도 Step 1은 다시 실행하지 않아도 된다.
Checkpoint는:
이 시점까지는 완료됐다는 확정된 사실
이다.
AI Workflow에서 매우 중요하다.
예:
CODE_MODIFIED
SUCCESS
TEST
SUCCESS
REPORT
PENDING
이면 Report부터 재개할 수 있다.
긴 작업을 안정적으로 운영하려면:
처음부터 Retry
보다:
마지막 성공 Step 이후부터 Resume
가 훨씬 낫다.
예:
Step 2에서 파일 생성 완료
라고 DB에 기록되어 있어도 실제 파일이 삭제됐을 수 있다.
그래서 Resume 전:
Artifact 존재 여부
Version
External State
Commit SHA
등을 확인한다.
예:
PENDING
↓
VALIDATING
↓
PROCESSING
↓
WAITING_EXTERNAL
↓
FINALIZING
↓
SUCCESS
잘못된 상태 전이는 차단한다.
예:
FAILED
→ SUCCESS
를 아무 조건 없이 허용하면 문제가 생긴다.
정상 흐름은:
FAILED
→ RETRY_PENDING
→ RUNNING
→ SUCCESS
처럼 명확해야 한다.
예:
1. 주문 상태 변경
SUCCESS
2. 알림톡 발송
SUCCESS
3. 관리자 통계 업데이트
FAILED
이때:
전체를 Rollback
할 수 있을까?
알림톡은 이미 고객에게 발송됐다.
되돌릴 수 없다.
DB 내부에서는:
BEGIN
Order Update
Audit Log
Outbox Insert
COMMIT
처럼 Transaction을 사용할 수 있다.
하지만 외부 API까지 포함하면:
DB Commit
↓
외부 알림톡 전송
을 하나의 DB Transaction으로 묶을 수 없다.
예:
우리 DB
외부 Kakao API
Notion
S3
GitHub
는 서로 다른 시스템이다.
이 모든 시스템에:
BEGIN TRANSACTION
을 걸 수 없다.
그래서 Saga 같은 사고방식이 필요하다.
긴 비즈니스 Transaction을 여러 Local Transaction으로 나누고,
중간에 실패하면 이미 완료된 작업을:
Compensation
으로 되돌리거나 보정하는 패턴이다.
일반 Transaction:
A
B
C
하나 실패
↓
전체 Rollback
Saga:
A SUCCESS
B SUCCESS
C FAILED
↓
Compensate B
↓
Compensate A
이다.
완벽한 Rollback이 아니라:
이미 일어난 업무 효과를
비즈니스적으로 반대되는 작업으로 보정
하는 것이다.
좌석 예약
↓
결제
↓
티켓 발급
티켓 발급이 실패했다면:
결제 취소
좌석 예약 해제
가 Compensation이 될 수 있다.
예:
고객에게 카카오톡 메시지 발송
은 이미 전송됐다.
이를:
발송 취소
할 수 없다.
이런 Action은 Irreversible Side Effect다.
가능하다면 Workflow를:
검증
↓
DB 상태 변경
↓
필수 데이터 저장
↓
외부 메시지 발송
순으로 둔다.
실패 가능성이 높은 검증을 먼저 한다.
되돌릴 수 없는 Action은 가능한 한 뒤쪽에 둔다.
업무상 메시지 발송이 먼저 필요한 경우도 있다.
이때는 Rollback보다:
Forward Recovery
를 고려해야 한다.
Rollback:
실행 전 상태로 완전히 되돌림
Compensation:
업무적으로 최대한 원상태에 가깝게 보정
이다.
예:
고객에게 잘못된 안내 발송
↓
메시지 삭제 불가능
↓
정정 메시지 발송
은 Compensation이다.
이미 일어난 Side Effect를 되돌리지 않고:
앞으로 진행하면서 정상 상태를 만든다.
예:
주문 완료
알림톡 성공
통계 반영 실패
↓
통계 반영 Retry
이다.
이 상황에서는 주문과 알림톡을 되돌릴 필요가 없다.
특히:
메시지 발송
파일 업로드
외부 API
Webhook
AI 보고서
같은 작업은 완전 Rollback보다:
실패한 단계만 복구
하는 것이 낫다.
각 Step마다 먼저 판단한다.
Retry 가능한가?
Compensation 가능한가?
Forward Recovery가 더 나은가?
사람 판단이 필요한가?
예:
interface StepPolicy {
retryable: boolean;
compensatable: boolean;
irreversible: boolean;
requiresApproval?: boolean;
}
같은 Metadata를 둘 수 있다.
| Step | Retry | Compensation | Irreversible |
|---|---|---|---|
| DB 상태 변경 | 가능 | 가능 | X |
| S3 업로드 | 가능 | 파일 삭제 가능 | X |
| Notion 페이지 생성 | 가능 | 삭제/Archive 가능 | X |
| 알림톡 발송 | 제한적 | 사실상 어려움 | O |
| Git Commit | 가능 | Revert 가능 | 부분적 |
| Production 배포 | 가능 | Rollback 가능 | 조건부 |
예:
Step B 실패
↓
Compensation A 실행
↓
Compensation A도 실패
할 수 있다.
그래서 Compensation 자체도 하나의 Job으로 관리해야 한다.
예:
COMPENSATION_PENDING
COMPENSATING
COMPENSATED
COMPENSATION_FAILED
MANUAL_REQUIRED
를 둘 수 있다.
예:
S3 파일 삭제
가 두 번 실행돼도 최종 상태가 같아야 한다.
이미 파일 없음
→ 성공 취급
처럼 처리할 수 있다.
Workflow:
A
→ B
→ C
가 실행됐고 C에서 실패했다면 일반적으로:
Compensate B
↓
Compensate A
순서로 간다.
1. Report 생성
2. S3 업로드
3. DB Record 생성
4. 외부 공유 링크 발송
3번 실패했다고 가정한다.
2번 S3 파일을 삭제할 수 있다면 Compensation을 수행할 수 있다.
발송한 메시지를 되돌릴 수 없다.
따라서:
보정 안내
새 링크 발송
관리자 개입
등이 필요하다.
이게 비즈니스 Compensation이다.
모든 기능에 Saga가 필요한 것은 아니다.
특히 다음처럼 여러 시스템이 연결되는 경우 가치가 있다.
주문 상태 변경
+
알림톡
+
외부 API
대량 Export
+
S3
AI Report
+
파일
+
Notion
+
Git
배포
+
Health
+
Rollback
예:
관리자 메모 수정
같은 단일 DB 작업은 기존 Transaction이면 충분하다.
Saga를 붙이면 오히려 복잡해진다.
대략 다음 조건을 볼 수 있다.
여러 시스템에 Side Effect 발생
Workflow가 오래 실행됨
부분 실패 가능성 높음
Rollback이 단순 DB Transaction으로 불가능
실패 후 복구 과정이 중요
예:
OrderStatusChangeWorkflow
Step 1
Validate Transition
Step 2
Update Order
Step 3
Create Audit
Step 4
Create NotificationJob
Step 5
Wait Notification Result
Step 6
Complete
굳이 각각 Workflow Step으로 쪼갤 필요는 없다.
예:
DB Transaction Step
- Order Update
- Audit
- Outbox
를 하나의 Local Transaction으로 처리한다.
중요한 점이다.
Saga가 Transaction을 대체하는 것이 아니다.
구조는:
Workflow
↓
Local Transaction A
↓
External Side Effect
↓
Local Transaction B
처럼 된다.
예:
Order Transaction
- 상태 변경
- Audit
- Outbox Event
COMMIT
후:
Outbox Worker
↓
Workflow 다음 Step
으로 진행할 수 있다.
Orchestrator는:
다음 Step 결정
상태 기록
실행 요청
정도를 담당한다.
실제 작업은 기존 Service/Worker가 한다.
좋지 않은 구조:
WorkflowService
5000줄
주문 로직
알림톡 로직
DB 로직
Notion 로직
Git 로직
좋은 구조:
Workflow Orchestrator
↓
Use Case / Worker 호출
이다.
예:
interface WorkflowStepExecutor {
execute(context: WorkflowContext): Promise<StepResult>;
}
각 Step을 독립 Executor로 구현할 수 있다.
예:
type StepResult =
| {
type: 'SUCCESS';
output?: unknown;
}
| {
type: 'WAIT';
waitFor: string;
}
| {
type: 'RETRY';
errorCode: string;
}
| {
type: 'FAIL';
errorCode: string;
};
예:
외부 Webhook 기다리기
Human Approval 기다리기
다른 Job 완료 기다리기
같은 장시간 흐름에서 필요하다.
예:
AI 코드 수정
↓
Test
↓
Release Risk HIGH
↓
WAITING_APPROVAL
↓
승인
↓
Deploy
처럼 모델링할 수 있다.
영원히 기다리지 않도록:
expiresAt
를 둘 수 있다.
예:
24시간 내 승인 없음
→ CANCELLED
또는 MANUAL_REQUIRED로 보낸다.
예:
Notification Provider 발송
↓
WAITING_PROVIDER_RESULT
Webhook 수신 시:
workflowRunId
또는 Provider Message ID로 Workflow를 찾아 Resume한다.
Workflow 전체가 하나의:
correlationId
를 공유한다.
각 Step에는:
workflowRunId
stepRunId
jobId
correlationId
가 연결된다.
운영 화면에서:
09:00
Workflow 시작
09:00
VALIDATE SUCCESS
09:01
UPDATE_ORDER SUCCESS
09:01
NOTIFICATION QUEUED
09:02
WAITING_PROVIDER
09:04
Webhook 수신
09:04
FINALIZE SUCCESS
09:04
Workflow SUCCESS
처럼 볼 수 있다.
중요한 상태는 DB에 저장한다.
로그는 분석용이고,
Workflow DB 상태는:
현재 어디까지 진행됐는가?
를 결정하는 Source of Truth다.
model WorkflowRun {
id String @id @default(cuid())
type String
status String
correlationId String
idempotencyKey String @unique
currentStep String?
input Json?
output Json?
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
steps WorkflowStepRun[]
}
model WorkflowStepRun {
id String @id @default(cuid())
workflowRunId String
name String
sequence Int
status String
attemptCount Int @default(0)
input Json?
output Json?
startedAt DateTime?
completedAt DateTime?
errorCode String?
workflowRun WorkflowRun @relation(
fields: [workflowRunId],
references: [id]
)
@@unique([workflowRunId, name])
}
예:
고객 개인정보 전체
Secret
큰 파일
을 Workflow DB에 그대로 넣으면 안 된다.
가능하면:
orderId
artifactId
filePath
providerMessageId
같은 참조만 저장한다.
오래 실행되는 Workflow에서는 입력 구조가 바뀔 수 있다.
예:
{
"version": 2,
"orderId": "order_123"
}
처럼 Version을 둔다.
코드가 배포되면서 Workflow Step이 변경될 수 있다.
예:
v1
A → B → C
새 버전:
v2
A → B → X → C
기존에 실행 중인 Workflow는 어떻게 할까?
예:
v1 Workflow
B까지 완료
했는데 새 코드가:
v2
로 배포됐다.
Resume했더니 갑자기 X Step이 생길 수 있다.
예:
workflowType
DAILY_REPORT
workflowVersion
3
Run 생성 시 Version을 고정한다.
가능하면 기존 실행 중 Run은 해당 Version 규칙으로 끝낸다.
Workflow가 몇 분 안에 끝난다면 배포 시 Drain 후 교체할 수 있다.
하지만:
며칠 동안 외부 승인을 기다리는 Workflow
라면 Version 호환성을 더 신경 써야 한다.
장시간 Workflow에서 새 Version으로 옮겨야 한다면 별도 Migration이 필요할 수 있다.
예:
v1 Step B 완료
↓
v2 Step X부터 Resume
같은 명시적인 전환이다.
현재 규모에서는 복잡하게 만들 필요 없다.
같은 업무 요청이 두 번 들어올 수 있다.
예:
Order Completion Workflow
order_123
에:
order-complete:order_123
같은 Idempotency Key를 사용할 수 있다.
Workflow Run
1개
라고 해도 Queue 중복으로 Step이 두 번 실행될 수 있다.
따라서 Step Side Effect 역시 Idempotent해야 한다.
예:
{workflowRunId}:{stepName}
을 사용할 수 있다.
예:
run_123:UPLOAD_NOTION
이다.
Retry마다:
attempt 1
attempt 2
attempt 3
은 바뀌지만,
Logical Action은 동일하다.
따라서 Idempotency Key는 Attempt마다 바꾸지 않는다.
Retry 가능한 실패:
Timeout
503
Rate Limit
일시 DB 연결 오류
등은 해당 Step만 Retry한다.
예:
UPLOAD_NOTION
maxAttempts = 3
초과:
Workflow FAILED
또는:
MANUAL_REQUIRED
로 보낸다.
예:
Read-only External Query
5회
가능할 수 있지만:
대량 메시지 발송
은 더 보수적이어야 한다.
예:
const policy = {
maxAttempts: 3,
retryableErrors: [
'TIMEOUT',
'RATE_LIMIT',
],
};
예:
const policy = {
compensation:
'DELETE_UPLOADED_FILE',
};
또는:
NONE
일 수 있다.
예:
Temporary File 삭제
→ AUTO
고객 주문 취소
→ MANUAL_APPROVAL
처럼 위험도가 다르다.
예:
상품 예약 완료
↓
후속 통계 실패
했다고 예약을 취소해버리면 더 큰 문제가 된다.
따라서:
후속 통계 Retry
가 맞다.
정확히는 업무 의미에 따라 선택하는 전략이다.
다음 중 하나를 결정한다.
Retry
Resume
Compensate
Roll Forward
Manual Intervention
| 상황 | 대응 |
|---|---|
| 일시적 API 오류 | Retry |
| 이전 Step 완료 | Resume |
| 예약 등 되돌릴 수 있음 | Compensation |
| 메시지 발송 완료 | Roll Forward |
| 금전/데이터 영향 큼 | Manual |
Workflow 자체에도 최대 시간이 필요할 수 있다.
예:
Daily Report
maxDuration = 30m
초과하면 이상 상태로 본다.
예:
LLM Step
10분 제한
전체 Workflow
30분 제한
처럼 둘 수 있다.
예:
Human Approval
24시간 대기
를 일반 실행 시간과 똑같이 계산하면 안 될 수 있다.
Workflow에:
deadlineAt
을 둘 수 있다.
예:
사전예약 고객 알림
오늘 18:00 이전 완료
처럼 업무 기한이 있는 경우다.
예:
오전 9시 안내
가 오후 10시에 Retry된다면 보내지 않는 편이 나을 수 있다.
이때:
EXPIRED
또는:
MANUAL_REQUIRED
상태로 보낸다.
운영자가 진행 중 Workflow를 중단하고 싶을 수 있다.
하지만:
CANCEL 버튼
을 누른다고 이미 발생한 Side Effect가 사라지는 것은 아니다.
예:
Step A SUCCESS
Step B SUCCESS
Step C PENDING
에서 Cancel하면:
C 실행 안 함
이다.
필요하다면 별도 Compensation을 실행한다.
즉시 강제 종료보다:
CANCEL_REQUESTED
를 기록하고 안전한 경계에서 중단하는 것이 낫다.
Worker가:
Step 시작 전
Batch 사이
외부 호출 전
Cancel 상태를 확인해 중단한다.
중간 Transaction이나 파일 작업이 깨질 수 있기 때문이다.
같은 대상에 여러 Workflow가 동시에 실행될 수 있다.
예:
order_123
Status Change Workflow A
Status Change Workflow B
충돌할 수 있다.
예:
resourceKey
order:123
에 대해 동시에 하나의 Workflow만 허용할 수 있다.
그러면 처리량이 크게 떨어진다.
같은 Order
끼리만 제한하고 다른 Order는 병렬 처리한다.
예:
Order.version = 12
를 읽고 Workflow가 상태를 변경한다.
업데이트할 때:
WHERE id = ?
AND version = 12
로 처리한다.
다른 Workflow가 먼저 변경했다면 Conflict가 발생한다.
Conflict가 나면:
무조건 Retry
하지 않는다.
다시 최신 상태를 읽고:
현재 Workflow가 아직 유효한가?
를 판단한다.
Workflow A:
WAITING
→ COMPLETED
Workflow B:
WAITING
→ CANCELLED
둘이 동시에 실행되면 한쪽은 Conflict 처리해야 한다.
상태 머신 규칙이 필요하다.
한 Workflow가 다른 Workflow 완료를 기다릴 수도 있다.
예:
DailyReportWorkflow
↓
DataAggregationWorkflow
이 완료돼야 진행 가능하다.
나쁜 구조:
03:00 Aggregation
03:30 Report
Aggregation이 40분 걸리면 Report가 먼저 실행될 수 있다.
Aggregation SUCCESS
↓
REPORT_READY Event
↓
Report Workflow 시작
이 더 안전하다.
예:
CampaignWorkflow
├─ CustomerBatchWorkflow 1
├─ CustomerBatchWorkflow 2
├─ CustomerBatchWorkflow 3
형태로 만들 수 있다.
Parent는:
3개 Child 모두 SUCCESS
이면 SUCCESS.
일부 실패하면:
PARTIAL_SUCCESS
또는 FAILED로 판단한다.
대량 작업에서는 매우 중요하다.
예:
고객 10,000명
9,980 성공
20 실패
전체를 실패라고만 하면 운영상 불편하다.
예:
95% 이상 성공
→ PARTIAL_SUCCESS
80% 미만
→ FAILED
처럼 업무별 기준을 둘 수 있다.
단 숫자는 업무 특성에 맞게 정해야 한다.
대량 알림에서:
20명 실패
했다고 10,000명을 다시 보내면 안 된다.
실패 Item만 별도로 Retry한다.
각 고객 메시지에는:
campaignId + recipientId
같은 Idempotency Key를 둘 수 있다.
Workflow가 오래 실행되며 Step Output을 모두 JSON으로 저장하면 DB가 커질 수 있다.
예:
LLM 전체 Prompt
파일 전체 내용
대량 고객 데이터
를 저장하지 않는다.
큰 데이터는:
S3
File System
별도 Artifact Storage
에 저장하고 Workflow DB에는:
artifactId
만 넣는다.
작은 Metadata만 유지한다.
예:
{
"project": "platform",
"businessDate": "2026-09-30",
"reportArtifactId": "artifact_123"
}
Step마다 하나의 거대한 JSON Context를 계속 덮어쓰면:
어떤 Step이 어떤 값을 만들었는지
알기 어려워진다.
예:
COLLECT_GIT.output
commitRange
GENERATE_REPORT.output
artifactId
UPLOAD_NOTION.output
notionPageId
처럼 구분한다.
상태 변경을 별도 Event로 남길 수 있다.
예:
workflow.started
step.started
step.completed
step.failed
workflow.waiting
workflow.resumed
workflow.completed
Current State:
지금 Workflow가 어디에 있는가?
Event Log:
어떻게 지금 상태까지 왔는가?
를 설명한다.
둘 다 있으면 운영성이 높아진다.
모든 상태를 Event만으로 재구축하는 복잡한 Event Sourcing을 도입할 필요는 없다.
현재는:
Current State Table
+
History/Event Log
면 충분하다.
중요 Metric:
workflow_started_total
workflow_success_total
workflow_failed_total
workflow_duration
step_duration
step_retry_total
workflow_waiting_count
compensation_total
등이다.
예:
DailyReportWorkflow
지난 30일
30 Run
SUCCESS
28
FAILED
1
PARTIAL
1
처럼 볼 수 있다.
Workflow 전체보다 더 유용할 때가 많다.
예:
UPLOAD_NOTION
Failure Rate
12%
이면 해당 Step이 병목이라는 뜻이다.
예:
COLLECT_GIT
2초
LLM_GENERATE
47초
SAVE_FILE
100ms
NOTION_UPLOAD
800ms
어디에서 시간이 많이 걸리는지 확인할 수 있다.
전체 시간을 결정하는 가장 긴 경로를 생각할 수 있다.
현재처럼 대부분 순차 Workflow면 단순하다.
나중에 병렬 Step이 생기면 중요해진다.
예:
Prepare Report
├─ Git 분석
├─ Incident 분석
└─ Metric 분석
↓
Merge
↓
Final Report
처럼 일부 Step을 병렬화할 수 있다.
여러 병렬 Step이 끝난 뒤:
모두 완료
를 기다리는 Step이다.
예:
GIT_ANALYSIS
SUCCESS
METRIC_ANALYSIS
SUCCESS
INCIDENT_ANALYSIS
SUCCESS
↓
MERGE_REPORT
정책을 정해야 한다.
ALL_REQUIRED
BEST_EFFORT
MINIMUM_N
같은 방식을 생각할 수 있다.
예:
Git 분석 성공
Metric 분석 실패
Incident 분석 성공
이어도 Report를 생성하되:
Metric 데이터 unavailable
이라고 표시할 수 있다.
Step Metadata에:
required = true / false
를 둘 수 있다.
선택 Step 실패가 전체 Workflow 실패로 이어지지 않게 한다.
0923~0925에서 배운 원칙과 같다.
부가 기능 실패 때문에 핵심 Workflow 전체가 중단되지 않도록 한다.
Workflow 생성 자체가 폭주할 수도 있다.
예:
Webhook 10,000건
→ Workflow 10,000개
가 한 번에 실행되면 부하가 커진다.
Workflow Run을 생성하더라도 실제 Step 실행은 Queue를 통해 제한할 수 있다.
Workflow
10000개 생성
↓
Worker Concurrency
20
처럼 처리한다.
예:
Order
HIGH
Notification Recovery
HIGH
Daily Report
NORMAL
AI Analysis
LOW
이다.
Queue 분리 또는 Priority를 고려한다.
특히 AI Workflow가 CPU/RAM을 많이 쓰는 경우 중요하다.
현재 자동화에 적용하면:
DailyReportWorkflow
Step 1
COLLECT_GIT
Step 2
BUILD_CONTEXT
Step 3
RUN_LLM
Step 4
VALIDATE_OUTPUT
Step 5
SAVE_MARKDOWN
Step 6
UPLOAD_NOTION
정도로 나눌 수 있다.
LLM 결과를 바로 저장하지 말고:
Empty인가?
필수 Section이 있는가?
파일 크기가 이상하지 않은가?
Markdown 형식이 깨졌는가?
를 확인한다.
예:
INVALID_OUTPUT
이면 Prompt를 그대로 한 번 더 실행할 수 있다.
하지만 무한 Retry는 금지한다.
같은 입력이어도 결과가 달라질 수 있다.
따라서 Workflow Idempotency가:
LLM을 두 번 실행하지 않게 하는 것
과:
같은 결과를 보장하는 것
은 다른 문제다.
LLM 결과 생성에 성공했다면:
artifactId
를 저장한다.
Notion Upload 실패 때문에 LLM을 다시 호출할 필요가 없다.
예:
RUN_LLM
model
shn-coder
promptVersion
v24
contextHash
abc123
를 기록한다.
예:
Markdown 생성
Notion 페이지 생성
Git Commit
까지 했는데 Push가 실패했다고 하자.
무조건 이전 모든 결과를 지울 필요는 없다.
Git Push Retry
하면 된다.
Markdown과 Notion을 되돌리는 것은 오히려 불필요하다.
페이지를 Archive할 수 있다면 Compensation이 가능하다.
Archive Notion Page
가 Compensation이다.
Commit을 만들었지만 아직 Push 전이라면:
commit reset
이 가능할 수 있다.
Push 후라면:
revert commit
이 더 안전하다.
예:
DB 상태 변경
고객 알림
배포
등은 자동 Compensation이 실제 사고를 더 키울 수 있다.
Risk Level을 둔다.
예:
LOW
임시 파일 삭제
MEDIUM
Notion 페이지 Archive
HIGH
주문 취소
CRITICAL
금전 환불
이다.
초기에는:
COMPENSATION_PENDING
↓
Approval
↓
실행
형태가 안전하다.
예:
Workflow
run_123
Failed Step
PAYMENT_CONFIRM
Compensation
RELEASE_RESERVATION
Result
SUCCESS
Actor
SYSTEM
처럼 기록한다.
Workflow가 자동으로 해결할 수 없는 경우:
MANUAL_REQUIRED
로 멈춘다.
관리자가 다음 선택을 한다.
Retry
Skip Step
Compensate
Mark Success
Cancel
운영자가 문제를 해결했는데 시스템에서 상태만 맞춰야 할 수 있다.
하지만 아무 확인 없이:
SUCCESS 처리
하면 데이터가 꼬일 수 있다.
예:
Reason:
Provider 관리자 페이지에서
실제 발송 완료 확인
처럼 기록한다.
actorId
action
before
after
reason
workflowRunId
를 저장한다.
관리자 페이지에:
자동화 운영
├─ 실행 중
├─ 실패
├─ 승인 대기
├─ 보상 대기
└─ 수동 처리 필요
같은 Inbox를 만들 수 있다.
예:
DailyReportWorkflow
Run
run_20260930_01
Status
FAILED
Current Step
UPLOAD_NOTION
Timeline
COLLECT_GIT
SUCCESS
RUN_LLM
SUCCESS
SAVE_MARKDOWN
SUCCESS
UPLOAD_NOTION
FAILED
Attempts
3
그리고 조치 버튼:
Retry Step
Cancel Workflow
View Artifact
등이다.
예:
Order Status Change
Order
order_123
Status
WAITING
Current Step
WAIT_NOTIFICATION_RESULT
Provider Message ID
msg_9921
처럼 확인할 수 있다.
검색 기준:
workflowRunId
correlationId
orderId
jobId
status
type
기간
정도면 운영성이 높아진다.
모든 Workflow History를 영구 보관할 필요는 없다.
예:
운영 Workflow
90일
Audit 관련
더 길게
AI 내부 Report
필요한 기간
처럼 정책을 정한다.
Workflow History 삭제와 Audit Log 삭제를 동일하게 처리하지 않는다.
예:
매일 새벽
완료 후 90일 지난
Workflow Step 상세 데이터 정리
할 수 있다.
0929와 다시 연결된다.
별도의 Retention Policy가 필요하다.
관리자 UI에서 자유롭게 Step을 조립하는 Workflow Builder까지 만들 필요는 없다.
예:
DailyReportWorkflowDefinition
을 코드로 작성한다.
조건 분기
Loop
Retry
Compensation
Version
Permission
등까지 일반화하면 자체 Airflow/Temporal 같은 시스템을 만드는 셈이다.
현재 규모에는 불필요하다.
목표는:
업무 자동화를 안정화
이지:
범용 Workflow Platform 개발
이 아니다.
기능은 대략 이 정도면 된다.
Run 생성
Step 순서 관리
상태 저장
Retry
Resume
WAIT
Manual Required
일부 Compensation
이면 충분하다.
예:
Workflow 수십~수백 개
며칠 이상 실행
복잡한 Timer
다수 서비스
수많은 Compensation
대규모 Parallel/Join
Workflow Version 관리 어려움
정도가 되면 Temporal 같은 전문 도구를 검토할 가치가 있다.
현재는 기존:
NestJS
PostgreSQL
Queue
를 활용하는 편이 단순하다.
기존 Use Case를 버리지 않는다.
예:
UpdateOrderStatusUseCase
SendNotificationUseCase
UploadReportUseCase
를 Workflow가 조합한다.
구조 예:
presentation
application
├─ use-cases
├─ workflows
└─ queries
domain
infrastructure
로 둘 수 있다.
src/
└─ automation/
├─ workflows/
│ ├─ workflow-runner.ts
│ ├─ workflow-registry.ts
│ ├─ daily-report/
│ │ ├─ daily-report.workflow.ts
│ │ └─ steps/
│ └─ order-notification/
│ ├─ order-notification.workflow.ts
│ └─ steps/
│
├─ jobs/
├─ reconciliation/
└─ infrastructure/
예:
workflowRegistry.register(
'DAILY_REPORT',
DailyReportWorkflow,
);
Run의 type을 기준으로 정의를 불러온다.
예:
stepRegistry.register(
'UPLOAD_NOTION',
UploadNotionStep,
);
같이 구성할 수 있다.
async function runWorkflow(
workflowRunId: string,
) {
const run =
await workflowRepository.findById(
workflowRunId,
);
const definition =
workflowRegistry.get(
run.type,
run.version,
);
const nextStep =
definition.getNextStep(run);
if (!nextStep) {
await completeWorkflow(run.id);
return;
}
await executeStep(
run,
nextStep,
);
}
async function executeStep(
run: WorkflowRun,
step: StepDefinition,
) {
const claim =
await claimStepAtomically(
run.id,
step.name,
);
if (!claim) {
return;
}
const result =
await step.executor.execute({
workflowRunId: run.id,
});
await applyStepResult(
run,
step,
result,
);
}
같은 Step을 Worker 두 개가 동시에 실행할 수 있다.
따라서:
PENDING
→ RUNNING
전환을 조건부 Update로 처리한다.
Step을 Claim한 뒤 Worker가 죽으면:
RUNNING
에 남는다.
0917에서 배운:
Heartbeat
Lease
Stale Recovery
를 그대로 적용한다.
주기적으로:
RUNNING인데 Step 없음
WAITING인데 Event 이미 존재
FAILED인데 Retry 가능한 상태
COMPENSATING인데 멈춤
등을 확인한다.
예:
Workflow가 WAITING으로 저장되기 직전
Webhook이 먼저 도착
할 수 있다.
이 경우 Event를 잃어버리면 Workflow가 영원히 기다린다.
외부 Event를 먼저 DB에 저장한다.
WebhookEvent
providerEventId
receivedAt
processed
Workflow가 WAITING에 들어갈 때 이미 해당 Event가 있는지 확인한다.
외부 Event는:
수신
↓
Inbox 저장
↓
처리
↓
processedAt 기록
한다.
중복 Event도 UNIQUE로 막는다.
예:
providerMessageId
로:
WAITING Workflow
를 찾는다.
Webhook이 먼저 왔든 나중에 왔든 최종적으로 연결할 수 있다.
외부 Event가 영원히 안 올 수도 있다.
WAIT Step에는:
timeoutAt
을 둔다.
예:
30분 안에 Provider Result 없음
이면 Reconciliation을 수행한다.
예:
WAIT 30분
↓
Timer Fired
↓
Reconcile
로 생각할 수 있다.
0929 Scheduler와 연결된다.
예:
24시간 동안 승인 없음
↓
Workflow Cancel
같은 정책이다.
CreateApplication
Step 1
Create Order
SUCCESS
Step 2
Reserve Resource
SUCCESS
Step 3
External Submit
FAILED
Policy:
Compensate Reserve
↓
Order 상태
SUBMISSION_FAILED
처럼 처리한다.
예:
Order 생성됨
을 DB에서 삭제하는 것보다:
Order 상태
FAILED
로 보존하는 편이 Audit상 좋을 수 있다.
실패한 Workflow 기록을 완전히 삭제하면:
고객이 실제 신청했는지
왜 실패했는지
확인하기 어렵다.
운영 데이터는 보통 삭제보다 상태 전환이 낫다.
예:
COMPENSATED
라는 상태는:
원래 Workflow 목표는 달성하지 못했지만
발생한 Side Effect는 안전하게 정리됨
을 의미한다.
FAILED
만 보면 아직 Side Effect가 남아 있을 수 있다.
COMPENSATED
라면 복구 조치까지 끝났다는 의미다.
COMPENSATION_FAILED
는 우선순위를 높게 봐야 한다.
자동 Workflow도 실패했고 복구도 실패했기 때문이다.
보통:
MANUAL_REQUIRED
로 넘긴다.
중요 Workflow에서:
COMPENSATION_FAILED
가 발생하면 자동으로 Incident Candidate를 생성할 수 있다.
Workflow Type별 중요도를 둔다.
예:
ORDER
CRITICAL
NOTIFICATION
HIGH
EXPORT
NORMAL
AI_REPORT
LOW
이다.
예:
ORDER Workflow
1건 Compensation Failed
→ Critical
AI Report
1건 Failed
→ Info / P3
처럼 처리한다.
예:
OrderNotificationWorkflow
99%가
1분 이내 완료
또는:
UNKNOWN / WAITING stuck
0.1% 이하
같은 목표를 둘 수 있다.
Compensation이 자주 발생한다면 이상 신호다.
예:
전체 Workflow 1,000건
Compensation 120건
이면 설계나 외부 시스템에 문제가 있을 수 있다.
자동화가 많지만:
30%가 Manual Required
라면 자동화 효과가 낮다.
중요 운영 Metric이 될 수 있다.
예:
실패 Run 100건
Resume 성공
92건
같은 지표도 볼 수 있다.
예:
오늘
Running
8
Waiting
3
Failed
2
Manual Required
1
Compensating
0
정도로 운영 상태를 볼 수 있다.
| Workflow | Success | Failed | Waiting |
|---|---|---|---|
| OrderNotification | 982 | 2 | 4 |
| Export | 32 | 1 | 0 |
| DailyReport | 3 | 0 | 0 |
| AI Analysis | 14 | 2 | 1 |
예:
RUNNING
2시간
현재 Step
UPLOAD_NOTION
Heartbeat
90분 전
이면 Stuck 상태다.
예:
Order
5분
AI 분석
30분
Approval Workflow
24시간
등이다.
예:
Workflow가 멈췄을 때
1. Current Step 확인
2. Step Error 확인
3. 외부 상태 확인
4. Retryable 여부 확인
5. Artifact 확인
6. Resume / Compensate / Manual 결정
AI에게:
실패 Run 요약
현재 상태 설명
관련 로그 정리
Retry 가능성 분석
Compensation 후보 설명
관련 Runbook 추천
을 맡길 수 있다.
특히:
주문 취소
환불
Production Rollback
대량 메시지
같은 업무는 Human Approval이 필요하다.
예:
AITaskWorkflow
Step 1
ANALYZE
Step 2
PLAN
Step 3
MODIFY
Step 4
TEST
Step 5
REVIEW
Step 6
REPORT
Step 7
OPTIONAL_COMMIT
으로 모델링할 수 있다.
예:
TEST FAILED
이면 바로 Report로 가지 않고:
FIX
↓
TEST
Loop를 제한적으로 만들 수 있다.
예:
maxFixAttempts = 3
없으면 AI가:
수정
→ 테스트 실패
→ 수정
→ 실패
→ ...
무한 루프에 빠질 수 있다.
상한 예:
maxSteps
maxAttempts
maxTokens
maxDuration
maxToolCalls
이다.
예:
AI_BUDGET_EXCEEDED
로 중단하고 Manual Review로 보낸다.
Runner가 죽어도:
ANALYZE SUCCESS
MODIFY SUCCESS
TEST SUCCESS
REPORT RUNNING
이었다면 Report부터 복구한다.
Resume 전에:
Git SHA
Working Tree
Artifact
Test Report
Prompt Version
Policy Version
을 확인한다.
코드 분석용 AI Workflow가 실패했다고 Production 주문 처리까지 영향을 받으면 안 된다.
Queue/Worker Resource를 분리한다.
Orchestrator가 죽더라도 Workflow 상태는 DB에 남아 있어야 한다.
새 Worker가 다시 읽고 이어갈 수 있어야 한다.
예:
let currentStep = 3;
만 메모리에 저장했다가 Process가 죽으면 상태를 잃는다.
중요한 Workflow 상태는 Durable Storage에 저장한다.
Process는 죽어도 된다.
Workflow 상태는 살아 있어야 한다.
라고 보면 된다.
Workflow 규모가 크지 않다면 별도 전문 저장소보다:
PostgreSQL
에 Run/Step 상태를 저장하면 충분하다.
좋은 분리다.
DB
현재 Workflow State
Queue
이 Step을 실행하라는 신호
이다.
Queue를 유일한 Source of Truth로 삼지 않는다.
DB에는:
Step = PENDING
인데 Queue 메시지가 없다면 Reconciler가 다시 Queue에 등록한다.
Atomic Claim + Step Idempotency로 하나만 실행한다.
새로운 마법 같은 기술이 아니다.
State Machine
Idempotency
Queue
Retry
Lease
Heartbeat
Reconciliation
Audit
Observability
를 하나의 긴 흐름에 적용한 것이다.
현재 프로젝트라면:
WorkflowRun
WorkflowStepRun
명시적 상태
Step Retry
Resume
Atomic Claim
WAIT / External Event Resume
Timeout
Stale Recovery
Manual Required
Compensation
Parallel / Join
Parent/Child
Advanced Saga
순으로 가는 것이 현실적이다.
현재 환경에서 첫 대상으로는 Local LLM Report 자동화가 좋다.
이유:
Production 고객 영향이 낮음
여러 Step 존재
Resume 효과 큼
Notion이라는 외부 시스템 존재
실패 테스트하기 쉬움
이다.
Export Workflow
Notification Workflow
배포 Workflow
순으로 확장한다.
핵심 주문 흐름은 구조가 안정된 후 적용해도 된다.
Schedule Trigger
↓
WorkflowRun 생성
↓
COLLECT_GIT
↓
GENERATE_REPORT
↓
VALIDATE_REPORT
↓
SAVE_MARKDOWN
↓
UPLOAD_NOTION
↓
SUCCESS
COLLECT_GIT
SUCCESS
GENERATE_REPORT
SUCCESS
VALIDATE_REPORT
SUCCESS
SAVE_MARKDOWN
SUCCESS
UPLOAD_NOTION
FAILED
Retry:
UPLOAD_NOTION
만 실행한다.
GENERATE_REPORT
INVALID_OUTPUT
이면:
attempt < 3
→ LLM Retry
3회 실패:
MANUAL_REQUIRED
로 전환한다.
MODEL_UNAVAILABLE
이면:
Retryable
Backoff
최대 횟수
정책을 적용한다.
계속 실패하면 Workflow를 보류한다.
NOTION_TIMEOUT
은 Upload Step만 Retry한다.
생성된 Markdown은 Artifact로 유지한다.
가장 까다로운 상황이다.
실제로는 페이지가 생성됐지만 우리 시스템은 Timeout이라고 볼 수 있다.
예:
externalId
daily-report:platform:2026-09-30
같은 식별자를 Notion 데이터에 저장하거나 기존 페이지를 검색할 수 있어야 한다.
Timeout 후:
Notion 검색
↓
이미 페이지 있음
→ SUCCESS 복구
없음
→ Retry
하는 흐름이다.
UPLOAD_NOTION
UNKNOWN
상태를 둘 수 있다.
즉 실패와 결과 불명확을 구분한다.
예:
PENDING
RUNNING
SUCCESS
FAILED
UNKNOWN
WAITING
SKIPPED
정도로 둘 수 있다.
각 Step별로 실제 상태 확인 방법이 있어야 한다.
예:
S3
→ Object 존재 확인
Notion
→ externalId 검색
Deploy
→ Running Release 확인
Notification
→ providerMessageId 조회
그런 Side Effect는 Idempotency Key나 자체 처리 기록을 더 강하게 해야 한다.
현재 NestJS + Prisma + 기존 Queue/Worker 구조를 기반으로
가벼운 Workflow Orchestration 구조를 추가해줘.
목표는 Temporal/Airflow 같은 범용 Workflow Engine을 만드는 것이 아니라,
여러 Step으로 이루어진 자동화를
부분 실패, Retry, Resume, External Event 대기 상황에서도
안전하게 실행할 수 있게 하는 것이다.
기존 Job, Queue, Audit, Reconciliation 구조가 있다면 반드시 재사용한다.
1. WorkflowRun 모델을 만든다.
필드 예:
- id
- type
- version
- status
- correlationId
- idempotencyKey
- currentStep
- input
- output
- startedAt
- completedAt
- createdAt
- updatedAt
2. WorkflowRun 상태는 다음을 고려한다.
- PENDING
- RUNNING
- WAITING
- SUCCESS
- FAILED
- COMPENSATING
- COMPENSATED
- MANUAL_REQUIRED
- CANCEL_REQUESTED
- CANCELLED
3. WorkflowStepRun 모델을 만든다.
필드:
- id
- workflowRunId
- name
- sequence
- status
- attemptCount
- input
- output
- errorCode
- startedAt
- completedAt
- heartbeatAt
4. Step 상태:
- PENDING
- RUNNING
- SUCCESS
- FAILED
- UNKNOWN
- WAITING
- SKIPPED
5. Workflow Definition과 WorkflowRun을 분리한다.
Workflow Definition은 코드로 관리한다.
현재 단계에서는 관리자 UI에서 자유롭게
Workflow를 조립하는 기능은 만들지 않는다.
6. WorkflowRun 생성 시
workflow version을 고정한다.
7. 동일 Logical Workflow가
중복 생성되지 않도록 idempotencyKey를 지원한다.
8. 각 Step 실행은 Queue / Worker를 통해 처리한다.
Workflow Orchestrator가 실제 Business Logic을
직접 구현하지 않도록 한다.
9. 각 Step은 기존 Use Case 또는 Service를 호출한다.
10. Step 실행 전에
PENDING → RUNNING 전환을
atomic하게 Claim한다.
동일 Step Queue Message가 중복되어도
Side Effect가 한 번만 실행되어야 한다.
11. Step Retry는 기존 WorkflowRun을 유지하고
Step attemptCount만 증가시킨다.
12. Retry 가능한 Error와
Non-Retryable Error를 구분한다.
13. Step 성공 시 Checkpoint를 DB에 기록하고
다음 Step을 Queue에 등록한다.
14. Worker가 종료되어도
Workflow 상태를 DB에서 복구할 수 있게 한다.
중요 상태를 Process Memory에만 저장하지 않는다.
15. RUNNING Step에는 heartbeat 또는
stale detection이 가능하도록 한다.
16. Stale Step은 무조건 처음부터 Retry하지 않고
기존 Reconciliation 정책과 연결한다.
17. UNKNOWN Step을 지원한다.
외부 Side Effect 결과가 불확실한 경우
FAILED로 확정하지 말고 Reconciliation 후
SUCCESS / RETRY / MANUAL_REQUIRED를 판단한다.
18. WAITING Step을 지원한다.
예:
- Webhook 대기
- Human Approval 대기
- 다른 Job 결과 대기
WAITING 상태에서는 Worker를 점유하지 않는다.
19. External Event 수신 시
correlation key 또는 provider ID를 이용해
대기 중 Workflow를 찾아 Resume할 수 있게 한다.
20. Webhook Event는 기존 Inbox / WebhookEvent 구조가 있다면 재사용한다.
Webhook이 Workflow가 WAITING으로 전환되기 전에
먼저 도착하는 Race Condition도 고려한다.
21. WAITING Step에는 timeoutAt을 지원할 수 있게 한다.
Timeout 발생 시
Reconciliation / Retry / Manual 정책을 적용한다.
22. Workflow Cancel을 지원한다.
즉시 Process Kill 방식이 아니라
CANCEL_REQUESTED 상태를 두고
안전한 Step 경계에서 중단한다.
23. Step Metadata 또는 Policy에 다음을 정의할 수 있게 한다.
- maxAttempts
- retryable
- compensatable
- irreversible
- required
- timeout
24. Compensation 구조를 추가한다.
자동 Compensation은
안전하고 되돌릴 수 있는 Action부터 지원한다.
예:
- 임시 파일 삭제
- Notion 페이지 Archive
주문 취소, 환불, Production Rollback 같은
고위험 Compensation은 자동 실행하지 않고
MANUAL_REQUIRED 또는 Approval 흐름으로 보낸다.
25. Compensation도 Idempotent하게 처리한다.
26. Compensation 실패 시
COMPENSATION_FAILED에 준하는 기록을 남기고
MANUAL_REQUIRED로 전환한다.
27. Workflow Step 전체 input/output에
개인정보나 Secret을 저장하지 않는다.
큰 데이터는 artifactId 같은 참조만 저장한다.
28. Workflow Event Log를 남긴다.
예:
- workflow.started
- step.started
- step.completed
- step.failed
- workflow.waiting
- workflow.resumed
- compensation.started
- workflow.completed
29. 모든 Workflow / Step 로그에
다음 Context를 연결한다.
- workflowRunId
- stepRunId
- correlationId
- jobId
30. Workflow Reconciliation을 구현할 수 있는 구조를 만든다.
탐지 대상:
- RUNNING인데 heartbeat 없음
- PENDING인데 Queue Job 없음
- WAITING인데 대응 Event 존재
- UNKNOWN Step
- COMPENSATING 상태에서 멈춘 Run
31. 다음 Metric을 기록할 수 있게 한다.
- workflow started
- workflow success
- workflow failed
- workflow duration
- step duration
- step retry
- waiting count
- compensation count
- manual required count
32. 우선 첫 적용 예시는
DailyReportWorkflow로 만든다.
Step:
- COLLECT_GIT
- GENERATE_REPORT
- VALIDATE_REPORT
- SAVE_MARKDOWN
- UPLOAD_NOTION
33. GENERATE_REPORT 성공 후
UPLOAD_NOTION만 실패하면
LLM을 다시 실행하지 않고
UPLOAD_NOTION만 Resume해야 한다.
34. Notion 업로드 Timeout으로
결과가 불확실하다면 UNKNOWN 처리하고,
가능한 경우 기존 Page 존재 여부를 확인한 뒤
Retry 여부를 결정한다.
35. 테스트를 작성한다.
필수 Scenario:
- 정상 Workflow 완료
- Step 실패 Retry
- Retry Limit 초과
- Step 중복 Queue
- Atomic Claim
- Worker Crash 후 Stale Recovery
- 중간 Step 완료 후 Resume
- WAITING → External Event → Resume
- Event가 먼저 도착하는 Race Condition
- UNKNOWN → Reconciliation → SUCCESS
- Cancel Requested
- Safe Compensation
- Compensation 실패
- Idempotency 중복 Workflow 생성 방지
현재 코드베이스를 먼저 분석하고,
범용 Workflow Platform을 새로 만들지 말고
현재 프로젝트에 필요한 최소 구조로 구현해줘.
현재 local-llm-work-report v2.0의 실행 흐름을 분석해서
Daily Report 생성을 Step 기반 Workflow로 개선해줘.
현재 동작과 CLI 사용 방식은 최대한 유지한다.
목표는 일부 단계 실패 시 전체 작업을 처음부터 재실행하지 않고,
마지막 성공 Step 이후부터 Resume할 수 있게 하는 것이다.
Workflow:
1. COLLECT_GIT
2. BUILD_CONTEXT
3. GENERATE_REPORT
4. VALIDATE_REPORT
5. SAVE_MARKDOWN
6. UPLOAD_NOTION
요구사항:
1. 날짜 + 프로젝트 기준으로 WorkflowRun을 식별한다.
예:
daily-report:platform:2026-09-30
2. 각 Run에 다음 정보를 저장한다.
- project
- businessDate
- fromCommit
- toCommit
- model
- promptVersion
- workflowVersion
- status
3. 각 Step 상태와 결과를 저장한다.
4. GENERATE_REPORT가 성공했다면
생성된 Report를 Artifact로 저장하고
artifact path/id를 기록한다.
5. SAVE_MARKDOWN 실패 또는
UPLOAD_NOTION 실패 때문에
LLM을 다시 호출하지 않는다.
6. VALIDATE_REPORT에서 다음을 확인한다.
- 결과가 비어 있지 않음
- 최소 길이
- 예상 Markdown 형식
- 필수 제목/섹션 존재
7. VALIDATE_REPORT 실패 시
GENERATE_REPORT를 제한된 횟수만 Retry한다.
8. Ollama unavailable은
MODEL_UNAVAILABLE로 분류한다.
9. Notion Timeout은
결과 불확실 상태로 볼 수 있게 한다.
가능하다면 날짜+프로젝트 External ID 또는 기존 notionPageId를 이용해
이미 업로드됐는지 확인하고
중복 페이지 생성을 방지한다.
10. Notion 업로드만 실패한 Run을
CLI에서 resume할 수 있게 한다.
예:
report resume <run-id>
11. 다음 CLI 조회도 고려한다.
report runs
report status <run-id>
12. Backfill과 Schedule 실행 역시
같은 Workflow Engine을 재사용할 수 있게 한다.
13. Workflow 상태 데이터 때문에
기존 Markdown 파일 경로나 Notion 구조가
불필요하게 바뀌지 않게 한다.
14. Git Repository에 예상하지 않은 수정,
reset, commit, push를 자동으로 수행하지 않는다.
15. 테스트:
- 정상 완료
- Ollama 실패
- Invalid LLM Output
- Markdown 저장 실패
- Notion 실패
- Notion 실패 후 Resume
- 동일 날짜 중복 실행
- Artifact 재사용
현재 구현을 먼저 확인하고,
필요 이상의 추상화는 만들지 말고
향후 다른 자동화에도 재사용 가능한 최소 구조로 설계해줘.
0929에서는:
언제 실행할 것인가?
를 다뤘다.
0930에서는 그 실행이 여러 단계로 이어질 때:
어떻게 끝까지 안전하게 완료할 것인가?
를 다뤘다.
핵심은 모든 자동화를 하나의 거대한 함수로 만들지 않는 것이다.
Workflow
↓
Step
↓
Checkpoint
↓
다음 Step
형태로 나누면 부분 실패가 발생했을 때:
처음부터 다시 실행
할 필요가 없다.
대신:
마지막 성공 Step 이후부터 Resume
할 수 있다.
특히 외부 API가 섞이기 시작하면 일반 DB Transaction만으로 전체 업무를 Rollback할 수 없다.
그래서:
Local Transaction
+
Workflow
+
Saga / Compensation
의 조합을 사용한다.
Saga의 핵심은:
A SUCCESS
B SUCCESS
C FAILED
라고 해서 무조건 모든 것을 없애는 것이 아니다.
업무 특성에 따라:
Retry
Resume
Compensate
Roll Forward
Manual Intervention
중 적절한 복구 전략을 선택한다.
특히:
알림톡 발송
외부 메시지
외부 API
Production 변경
같은 Side Effect는 실제로 완전히 되돌릴 수 없는 경우가 많다.
그래서:
Rollback 가능한 것
Compensation 가능한 것
Irreversible한 것
을 구분해야 한다.
Workflow가 외부 응답이나 사람 승인을 기다리는 경우에는:
WAITING
상태로 저장하고 Worker를 해제한다.
Event 도착
↓
Workflow Resume
방식으로 동작한다.
중요한 Workflow 상태는 메모리가 아니라 DB에 저장한다.
즉:
Process는 죽어도 된다.
Workflow 상태는 살아 있어야 한다.
는 것이 Durable Workflow의 핵심이다.
현재 프로젝트에서는 별도의 거대한 Workflow Platform을 도입하기보다:
PostgreSQL
기존 Queue
WorkflowRun
WorkflowStepRun
Reconciliation
정도로 시작하는 것이 현실적이다.
특히 현재 Local LLM 보고서 자동화는 첫 적용 대상으로 좋다.
Git 수집
↓
LLM 생성
↓
검증
↓
Markdown 저장
↓
Notion 업로드
를 Step으로 나누면 Notion만 실패했을 때:
LLM 다시 실행
하지 않고:
Notion Upload만 Retry
할 수 있다.
지금까지의 전체 시리즈와 연결하면:
Time / Event
↓
Trigger
↓
Workflow Run
↓
Step
↓
Checkpoint
↓
Retry / Wait / Resume
↓
Compensate / Roll Forward
↓
Reconcile
↓
Complete
↓
Audit
가 된다.
결국 Workflow Orchestration의 핵심은
“자동화를 길게 연결하는 것”이 아니라, 중간에 어디서든 실패할 수 있다는 전제 아래 각 단계의 상태를 남기고, 이미 성공한 작업은 보존하면서 필요한 부분만 안전하게 다시 실행할 수 있게 만드는 것
이다.