-
Notifications
You must be signed in to change notification settings - Fork 0
10 Reliability and Observability
- 외부 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
Run만 저장하고 event를 memory에서 발행하면 commit 직후 process 종료 때 실행 요청을 잃을 수 있습니다. 상태와 publication record를 같은 transaction에 저장해 이 틈을 없앱니다.
- package-by-feature modular monolith 경계를 먼저 지킵니다.
- Spring Boot 4.1과 Spring Modulith의 module verification/Event Publication Registry 호환성을 작은 spike로 확인합니다.
- 적합하면 PostgreSQL-backed Event Publication Registry와 재발행을 사용합니다.
- 부적합하면 같은 내부
DomainEventPublisherport 뒤에 Transactional Outbox를 구현합니다. - 처리량·팀 경계가 실제로 필요해질 때 Kafka/RabbitMQ를 다시 검토합니다.
Spring Modulith를 “유행해서” 넣거나 Outbox와 동시에 이중 구현하지 않습니다. #23 ADR에 선택·근거·대안·전환 기준을 기록합니다.
{
"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 전문을 넣지 않습니다.
| 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 자체가 “승인하라”는 권한이 되지 않습니다.
| 상태 | 의미 | 처리 |
|---|---|---|
PENDING |
commit됐지만 handler 미완료 | 실행 대상 |
PROCESSING |
한 worker가 처리 중 | lease/timeout 정책 필요 |
COMPLETED |
성공 | 일반 재처리 금지 |
RETRY_WAIT |
일시 실패 후 대기 | 다음 시각에 backoff retry |
FAILED |
최대 횟수/영구 오류 | 운영자 검토·수동 replay |
Framework가 자체 publication schema를 제공하면 그 상태를 사용하고 별도 table을 중복 생성하지 않습니다.
- 변경 endpoint는 필요한 곳에
Idempotency-Key를 받습니다. - tenant + key + operation에 unique constraint를 둡니다.
- 같은 key·같은 payload는 이전 결과를 반환합니다.
- 같은 key·다른 payload는 충돌 오류를 반환합니다.
- key 원문 대신 hash와 만료/보존 정책을 검토합니다.
-
event_id + consumer_name처리 record 또는 결과 unique constraint를 사용합니다. - “먼저 외부 side effect, 나중 DB 기록” 순서를 피합니다.
- 정확히 한 번 전달을 가정하지 않고 at-least-once 전달 + idempotent 처리를 설계합니다.
- optimistic lock과 expected version으로 오래된 event가 최신 상태를 덮지 않게 합니다.
| 기능 | 적용 위치 | 주의 |
|---|---|---|
| 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
-
request_id: 사용자·지원 담당자가 API 응답과 log를 연결하는 식별자 -
trace_id: API, async event, Provider span을 하나의 분산 trace로 연결 - business ID는 log field로 필요할 때 안전하게 사용하되 metric label로 남발하지 않습니다.
| 영역 | 지표 |
|---|---|
| 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을 망가뜨립니다.
- 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하지 않게 합니다.
- Run과 publication commit 직후 process 종료 → restart 후 실행
- Provider 호출 성공 직후 handler 종료 → duplicate delivery에도 결과 하나
- 같은 Idempotency-Key를 동시에 전송 → Run 하나
- 같은 confirm을 동시에 전송 → Task 하나
- timeout·429·5xx → retry budget·circuit 동작
- malformed JSON·Schema 위반 → 자동 발송/Task 없음
- 핵심값 변경 후 재승인 없이 link 발급 → 거부
- event 최대 retry 소진 → 추적 가능한 FAILED와 수동 replay
- 타 사업장 Run/event/audit 조회 → 차단
- log·metric·trace fixture에 token·PII 없음
- 사용자 응답의
request_id와runId를 확인합니다. - Run 상태·
error_code·retry_count·version을 봅니다. -
trace_id로 API→event→Provider 구간을 찾습니다. - publication 상태와 다음 retry 시각을 확인합니다.
- retry 가능 오류인지, 수동 입력/검토가 필요한지 판단합니다.
- replay 전 idempotency와 현재 aggregate version을 확인합니다.
- 해결 뒤 AuditEvent와 metric 회복을 확인합니다.
개인정보나 Secret을 log에 추가 출력해 진단하지 않습니다.