TIL - 20260916

juni·2026년 9월 16일

TIL

목록 보기
457/472

0916 운영 자동화/AI 워크플로우 심화 (10/N): Idempotency, 재실행과 중복 실행 방지


✅ 1. 자동화가 실패하는 것보다 더 위험한 것

  • 자동화 시스템에서 가장 어려운 문제 중 하나는 “실패” 자체가 아닙니다.
  • 진짜 위험한 것은 실패했는지 성공했는지 모르는 상태에서 같은 작업을 다시 실행하는 것입니다.

예를 들어:

AI Task
  ↓
Production Deploy
  ↓
서버 응답 없음
  ↓
실제로는 배포 성공
  ↓
AI가 실패로 판단
  ↓
Deploy 재실행
  • 이때 단순 Retry를 적용하면 동일 작업이 두 번 실행될 수 있습니다.

✅ 2. Idempotency란?

  • 같은 작업을 여러 번 요청해도 최종 결과가 한 번 실행한 것과 같도록 만드는 성질입니다.
요청 A
요청 A
요청 A

가 들어와도:

결과:
A 한 번 실행

이 되도록 만드는 것입니다.

➕ 2-1. 자동화에서 중요한 이유

Network Timeout
Process Crash
Worker Restart
User Retry
AI Retry
Webhook Duplicate

등은 언제든 발생할 수 있습니다.

  • 따라서 Retry를 설계한다면 Idempotency도 같이 설계해야 합니다.

✅ 3. Retry와 Idempotency는 한 세트다

잘못된 구조:

실패
 ↓
Retry
 ↓
Retry
 ↓
Retry

좋은 구조:

실패
 ↓
Retry 가능한 실패인지 판단
 ↓
Idempotency Key 확인
 ↓
재실행
 ↓
중복이면 기존 결과 반환
  • “다시 실행할 수 있다”와 “다시 실행해도 안전하다”는 서로 다른 문제입니다.

✅ 4. 현재 프로젝트에서 이미 필요한 이유

현재까지 다룬 구조만 봐도:

Consult
NotificationJob
ExportJob
WebhookEvent
AuditLog
Event/Outbox
AI Task
Release
Deploy

모두 중복 실행 가능성이 있습니다.

특히:

알림톡 재발송
Excel Export
Webhook 처리
Worker Job
Production Deploy
AI Task

는 우선적으로 Idempotency를 고려할 가치가 있습니다.


✅ 5. Idempotency Key

가장 일반적인 방법은 요청마다 고유한 Key를 부여하는 것입니다.

idempotencyKey:
consult-status-123-20260915-001

또는:

requestId:
req_20260915_abcd
  • 다만 requestId와 idempotencyKey는 목적이 다릅니다.

✅ 6. Request ID와 Idempotency Key의 차이

구분Request IDIdempotency Key
목적추적중복 실행 방지
범위요청 단위작업 의미 단위
중복 요청서로 다른 ID일 수 있음같은 Key
로그매우 중요중요
저장로그/추적DB/상태 저장소

예:

사용자가 같은 버튼을 두 번 클릭

Request:
req_001
req_002

Idempotency:
status-update-consult-123
  • 두 요청의 Request ID는 달라도 같은 작업이라면 같은 Idempotency Key를 사용할 수 있습니다.

✅ 7. 현재 구조에서는 Task ID도 부족할 수 있다

예:

Task #182

가 실패해서 재실행될 수 있습니다.

Task #182
Run #1
Run #2
Run #3
  • Task는 “무엇을 해야 하는가”
  • Run은 “실제로 몇 번째 실행인가”

입니다.

따라서 자동화 시스템에서는 최소한:

Task
Run
Step
Action

의 개념을 구분하는 것이 좋습니다.


✅ 8. Task / Run / Step / Action

Task
└── Run #1
    ├── Context
    ├── Codex
    ├── QA
    └── Report

Task
└── Run #2
    ├── Context
    ├── Codex
    ├── QA
    └── Report

그리고 위험 작업은:

Run
└── Action
    └── DEPLOY_EXECUTE

처럼 별도 관리할 수 있습니다.


✅ 9. Run 재실행은 전체 재실행이 아니다

잘못된 Retry:

Task 실패
 ↓
처음부터 다시

더 좋은 방식:

Task
 ↓
Run
 ├─ Context      DONE
 ├─ Codex        DONE
 ├─ QA           DONE
 ├─ Release      FAILED
 └─ Deploy       NOT_STARTED

재실행:

Release부터 재개
  • 이미 성공한 Step을 불필요하게 다시 실행하지 않는 것이 중요합니다.

✅ 10. Step 상태

PENDING
RUNNING
SUCCEEDED
FAILED
SKIPPED
CANCELED
BLOCKED
WAITING_APPROVAL
  • 0915의 Approval과도 자연스럽게 연결됩니다.

예:

Deploy:
WAITING_APPROVAL

승인 후:

RUNNING

완료:

SUCCEEDED

✅ 11. Step Idempotency

각 Step도 고유한 식별자를 가질 수 있습니다.

runId:
run_182_002

stepId:
run_182_002_qa

또는:

stepKey:
QA

그리고 DB에서:

(runId, stepKey)

를 Unique하게 만들 수 있습니다.


