TIL - 20260917

juni·2026년 9월 17일

TIL

목록 보기
458/468

0917 운영 자동화/AI 워크플로우 심화 (11/N): Reconciliation, Self-Healing과 자동 복구 전략


✅ 1. 자동화 시스템에서 더 위험한 것은 ‘실패’보다 ‘상태 불일치’다

자동화 시스템을 운영하다 보면 단순히 작업이 실패하는 것보다 더 까다로운 상황이 생긴다.

예를 들어 알림톡 발송 Worker가 외부 API를 호출했다고 하자.

우리 서버
  ↓
알림톡 발송 API 호출
  ↓
외부 서버에서는 발송 성공
  ↓
응답이 돌아오는 순간 네트워크 끊김
  ↓
우리 서버에서는 FAILED 또는 UNKNOWN

실제로는 고객에게 메시지가 발송됐지만 우리 DB에는 실패로 기록될 수 있다.

이 상태에서 단순 Retry를 수행하면:

첫 번째 발송 → 실제 성공
두 번째 Retry → 또 발송

고객에게 같은 알림톡이 두 번 전송될 수 있다.

따라서 운영 자동화에서는 단순히

실패하면 재시도한다

만으로는 부족하다.

필요한 것은

원래 기대했던 상태(Desired State)
와
실제 현재 상태(Actual State)

가 일치하는지 다시 확인하고

불일치하면 안전하게 복구하는 과정

이다.

이 과정을 Reconciliation이라고 볼 수 있다.


✅ 2. Reconciliation이란?

Reconciliation은 쉽게 말하면

“시스템이 지금 실제로 어떤 상태인지 다시 확인한 뒤, 우리가 원래 기대했던 상태와 맞춰주는 과정”

이다.

예를 들어 주문 상태가 다음처럼 되어야 한다고 하자.

결제 완료
→ 개통 대기
→ 개통 완료
→ 완료 알림톡 발송

DB에는

status = COMPLETED
notificationStatus = PROCESSING

으로 남아 있는데 실제 알림톡 플랫폼에서는 이미 발송 완료라면,

Reconciliation 과정에서 외부 상태를 조회한 뒤

notificationStatus = SENT

로 맞춰준다.

즉,

DB 상태를 무조건 신뢰하지 않는다.
Worker 상태도 무조건 신뢰하지 않는다.
외부 API 응답도 한 번만 믿지 않는다.

필요하면 실제 상태를 다시 확인한다.

가 핵심이다.


✅ 3. Retry와 Reconciliation의 차이

둘은 비슷해 보이지만 목적이 다르다.

구분RetryReconciliation
목적작업을 다시 수행실제 상태 확인 및 동기화
기준실패 여부Desired vs Actual
대표 상황Timeout, 500 Error성공 여부 자체가 불확실
위험중복 실행비교적 안전
외부 API다시 호출조회 API 우선
실행 결과Action 재수행상태 수정 또는 필요한 Action 수행

예를 들어:

HTTP 500

이고 외부 시스템에서 요청 자체를 처리하지 않은 것이 확실하다면 Retry가 적합하다.

반대로

HTTP Timeout

이 발생해서 외부 시스템이 실제로 처리했는지 모른다면 바로 Retry하지 않는다.

UNKNOWN
→ Reconcile
→ 실제 처리 여부 확인

이 먼저다.


✅ 4. UNKNOWN은 실패와 다른 상태다

기존에는 보통 상태를 이렇게 생각하기 쉽다.

PENDING
PROCESSING
SUCCESS
FAILED

하지만 자동화가 많아질수록 다음 상태가 중요해진다.

UNKNOWN

UNKNOWN은

실패가 확정된 것도 아니고
성공이 확정된 것도 아닌 상태

다.

예:

POST /alimtalk/send

요청 전달
→ Connection Reset

이때 우리 서버는 알 수 없다.

외부 서버가 요청을 처리하기 전에 연결이 끊긴 것인지

또는

외부 서버가 처리한 후 응답만 못 받은 것인지

따라서

PROCESSING
   ↓
UNKNOWN
   ↓
RECONCILING
   ↓
SUCCESS / RETRYABLE / MANUAL_REQUIRED

같은 상태 흐름을 고려할 수 있다.


✅ 5. Desired State와 Actual State를 분리해서 생각하기

운영 자동화 시스템을 설계할 때 유용한 사고방식이 있다.

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 배포를 재개하거나

배포 실패 원인을 판단하거나

