Skip to content

10 Reliability and Observability

hywznn edited this page Jul 21, 2026 · 3 revisions

신뢰성 있는 AI 실행과 관측성

이 페이지가 답하는 질문

  • 외부 LLM이 느리거나 실패하면 HR 요청은 어떻게 되나요?
  • DB 저장 직후 서버가 꺼지면 후속 작업을 잃지 않나요?
  • 같은 요청/event가 두 번 와도 Task·Link가 중복되지 않나요?
  • 장애가 어디서 났는지 개인정보 없이 어떻게 찾나요?

핵심 답은 DB에 남는 비동기 AI Run, 내구성 있는 event publication, 멱등 consumer, 명시적 실패, request/trace 기반 관측입니다.

동기 호출을 피하는 이유

나쁜 실패 예: HTTP 요청 → LLM 40초 대기 → 연결 종료 → 결과/진행상태 불명

목표 흐름: 요청 저장 + event 저장 → 202/runId
          → background 실행 → 검증 결과 저장
          → Client가 상태 조회 → HR 검토

사용자가 browser를 닫거나 Spring process가 재시작돼도 Run 상태를 다시 조회할 수 있어야 합니다.

신뢰성 경계

flowchart TD
  R["POST /ai-runs"] --> T["DB Transaction"]
  T --> RUN["AiRun = QUEUED"]
  T --> PUB["Event Publication = PENDING"]
  PUB --> H["Idempotent Handler"]
  H --> P["AI Provider"]
  P --> V["Schema · Core Value · PII Validation"]
  V --> S["Persist Result / Failure"]
  H -. "failure" .-> RETRY["Backoff · Retry · Manual Review"]
  RETRY --> H
Loading

Run만 저장하고 event를 memory에서 발행하면 commit 직후 process 종료 때 실행 요청을 잃을 수 있습니다. 상태와 publication record를 같은 transaction에 저장해 이 틈을 없앱니다.

기술 선택 순서

  1. package-by-feature modular monolith 경계를 먼저 지킵니다.
  2. Spring Boot 4.1과 Spring Modulith의 module verification/Event Publication Registry 호환성을 작은 spike로 확인합니다.
  3. 적합하면 PostgreSQL-backed Event Publication Registry와 재발행을 사용합니다.
  4. 부적합하면 같은 내부 DomainEventPublisher port 뒤에 Transactional Outbox를 구현합니다.
  5. 처리량·팀 경계가 실제로 필요해질 때 Kafka/RabbitMQ를 다시 검토합니다.

Spring Modulith를 “유행해서” 넣거나 Outbox와 동시에 이중 구현하지 않습니다. #23 ADR에 선택·근거·대안·전환 기준을 기록합니다.

Event envelope

{
  "eventId": "uuid",
  "eventType": "AiRunRequested",
  "payloadVersion": 1,
  "aggregateType": "AI_RUN",
  "aggregateId": "uuid",
  "companyId": "uuid",
  "actorType": "HR_USER",
  "actorRef": "safe-reference",
  "requestId": "request-id",
  "traceId": "trace-id",
  "occurredAt": "2026-07-21T00:00:00Z",
  "payload": {
    "runId": "uuid",
    "expectedVersion": 0
  }
}

Payload에는 JWT·Worker token·외국인등록번호·여권번호·전화번호·계좌번호·Prompt 전문을 넣지 않습니다.

MVP Event catalog

Event 발생 시점 대표 consumer
AiRunRequested Run이 QUEUED로 저장됨 AI orchestration worker
AiRunValidated 후보 검증 완료 audit·review projection
AiRunFailed 안전한 실패 확정 audit·운영 지표
TaskCandidateConfirmed HR이 후보를 Task로 확정 activity·audit
TaskStatusChanged Workflow 명령으로 전이 activity·후속 rule
ApprovalRequested 사람 검토 요청 activity·notification 준비
TaskApproved / TaskRejected 사람 결정 저장 link 가능 여부·audit
WorkerLinkIssued 승인된 link 발급 audit·전달 준비
WorkerResponded 근로자 응답 HR 후속 Task/activity
WorkerDocumentSubmitted token 문서 제출 validation·HR review
EvidenceAttached 완료증빙 연결 완료 guard
TaskCompleted / TaskCancelled terminal 전이 audit·dashboard projection

Event 이름은 이미 일어난 사실을 과거형으로 표현합니다. Event 자체가 “승인하라”는 권한이 되지 않습니다.

Publication 상태와 재처리

상태 의미 처리
PENDING commit됐지만 handler 미완료 실행 대상
PROCESSING 한 worker가 처리 중 lease/timeout 정책 필요
COMPLETED 성공 일반 재처리 금지
RETRY_WAIT 일시 실패 후 대기 다음 시각에 backoff retry
FAILED 최대 횟수/영구 오류 운영자 검토·수동 replay

Framework가 자체 publication schema를 제공하면 그 상태를 사용하고 별도 table을 중복 생성하지 않습니다.

멱등성 설계

API command

  • 변경 endpoint는 필요한 곳에 Idempotency-Key를 받습니다.
  • tenant + key + operation에 unique constraint를 둡니다.
  • 같은 key·같은 payload는 이전 결과를 반환합니다.
  • 같은 key·다른 payload는 충돌 오류를 반환합니다.
  • key 원문 대신 hash와 만료/보존 정책을 검토합니다.

Event consumer

  • event_id + consumer_name 처리 record 또는 결과 unique constraint를 사용합니다.
  • “먼저 외부 side effect, 나중 DB 기록” 순서를 피합니다.
  • 정확히 한 번 전달을 가정하지 않고 at-least-once 전달 + idempotent 처리를 설계합니다.
  • optimistic lock과 expected version으로 오래된 event가 최신 상태를 덮지 않게 합니다.