✅ 12. Worker 중복 실행

예:

Worker A
  ↓
ExportJob #100

Worker B
  ↓
ExportJob #100

둘이 동시에 같은 Job을 실행하면:

Excel 생성
S3 Upload
DONE

이 두 번 발생할 수 있습니다.


✅ 13. Job Claim

Worker가 Job을 가져갈 때 먼저 소유권을 확보합니다.

PENDING
 ↓
PROCESSING
 ↓
Worker A

Worker B가 같은 Job을 가져가려고 하면:

PENDING → PROCESSING

조건이 맞지 않아 실패해야 합니다.


✅ 14. Conditional Update

개념적으로:

UPDATE export_jobs
SET status = 'PROCESSING',
    started_at = NOW()
WHERE id = 100
  AND status = 'PENDING';

그리고:

affectedRows = 1
→ 내가 Claim 성공

affectedRows = 0
→ 이미 다른 Worker가 처리
  • 단순한 구조지만 매우 강력한 중복 실행 방지 방법입니다.

✅ 15. Prisma에서의 Claim

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가 안전합니다.

✅ 16. 상태 확인 후 실행의 문제

잘못된 방식:

const job = await repo.findById(id);

if (job.status === 'PENDING') {
  await worker.process(job);
}
  • Worker A와 B가 동시에 PENDING을 읽을 수 있습니다.
Worker A → PENDING
Worker B → PENDING

A 실행
B 실행
  • 읽고 나서 판단하는 것보다 상태 변경 자체를 원자적으로 만들어야 합니다.

✅ 17. Lease 개념

Worker가 Job을 잡았는데 갑자기 죽을 수도 있습니다.

PENDING
 ↓
PROCESSING
 ↓
Worker Crash

그러면 영원히 PROCESSING에 갇힐 수 있습니다.

따라서:

leaseUntil

같은 개념을 둘 수 있습니다.

PROCESSING
leaseUntil = 10:35

시간이 지나면:

STUCK

으로 판단할 수 있습니다.


✅ 18. Lease Timeout

예:

Worker가 10:30에 Claim

leaseUntil:
10:40

10:40 이후:

Worker가 살아있는가?

를 확인합니다.

응답이 없으면:

RETRYABLE_STUCK

으로 처리할 수 있습니다.

  • 다만 무조건 재실행하면 위험한 작업에서는 별도 검증이 필요합니다.

✅ 19. “Worker가 죽었다”와 “작업이 성공했다”는 다르다

가장 어려운 상황:

Worker
 ↓
External API 요청
 ↓
Provider:
성공

Worker:
응답 받기 전에 Crash

DB:

PROCESSING

Provider:

SUCCESS
  • Worker가 재시작되면 다시 요청할 수 있습니다.

그래서 External API에는:

Idempotency Key

가 필요합니다.


✅ 20. External API Idempotency

예:

providerRequestId:
notification-job-100

첫 요청:

notification-job-100
→ SUCCESS

두 번째 요청:

notification-job-100
→ 기존 결과 반환
  • 외부 Provider가 Idempotency를 지원한다면 적극적으로 사용하는 것이 좋습니다.

✅ 21. 알림톡에 적용

NotificationJob #100

Key:

notification-job-100

Worker:

sendTemplate({
  idempotencyKey: "notification-job-100"
});

그리고:

providerMessageId

를 저장합니다.


✅ 22. 알림톡 중복 발송 방지

예:

PENDING
 ↓
PROCESSING
 ↓
Provider SUCCESS
 ↓
Worker Crash

재시작:

PROCESSING
 ↓
Provider에 동일 Idempotency Key 요청

또는:

providerMessageId 존재
→ DONE 처리
  • 실제 Provider의 지원 방식에 맞춰 설계해야 합니다.

✅ 23. Webhook은 기본적으로 중복될 수 있다고 생각한다

외부 시스템이:

event_100

을 여러 번 보낼 수 있습니다.

Webhook #1
Webhook #2
Webhook #3

모두:

providerEventId:
event_100

라면:

UNIQUE(provider, providerEventId)

로 중복을 방지할 수 있습니다.


✅ 24. Webhook 처리 흐름

Webhook Request
 ↓
Signature Verify
 ↓
providerEventId 중복 확인
 ↓
WebhookEvent 저장
 ↓
200 Response
 ↓
Worker 처리
  • 이미 0828에서 다뤘던 구조를 Idempotency 관점에서 다시 보면 더 명확합니다.

✅ 25. Webhook을 바로 Business Logic으로 처리하지 않기

나쁜 구조:

Webhook
 ↓
Controller
 ↓
Order Update
 ↓
Notification
 ↓
External API
  • Provider가 재전송하면 모든 로직이 반복될 수 있습니다.

좋은 구조:

Webhook
 ↓
Verify
 ↓
Record Event
 ↓
Worker
 ↓
Handler
 ↓
Business Logic

✅ 26. Webhook Event 처리 상태

RECEIVED
PROCESSING
PROCESSED
FAILED
IGNORED
  • 중복 Event는:
IGNORED

또는 기존 PROCESSED 결과를 반환할 수 있습니다.


✅ 27. AI Task도 Idempotency가 필요하다

AI 자동화에서는 다음과 같은 문제가 생길 수 있습니다.

