자동화 시스템을 운영하다 보면 단순히 작업이 실패하는 것보다 더 까다로운 상황이 생긴다.
예를 들어 알림톡 발송 Worker가 외부 API를 호출했다고 하자.
우리 서버
↓
알림톡 발송 API 호출
↓
외부 서버에서는 발송 성공
↓
응답이 돌아오는 순간 네트워크 끊김
↓
우리 서버에서는 FAILED 또는 UNKNOWN
실제로는 고객에게 메시지가 발송됐지만 우리 DB에는 실패로 기록될 수 있다.
이 상태에서 단순 Retry를 수행하면:
첫 번째 발송 → 실제 성공
두 번째 Retry → 또 발송
고객에게 같은 알림톡이 두 번 전송될 수 있다.
따라서 운영 자동화에서는 단순히
실패하면 재시도한다
만으로는 부족하다.
필요한 것은
원래 기대했던 상태(Desired State)
와
실제 현재 상태(Actual State)
가 일치하는지 다시 확인하고
불일치하면 안전하게 복구하는 과정
이다.
이 과정을 Reconciliation이라고 볼 수 있다.
Reconciliation은 쉽게 말하면
“시스템이 지금 실제로 어떤 상태인지 다시 확인한 뒤, 우리가 원래 기대했던 상태와 맞춰주는 과정”
이다.
예를 들어 주문 상태가 다음처럼 되어야 한다고 하자.
결제 완료
→ 개통 대기
→ 개통 완료
→ 완료 알림톡 발송
DB에는
status = COMPLETED
notificationStatus = PROCESSING
으로 남아 있는데 실제 알림톡 플랫폼에서는 이미 발송 완료라면,
Reconciliation 과정에서 외부 상태를 조회한 뒤
notificationStatus = SENT
로 맞춰준다.
즉,
DB 상태를 무조건 신뢰하지 않는다.
Worker 상태도 무조건 신뢰하지 않는다.
외부 API 응답도 한 번만 믿지 않는다.
필요하면 실제 상태를 다시 확인한다.
가 핵심이다.
둘은 비슷해 보이지만 목적이 다르다.
| 구분 | Retry | Reconciliation |
|---|---|---|
| 목적 | 작업을 다시 수행 | 실제 상태 확인 및 동기화 |
| 기준 | 실패 여부 | Desired vs Actual |
| 대표 상황 | Timeout, 500 Error | 성공 여부 자체가 불확실 |
| 위험 | 중복 실행 | 비교적 안전 |
| 외부 API | 다시 호출 | 조회 API 우선 |
| 실행 결과 | Action 재수행 | 상태 수정 또는 필요한 Action 수행 |
예를 들어:
HTTP 500
이고 외부 시스템에서 요청 자체를 처리하지 않은 것이 확실하다면 Retry가 적합하다.
반대로
HTTP Timeout
이 발생해서 외부 시스템이 실제로 처리했는지 모른다면 바로 Retry하지 않는다.
UNKNOWN
→ Reconcile
→ 실제 처리 여부 확인
이 먼저다.
기존에는 보통 상태를 이렇게 생각하기 쉽다.
PENDING
PROCESSING
SUCCESS
FAILED
하지만 자동화가 많아질수록 다음 상태가 중요해진다.
UNKNOWN
UNKNOWN은
실패가 확정된 것도 아니고
성공이 확정된 것도 아닌 상태
다.
예:
POST /alimtalk/send
요청 전달
→ Connection Reset
이때 우리 서버는 알 수 없다.
외부 서버가 요청을 처리하기 전에 연결이 끊긴 것인지
또는
외부 서버가 처리한 후 응답만 못 받은 것인지
따라서
PROCESSING
↓
UNKNOWN
↓
RECONCILING
↓
SUCCESS / RETRYABLE / MANUAL_REQUIRED
같은 상태 흐름을 고려할 수 있다.
운영 자동화 시스템을 설계할 때 유용한 사고방식이 있다.
Desired State
= 우리가 원하는 상태
Actual State
= 현재 실제 상태
예를 들어 배포라면:
Desired State
releaseId = v2026.09.17.3
environment = production
status = ACTIVE
실제 서버는:
Actual State
releaseId = v2026.09.17.2
라면 Drift가 발생한 것이다.
Reconciliation은 이를 감지하고
v2026.09.17.3 배포를 재개하거나
배포 실패 원인을 판단하거나
안전하지 않다면 사람에게 넘긴다.
Desired State와 Actual State가 달라진 상태를 Drift라고 볼 수 있다.
대표적인 Drift는 다음과 같다.
DB에서는 알림톡 SENT
→ 실제 Provider에서는 FAILED
DB에서는 배포 SUCCESS
→ 실제 서버는 이전 Commit 실행 중
ExportJob = PROCESSING
→ 실제 Worker는 죽어 있음
AI Run = RUNNING
→ 실행 프로세스 없음
Webhook = RECEIVED
→ 실제 Domain Event 반영 안 됨
이런 상태를 주기적으로 확인하는 것이 Drift Detection이다.
현재 프로젝트 기준으로 특히 다음 영역에 적용 가치가 높다.
NotificationJob
ExportJob
WebhookEvent
DeployJob
AI Run
Order 상태 변경
외부 API 연동
파일 업로드
결제/정산 연동
특히 외부 시스템과 연결되는 기능은 거의 모두 Reconciliation 대상이 될 수 있다.
알림톡을 예로 들면 구조를 다음처럼 가져갈 수 있다.
enum NotificationStatus {
PENDING
PROCESSING
SENT
FAILED
UNKNOWN
RECONCILING
MANUAL_REQUIRED
}
외부 Provider 메시지 ID도 저장한다.
{
id: string
providerMessageId: string | null
idempotencyKey: string
status: NotificationStatus
attemptCount: number
sentAt: Date | null
lastCheckedAt: Date | null
}
UNKNOWN 상태가 발생하면
providerMessageId 존재
↓
Provider 상태 조회
↓
DELIVERED
→ SENT로 변경
FAILED
→ Retry 가능 여부 판단
NOT_FOUND
→ 최초 요청이 접수되지 않았을 가능성
→ Retry 후보
조회 API 오류
→ UNKNOWN 유지
Self-Healing이라는 단어만 보면
문제가 생기면 시스템이 알아서 전부 고쳐주는 구조
라고 생각하기 쉽지만 실제 운영에서는 위험하다.
자동 복구에는 반드시 경계가 필요하다.
예를 들어 다음은 자동 복구하기 좋다.
Worker 재시작
stale lock 해제
Webhook 재처리
캐시 재생성
실패한 Export 재개
Provider 상태 재조회
읽기 모델 재구축
반대로 이런 작업은 자동으로 하면 위험할 수 있다.
고객 주문 삭제
결제 취소
환불
운영 데이터 강제 수정
Production DB 데이터 삭제
배포 강제 Rollback
대량 메시지 재발송
권한 변경
즉 Self-Healing의 기준은
안전하고
되돌릴 수 있고
중복 실행에 안전하고
범위가 제한된 작업
이어야 한다.
자동 복구 정도를 단계로 나눌 수 있다.
Level 0
감지만 함
Level 1
안전한 상태 동기화
Level 2
안전한 Retry
Level 3
제한적인 Repair Action
Level 4
Human Approval 필요
Level 5
Manual Only
예를 들어:
알림톡 Provider 상태 조회
→ Level 1
ExportJob 재개
→ Level 2
Worker Stale Lock 해제
→ Level 2
Production Rollback
→ Level 4
고객 주문 삭제
→ Level 5
이렇게 자동화 수준을 기능별로 명확하게 정해놓으면 AI Agent를 붙일 때도 안전하다.
전체 데이터를 매번 검사하면 비효율적이다.
따라서 보통 이상 상태만 대상으로 한다.
예:
SELECT *
FROM notification_jobs
WHERE status IN ('PROCESSING', 'UNKNOWN')
AND updated_at < NOW() - INTERVAL '5 minutes';
그리고 Scheduler가 주기적으로 실행한다.
ReconciliationScheduler
↓
Candidate 찾기
↓
상태 확인
↓
필요한 Repair Action 결정
↓
Action 실행
↓
Audit Log
Scheduler가 직접 복구 작업까지 수행하기보다,
Scheduler
→ 복구가 필요한 대상 찾기
Worker
→ 실제 복구 Action 실행
으로 나누는 것이 낫다.
예:
ReconciliationScheduler
↓
NotificationReconcileJob 생성
↓
Queue
↓
NotificationReconcileWorker
이렇게 하면
Concurrency
Retry
Rate Limit
Monitoring
Audit
을 Worker 쪽에서 통제할 수 있다.
0916에서 Worker가 PROCESSING 상태로 멈춘 경우를 다뤘다.
예:
ExportJob
PROCESSING
updatedAt = 40분 전
정상적인 Export가 보통 2분 안에 끝난다면 사실상 Worker가 죽었다고 볼 수 있다.
이때 Heartbeat를 사용할 수 있다.
{
status: "PROCESSING",
startedAt: "...",
heartbeatAt: "...",
}
Worker 실행 중 일정 간격으로
heartbeatAt 갱신
을 수행한다.
Reconciliation Job은 다음 조건을 찾는다.
status = PROCESSING
AND heartbeatAt < now - staleThreshold
그리고
STALLED
또는
RECOVERY_PENDING
상태로 바꾼다.
Worker A가 느리지만 실제로 살아있을 수도 있다.
그런데 Reconciler가
PROCESSING 상태가 오래됐다
→ Lock 해제
해버리면 Worker B가 같은 작업을 시작할 수 있다.
그러면 두 Worker가 동시에 처리할 수 있다.
따라서 Lease 개념을 사용할 수 있다.
{
lockedBy: "worker-123",
leaseUntil: "...",
}
Worker는 Lease를 주기적으로 연장한다.
leaseUntil = now + 30 sec
Reconciler는
leaseUntil < now
인 경우에만 작업을 회수한다.
계속 실패하는 작업을 무한 Retry하면 시스템에 부담만 준다.
예:
알림톡 Retry
1회 실패
2회 실패
3회 실패
...
1000회 실패
그래서 일정 횟수를 넘긴 작업은 별도로 격리한다.
Main Queue
↓
Retry
↓
Retry
↓
Retry Limit 초과
↓
Dead Letter Queue
DLQ는
자동 처리가 불가능해진 작업
을 모아두는 곳이다.
특정 데이터 때문에 항상 실패하는 작업을 Poison Job이라고 생각할 수 있다.
예:
잘못된 전화번호
깨진 JSON
존재하지 않는 주문 ID
잘못된 Provider Template ID
유효하지 않은 Export 조건
이런 Job은 몇 번 Retry해도 성공하지 않는다.
따라서
Retryable Error
와
Non-Retryable Error
를 반드시 구분해야 한다.
다음은 Retry 가치가 있다.
HTTP 502
HTTP 503
HTTP 504
Connection Timeout
Rate Limit
Temporary Network Error
DB Connection Timeout
반대로 다음은 보통 Retry해도 해결되지 않는다.
400 Bad Request
잘못된 Template ID
존재하지 않는 대상
Validation Error
Permission Denied
정책 위반
따라서 Error Classification이 중요하다.
Dead Letter Queue를 만들고 방치하면 사실상
실패 작업 쓰레기통
이 된다.
DLQ에는 최소한 다음 정보가 필요하다.
{
jobId: string
originalQueue: string
payload: Json
errorCode: string
errorMessage: string
attemptCount: number
firstFailedAt: Date
lastFailedAt: Date
recoverable: boolean
status:
| "OPEN"
| "RETRY_PENDING"
| "RESOLVED"
| "IGNORED"
}
자동 복구는 시스템이 데이터를 바꾸는 행위다.
따라서 기록이 필요하다.
예:
2026-09-17 03:12:14
Actor:
SYSTEM_RECONCILER
Target:
NotificationJob #1234
Before:
UNKNOWN
External State:
DELIVERED
After:
SENT
Reason:
Provider reconciliation
CorrelationId:
rec_9f123
운영 중 문제가 생겼을 때
누가 바꿨지?
가 아니라
어떤 자동화가
왜
무슨 근거로
어떤 상태를 변경했는지
확인할 수 있어야 한다.
Reconciliation이 상태를 확인하는 역할이라면 실제 복구 동작을 Repair Action으로 분리할 수 있다.
예:
type RepairAction =
| "MARK_AS_SUCCESS"
| "RETRY"
| "RELEASE_LOCK"
| "RESUME"
| "REBUILD"
| "ROLLBACK"
| "ESCALATE";
Reconciler가 직접 임의 동작을 하는 대신
Observation
↓
Decision
↓
Repair Action
↓
Execution
단계를 명확하게 둔다.
자동 복구 로직을 다음처럼 작성하면 위험하다.
if (job.status === "UNKNOWN") {
await provider.sendAgain();
}
상태 확인과 실행이 한 번에 섞여 있다.
더 나은 구조는
const observation =
await reconciliationService.inspect(job);
const decision =
recoveryPolicy.decide(observation);
await recoveryExecutor.execute(decision);
처럼 나누는 것이다.
이렇게 하면
테스트하기 쉽고
정책 변경이 쉽고
AI 판단을 끼워 넣기도 쉽고
위험한 Action을 차단하기 쉽다.
복구 판단을 Policy로 분리할 수 있다.
class NotificationRecoveryPolicy {
decide(context: ReconciliationContext) {
if (context.providerStatus === 'DELIVERED') {
return {
action: 'MARK_AS_SUCCESS',
};
}
if (
context.providerStatus === 'NOT_FOUND' &&
context.attemptCount < 3
) {
return {
action: 'RETRY',
};
}
return {
action: 'ESCALATE',
};
}
}
이렇게 하면 복구 정책이 코드 곳곳에 흩어지지 않는다.
모든 예외를 자동화하는 것은 목표가 아니다.
다음과 같은 경우 사람에게 넘기는 것이 안전하다.
외부 시스템 상태가 계속 UNKNOWN
금전 영향이 있는 작업
고객에게 중복 영향 가능
Production 배포
데이터 삭제 가능성
복구 방법이 여러 개인 경우
정책 판단이 필요한 경우
상태를
MANUAL_REQUIRED
로 두고 운영 화면에서 확인하게 한다.
관리자 시스템에 다음 같은 메뉴를 생각해볼 수 있다.
운영
├─ 실패 작업
├─ 복구 대기
├─ Dead Letter
├─ UNKNOWN
└─ 수동 승인
예:
| 작업 | 상태 | 오류 | 시도 | 마지막 실행 | 조치 |
|---|---|---|---|---|---|
| 알림톡 #392 | UNKNOWN | Timeout | 1 | 10:21 | 상태 확인 |
| 엑셀 #113 | FAILED | S3 Error | 3 | 10:18 | 재시도 |
| Webhook #944 | DLQ | Invalid Data | 5 | 09:52 | 상세 |
| AI Run #23 | MANUAL_REQUIRED | Policy | 1 | 09:31 | 검토 |
이런 화면이 있으면 자동화가 늘어도 운영 가능성이 높아진다.
단순히 성공률만 보는 것보다 다음 지표가 유용하다.
UNKNOWN 건수
Stale Job 건수
Reconciliation 수행 건수
자동 복구 성공 건수
Manual Required 건수
DLQ 건수
평균 복구 시간
Retry 횟수
같은 오류 반복 건수
예:
오늘 Worker 처리
정상 완료 1,842
Retry 27
Auto Recovered 13
UNKNOWN 3
DLQ 1
Manual Required 1
운영에서는 장애 자체를 완전히 없애기 어렵다.
대신 중요한 지표 중 하나가
MTTR
Mean Time To Recovery
이다.
즉
문제 발생
→ 발견
→ 원인 확인
→ 복구
까지 얼마나 걸리는지 보는 것이다.
Self-Healing과 Reconciliation의 목적도 결국
장애 발생 자체 0건
이 아니라
장애를 빨리 발견하고
안전하게 원래 상태로 돌려놓는 것
이다.
AI Agent도 실행 상태가 꼬일 수 있다.
예:
AI가 코드 수정
↓
테스트 실행
↓
프로세스 Timeout
↓
Run FAILED
그런데 실제 Git Working Tree에는 수정 파일이 남아 있을 수 있다.
이 상황에서 처음부터 다시 실행하면
이미 수정된 코드 위에
AI가 또 수정
할 수 있다.
따라서 AI Run 역시 실제 상태를 점검해야 한다.
AI Run에 대해 다음 항목을 확인할 수 있다.
현재 Git Commit
Working Tree 변경
생성된 파일
테스트 결과
실행 중 프로세스
Artifact 존재 여부
Approval 상태
Tool 실행 로그
예:
Run Status = UNKNOWN
Reconciliation 실행
git diff 존재
test-report.json 존재
deployment 없음
→ CODE_CHANGED
→ TEST_COMPLETED
→ DEPLOY_NOT_STARTED
따라서 전체 작업을 다시 시작할 필요 없이
Deploy 단계부터 Resume
할 수 있다.
예:
Task
└─ Run
├─ Step 1 Analyse SUCCESS
├─ Step 2 Modify SUCCESS
├─ Step 3 Test SUCCESS
├─ Step 4 Review SUCCESS
└─ Step 5 Deploy UNKNOWN
전체 Run을 Retry하는 것이 아니라
Step 5 상태 확인
부터 시작한다.
Deploy가 실제로 성공했다면
Run → SUCCESS
로 복구한다.
배포되지 않았다면
Step 5 재실행
한다.
복구 가능한 AI 자동화를 만들려면 실행 중간 상태를 남겨두는 것이 좋다.
{
"taskId": "task-102",
"runId": "run-299",
"baseCommit": "a1b2c3d",
"currentCommit": "f7e8d9a",
"steps": {
"analyse": "SUCCESS",
"modify": "SUCCESS",
"test": "SUCCESS",
"review": "SUCCESS",
"deploy": "UNKNOWN"
},
"artifacts": [
"test-report.json",
"review-report.md"
]
}
이 정보를 Resume Manifest처럼 사용할 수 있다.
Resume 전에 무조건 이전 상태를 그대로 믿으면 안 된다.
다음을 다시 확인한다.
현재 Commit이 같은가?
Working Tree가 변경되지 않았는가?
Prompt Version이 같은가?
Policy Version이 같은가?
Dependency가 바뀌지 않았는가?
Artifact가 여전히 유효한가?
Approval이 아직 유효한가?
하나라도 크게 달라졌다면 Resume보다
새 Run 생성
이 안전할 수 있다.
배포 작업은 특히 중요하다.
DB에는
DeployJob = SUCCESS
인데 실제 서버가 이전 버전을 실행할 수도 있다.
따라서 배포 이후
Deployment API 상태
Running Commit SHA
Application Version
Health Check
Database Migration Version
등을 확인한다.
예:
Desired Release
abc123
Running Release
def456
→ DRIFT
다음 정도는 자동화하기 좋다.
Health Check 재시도
Instance 재시작
배포 상태 다시 조회
Traffic 상태 확인
Rollback 조건 판단
하지만 Production Rollback은 운영 환경에 따라
자동 실행
보다
자동 제안
→ 관리자 승인
→ Rollback
이 더 안전할 수 있다.
자동 Rollback을 한다면 매우 명확한 조건이 필요하다.
예:
배포 이후 5분 동안
Health Check 실패율 > 50%
또는
HTTP 5xx 비율 > 20%
또는
필수 API 3회 연속 실패
같은 정책이다.
단,
단순 로그 몇 개
만으로 Rollback하면 안 된다.
운영 장애가 발생할 때 사람이 항상 같은 작업을 한다면 Runbook으로 만들 수 있다.
예:
알림톡 Worker 정지 시
1. Worker Health 확인
2. Queue Lag 확인
3. DB Lock 확인
4. Worker 재시작
5. Stale Job Reconcile
6. Queue 정상화 확인
처음에는 Markdown 문서로 작성하고,
안전한 단계부터 자동화한다.
Runbook
→ Semi Automation
→ Self-Healing
순서가 현실적이다.
예:
/docs/runbooks/
notification-worker.md
export-job-recovery.md
webhook-recovery.md
deployment-rollback.md
ai-run-recovery.md
Runbook에는 최소 다음 내용을 넣는 것이 좋다.
증상
확인 방법
영향 범위
자동 복구 가능 여부
수동 복구 방법
Rollback 방법
재발 방지 항목
현재 규모에서 Kubernetes Operator 수준의 Self-Healing까지 만들 필요는 없다.
먼저 ROI가 높은 것부터 적용하는 것이 낫다.
UNKNOWN 상태 추가
Stale Job 탐지
Heartbeat
Retryable / Non-Retryable Error 분류
Webhook Reconciliation
Notification Provider 상태 조회
DLQ
Manual Required
ExportJob Resume
Reconciliation Scheduler
Recovery Dashboard
Repair Action Audit Log
AI Run Resume
AI Step Reconciliation
Deploy Verification
Semi-Automatic Rollback
이 정도면 1인 개발 환경에서도 과한 구조가 아니다.
예:
PENDING
↓
PROCESSING
↓
SUCCESS
실패 시:
PROCESSING
↓
FAILED
↓
RETRY_PENDING
결과 불명확 시:
PROCESSING
↓
UNKNOWN
↓
RECONCILING
Reconciliation 결과:
SUCCESS
또는
RETRY_PENDING
또는
MANUAL_REQUIRED
재시도 한도 초과:
DEAD_LETTER
src/
└─ automation/
├─ application/
│ ├─ jobs/
│ ├─ reconciliation/
│ │ ├─ reconcile-notification.use-case.ts
│ │ ├─ reconcile-export.use-case.ts
│ │ └─ reconcile-ai-run.use-case.ts
│ │
│ └─ recovery/
│ ├─ recovery-policy.ts
│ └─ recovery-executor.ts
│
├─ domain/
│ ├─ job-status.ts
│ ├─ recovery-action.ts
│ └─ recovery-policy.ts
│
├─ infrastructure/
│ ├─ scheduler/
│ ├─ queue/
│ └─ provider/
│
└─ presentation/
└─ admin/
핵심은
감지
판단
실행
을 한 Service 안에 다 넣지 않는 것이다.
model AutomationJob {
id String @id @default(cuid())
type String
status String
idempotencyKey String @unique
attemptCount Int @default(0)
lockedBy String?
leaseUntil DateTime?
heartbeatAt DateTime?
lastErrorCode String?
lastError String?
nextRetryAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, nextRetryAt])
@@index([status, heartbeatAt])
}
model ReconciliationRun {
id String @id @default(cuid())
targetType String
targetId String
beforeStatus String
actualStatus String?
afterStatus String?
action String?
result String
reason String?
createdAt DateTime @default(now())
@@index([targetType, targetId])
}
운영 중에는 이 기록 자체가 상당히 중요해진다.
async function reconcile(job: AutomationJob) {
const actualState =
await inspectActualState(job);
const decision =
recoveryPolicy.decide({
job,
actualState,
});
await reconciliationRepository.save({
jobId: job.id,
actualState,
decision,
});
if (decision.action === 'NONE') {
return;
}
if (decision.requiresApproval) {
await markManualRequired(job.id);
return;
}
await recoveryExecutor.execute({
job,
action: decision.action,
});
}
0916 내용과 연결되는 핵심이다.
Reconciler가 두 번 실행되더라도
고객에게 알림톡 두 번 발송
Export 파일 두 개 생성
Rollback 두 번 실행
Webhook 두 번 반영
등이 발생하면 안 된다.
즉:
Recovery Action도 Idempotent
해야 한다.
같은 대상에 Reconciler 두 개가 동시에 붙는 것도 막아야 한다.
예:
Scheduler A
Scheduler B
둘 다 UNKNOWN Notification 발견
둘이 동시에 복구하지 않도록
UNKNOWN
→ RECONCILING
변경을 Atomic하게 수행한다.
예:
UPDATE notification_jobs
SET status = 'RECONCILING'
WHERE id = ?
AND status = 'UNKNOWN';
affected rows가 1인 Worker만 작업을 수행한다.
AI Agent가
"문제가 발생했으니 알아서 고쳐"
라는 권한을 가지면 위험하다.
특히 AI는 상황 해석이 틀릴 수 있기 때문이다.
따라서 AI의 역할은 초기에는
문제 분석
가능한 복구 방법 제안
위험도 판단
Runbook 선택
안전한 Read-Only 검사
정도로 제한하는 것이 좋다.
READ_ONLY
- 로그 조회
- Job 조회
- Queue 상태 확인
- Git 상태 확인
SAFE_REPAIR
- Retry 등록
- 상태 재조회
- Cache 재생성
APPROVAL_REQUIRED
- Production 재배포
- Rollback
- 대량 알림톡 재전송
FORBIDDEN
- 운영 DB 삭제
- 고객 데이터 삭제
- 권한 임의 변경
0915에서 다뤘던 AI 권한 모델과 그대로 연결된다.
AI가
복구하겠습니다.
로 끝내면 안 된다.
최소 다음 정보를 만들게 하는 것이 좋다.
현재 관찰된 상태
정상적으로 기대되는 상태
발견된 Drift
제안하는 Repair Action
해당 Action 위험도
중복 실행 안전성
Rollback 가능 여부
Human Approval 필요 여부
Incident:
NotificationJob #3321
Expected:
SENT
Current DB:
UNKNOWN
Provider:
DELIVERED
Proposed Repair:
DB status UNKNOWN → SENT
Risk:
LOW
External Side Effect:
NONE
Idempotent:
YES
Approval:
NOT REQUIRED
반대로:
Incident:
Production Release #412
Expected:
abc123
Current:
def456
Proposed Repair:
Rollback production
Risk:
HIGH
Approval:
REQUIRED
이런 구조가 훨씬 안전하다.
현재 NestJS + Prisma 기반 프로젝트에
운영 자동화 Job 복구 구조를 추가해줘.
목표는 Worker 실패 시 단순 Retry만 수행하는 것이 아니라,
UNKNOWN / stale 상태를 감지하고 실제 상태를 확인한 뒤
안전하게 복구할 수 있는 Reconciliation 구조를 만드는 것이다.
다음 요구사항을 반영해줘.
1. AutomationJob 상태
- PENDING
- PROCESSING
- SUCCESS
- FAILED
- RETRY_PENDING
- UNKNOWN
- RECONCILING
- MANUAL_REQUIRED
- DEAD_LETTER
2. Worker 실행 중 다음 필드를 관리
- lockedBy
- leaseUntil
- heartbeatAt
- attemptCount
- nextRetryAt
3. PROCESSING 상태에서 heartbeat 또는 lease가 오래된 Job을
stale 상태로 판단할 수 있는 구조를 만든다.
4. Reconciliation은
- actual state 조회
- recovery policy 판단
- repair action 실행
단계로 분리한다.
5. 같은 Job에 두 Reconciler가 동시에 실행되지 않도록
UNKNOWN → RECONCILING 전환을 atomic하게 처리한다.
6. Repair Action은 다음 개념을 지원한다.
- NONE
- MARK_AS_SUCCESS
- RETRY
- RESUME
- RELEASE_LOCK
- REBUILD
- ESCALATE
7. 위험한 작업은 직접 실행하지 않고
MANUAL_REQUIRED 상태로 보낸다.
8. Reconciliation 실행 결과를 별도의
ReconciliationRun 또는 Audit Log에 기록한다.
9. Retryable / Non-Retryable Error를 분리하고
Retry 횟수 초과 시 DEAD_LETTER 상태로 보낸다.
10. 모든 Recovery Action은 가능한 한 idempotent하게 구현한다.
11. 기존 비즈니스 로직을 크게 수정하지 말고,
확장 가능한 application/domain/infrastructure 구조를 사용한다.
12. 먼저 NotificationJob 또는 ExportJob 중 하나를
예제로 구현하고 공통 구조를 추출한다.
13. 구현 후 다음 테스트를 작성한다.
- UNKNOWN → SUCCESS reconciliation
- UNKNOWN → RETRY_PENDING
- UNKNOWN → MANUAL_REQUIRED
- stale Worker recovery
- concurrent reconciler
- retry limit → DEAD_LETTER
- 동일 reconciliation 중복 실행
오늘 핵심은 “자동화 시스템은 실패를 재시도하는 것에서 끝나지 않고, 실제 상태를 다시 확인해 정상 상태로 되돌릴 수 있어야 한다”는 것이다.
특히 중요한 개념은 다음과 같다.
Retry
≠
Reconciliation
Retry는 작업을 다시 실행하는 것이고,
Reconciliation은
Desired State
vs
Actual State
를 비교해 시스템을 정상 상태로 돌려놓는 과정이다.
결과가 불확실한 작업은
FAILED
로 단정하지 말고
UNKNOWN
→ RECONCILING
과정을 거치는 것이 안전하다.
또한 Worker가 비정상 종료되었을 때를 대비해
Heartbeat
Lease
Stale Job Detection
이 필요하고,
계속 실패하는 작업은 무한 Retry하는 대신
Dead Letter Queue
로 격리해야 한다.
Self-Healing 역시 모든 문제를 자동으로 해결하는 것이 목표가 아니다.
안전한 복구
→ 자동
위험한 복구
→ 승인
파괴적인 복구
→ 수동
처럼 경계를 만드는 것이 더 중요하다.
AI 자동화에서도 동일하다.
AI Run이 중간에 끊기면 처음부터 다시 수행하는 것이 아니라
현재 Git 상태
Artifact
Test 결과
Deploy 상태
Approval
를 다시 확인하고 마지막으로 확정된 Step부터 Resume할 수 있어야 한다.
결국 운영 자동화가 성숙해지는 흐름은 다음과 같다.
실패 감지
→ Retry
→ Idempotency
→ UNKNOWN
→ Reconciliation
→ Self-Healing
→ Human Escalation
그리고 가장 중요한 원칙은
자동 복구 자체보다, 잘못된 자동 복구를 하지 않는 구조가 먼저다.