안전하지 않다면 사람에게 넘긴다.

✅ 6. Drift Detection

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이다.


✅ 7. 투게더몰에서 Reconciliation이 필요한 영역

현재 프로젝트 기준으로 특히 다음 영역에 적용 가치가 높다.

NotificationJob
ExportJob
WebhookEvent
DeployJob
AI Run
Order 상태 변경
외부 API 연동
파일 업로드
결제/정산 연동

특히 외부 시스템과 연결되는 기능은 거의 모두 Reconciliation 대상이 될 수 있다.


✅ 8. NotificationJob 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 유지

✅ 9. 무조건 자동 복구하면 안 된다

Self-Healing이라는 단어만 보면

문제가 생기면 시스템이 알아서 전부 고쳐주는 구조

라고 생각하기 쉽지만 실제 운영에서는 위험하다.

자동 복구에는 반드시 경계가 필요하다.

예를 들어 다음은 자동 복구하기 좋다.

Worker 재시작
stale lock 해제
Webhook 재처리
캐시 재생성
실패한 Export 재개
Provider 상태 재조회
읽기 모델 재구축

반대로 이런 작업은 자동으로 하면 위험할 수 있다.

고객 주문 삭제
결제 취소
환불
운영 데이터 강제 수정
Production DB 데이터 삭제
배포 강제 Rollback
대량 메시지 재발송
권한 변경

즉 Self-Healing의 기준은

안전하고
되돌릴 수 있고
중복 실행에 안전하고
범위가 제한된 작업

이어야 한다.


✅ 10. Self-Healing Level을 나눠두는 것도 좋다

자동 복구 정도를 단계로 나눌 수 있다.

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를 붙일 때도 안전하다.


✅ 11. Reconciliation Job 구조

전체 데이터를 매번 검사하면 비효율적이다.

따라서 보통 이상 상태만 대상으로 한다.

예:

SELECT *
FROM notification_jobs
WHERE status IN ('PROCESSING', 'UNKNOWN')
  AND updated_at < NOW() - INTERVAL '5 minutes';

그리고 Scheduler가 주기적으로 실행한다.

ReconciliationScheduler
        ↓
Candidate 찾기
        ↓
상태 확인
        ↓
필요한 Repair Action 결정
        ↓
Action 실행
        ↓
Audit Log

✅ 12. Scheduler와 Worker 역할 분리

Scheduler가 직접 복구 작업까지 수행하기보다,

Scheduler
→ 복구가 필요한 대상 찾기

Worker
→ 실제 복구 Action 실행

으로 나누는 것이 낫다.

예:

ReconciliationScheduler
        ↓
NotificationReconcileJob 생성
        ↓
Queue
        ↓
NotificationReconcileWorker

이렇게 하면

Concurrency
Retry
Rate Limit
Monitoring
Audit

을 Worker 쪽에서 통제할 수 있다.


✅ 13. Heartbeat와 Stale Job 복구

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

상태로 바꾼다.


✅ 14. Stale Lock을 바로 제거하면 안 되는 이유

Worker A가 느리지만 실제로 살아있을 수도 있다.

그런데 Reconciler가

PROCESSING 상태가 오래됐다
→ Lock 해제

해버리면 Worker B가 같은 작업을 시작할 수 있다.

그러면 두 Worker가 동시에 처리할 수 있다.

따라서 Lease 개념을 사용할 수 있다.

{
  lockedBy: "worker-123",
  leaseUntil: "...",
}

Worker는 Lease를 주기적으로 연장한다.

leaseUntil = now + 30 sec

Reconciler는

leaseUntil < now

인 경우에만 작업을 회수한다.


✅ 15. Dead Letter Queue

계속 실패하는 작업을 무한 Retry하면 시스템에 부담만 준다.

예:

알림톡 Retry
1회 실패
2회 실패
3회 실패
...
1000회 실패

그래서 일정 횟수를 넘긴 작업은 별도로 격리한다.

Main Queue
   ↓
Retry
   ↓
Retry
   ↓
Retry Limit 초과
   ↓
Dead Letter Queue

DLQ는

자동 처리가 불가능해진 작업

을 모아두는 곳이다.


✅ 16. Poison Job

특정 데이터 때문에 항상 실패하는 작업을 Poison Job이라고 생각할 수 있다.

예:

잘못된 전화번호
깨진 JSON
존재하지 않는 주문 ID
잘못된 Provider Template ID
유효하지 않은 Export 조건