Task #182
 ↓
Codex 실행
 ↓
Timeout
 ↓
시스템:
실패로 판단
 ↓
Retry

그런데 실제로는:

Codex:
파일 수정 완료

일 수 있습니다.

  • 따라서 Retry 전에 Workspace/Run 상태를 확인해야 합니다.

✅ 28. AI Run Fingerprint

실행할 Task를 다음처럼 식별할 수 있습니다.

taskId
+
commitSha
+
promptVersion
+
contextHash
+
mode

→

runFingerprint
  • 같은 입력에 대한 실행을 식별하는 데 사용할 수 있습니다.

✅ 29. Context Hash

예:

contextHash:
sha256(
  taskSpec
  +
  relevantFiles
  +
  rules
)
  • Context가 바뀌었는데 기존 Run을 그대로 재사용하면 잘못된 결과가 나올 수 있습니다.

✅ 30. Prompt Version

Prompt도 버전이 필요합니다.

promptVersion:
v2.4.0

Task:

Task #182
Prompt v2.4.0

다음 실행:

Prompt v2.5.0

이면 동일 Task라도 실행 환경이 달라진 것으로 볼 수 있습니다.


✅ 31. AI Run을 재사용할 조건

예:

재사용 가능:
- 같은 Task
- 같은 Commit
- 같은 Prompt Version
- 같은 Context Hash
- 같은 Mode

하나라도 다르면:

새 Run
  • 이 구조를 만들면 AI 결과 재현성이 좋아집니다.

✅ 32. AI 결과물 중복 생성

예:

AI Run #1
→ report.md

AI Run #2
→ report.md
  • 동일 파일을 덮어쓸 수 있습니다.

따라서:

Run별 Output Directory

를 둘 수도 있습니다.

.ai-runs/
  run_001/
  run_002/

✅ 33. Output Artifact

AI 결과물도 Artifact로 관리할 수 있습니다.

Artifact:
type = REPORT
path = reports/0915.md
hash = ...
createdByRun = run_182_002
  • 어떤 Run이 어떤 결과물을 만들었는지 추적할 수 있습니다.

✅ 34. Artifact Hash

report.md
 ↓
SHA-256
 ↓
artifactHash
  • 동일한 결과인지 비교할 수 있습니다.

예:

Run #1:
hash abc123

Run #2:
hash abc123

→ 결과 동일


✅ 35. Release Artifact도 동일하게

Build
 ↓
Artifact
 ↓
Hash
 ↓
Release

예:

release_20260915_01

commit:
abc123

artifact:
sha256:xxxx
  • Deploy 대상이 정확히 무엇인지 확인할 수 있습니다.

✅ 36. Deploy Idempotency

배포도 중복 실행을 고려해야 합니다.

Release:
release_123

이미:

DEPLOYED

라면 다시:

DEPLOY

하지 않는 것이 좋습니다.


✅ 37. Deploy State

READY
APPROVED
DEPLOYING
DEPLOYED
FAILED
ROLLED_BACK
  • DEPLOYED → DEPLOY 요청이 들어오면:
ALREADY_DEPLOYED

처럼 처리할 수 있습니다.


✅ 38. Deploy Action Key

deployKey:
production:release_123

Unique하게 관리할 수 있습니다.

UNIQUE(environment, releaseId)
  • 같은 Release를 Production에 두 번 배포하는 것을 방지할 수 있습니다.

✅ 39. 배포 성공 여부 확인

Deploy 명령이:

exit code 0

이라고 해서 서비스가 정상이라는 의미는 아닙니다.

Deploy
 ↓
Process Success
 ↓
Health Check
 ↓
Smoke Test
 ↓
DEPLOYED
  • 실제 상태 검증이 필요합니다.

✅ 40. Deploy의 Idempotency Boundary

Deploy Command

와:

Health Check

를 별도 Step으로 보는 것이 좋습니다.

Action:
DEPLOY_EXECUTE

Step:
DEPLOY_VERIFY
  • Deploy 자체가 성공해도 Verify가 실패할 수 있습니다.

✅ 41. Release 재실행

Release #123
 ├─ Build       DONE
 ├─ QA          DONE
 ├─ Approval    DONE
 ├─ Deploy      FAILED
 └─ Verify      NOT_STARTED

Retry:

Deploy부터 재시작
  • Build/QA를 다시 실행하지 않아도 됩니다.

✅ 42. 하지만 무조건 Resume하면 안 된다

예:

10:00:
Release 승인

10:30:
Production 설정 변경

10:40:
Deploy Retry
  • 이전 Approval이 더 이상 유효하지 않을 수 있습니다.

따라서:

Resume
 ↓
Preflight
 ↓
현재 상태와 기존 Run 비교
 ↓
계속 진행 가능?

을 거쳐야 합니다.


✅ 43. Resume Validation

확인 대상:

Commit
Release
Artifact
Environment
Policy Version
Approval
Dependency Lockfile

하나라도 중요한 변경이 있으면:

RESUME_BLOCKED

또는:

REVIEW_REQUIRED

로 처리할 수 있습니다.


✅ 44. Policy Version도 저장

0915에서 만든 Policy:

automation-policy.yml

도 버전이 필요합니다.

policyVersion:
2026-09-15.1

