예를 들어:
AI Task
↓
Production Deploy
↓
서버 응답 없음
↓
실제로는 배포 성공
↓
AI가 실패로 판단
↓
Deploy 재실행
요청 A
요청 A
요청 A
가 들어와도:
결과:
A 한 번 실행
이 되도록 만드는 것입니다.
Network Timeout
Process Crash
Worker Restart
User Retry
AI Retry
Webhook Duplicate
등은 언제든 발생할 수 있습니다.
잘못된 구조:
실패
↓
Retry
↓
Retry
↓
Retry
좋은 구조:
실패
↓
Retry 가능한 실패인지 판단
↓
Idempotency Key 확인
↓
재실행
↓
중복이면 기존 결과 반환
현재까지 다룬 구조만 봐도:
Consult
NotificationJob
ExportJob
WebhookEvent
AuditLog
Event/Outbox
AI Task
Release
Deploy
모두 중복 실행 가능성이 있습니다.
특히:
알림톡 재발송
Excel Export
Webhook 처리
Worker Job
Production Deploy
AI Task
는 우선적으로 Idempotency를 고려할 가치가 있습니다.
가장 일반적인 방법은 요청마다 고유한 Key를 부여하는 것입니다.
idempotencyKey:
consult-status-123-20260915-001
또는:
requestId:
req_20260915_abcd
requestId와 idempotencyKey는 목적이 다릅니다.| 구분 | Request ID | Idempotency Key |
|---|---|---|
| 목적 | 추적 | 중복 실행 방지 |
| 범위 | 요청 단위 | 작업 의미 단위 |
| 중복 요청 | 서로 다른 ID일 수 있음 | 같은 Key |
| 로그 | 매우 중요 | 중요 |
| 저장 | 로그/추적 | DB/상태 저장소 |
예:
사용자가 같은 버튼을 두 번 클릭
Request:
req_001
req_002
Idempotency:
status-update-consult-123
예:
Task #182
가 실패해서 재실행될 수 있습니다.
Task #182
Run #1
Run #2
Run #3
입니다.
따라서 자동화 시스템에서는 최소한:
Task
Run
Step
Action
의 개념을 구분하는 것이 좋습니다.
Task
└── Run #1
├── Context
├── Codex
├── QA
└── Report
Task
└── Run #2
├── Context
├── Codex
├── QA
└── Report
그리고 위험 작업은:
Run
└── Action
└── DEPLOY_EXECUTE
처럼 별도 관리할 수 있습니다.
잘못된 Retry:
Task 실패
↓
처음부터 다시
더 좋은 방식:
Task
↓
Run
├─ Context DONE
├─ Codex DONE
├─ QA DONE
├─ Release FAILED
└─ Deploy NOT_STARTED
재실행:
Release부터 재개
PENDING
RUNNING
SUCCEEDED
FAILED
SKIPPED
CANCELED
BLOCKED
WAITING_APPROVAL
예:
Deploy:
WAITING_APPROVAL
승인 후:
RUNNING
완료:
SUCCEEDED
각 Step도 고유한 식별자를 가질 수 있습니다.
runId:
run_182_002
stepId:
run_182_002_qa
또는:
stepKey:
QA
그리고 DB에서:
(runId, stepKey)
를 Unique하게 만들 수 있습니다.
예:
Worker A
↓
ExportJob #100
Worker B
↓
ExportJob #100
둘이 동시에 같은 Job을 실행하면:
Excel 생성
S3 Upload
DONE
이 두 번 발생할 수 있습니다.
Worker가 Job을 가져갈 때 먼저 소유권을 확보합니다.
PENDING
↓
PROCESSING
↓
Worker A
Worker B가 같은 Job을 가져가려고 하면:
PENDING → PROCESSING
조건이 맞지 않아 실패해야 합니다.
개념적으로:
UPDATE export_jobs
SET status = 'PROCESSING',
started_at = NOW()
WHERE id = 100
AND status = 'PENDING';
그리고:
affectedRows = 1
→ 내가 Claim 성공
affectedRows = 0
→ 이미 다른 Worker가 처리
const result = await prisma.exportJob.updateMany({
where: {
id: jobId,
status: 'PENDING',
},
data: {
status: 'PROCESSING',
startedAt: new Date(),
},
});
if (result.count !== 1) {
return;
}
find → update를 따로 하는 것보다 조건부 Update가 안전합니다.잘못된 방식:
const job = await repo.findById(id);
if (job.status === 'PENDING') {
await worker.process(job);
}
PENDING을 읽을 수 있습니다.Worker A → PENDING
Worker B → PENDING
A 실행
B 실행
Worker가 Job을 잡았는데 갑자기 죽을 수도 있습니다.
PENDING
↓
PROCESSING
↓
Worker Crash
그러면 영원히 PROCESSING에 갇힐 수 있습니다.
따라서:
leaseUntil
같은 개념을 둘 수 있습니다.
PROCESSING
leaseUntil = 10:35
시간이 지나면:
STUCK
으로 판단할 수 있습니다.
예:
Worker가 10:30에 Claim
leaseUntil:
10:40
10:40 이후:
Worker가 살아있는가?
를 확인합니다.
응답이 없으면:
RETRYABLE_STUCK
으로 처리할 수 있습니다.
가장 어려운 상황:
Worker
↓
External API 요청
↓
Provider:
성공
Worker:
응답 받기 전에 Crash
DB:
PROCESSING
Provider:
SUCCESS
그래서 External API에는:
Idempotency Key
가 필요합니다.
예:
providerRequestId:
notification-job-100
첫 요청:
notification-job-100
→ SUCCESS
두 번째 요청:
notification-job-100
→ 기존 결과 반환
NotificationJob #100
Key:
notification-job-100
Worker:
sendTemplate({
idempotencyKey: "notification-job-100"
});
그리고:
providerMessageId
를 저장합니다.
예:
PENDING
↓
PROCESSING
↓
Provider SUCCESS
↓
Worker Crash
재시작:
PROCESSING
↓
Provider에 동일 Idempotency Key 요청
또는:
providerMessageId 존재
→ DONE 처리
외부 시스템이:
event_100
을 여러 번 보낼 수 있습니다.
Webhook #1
Webhook #2
Webhook #3
모두:
providerEventId:
event_100
라면:
UNIQUE(provider, providerEventId)
로 중복을 방지할 수 있습니다.
Webhook Request
↓
Signature Verify
↓
providerEventId 중복 확인
↓
WebhookEvent 저장
↓
200 Response
↓
Worker 처리
나쁜 구조:
Webhook
↓
Controller
↓
Order Update
↓
Notification
↓
External API
좋은 구조:
Webhook
↓
Verify
↓
Record Event
↓
Worker
↓
Handler
↓
Business Logic
RECEIVED
PROCESSING
PROCESSED
FAILED
IGNORED
IGNORED
또는 기존 PROCESSED 결과를 반환할 수 있습니다.
AI 자동화에서는 다음과 같은 문제가 생길 수 있습니다.
Task #182
↓
Codex 실행
↓
Timeout
↓
시스템:
실패로 판단
↓
Retry
그런데 실제로는:
Codex:
파일 수정 완료
일 수 있습니다.
실행할 Task를 다음처럼 식별할 수 있습니다.
taskId
+
commitSha
+
promptVersion
+
contextHash
+
mode
→
runFingerprint
예:
contextHash:
sha256(
taskSpec
+
relevantFiles
+
rules
)
Prompt도 버전이 필요합니다.
promptVersion:
v2.4.0
Task:
Task #182
Prompt v2.4.0
다음 실행:
Prompt v2.5.0
이면 동일 Task라도 실행 환경이 달라진 것으로 볼 수 있습니다.
예:
재사용 가능:
- 같은 Task
- 같은 Commit
- 같은 Prompt Version
- 같은 Context Hash
- 같은 Mode
하나라도 다르면:
새 Run
예:
AI Run #1
→ report.md
AI Run #2
→ report.md
따라서:
Run별 Output Directory
를 둘 수도 있습니다.
.ai-runs/
run_001/
run_002/
AI 결과물도 Artifact로 관리할 수 있습니다.
Artifact:
type = REPORT
path = reports/0915.md
hash = ...
createdByRun = run_182_002
report.md
↓
SHA-256
↓
artifactHash
예:
Run #1:
hash abc123
Run #2:
hash abc123
→ 결과 동일
Build
↓
Artifact
↓
Hash
↓
Release
예:
release_20260915_01
commit:
abc123
artifact:
sha256:xxxx
배포도 중복 실행을 고려해야 합니다.
Release:
release_123
이미:
DEPLOYED
라면 다시:
DEPLOY
하지 않는 것이 좋습니다.
READY
APPROVED
DEPLOYING
DEPLOYED
FAILED
ROLLED_BACK
DEPLOYED → DEPLOY 요청이 들어오면:ALREADY_DEPLOYED
처럼 처리할 수 있습니다.
deployKey:
production:release_123
Unique하게 관리할 수 있습니다.
UNIQUE(environment, releaseId)
Deploy 명령이:
exit code 0
이라고 해서 서비스가 정상이라는 의미는 아닙니다.
Deploy
↓
Process Success
↓
Health Check
↓
Smoke Test
↓
DEPLOYED
Deploy Command
와:
Health Check
를 별도 Step으로 보는 것이 좋습니다.
Action:
DEPLOY_EXECUTE
Step:
DEPLOY_VERIFY
Release #123
├─ Build DONE
├─ QA DONE
├─ Approval DONE
├─ Deploy FAILED
└─ Verify NOT_STARTED
Retry:
Deploy부터 재시작
예:
10:00:
Release 승인
10:30:
Production 설정 변경
10:40:
Deploy Retry
따라서:
Resume
↓
Preflight
↓
현재 상태와 기존 Run 비교
↓
계속 진행 가능?
을 거쳐야 합니다.
확인 대상:
Commit
Release
Artifact
Environment
Policy Version
Approval
Dependency Lockfile
하나라도 중요한 변경이 있으면:
RESUME_BLOCKED
또는:
REVIEW_REQUIRED
로 처리할 수 있습니다.
0915에서 만든 Policy:
automation-policy.yml
도 버전이 필요합니다.
policyVersion:
2026-09-15.1
Run:
run_182
policyVersion:
2026-09-15.1
Run #1
Policy v1
FAILED
현재:
Policy v2
그냥 Resume하지 말고:
Policy Compatibility Check
를 먼저 수행하는 것이 좋습니다.
모든 실패를 Retry하면 안 됩니다.
FAILURE
↓
Classify
NETWORK_TIMEOUT
TEMPORARY_PROVIDER_ERROR
WORKER_CRASH
RATE_LIMIT
VALIDATION_ERROR
PERMISSION_DENIED
INVALID_STATE
RESOURCE_NOT_FOUND
POLICY_DENIED
예:
retryCount:
0
1
2
3
최대:
3회
이후:
FAILED
또는:
DEAD_LETTER
로 이동합니다.
단순:
1초
1초
1초
보다:
1분
5분
15분
처럼 간격을 늘리는 방식이 일반적입니다.
예:
delay = min(
base * 2^retryCount,
maxDelay
)
여러 Worker가 동시에 Retry하면:
Worker A → 5분 후
Worker B → 5분 후
Worker C → 5분 후
다시 동시에 요청할 수 있습니다.
그래서:
5분 ± random
처럼 약간의 랜덤 시간을 추가할 수 있습니다.
외부 API가:
429 Too Many Requests
를 반환한다면:
즉시 Retry
하지 않습니다.
가능하면:
Retry-After
를 따르고 그렇지 않다면 Backoff를 적용합니다.
Retry마다 새로운 Key를 만들면 안 되는 경우가 많습니다.
나쁜 구조:
Attempt #1:
key_001
Attempt #2:
key_002
외부 시스템 입장에서는:
서로 다른 요청
이 됩니다.
더 좋은 구조:
Job:
notification_100
Attempt #1:
notification_100
Attempt #2:
notification_100
Job:
notification_100
Attempt:
1
2
3
즉:
Job ID:
무엇을 해야 하는가
Attempt:
몇 번째 실행인가
AI 자동화도 동일합니다.
Run:
run_182
Attempt:
1
실패:
Attempt:
2
프로세스가 죽었을 때:
RUNNING
상태가 남을 수 있습니다.
재시작 시:
RUNNING
↓
stale 여부 확인
↓
Resume / Retry / Fail
를 판단해야 합니다.
장시간 실행되는 Worker/AI Run이라면:
lastHeartbeatAt
을 저장할 수 있습니다.
예:
lastHeartbeatAt:
09:20
현재:
09:25
인데 Heartbeat가 없다면:
STALE
로 판단할 수 있습니다.
예:
30초~1분
정도로 충분할 수 있습니다.
다만 너무 자주 DB를 업데이트하면:
DB Write 증가
가 발생합니다.
RUNNING
lastHeartbeatAt > 10분
이면:
STALE
처리.
그 다음:
Resume 가능?
Retry 가능?
Human Review?
를 판단합니다.
특히 Deploy나 외부 Build처럼:
요청:
START
후 결과가 비동기로 오는 경우:
PROCESSING
상태를 저장하고:
GET STATUS
로 확인할 수 있습니다.
자동화에서 가장 위험한 상태 중 하나입니다.
SUCCESS
FAILED
둘 중 하나를 모르는 상황입니다.
따라서:
UNKNOWN
상태를 고려할 수 있습니다.
예:
External Deploy Request:
응답 Timeout
→ UNKNOWN
UNKNOWN
↓
External Status Query
↓
SUCCESS
또는:
UNKNOWN
↓
External Status Query
↓
NOT_FOUND
↓
Retry
자동화 시스템의 상태와 실제 외부 시스템의 상태가 다를 수 있습니다.
우리 DB:
PROCESSING
Provider:
SUCCESS
Reconcile:
우리 DB:
DONE
NotificationJob
ExportJob
WebhookEvent
Deploy
AI Run
예:
NotificationJob:
PROCESSING
Provider:
이미 발송 완료
→ Job을 DONE으로 복구.
주기적으로:
PROCESSING이 너무 오래된 Job
을 조회할 수 있습니다.
Cron
↓
Stale Job Query
↓
Provider/Worker 상태 확인
↓
Reconcile
0830에서 다룬 Outbox도 중복 처리될 수 있습니다.
Event Outbox
↓
Worker
↓
External System
Worker가:
External Success
↓
DB Update 전에 Crash
하면 다시 처리될 수 있습니다.
eventId:
evt_consult_status_123_001
를 생성하고:
UNIQUE(eventId)
로 관리할 수 있습니다.
Consumer:
이미 처리했는가?
를 확인합니다.
Event:
CONSULT_STATUS_CHANGED
eventId:
evt_100
Consumer가:
evt_100 처리 완료
를 기록합니다.
다시:
evt_100
이 들어오면:
SKIP
합니다.
이벤트 자체는 중복될 수 있지만:
Event
↓
Email
↓
SMS
↓
Webhook
같은 Side Effect는 중복되면 문제가 될 수 있습니다.
Application 코드만:
if (!exists) {
create();
}
하는 것은 부족합니다.
동시 요청:
A → exists false
B → exists false
가 가능하기 때문입니다.
DB:
UNIQUE(...)
가 최종 방어선이 됩니다.
Webhook:
(provider, providerEventId)
Notification:
idempotencyKey
Export:
requestId 또는 exportKey
Deploy:
(environment, releaseId)
AI Run:
runFingerprint
Audit:
필요한 Event/Action Identifier
중복 생성 시:
P2002
가 발생할 수 있습니다.
0831에서 만든 Error Handling과 연결하여:
DUPLICATE_REQUEST
또는:
IDEMPOTENCY_CONFLICT
같은 도메인 오류로 변환할 수 있습니다.
HTTP API에서 특히 유용합니다.
예:
POST /admin/consults/export
Idempotency-Key:
export_20260915_abc
서버:
idempotency_records
에 저장합니다.
필드:
key
requestHash
status
response
createdAt
expiresAt
첫 요청:
PROCESSING
두 번째 요청:
기존 작업 조회
→ 같은 Job 반환
예:
Key:
export_100
첫 요청:
status=CALLING
두 번째:
status=CONVERTED
이면:
IDEMPOTENCY_KEY_REUSED
로 처리하는 것이 안전합니다.
requestHash:
sha256(normalizedRequestBody)
모든 기록을 영구 보존할 필요는 없습니다.
예:
API Idempotency:
24시간
Export:
1~7일
Deploy:
Release lifetime
Webhook Event:
정책에 따라 수개월
Idempotency Record에:
phone
name
memo
같은 데이터를 그대로 저장하지 않는 것이 좋습니다.
필요하면:
requestHash
만 저장합니다.
AI Task에는:
Prompt
Context
Code
등이 포함될 수 있습니다.
따라서:
taskId
runId
contextHash
promptVersion
정도만 참조하고 실제 내용은 필요한 저장소에서 접근하는 방식이 더 안전합니다.
AI Run을 Retry한 경우:
Run #1
Run #2
결과가 달라질 수 있습니다.
따라서:
artifactHash
diff
testResult
를 비교하면 좋습니다.
첫 번째:
파일 수정 A
두 번째:
파일 수정 B
가 누적될 수 있습니다.
따라서 Retry 전에:
Git Worktree
+
Known Base Commit
을 기준으로 작업 상태를 확인해야 합니다.
권장:
Retry
↓
Worktree 상태 확인
↓
변경사항 보존 필요?
├─ YES → Human Review
└─ NO → Known Base로 재구성
장시간 Task라면 Checkpoint를 둘 수 있습니다.
Checkpoint 1:
Context Prepared
Checkpoint 2:
Code Generated
Checkpoint 3:
QA Passed
Checkpoint 4:
Release Prepared
Checkpoint:
QA_PASSED
Commit:
abc123
Artifact:
report_hash
Policy:
v1.3
runId: run_182_002
baseCommit: abc123
promptVersion: v2.4.0
policyVersion: 2026-09-15.1
completedSteps:
- context
- codex
- qa
nextStep:
release
artifacts:
qaReport: sha256:xxxx
Manifest
↓
현재 Repository
현재 Policy
현재 Task
현재 Artifact
↓
Compare
결과:
VALID
또는:
INVALID
INVALID이면 무조건 이어서 실행하지 않고 재검토합니다.Base Commit 동일
Task Spec 동일
Prompt Version 동일
Policy Version 호환
Worktree 상태 정상
Output Artifact 존재
Approval 유효
자동화 상태를 단순 문자열로 관리하기보다 허용된 전이만 정의하는 것이 좋습니다.
PENDING
↓
RUNNING
↓
SUCCEEDED
RUNNING
↓
FAILED
↓
RETRYING
↓
RUNNING
그리고:
SUCCEEDED → RUNNING
같은 비정상 전이는 막습니다.
PENDING → RUNNING
RUNNING → QA_PENDING
QA_PENDING → QA_PASSED
QA_PENDING → QA_FAILED
QA_FAILED → RETRYING
QA_PASSED → RELEASE_READY
RELEASE_READY → WAITING_APPROVAL
WAITING_APPROVAL → DEPLOYING
DEPLOYING → VERIFYING
VERIFYING → SUCCEEDED
예:
QA_PASSED → DEPLOYING
은 단순 상태 변경이 아니라:
DEPLOY_EXECUTE
+
APPROVAL
가 필요할 수 있습니다.
이미:
DEPLOYED
인데:
DEPLOYED → DEPLOYING
을 허용하면 중복 배포 문제가 생길 수 있습니다.
따라서:
DEPLOYED → DEPLOYING
은 기본적으로 금지합니다.
두 개념을 구분해야 합니다.
At-most-once:
최대 한 번 실행
At-least-once:
최소 한 번 실행될 때까지 Retry
Effectively-once:
여러 번 시도될 수 있지만 결과적으로 한 번만 적용
모든 시스템을 완벽한 Exactly-Once로 만들려고 할 필요는 없습니다.
현실적으로:
DB:
Unique + Transaction
Worker:
Claim + Retry
External API:
Idempotency Key
Webhook:
Event ID Unique
Deploy:
Release Unique
AI:
Run Fingerprint + Checkpoint
정도로 상당히 안정적인 구조를 만들 수 있습니다.
"이 작업은 무조건 딱 한 번만 실행된다."
라는 보장은 외부 시스템까지 포함하면 어렵습니다.
대신:
여러 번 실행 요청이 발생해도
실제 비즈니스 결과는 중복되지 않는다.
를 목표로 하는 것이 현실적입니다.
NotificationJob:
idempotencyKey
WebhookEvent:
providerEventId UNIQUE
ExportKey
+
Job Claim
+
Retry
environment + releaseId UNIQUE
그리고:
APPROVED
→ DEPLOYING
전이를 원자적으로 관리합니다.
Task
+
Commit
+
Prompt Version
+
Context Hash
+
Policy Version
으로 Run을 식별합니다.
model IdempotencyRecord {
id String @id @default(cuid())
key String @unique
requestHash String
status String
resourceType String?
resourceId String?
responseCode Int?
createdAt DateTime @default(now())
expiresAt DateTime?
@@index([resourceType, resourceId])
}
status, response, 보존 정책 등을 프로젝트에 맞게 조정합니다.model AiRun {
id String @id @default(cuid())
taskId String
attempt Int @default(1)
baseCommit String
promptVersion String
contextHash String
policyVersion String
status String
startedAt DateTime?
finishedAt DateTime?
lastHeartbeatAt DateTime?
createdAt DateTime @default(now())
@@index([taskId, createdAt])
@@index([status, lastHeartbeatAt])
}
model AutomationAction {
id String @id @default(cuid())
runId String
type String
target String
idempotencyKey String @unique
status String
approvalId String?
createdAt DateTime @default(now())
startedAt DateTime?
finishedAt DateTime?
}
async function executeJob(jobId: string) {
const claimed = await claimJob(jobId);
if (!claimed) {
return;
}
try {
await processJob(claimed);
await markDone(jobId);
} catch (error) {
await handleFailure(jobId, error);
}
}
핵심은:
claim
→ execute
→ classify failure
→ retry / failed
입니다.
type ExternalResult<T> =
| {
ok: true;
value: T;
}
| {
ok: false;
retryable: boolean;
code: string;
message: string;
};
그리고:
send({
idempotencyKey,
payload,
});
처럼 작업의 의미를 명시적으로 전달합니다.
function shouldRetry(error: ExternalError) {
if (error.code === 'RATE_LIMIT') return true;
if (error.code === 'TIMEOUT') return true;
if (error.code === 'AUTH_FAILED') return false;
if (error.code === 'VALIDATION_ERROR') return false;
return false;
}
AI Run Failed
↓
Error Classification
├─ POLICY_DENIED → STOP
├─ VALIDATION_ERROR → STOP
├─ TOOL_TIMEOUT → RETRY
├─ MODEL_TIMEOUT → RETRY
├─ QA_FAILED → REVIEW / RETRY
└─ UNKNOWN → HUMAN REVIEW
Tool Error:
Codex process crash
QA Error:
Test failed
Policy Error:
Permission denied
세 가지는 처리 방법이 달라야 합니다.
Tool Error → Retry 가능
QA Error → AI 수정 후 재검증
Policy Error → 자동 Retry 금지
Retry
Retry
Retry
Retry
...
가 되면 자동화가 장애를 확대합니다.
따라서:
maxAttempts
maxRuntime
maxCost
등을 둘 수 있습니다.
AI Task가:
실패
↓
Retry
↓
Retry
↓
Retry
하면 API 비용도 증가합니다.
예:
maxAttempts:
3
maxTokens:
500k
maxRuntime:
30m
Task Budget
Tokens:
500,000
Time:
30 min
Tool Calls:
100
초과:
BUDGET_EXCEEDED
Retry할 때:
Attempt 1:
100k
Attempt 2:
100k
Attempt 3:
100k
총:
300k
가 될 수 있습니다.
따라서 Budget은:
Attempt별
와:
Task 전체
를 구분할 수 있습니다.
초기에는 복잡하게 만들 필요 없이:
maxAttempts = 3
정도부터 시작하고:
AI Task:
maxRuntime 30m
Worker:
maxRetry 3
Notification:
maxRetry 3
Webhook:
provider 정책
처럼 적용하면 충분합니다.
자동화 시스템에서 중요한 것은:
"실패하지 않는 시스템"
보다:
"실패해도 어디까지 했는지 알 수 있는 시스템"
입니다.
따라서:
Task
Run
Attempt
Step
Action
Artifact
Checkpoint
를 기록하면 복구가 쉬워집니다.
Task
↓
Run
↓
Attempt
↓
Step
├─ Context
├─ Codex
├─ QA
├─ Release
└─ Deploy
↓
Action
↓
Idempotency Key
↓
Execute
↓
Verify
↓
Artifact / Audit
Task
↓
Run #1
↓
Step FAILED
↓
Error Classification
↓
Retryable?
├─ NO → FAILED / HUMAN REVIEW
└─ YES
↓
Idempotency Check
↓
Retry Budget
↓
Run #2 / Attempt #2
Action Requested
↓
Validate State
↓
Idempotency Check
↓
Approval Check
↓
Claim
↓
Execute
↓
Unknown?
├─ YES → Reconcile
└─ NO
↓
Verify
↓
DONE
1. Job Claim
2. Retry Classification
3. Idempotency Key
4. Webhook Unique Event
5. Notification Idempotency
6. ExportJob Resume
7. Deploy Idempotency
8. AI Run Checkpoint
9. Heartbeat / Stale Recovery
10. Reconciliation
현재 구조라면 우선:
① Worker Claim
② maxRetry
③ retryable/non-retryable 분류
④ Webhook providerEventId UNIQUE
⑤ Notification idempotencyKey
부터 하는 것을 추천합니다.
그 다음:
ExportJob:
Resume
Deploy:
Release + Environment Unique
AI:
Run Fingerprint
Long Running:
Heartbeat
External:
Reconciliation
순으로 확장하면 됩니다.
현재 프로젝트의 Queue/Worker, NotificationJob, ExportJob, WebhookEvent, AI Run 구조에 Idempotency와 재실행/복구 정책을 추가해줘.
목표:
동일 작업이 여러 번 요청되거나 Worker/AI 프로세스가 중간에 죽어도 중복 실행을 최소화하고, 실제 상태를 확인한 후 안전하게 Retry/Resume할 수 있게 한다.
반드시 지켜야 할 원칙:
1. Request ID와 Idempotency Key를 구분한다.
2. Request ID는 추적용, Idempotency Key는 중복 실행 방지용으로 사용한다.
3. 모든 Retryable 작업은 Idempotency 전략을 함께 갖도록 한다.
4. Worker는 find → if pending → process 구조가 아니라 조건부 상태 변경을 통한 Claim을 사용한다.
5. PENDING → PROCESSING 전환을 원자적으로 처리한다.
6. Worker Crash로 PROCESSING 상태가 고착될 수 있으므로 lease/heartbeat/stale recovery 구조를 고려한다.
7. Retry는 모든 Error에 적용하지 않는다.
8. Validation, Permission, Invalid State, Policy Denied 등은 Non-Retryable로 처리한다.
9. Timeout, Temporary External Error, Rate Limit 등은 Retryable로 분류한다.
10. 최대 Retry 횟수를 둔다.
11. Exponential Backoff + 필요한 경우 Jitter를 적용한다.
12. External API 요청에는 가능하면 동일한 Idempotency Key를 Retry에도 유지한다.
13. WebhookEvent는 provider + providerEventId UNIQUE로 중복 이벤트를 방지한다.
14. NotificationJob은 idempotencyKey와 providerMessageId를 저장한다.
15. ExportJob은 동일 요청이 중복 생성되지 않도록 Export Key 또는 Idempotency Key를 사용한다.
16. Production Deploy는 environment + releaseId 기준으로 중복 배포를 방지한다.
17. Deploy 명령 성공과 실제 서비스 정상 여부를 분리하고 Verify 단계에서 Health/Smoke Test를 수행한다.
18. 외부 작업의 응답이 Timeout되어 성공/실패를 알 수 없는 경우 즉시 Retry하지 말고 UNKNOWN 상태에서 Reconciliation을 수행한다.
19. AI Run은 Task, Base Commit, Prompt Version, Context Hash, Policy Version을 기록한다.
20. AI Run은 Task와 Attempt를 구분한다.
21. 이미 완료된 Step은 재실행하지 않고 마지막 정상 Checkpoint부터 Resume할 수 있게 한다.
22. Resume 전 Base Commit, Task Spec, Policy Version, Artifact, Approval 상태를 검증한다.
23. 승인 대상 Release/Commit이 변경되면 기존 Approval을 무효화한다.
24. Idempotency Record에는 불필요한 개인정보나 Secret을 저장하지 않는다.
25. DB Unique Constraint를 최종 방어선으로 사용한다.
26. 기존 GlobalExceptionFilter의 Error Code 체계와 연결한다.
27. Retry 상태와 Attempt를 Audit/Run Log에서 추적할 수 있게 한다.
28. 실제 외부 API나 Production DB를 테스트에서 호출하지 않는다.
29. Policy Denied, Production DB Write, Force Push 등은 실제 실행하지 않고 정책 테스트로만 검증한다.
30. 기존 아키텍처를 크게 깨지 말고 Repository/Use Case/Worker/Adapter 경계를 유지한다.
추가 구현:
- IdempotencyRecord
- Job Claim
- Retry Policy
- Backoff
- Stale Job Recovery
- Reconciliation
- AI Run Checkpoint
- Resume Validation
- Deploy Idempotency
를 각각 별도 책임으로 분리해줘.
구현 후 다음을 함께 작성해줘.
1. 변경된 파일 목록
2. DB Migration 내용
3. API 변경사항
4. Worker 변경사항
5. 상태 전이표
6. Retry 정책표
7. 테스트 목록
8. 기존 코드와의 호환성
9. Rollback 방법
10. 실제 운영 시 주의사항
Retry를 도입한다면 반드시 Idempotency를 함께 설계해야 합니다.Request ID는 추적용이고 Idempotency Key는 동일 작업의 중복 실행을 방지하기 위한 값입니다.조회 → 실행이 아니라 조건부 Claim → 실행 구조로 만들어야 동시 실행을 막을 수 있습니다.PROCESSING 상태에 갇히지 않도록 lease, heartbeat, stale recovery를 고려할 수 있습니다.provider + providerEventId UNIQUE로 중복 이벤트를 방지하는 것이 좋습니다.Task → Run → Attempt → Step → Action을 구분하면 실패한 부분부터 안전하게 재개할 수 있습니다.Base Commit, Prompt Version, Context Hash, Policy Version 등을 저장하면 재현성과 Resume 검증이 쉬워집니다.environment + releaseId 기준으로 중복을 방지하고, 명령 실행 성공과 실제 서비스 정상 여부를 Verify 단계에서 분리해야 합니다.Validation, Permission, Policy Denied 같은 오류까지 무작정 재실행하면 안 되며, Timeout/Rate Limit/일시적 외부 오류처럼 실제로 재시도할 가치가 있는 오류만 대상으로 해야 합니다.Export Resume → Deploy Idempotency → AI Run Checkpoint → Heartbeat/Stale Recovery → Reconciliation 순으로 확장하면 지금까지 만든 AI 자동화 구조가 단순한 “자동 실행 스크립트”에서 실패와 중복을 스스로 통제할 수 있는 운영 시스템으로 발전합니다.