이런 Job은 몇 번 Retry해도 성공하지 않는다.

따라서

Retryable Error

와

Non-Retryable Error

를 반드시 구분해야 한다.


✅ 17. 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이 중요하다.


✅ 18. DLQ에 들어갔다고 끝이 아니다

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"
}

✅ 19. Reconciliation 결과도 감사 로그에 남겨야 한다

자동 복구는 시스템이 데이터를 바꾸는 행위다.

따라서 기록이 필요하다.

예:

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

운영 중 문제가 생겼을 때

누가 바꿨지?

가 아니라

어떤 자동화가
왜
무슨 근거로
어떤 상태를 변경했는지

확인할 수 있어야 한다.


✅ 20. Repair Action이라는 개념을 별도로 두기

Reconciliation이 상태를 확인하는 역할이라면 실제 복구 동작을 Repair Action으로 분리할 수 있다.

예:

type RepairAction =
  | "MARK_AS_SUCCESS"
  | "RETRY"
  | "RELEASE_LOCK"
  | "RESUME"
  | "REBUILD"
  | "ROLLBACK"
  | "ESCALATE";

Reconciler가 직접 임의 동작을 하는 대신

Observation
↓
Decision
↓
Repair Action
↓
Execution

단계를 명확하게 둔다.


✅ 21. Observer와 Executor를 분리하는 이유

자동 복구 로직을 다음처럼 작성하면 위험하다.

if (job.status === "UNKNOWN") {
  await provider.sendAgain();
}

상태 확인과 실행이 한 번에 섞여 있다.

더 나은 구조는

const observation =
  await reconciliationService.inspect(job);

const decision =
  recoveryPolicy.decide(observation);

await recoveryExecutor.execute(decision);

처럼 나누는 것이다.

이렇게 하면

테스트하기 쉽고
정책 변경이 쉽고
AI 판단을 끼워 넣기도 쉽고
위험한 Action을 차단하기 쉽다.

✅ 22. Recovery Policy

복구 판단을 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',
    };
  }
}

이렇게 하면 복구 정책이 코드 곳곳에 흩어지지 않는다.


✅ 23. Human-in-the-loop가 필요한 시점

모든 예외를 자동화하는 것은 목표가 아니다.

다음과 같은 경우 사람에게 넘기는 것이 안전하다.

외부 시스템 상태가 계속 UNKNOWN

금전 영향이 있는 작업

고객에게 중복 영향 가능

Production 배포

데이터 삭제 가능성

복구 방법이 여러 개인 경우

정책 판단이 필요한 경우

상태를

MANUAL_REQUIRED

로 두고 운영 화면에서 확인하게 한다.


✅ 24. 운영 화면에 ‘복구함’이 있으면 좋다

관리자 시스템에 다음 같은 메뉴를 생각해볼 수 있다.

운영
 ├─ 실패 작업
 ├─ 복구 대기
 ├─ Dead Letter
 ├─ UNKNOWN
 └─ 수동 승인

예:

작업상태오류시도마지막 실행조치
알림톡 #392UNKNOWNTimeout110:21상태 확인
엑셀 #113FAILEDS3 Error310:18재시도
Webhook #944DLQInvalid Data509:52상세
AI Run #23MANUAL_REQUIREDPolicy109:31검토

이런 화면이 있으면 자동화가 늘어도 운영 가능성이 높아진다.


✅ 25. Reconciliation Dashboard에서 보면 좋은 지표

단순히 성공률만 보는 것보다 다음 지표가 유용하다.

UNKNOWN 건수

Stale Job 건수

Reconciliation 수행 건수

자동 복구 성공 건수

Manual Required 건수

DLQ 건수

평균 복구 시간

Retry 횟수

같은 오류 반복 건수

예:

오늘 Worker 처리

정상 완료       1,842
Retry             27
Auto Recovered     13
UNKNOWN             3
DLQ                 1
Manual Required      1

✅ 26. MTTR 관점

운영에서는 장애 자체를 완전히 없애기 어렵다.

대신 중요한 지표 중 하나가

MTTR
Mean Time To Recovery

이다.

즉

문제 발생
→ 발견
→ 원인 확인
→ 복구

까지 얼마나 걸리는지 보는 것이다.

Self-Healing과 Reconciliation의 목적도 결국

장애 발생 자체 0건

이 아니라

장애를 빨리 발견하고
안전하게 원래 상태로 돌려놓는 것

이다.


✅ 27. AI 자동화에도 Reconciliation이 필요하다