Run:

run_182
policyVersion:
2026-09-15.1
  • 나중에 Policy가 바뀌어도 과거 Run의 실행 조건을 알 수 있습니다.

✅ 45. Policy 변경 후 Retry

Run #1
Policy v1
FAILED

현재:
Policy v2

그냥 Resume하지 말고:

Policy Compatibility Check

를 먼저 수행하는 것이 좋습니다.


✅ 46. Retry Policy

모든 실패를 Retry하면 안 됩니다.

FAILURE
 ↓
Classify

➕ 46-1. Retryable

NETWORK_TIMEOUT
TEMPORARY_PROVIDER_ERROR
WORKER_CRASH
RATE_LIMIT

➕ 46-2. Non-Retryable

VALIDATION_ERROR
PERMISSION_DENIED
INVALID_STATE
RESOURCE_NOT_FOUND
POLICY_DENIED
  • 0831의 Error Code 체계와 연결할 수 있습니다.

✅ 47. Retry Count

예:

retryCount:
0
1
2
3

최대:

3회

이후:

FAILED

또는:

DEAD_LETTER

로 이동합니다.


✅ 48. Exponential Backoff

단순:

1초
1초
1초

보다:

1분
5분
15분

처럼 간격을 늘리는 방식이 일반적입니다.

예:

delay = min(
  base * 2^retryCount,
  maxDelay
)
  • 외부 Provider 장애가 발생했을 때 계속 즉시 요청하는 것을 막을 수 있습니다.

✅ 49. Jitter

여러 Worker가 동시에 Retry하면:

Worker A → 5분 후
Worker B → 5분 후
Worker C → 5분 후

다시 동시에 요청할 수 있습니다.

그래서:

5분 ± random

처럼 약간의 랜덤 시간을 추가할 수 있습니다.


✅ 50. Retry와 Rate Limit

외부 API가:

429 Too Many Requests

를 반환한다면:

즉시 Retry

하지 않습니다.

가능하면:

Retry-After

를 따르고 그렇지 않다면 Backoff를 적용합니다.


✅ 51. Retry와 Idempotency Key

Retry마다 새로운 Key를 만들면 안 되는 경우가 많습니다.

나쁜 구조:

Attempt #1:
key_001

Attempt #2:
key_002

외부 시스템 입장에서는:

서로 다른 요청

이 됩니다.

더 좋은 구조:

Job:
notification_100

Attempt #1:
notification_100

Attempt #2:
notification_100
  • Attempt와 작업의 Idempotency Identity를 분리합니다.

✅ 52. Attempt와 Job

Job:
notification_100

Attempt:
1
2
3

즉:

Job ID:
무엇을 해야 하는가

Attempt:
몇 번째 실행인가
  • 이 구분이 로그와 Retry 분석에 매우 중요합니다.

✅ 53. Run Attempt

AI 자동화도 동일합니다.

Run:
run_182

Attempt:
1

실패:

Attempt:
2
  • Run 자체와 실제 Process 실행 횟수를 분리하면 장애 분석이 쉬워집니다.

✅ 54. Crash Recovery

프로세스가 죽었을 때:

RUNNING

상태가 남을 수 있습니다.

재시작 시:

RUNNING
 ↓
stale 여부 확인
 ↓
Resume / Retry / Fail

를 판단해야 합니다.


✅ 55. Heartbeat

장시간 실행되는 Worker/AI Run이라면:

lastHeartbeatAt

을 저장할 수 있습니다.

예:

lastHeartbeatAt:
09:20

현재:

09:25

인데 Heartbeat가 없다면:

STALE

로 판단할 수 있습니다.


✅ 56. Heartbeat 주기

예:

30초~1분

정도로 충분할 수 있습니다.

다만 너무 자주 DB를 업데이트하면:

DB Write 증가

가 발생합니다.

  • 실제 작업 길이에 맞춰 조정합니다.

✅ 57. Stale Run 복구

RUNNING
lastHeartbeatAt > 10분

이면:

STALE

처리.

그 다음:

Resume 가능?
Retry 가능?
Human Review?

를 판단합니다.


✅ 58. 외부 작업의 상태 조회

특히 Deploy나 외부 Build처럼:

요청:
START

후 결과가 비동기로 오는 경우:

PROCESSING

상태를 저장하고:

GET STATUS

로 확인할 수 있습니다.

  • “응답이 없었으니 실패”라고 판단하면 중복 실행 위험이 생깁니다.

✅ 59. Unknown State

자동화에서 가장 위험한 상태 중 하나입니다.

SUCCESS
FAILED

둘 중 하나를 모르는 상황입니다.

따라서:

UNKNOWN

상태를 고려할 수 있습니다.

예:

External Deploy Request:
응답 Timeout

→ UNKNOWN
  • 바로 Retry하지 않고 상태 조회부터 해야 합니다.

✅ 60. Unknown → Reconcile

UNKNOWN
 ↓
External Status Query
 ↓
SUCCESS

또는:

UNKNOWN
 ↓
External Status Query
 ↓
NOT_FOUND
 ↓
Retry
  • 이 과정을 Reconciliation이라고 볼 수 있습니다.

✅ 61. Reconciliation

자동화 시스템의 상태와 실제 외부 시스템의 상태가 다를 수 있습니다.