Resilience4j 경계

기능 적용 위치 주의
TimeLimiter/HTTP timeout Provider client 전체 API transaction을 오래 잡지 않음
Retry 일시적 network·429·5xx parsing/business error를 무한 retry하지 않음
Circuit Breaker Provider별 open 때 빠르게 명시적 실패
Bulkhead AI 호출 thread/concurrency 일반 HR API 자원과 격리

Fallback은 “수동 입력”, “나중 재시도”, “담당자 검토”처럼 실제 가능한 다음 행동입니다. Fake candidate를 반환하지 않습니다.

오류 분류 초안

code 범주 retry 예시
AI_TEMPORARY_* 제한적 자동/수동 timeout, 429, 503
AI_PROVIDER_AUTH_* 자동 금지 API key/권한 오류
AI_RESPONSE_PARSE_* 기본 자동 금지 malformed JSON
AI_RESPONSE_SCHEMA_* 자동 금지·검토 required field/enum 위반
AI_CORE_VALUE_* 자동 금지·검토 대상·날짜·금액·문서 손실
AI_PRIVACY_* 자동 금지·보안 검토 민감정보 탐지
WORKFLOW_TRANSITION_* 입력/상태 수정 허용되지 않은 전이
IDEMPOTENCY_CONFLICT 새 key 또는 원 요청 확인 같은 key·다른 payload
CONCURRENT_MODIFICATION 최신 조회 후 재명령 expected version 불일치

외부 Provider 오류 전문과 stack trace를 Client에게 노출하지 않습니다.

관측성 연결

sequenceDiagram
  participant C as Client
  participant B as Spring Boot
  participant E as Event Handler
  participant P as Provider/Python
  participant A as Audit

  C->>B: X-Request-Id(optional), trace context
  B->>B: request_id 검증/생성, trace 시작
  B->>E: publication에 request_id/trace_id 저장
  E->>P: traceparent 전달
  P-->>E: response/error + 같은 trace
  E->>A: safe event reference
  B-->>C: request_id, runId
Loading
  • request_id: 사용자·지원 담당자가 API 응답과 log를 연결하는 식별자
  • trace_id: API, async event, Provider span을 하나의 분산 trace로 연결
  • business ID는 log field로 필요할 때 안전하게 사용하되 metric label로 남발하지 않습니다.

최소 Metric

영역 지표
HTTP request 수·오류율·p50/p95 latency
Workflow Task 생성·승인·완료 수, 상태 체류시간, 실패율
AI Run queue wait·전체 latency·성공/검토/실패 비율
Validation parsing·Schema·핵심값·privacy 실패 수
Resilience timeout·retry·circuit open·bulkhead reject 수
Event pending backlog·처리 latency·retry·영구 실패 수
Worker Link 조회·응답·문서·만료·거부 수

금지 label: company_id, worker_id, run_id, 이름, token, 자유 입력, Prompt, request_id, trace_id. 값 종류가 무한히 늘어나는 high-cardinality label은 metric system을 망가뜨립니다.

Log·Trace 안전 규칙

  • Authorization, Cookie, Worker Link URL/token을 redact합니다.
  • request/response body 자동 수집은 기본 비활성 또는 allow-list로 제한합니다.
  • Provider prompt/response 전문 대신 size·hash·version·validation code를 기록합니다.
  • file name과 OCR text를 그대로 기록하지 않습니다.
  • sampling을 사용해도 AuditEvent 보존과 혼동하지 않습니다.
  • observability exporter 장애가 Task transaction을 rollback하지 않게 합니다.

꼭 통과할 실패 Test

  1. Run과 publication commit 직후 process 종료 → restart 후 실행
  2. Provider 호출 성공 직후 handler 종료 → duplicate delivery에도 결과 하나
  3. 같은 Idempotency-Key를 동시에 전송 → Run 하나
  4. 같은 confirm을 동시에 전송 → Task 하나
  5. timeout·429·5xx → retry budget·circuit 동작
  6. malformed JSON·Schema 위반 → 자동 발송/Task 없음
  7. 핵심값 변경 후 재승인 없이 link 발급 → 거부
  8. event 최대 retry 소진 → 추적 가능한 FAILED와 수동 replay
  9. 타 사업장 Run/event/audit 조회 → 차단
  10. log·metric·trace fixture에 token·PII 없음

운영자가 장애를 볼 때

  1. 사용자 응답의 request_idrunId를 확인합니다.
  2. Run 상태·error_code·retry_count·version을 봅니다.
  3. trace_id로 API→event→Provider 구간을 찾습니다.
  4. publication 상태와 다음 retry 시각을 확인합니다.
  5. retry 가능 오류인지, 수동 입력/검토가 필요한지 판단합니다.
  6. replay 전 idempotency와 현재 aggregate version을 확인합니다.
  7. 해결 뒤 AuditEvent와 metric 회복을 확인합니다.

개인정보나 Secret을 log에 추가 출력해 진단하지 않습니다.

구현 단계

  1. #23에서 module/API/event ADR과 compatibility spike
  2. #6에서 상태 명령·@Version
  3. #25에서 한 event의 commit→restart→replay vertical slice
  4. #24에서 persistent AI Run·idempotency
  5. #8에서 Provider resilience·strict validation
  6. #11에서 approval/audit transaction
  7. #9에서 배포 recovery smoke
  8. #26에서 OTel·Micrometer dashboard 고도화

Clone this wiki locally