AI Agent도 실행 상태가 꼬일 수 있다.

예:

AI가 코드 수정
↓
테스트 실행
↓
프로세스 Timeout
↓
Run FAILED

그런데 실제 Git Working Tree에는 수정 파일이 남아 있을 수 있다.

이 상황에서 처음부터 다시 실행하면

이미 수정된 코드 위에
AI가 또 수정

할 수 있다.

따라서 AI Run 역시 실제 상태를 점검해야 한다.


✅ 28. AI Run Reconciliation

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

할 수 있다.


✅ 29. AI 실행 상태를 Step 단위로 복구하기

예:

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 재실행

한다.


✅ 30. Resume Manifest

복구 가능한 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처럼 사용할 수 있다.


✅ 31. Reconciliation 전에 반드시 검증할 것

Resume 전에 무조건 이전 상태를 그대로 믿으면 안 된다.

다음을 다시 확인한다.

현재 Commit이 같은가?

Working Tree가 변경되지 않았는가?

Prompt Version이 같은가?

Policy Version이 같은가?

Dependency가 바뀌지 않았는가?

Artifact가 여전히 유효한가?

Approval이 아직 유효한가?

하나라도 크게 달라졌다면 Resume보다

새 Run 생성

이 안전할 수 있다.


✅ 32. Production 배포 Reconciliation

배포 작업은 특히 중요하다.

DB에는

DeployJob = SUCCESS

인데 실제 서버가 이전 버전을 실행할 수도 있다.

따라서 배포 이후

Deployment API 상태

Running Commit SHA

Application Version

Health Check

Database Migration Version

등을 확인한다.

예:

Desired Release
abc123

Running Release
def456

→ DRIFT

✅ 33. Deployment Self-Healing의 경계

다음 정도는 자동화하기 좋다.

Health Check 재시도

Instance 재시작

배포 상태 다시 조회

Traffic 상태 확인

Rollback 조건 판단

하지만 Production Rollback은 운영 환경에 따라

자동 실행

보다

자동 제안
→ 관리자 승인
→ Rollback

이 더 안전할 수 있다.


✅ 34. Automatic Rollback 조건

자동 Rollback을 한다면 매우 명확한 조건이 필요하다.

예:

배포 이후 5분 동안

Health Check 실패율 > 50%

또는

HTTP 5xx 비율 > 20%

또는

필수 API 3회 연속 실패

같은 정책이다.

단,

단순 로그 몇 개

만으로 Rollback하면 안 된다.


✅ 35. Runbook Automation

운영 장애가 발생할 때 사람이 항상 같은 작업을 한다면 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

순서가 현실적이다.


✅ 36. Runbook 자체도 코드처럼 관리

예:

/docs/runbooks/

notification-worker.md
export-job-recovery.md
webhook-recovery.md
deployment-rollback.md
ai-run-recovery.md

Runbook에는 최소 다음 내용을 넣는 것이 좋다.

증상

확인 방법

영향 범위

자동 복구 가능 여부

수동 복구 방법

Rollback 방법

재발 방지 항목

✅ 37. 투게더몰 기준 현실적인 도입 순서

현재 규모에서 Kubernetes Operator 수준의 Self-Healing까지 만들 필요는 없다.

먼저 ROI가 높은 것부터 적용하는 것이 낫다.

1단계

UNKNOWN 상태 추가

Stale Job 탐지

Heartbeat

Retryable / Non-Retryable Error 분류

2단계

Webhook Reconciliation

Notification Provider 상태 조회

DLQ

Manual Required

3단계

ExportJob Resume

Reconciliation Scheduler

Recovery Dashboard

Repair Action Audit Log

4단계

AI Run Resume

AI Step Reconciliation

Deploy Verification

Semi-Automatic Rollback

이 정도면 1인 개발 환경에서도 과한 구조가 아니다.


✅ 38. 현재 시스템에 추천하는 상태 모델

예:

PENDING
  ↓
PROCESSING
  ↓
SUCCESS

실패 시:

PROCESSING
  ↓
FAILED
  ↓
RETRY_PENDING

결과 불명확 시:

PROCESSING
  ↓
UNKNOWN
  ↓
RECONCILING

Reconciliation 결과:

SUCCESS

또는

RETRY_PENDING

또는

MANUAL_REQUIRED

재시도 한도 초과:

DEAD_LETTER

✅ 39. NestJS 구조 예시

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 안에 다 넣지 않는 것이다.


✅ 40. Prisma 모델 예시

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])
}