우리 DB:
PROCESSING

Provider:
SUCCESS

Reconcile:

우리 DB:
DONE
  • 외부 시스템과 연결되는 자동화에서는 매우 중요한 패턴입니다.

✅ 62. 현재 프로젝트에 적용할 Reconcile 대상

NotificationJob
ExportJob
WebhookEvent
Deploy
AI Run

예:

NotificationJob:
PROCESSING

Provider:
이미 발송 완료

→ Job을 DONE으로 복구.


✅ 63. Reconcile Job

주기적으로:

PROCESSING이 너무 오래된 Job

을 조회할 수 있습니다.

Cron
 ↓
Stale Job Query
 ↓
Provider/Worker 상태 확인
 ↓
Reconcile
  • 무조건 Retry하는 것이 아니라 먼저 실제 상태를 확인합니다.

✅ 64. Outbox와 Idempotency

0830에서 다룬 Outbox도 중복 처리될 수 있습니다.

Event Outbox
 ↓
Worker
 ↓
External System

Worker가:

External Success
 ↓
DB Update 전에 Crash

하면 다시 처리될 수 있습니다.

  • 따라서 Outbox Worker에도 Idempotency가 필요합니다.

✅ 65. Event ID

eventId:
evt_consult_status_123_001

를 생성하고:

UNIQUE(eventId)

로 관리할 수 있습니다.

Consumer:

이미 처리했는가?

를 확인합니다.


✅ 66. Consumer Idempotency

Event:
CONSULT_STATUS_CHANGED
eventId:
evt_100

Consumer가:

evt_100 처리 완료

를 기록합니다.

다시:

evt_100

이 들어오면:

SKIP

합니다.


✅ 67. Event와 Side Effect

이벤트 자체는 중복될 수 있지만:

Event
 ↓
Email
 ↓
SMS
 ↓
Webhook

같은 Side Effect는 중복되면 문제가 될 수 있습니다.

  • 따라서 Side Effect 실행 시 별도 Idempotency Key가 필요합니다.

✅ 68. DB Unique Constraint를 적극 활용

Application 코드만:

if (!exists) {
  create();
}

하는 것은 부족합니다.

동시 요청:

A → exists false
B → exists false

가 가능하기 때문입니다.

DB:

UNIQUE(...)

가 최종 방어선이 됩니다.


✅ 69. 현재 프로젝트 Unique 후보

Webhook:
(provider, providerEventId)

Notification:
idempotencyKey

Export:
requestId 또는 exportKey

Deploy:
(environment, releaseId)

AI Run:
runFingerprint

Audit:
필요한 Event/Action Identifier
  • 실제 Unique 설계는 도메인 의미에 맞춰 결정해야 합니다.

✅ 70. Unique Constraint와 409

중복 생성 시:

P2002

가 발생할 수 있습니다.

0831에서 만든 Error Handling과 연결하여:

DUPLICATE_REQUEST

또는:

IDEMPOTENCY_CONFLICT

같은 도메인 오류로 변환할 수 있습니다.


✅ 71. Idempotency Record

HTTP API에서 특히 유용합니다.

예:

POST /admin/consults/export
Idempotency-Key:
export_20260915_abc

서버:

idempotency_records

에 저장합니다.

필드:

key
requestHash
status
response
createdAt
expiresAt

✅ 72. 동일 Key + 동일 Request

첫 요청:
PROCESSING

두 번째 요청:
기존 작업 조회

→ 같은 Job 반환
  • 중복 ExportJob을 생성하지 않습니다.

✅ 73. 동일 Key + 다른 Request

예:

Key:
export_100

첫 요청:

status=CALLING

두 번째:

status=CONVERTED

이면:

IDEMPOTENCY_KEY_REUSED

로 처리하는 것이 안전합니다.

  • 같은 Key를 다른 의미로 재사용하면 안 됩니다.

✅ 74. Request Hash

requestHash:
sha256(normalizedRequestBody)
  • 동일 Key가 같은 요청인지 검증할 수 있습니다.

✅ 75. Idempotency Record 보존 기간

모든 기록을 영구 보존할 필요는 없습니다.

예:

API Idempotency:
24시간

Export:
1~7일

Deploy:
Release lifetime

Webhook Event:
정책에 따라 수개월
  • 작업 특성에 따라 결정합니다.

✅ 76. Idempotency와 개인정보

Idempotency Record에:

phone
name
memo

같은 데이터를 그대로 저장하지 않는 것이 좋습니다.

필요하면:

requestHash

만 저장합니다.

  • 0830/0831에서 다룬 민감정보 로그 원칙과 같습니다.

✅ 77. AI Prompt도 Idempotency Record에 그대로 저장하지 않기

AI Task에는:

Prompt
Context
Code

등이 포함될 수 있습니다.

따라서:

taskId
runId
contextHash
promptVersion

정도만 참조하고 실제 내용은 필요한 저장소에서 접근하는 방식이 더 안전합니다.


✅ 78. Retry 후 결과 비교

AI Run을 Retry한 경우:

Run #1
Run #2

결과가 달라질 수 있습니다.

따라서:

artifactHash
diff
testResult

를 비교하면 좋습니다.


✅ 79. AI Retry의 위험

첫 번째:
파일 수정 A

두 번째:
파일 수정 B

가 누적될 수 있습니다.

따라서 Retry 전에:

Git Worktree
+
Known Base Commit

을 기준으로 작업 상태를 확인해야 합니다.


✅ 80. Retry는 Clean Base에서

권장:

Retry
 ↓
Worktree 상태 확인
 ↓
변경사항 보존 필요?
 ├─ YES → Human Review
 └─ NO → Known Base로 재구성
  • AI가 이전 실패 결과 위에 계속 수정하도록 두면 원인을 추적하기 어려워집니다.

✅ 81. Checkpoint

장시간 Task라면 Checkpoint를 둘 수 있습니다.

Checkpoint 1:
Context Prepared

Checkpoint 2:
Code Generated

Checkpoint 3:
QA Passed

Checkpoint 4:
Release Prepared
  • 실패했을 때 마지막 정상 Checkpoint부터 재개할 수 있습니다.

✅ 82. Checkpoint와 Artifact

Checkpoint:
QA_PASSED

Commit:
abc123

Artifact:
report_hash

Policy:
v1.3
  • Resume할 때 필요한 상태를 함께 보존합니다.

✅ 83. Resume Manifest

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
  • Resume 시 이 정보를 기준으로 현재 상태와 비교합니다.

✅ 84. Resume Validation

Manifest
 ↓
현재 Repository
현재 Policy
현재 Task
현재 Artifact
 ↓
Compare

결과:

VALID

또는:

INVALID
  • INVALID이면 무조건 이어서 실행하지 않고 재검토합니다.

✅ 85. 현재 프로젝트에서 가장 중요한 Resume 조건

Base Commit 동일
Task Spec 동일
Prompt Version 동일
Policy Version 호환
Worktree 상태 정상
Output Artifact 존재
Approval 유효
  • 이 정도부터 시작하면 충분합니다.

✅ 86. State Machine으로 생각하기

자동화 상태를 단순 문자열로 관리하기보다 허용된 전이만 정의하는 것이 좋습니다.

PENDING
  ↓
RUNNING
  ↓
SUCCEEDED

RUNNING
  ↓
FAILED
  ↓
RETRYING
  ↓
RUNNING

그리고:

SUCCEEDED → RUNNING

같은 비정상 전이는 막습니다.


✅ 87. AI Run State Transition

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
  • 상태가 많아질수록 전이 규칙이 중요해집니다.

✅ 88. State Transition과 Permission

예:

QA_PASSED → DEPLOYING

은 단순 상태 변경이 아니라:

DEPLOY_EXECUTE
+
APPROVAL

가 필요할 수 있습니다.

  • 상태와 권한을 함께 검증합니다.

✅ 89. State Transition과 Idempotency

이미:

DEPLOYED

인데:

DEPLOYED → DEPLOYING

을 허용하면 중복 배포 문제가 생길 수 있습니다.

따라서:

DEPLOYED → DEPLOYING

은 기본적으로 금지합니다.


✅ 90. “한 번만 실행”과 “여러 번 실행해도 안전”은 다르다

두 개념을 구분해야 합니다.

At-most-once:
최대 한 번 실행

At-least-once:
최소 한 번 실행될 때까지 Retry

Effectively-once:
여러 번 시도될 수 있지만 결과적으로 한 번만 적용
  • 실제 분산 시스템에서는 Effectively-once를 목표로 하는 경우가 많습니다.

✅ 91. 현재 프로젝트의 현실적인 목표

모든 시스템을 완벽한 Exactly-Once로 만들려고 할 필요는 없습니다.

현실적으로:

DB:
Unique + Transaction

Worker:
Claim + Retry

External API:
Idempotency Key

Webhook:
Event ID Unique

Deploy:
Release Unique

AI:
Run Fingerprint + Checkpoint

정도로 상당히 안정적인 구조를 만들 수 있습니다.


✅ 92. Exactly-Once에 대한 오해

"이 작업은 무조건 딱 한 번만 실행된다."

라는 보장은 외부 시스템까지 포함하면 어렵습니다.

대신:

여러 번 실행 요청이 발생해도
실제 비즈니스 결과는 중복되지 않는다.

를 목표로 하는 것이 현실적입니다.


✅ 93. 현재 프로젝트 적용 우선순위

➕ 93-1. 1순위 — Notification / Webhook

NotificationJob:
idempotencyKey

WebhookEvent:
providerEventId UNIQUE
  • 외부 시스템과 직접 연결되어 있기 때문에 가장 먼저 적용할 가치가 있습니다.

➕ 93-2. 2순위 — ExportJob

ExportKey
+
Job Claim
+
Retry
  • Excel 생성이 중복되지 않도록 합니다.

➕ 93-3. 3순위 — Deploy

environment + releaseId UNIQUE

그리고:

APPROVED
→ DEPLOYING

전이를 원자적으로 관리합니다.


➕ 93-4. 4순위 — AI Run

Task
+
Commit
+
Prompt Version
+
Context Hash
+
Policy Version

으로 Run을 식별합니다.


✅ 94. 추천 DB 모델

➕ 94-1. IdempotencyRecord

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, 보존 정책 등을 프로젝트에 맞게 조정합니다.