✅ 41. Reconciliation 기록 모델

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])
}

운영 중에는 이 기록 자체가 상당히 중요해진다.


✅ 42. Reconciler 의사 코드

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,
  });
}

✅ 43. Reconciliation 자체도 Idempotent해야 한다

0916 내용과 연결되는 핵심이다.

Reconciler가 두 번 실행되더라도

고객에게 알림톡 두 번 발송

Export 파일 두 개 생성

Rollback 두 번 실행

Webhook 두 번 반영

등이 발생하면 안 된다.

즉:

Recovery Action도 Idempotent

해야 한다.


✅ 44. Reconciliation Lock

같은 대상에 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만 작업을 수행한다.


✅ 45. AI에게 Self-Healing 권한을 바로 주면 안 된다

AI Agent가

"문제가 발생했으니 알아서 고쳐"

라는 권한을 가지면 위험하다.

특히 AI는 상황 해석이 틀릴 수 있기 때문이다.

따라서 AI의 역할은 초기에는

문제 분석

가능한 복구 방법 제안

위험도 판단

Runbook 선택

안전한 Read-Only 검사

정도로 제한하는 것이 좋다.


✅ 46. AI Recovery Permission 예시

READ_ONLY
- 로그 조회
- Job 조회
- Queue 상태 확인
- Git 상태 확인

SAFE_REPAIR
- Retry 등록
- 상태 재조회
- Cache 재생성

APPROVAL_REQUIRED
- Production 재배포
- Rollback
- 대량 알림톡 재전송

FORBIDDEN
- 운영 DB 삭제
- 고객 데이터 삭제
- 권한 임의 변경

0915에서 다뤘던 AI 권한 모델과 그대로 연결된다.


✅ 47. AI가 복구 전에 제시해야 할 정보

AI가

복구하겠습니다.

로 끝내면 안 된다.

최소 다음 정보를 만들게 하는 것이 좋다.

현재 관찰된 상태

정상적으로 기대되는 상태

발견된 Drift

제안하는 Repair Action

해당 Action 위험도

중복 실행 안전성

Rollback 가능 여부

Human Approval 필요 여부

✅ 48. 예를 들어 AI 운영 에이전트가 이렇게 판단하게 만들기

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

이런 구조가 훨씬 안전하다.


✅ 49. Codex에게 시킬 수 있는 구현 프롬프트

현재 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 중복 실행

✅ 50. 실무 체크리스트

Job 상태

  • UNKNOWN 상태가 있는가?
  • FAILED와 UNKNOWN을 구분하는가?
  • PROCESSING 작업이 무기한 남지 않는가?
  • Heartbeat가 있는가?
  • Lease 또는 Lock 만료 시간이 있는가?

Retry

  • Retryable Error와 Non-Retryable Error를 구분하는가?
  • Retry Limit이 있는가?
  • Backoff가 있는가?
  • Retry Budget이 있는가?
  • UNKNOWN 상태를 무작정 Retry하지 않는가?

Reconciliation

  • Desired State를 정의할 수 있는가?
  • Actual State를 조회할 방법이 있는가?
  • Drift를 탐지할 수 있는가?
  • Reconciliation 자체가 Idempotent한가?
  • 동시 Reconciliation을 방지하는가?

Recovery

  • 자동 복구 가능한 Action을 정의했는가?
  • 위험한 Action은 Human Approval을 요구하는가?
  • Repair Action 기록이 남는가?
  • Rollback 방법이 있는가?

DLQ

  • Retry 한도를 넘은 작업을 격리하는가?
  • Poison Job을 식별할 수 있는가?
  • DLQ 재처리 기능이 있는가?
  • DLQ를 운영자가 확인할 수 있는가?

AI Automation

  • AI Run의 실제 상태를 재확인하는가?
  • Step별 상태를 기록하는가?
  • Checkpoint가 있는가?
  • Resume 전에 Commit/Artifact/Policy를 검증하는가?
  • AI가 위험한 Repair Action을 임의 실행하지 못하는가?

📌 요약

오늘 핵심은 “자동화 시스템은 실패를 재시도하는 것에서 끝나지 않고, 실제 상태를 다시 확인해 정상 상태로 되돌릴 수 있어야 한다”는 것이다.

특히 중요한 개념은 다음과 같다.

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

그리고 가장 중요한 원칙은

자동 복구 자체보다, 잘못된 자동 복구를 하지 않는 구조가 먼저다.

0개의 댓글