✅ 95. AI Run 모델 확장 예시

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])
}
  • Run의 재현성과 복구를 위한 핵심 정보입니다.

✅ 96. Action 모델 확장

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?
}
  • Deploy, Migration, Feature Flag 등의 위험 Action을 별도로 추적할 수 있습니다.

✅ 97. Worker 공통 실행 구조

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

입니다.


✅ 98. External Adapter 공통 구조

type ExternalResult<T> =
  | {
      ok: true;
      value: T;
    }
  | {
      ok: false;
      retryable: boolean;
      code: string;
      message: string;
    };

그리고:

send({
  idempotencyKey,
  payload,
});

처럼 작업의 의미를 명시적으로 전달합니다.


✅ 99. Retry Handler

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;
}
  • 모든 Error를 Retry하는 것이 아니라 Error Code 기반으로 분류합니다.

✅ 100. AI 자동화 Retry Handler

AI Run Failed
 ↓
Error Classification
 ├─ POLICY_DENIED → STOP
 ├─ VALIDATION_ERROR → STOP
 ├─ TOOL_TIMEOUT → RETRY
 ├─ MODEL_TIMEOUT → RETRY
 ├─ QA_FAILED → REVIEW / RETRY
 └─ UNKNOWN → HUMAN REVIEW
  • AI가 실패했다고 무조건 다시 실행하는 구조를 피합니다.

✅ 101. QA 실패와 Tool 실패 구분

Tool Error:
Codex process crash

QA Error:
Test failed

Policy Error:
Permission denied

세 가지는 처리 방법이 달라야 합니다.

Tool Error → Retry 가능

QA Error → AI 수정 후 재검증

Policy Error → 자동 Retry 금지

✅ 102. Infinite Retry 방지

Retry
Retry
Retry
Retry
...

가 되면 자동화가 장애를 확대합니다.

따라서:

maxAttempts
maxRuntime
maxCost

등을 둘 수 있습니다.


✅ 103. AI Cost Limit도 Retry와 연결

AI Task가:

실패
 ↓
Retry
 ↓
Retry
 ↓
Retry

하면 API 비용도 증가합니다.

예:

maxAttempts:
3

maxTokens:
500k

maxRuntime:
30m
  • 자동화의 안정성뿐 아니라 비용도 제한해야 합니다.

✅ 104. Budget Guard

Task Budget

Tokens:
500,000

Time:
30 min

Tool Calls:
100

초과:

BUDGET_EXCEEDED
  • AI 자동화가 예상보다 긴 작업에 빠지는 것을 막을 수 있습니다.

✅ 105. Budget과 Retry

Retry할 때:

Attempt 1:
100k

Attempt 2:
100k

Attempt 3:
100k

총:

300k

가 될 수 있습니다.

따라서 Budget은:

Attempt별

와:

Task 전체

를 구분할 수 있습니다.


✅ 106. 현재 프로젝트의 Retry Budget

초기에는 복잡하게 만들 필요 없이:

maxAttempts = 3

정도부터 시작하고:

AI Task:
maxRuntime 30m

Worker:
maxRetry 3

Notification:
maxRetry 3

Webhook:
provider 정책

처럼 적용하면 충분합니다.


✅ 107. 장애 발생 시 상태를 잃지 않는 것

자동화 시스템에서 중요한 것은:

"실패하지 않는 시스템"

보다:

"실패해도 어디까지 했는지 알 수 있는 시스템"

입니다.

따라서:

Task
Run
Attempt
Step
Action
Artifact
Checkpoint

를 기록하면 복구가 쉬워집니다.


✅ 108. 현재 전체 구조

Task
  ↓
Run
  ↓
Attempt
  ↓
Step
  ├─ Context
  ├─ Codex
  ├─ QA
  ├─ Release
  └─ Deploy
       ↓
     Action
       ↓
  Idempotency Key
       ↓
  Execute
       ↓
  Verify
       ↓
  Artifact / Audit
  • 이 구조가 지금까지 만든 자동화 아키텍처를 상당히 안정적으로 연결해줍니다.

✅ 109. 최종적인 Retry 흐름

Task
 ↓
Run #1
 ↓
Step FAILED
 ↓
Error Classification
 ↓
Retryable?
 ├─ NO → FAILED / HUMAN REVIEW
 └─ YES
      ↓
   Idempotency Check
      ↓
   Retry Budget
      ↓
   Run #2 / Attempt #2
  • 이 순서를 기본 패턴으로 생각하면 됩니다.

✅ 110. 최종적인 External Action 흐름

Action Requested
 ↓
Validate State
 ↓
Idempotency Check
 ↓
Approval Check
 ↓
Claim
 ↓
Execute
 ↓
Unknown?
 ├─ YES → Reconcile
 └─ NO
      ↓
   Verify
      ↓
   DONE
  • 특히 Deploy, Notification, Webhook처럼 외부 시스템과 연결되는 작업에 유용합니다.

✅ 111. 현재 프로젝트 구현 순서

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
  • 전부 한 번에 구현할 필요는 없습니다.

✅ 112. 지금 당장 가장 효과가 큰 것

현재 구조라면 우선:

① Worker Claim
② maxRetry
③ retryable/non-retryable 분류
④ Webhook providerEventId UNIQUE
⑤ Notification idempotencyKey

부터 하는 것을 추천합니다.

  • 이 다섯 가지는 구현 대비 실제 운영 효과가 큽니다.

✅ 113. 다음 단계에서 추가할 것

그 다음:

ExportJob:
Resume

Deploy:
Release + Environment Unique

AI:
Run Fingerprint

Long Running:
Heartbeat

External:
Reconciliation

순으로 확장하면 됩니다.


✅ 114. Codex에게 구현을 맡길 때

현재 프로젝트의 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. 실제 운영 시 주의사항

✅ 115. 구현 후 반드시 확인할 테스트

➕ Worker

  • PENDING → PROCESSING Claim이 원자적인가?
  • 두 Worker가 동시에 같은 Job을 실행하지 않는가?
  • Worker Crash 후 Stale Job을 감지하는가?
  • Retry 횟수가 제한되는가?
  • Non-Retryable Error를 재실행하지 않는가?
  • Backoff가 적용되는가?

➕ Webhook

  • 동일 providerEventId가 중복 저장되지 않는가?
  • 동일 Event가 두 번 처리되지 않는가?
  • 처리 중 Crash 후 Resume 가능한가?
  • Signature 검증 실패는 Retry하지 않는가?

➕ Notification

  • 동일 Idempotency Key가 중복 발송되지 않는가?
  • Provider Message ID를 저장하는가?
  • Timeout 후 바로 중복 발송하지 않는가?
  • Provider 상태를 확인할 수 있는가?

➕ Export

  • 같은 요청이 ExportJob을 중복 생성하지 않는가?
  • Worker 중복 Claim이 막히는가?
  • 실패 후 Retry 가능한가?
  • 완료된 Job을 다시 생성하지 않는가?

➕ Deploy

  • 같은 Release를 중복 배포하지 않는가?
  • 승인되지 않은 Release를 실행하지 않는가?
  • 승인 후 Commit이 변경되면 차단되는가?
  • Deploy 성공 후 Verify가 실행되는가?
  • Unknown 상태에서 무조건 재배포하지 않는가?

➕ AI Run

  • Task와 Run이 구분되는가?
  • Attempt가 기록되는가?
  • Context Hash가 기록되는가?
  • Policy Version이 기록되는가?
  • 완료된 Step을 다시 실행하지 않는가?
  • Retry 전에 Worktree 상태를 검사하는가?
  • Resume 조건이 맞지 않으면 Human Review로 보내는가?

📌 요약

  • 자동화에서 중요한 것은 실패하지 않는 것보다 실패했을 때 중복 실행 없이 복구할 수 있는 것입니다.
  • Retry를 도입한다면 반드시 Idempotency를 함께 설계해야 합니다.
  • Request ID는 추적용이고 Idempotency Key는 동일 작업의 중복 실행을 방지하기 위한 값입니다.
  • Worker는 조회 → 실행이 아니라 조건부 Claim → 실행 구조로 만들어야 동시 실행을 막을 수 있습니다.
  • Worker가 죽었을 때 PROCESSING 상태에 갇히지 않도록 lease, heartbeat, stale recovery를 고려할 수 있습니다.
  • 외부 API 호출은 성공했지만 응답 전에 프로세스가 죽을 수 있으므로, Retry만으로 해결하지 말고 Provider의 Idempotency Key나 상태 조회를 활용해야 합니다.
  • Webhook은 중복 전달을 전제로 하고 provider + providerEventId UNIQUE로 중복 이벤트를 방지하는 것이 좋습니다.
  • 알림톡, ExportJob, Webhook, Deploy는 각각 고유한 Idempotency 전략을 가져야 합니다.
  • AI 자동화도 Task → Run → Attempt → Step → Action을 구분하면 실패한 부분부터 안전하게 재개할 수 있습니다.
  • AI Run에는 Base Commit, Prompt Version, Context Hash, Policy Version 등을 저장하면 재현성과 Resume 검증이 쉬워집니다.
  • 외부 작업의 결과를 알 수 없는 상황에서는 무조건 Retry하지 말고 UNKNOWN → Reconcile → 실제 상태 확인 → Resume/Retry 흐름을 사용하는 것이 안전합니다.
  • Deploy 역시 environment + releaseId 기준으로 중복을 방지하고, 명령 실행 성공과 실제 서비스 정상 여부를 Verify 단계에서 분리해야 합니다.
  • Retry는 Validation, Permission, Policy Denied 같은 오류까지 무작정 재실행하면 안 되며, Timeout/Rate Limit/일시적 외부 오류처럼 실제로 재시도할 가치가 있는 오류만 대상으로 해야 합니다.
  • 현재 프로젝트에서는 모든 것을 한 번에 구현하기보다 Worker Claim → Retry 분류 → Idempotency Key → Webhook 중복 방지 → Notification 중복 방지 순으로 적용하는 것이 가장 현실적입니다.
  • 이후 Export Resume → Deploy Idempotency → AI Run Checkpoint → Heartbeat/Stale Recovery → Reconciliation 순으로 확장하면 지금까지 만든 AI 자동화 구조가 단순한 “자동 실행 스크립트”에서 실패와 중복을 스스로 통제할 수 있는 운영 시스템으로 발전합니다.

0개의 댓글