운영 가능한 API 설계
메뉴

AI Backend

운영 가능한 API 설계

LLM 백엔드에서 운영 가능한 API를 만들기 위해 성공 응답보다 실패 응답, trace ID, idempotency, rate limit, health check를 먼저 설계하는 방법을 정리합니다.

운영 가능한 API 설계 hero image
Markdown약 1710 tokens

엔드포인트보다 실패 응답과 추적 ID가 먼저다

이 글에서는 LLM 서비스의 API를 운영 가능한 형태로 설계하는 방법을 정리합니다.

API 설계라고 하면 보통 URL과 HTTP method부터 떠올립니다. POST /chat, GET /documents/{id} 같은 엔드포인트를 먼저 정합니다. 하지만 운영 관점에서는 엔드포인트보다 먼저 정해야 하는 것이 있습니다.

실패 응답입니다.
추적 ID입니다.
재시도 가능 여부입니다.
rate limit 정책입니다.
health check와 readiness 기준입니다.

분석 기준일: 2026-05-12
실습 기준 환경: FastAPI, Pydantic, PostgreSQL, OpenTelemetry
주요 참고자료: OpenAPI Specification, FastAPI Error Handling, W3C Trace Context, Google SRE 공식 문서의 사실 확인과 이 글의 운영 해석은 본문에서 구분합니다.

핵심 요약

  • API 계약은 성공 응답만이 아니라 실패 응답까지 포함한다.
  • 모든 요청에는 request_id 또는 trace_id가 있어야 한다.
  • LLM API는 retry, timeout, idempotency 여부를 문서화해야 한다.
  • health check는 프로세스 생존 확인과 의존성 준비 상태를 분리해야 한다.
  • API 문서는 프론트엔드용 스펙이 아니라 운영자와 장애 대응자를 위한 문서이기도 하다.

1. 운영 가능한 API란 무엇인가

운영 가능한 API는 정상 상황에서만 잘 동작하는 API가 아닙니다. 장애가 났을 때 원인을 추적할 수 있고, 호출자가 재시도 여부를 판단할 수 있으며, 운영자가 지표로 상태를 확인할 수 있는 API입니다.

항목단순 API운영 가능한 API
성공 응답데이터 반환schema와 version 포함
실패 응답문자열 또는 임의 JSON표준 error envelope
추적성로그 검색request_id, trace_id
재시도호출자 판단retryable 명시
제한 정책없음rate limit header
상태 확인/health 하나liveness/readiness 분리

LLM 서비스에서는 이 차이가 더 중요합니다. 모델 호출 실패, schema validation 실패, rate limit, 긴 지연이 자주 발생하기 때문입니다.

2. 성공 응답보다 실패 응답이 먼저다

실무에서 장애 대응을 어렵게 만드는 API는 대부분 실패 응답이 제각각입니다.

// 예시 JSON 구조입니다.{  "message": "failed"}

이 정도 응답으로는 원인을 알 수 없습니다. 어떤 요청인지, 재시도 가능한지, 사용자가 잘못 보낸 것인지, 서버 내부 문제인지 구분할 수 없습니다.

운영 가능한 실패 응답은 최소한 아래 정보를 포함해야 합니다.

// 예시 JSON 구조입니다.{  "error": {    "code": "LLM_SCHEMA_VALIDATION_FAILED",    "message": "Model response did not match the expected schema.",    "retryable": true,    "details": {      "schema_version": "answer.v1"    }  },  "request_id": "req_01HX...",  "trace_id": "0af7651916cd43dd8448eb211c80319c"}

3. Error Response Envelope 설계

추천하는 error envelope 구조는 다음과 같습니다.

필드설명
error.code시스템 내부에서 검색 가능한 고유 코드
error.message사용자 또는 개발자가 이해할 수 있는 설명
error.retryable클라이언트가 재시도해도 되는지 여부
error.details디버깅용 추가 정보. 민감정보 제외
request_id단일 요청 식별자
trace_id분산 추적 식별자

주의할 점은 details에 프롬프트 원문, 개인정보, API key, 내부 문서 전문을 넣지 않는 것입니다.

보안 주의
LLM 요청/응답은 민감한 문서를 포함할 수 있습니다. 실패 응답과 로그에 prompt, document chunk, user input 원문을 그대로 넣지 않도록 마스킹 정책을 먼저 정해야 합니다.

4. Trace ID와 Request ID 설계

request_id는 서비스 내부의 단일 요청 식별자입니다. trace_id는 여러 서비스와 외부 호출을 관통하는 분산 추적 식별자입니다.

# 예시입니다.Client→ API Server span→ Retrieval span→ LLM Provider span→ Validation span→ DB Save span

LLM 서비스에서는 한 요청이 여러 하위 작업으로 나뉩니다. 따라서 trace를 연결해야 “어디서 느려졌는지” 볼 수 있습니다.

응답 header 예시는 다음과 같습니다.

# 예시입니다.X-Request-Id: req_01HXABCtraceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

5. Idempotency Key가 필요한 API

모든 API에 idempotency key가 필요한 것은 아닙니다. 하지만 상태를 바꾸는 API에는 필요합니다.

APIIdempotency 필요 여부이유
GET /answers/{id}낮음읽기 요청
POST /questions중간같은 질문 중복 생성 방지 가능
POST /documents/{id}/index높음중복 색인 방지
POST /payments매우 높음중복 결제 방지

문서 색인 API는 대표적인 예입니다. 같은 문서가 여러 번 색인되면 vector row가 중복되고 검색 품질이 떨어질 수 있습니다.

# 예시입니다.POST /documents/doc_123/indexIdempotency-Key: doc_123:index:v4

서버는 이 key를 작업 테이블에 저장하고, 같은 key가 다시 들어오면 기존 작업 상태를 반환합니다.

6. Rate Limit 응답 설계

LLM API는 비용과 provider 제한이 함께 걸립니다. 따라서 rate limit은 단순 보안 장치가 아니라 비용 통제 장치입니다.

# 예시입니다.HTTP/1.1 429 Too Many RequestsX-RateLimit-Limit: 100X-RateLimit-Remaining: 0X-RateLimit-Reset: <unix_timestamp>Retry-After: 30

429 응답에도 request_id와 error code를 포함해야 합니다.

// 예시 JSON 구조입니다.{  "error": {    "code": "RATE_LIMIT_EXCEEDED",    "message": "Too many requests. Retry after 30 seconds.",    "retryable": true  },  "request_id": "req_01HX..."}

7. Health Check와 Readiness

/health 하나만 두면 충분하지 않습니다.

엔드포인트목적확인 대상
/livez프로세스 생존애플리케이션 프로세스
/readyz트래픽 수신 가능DB, Redis, Queue, config
/metrics지표 수집Prometheus 등

LLM provider 상태까지 readiness에 넣을지는 신중해야 합니다. 외부 모델 API가 일시적으로 느리다고 전체 서비스를 트래픽에서 제외하면 오히려 장애가 커질 수 있습니다. 보통은 provider 상태를 별도 metric으로 보고 fallback 정책을 둡니다.

8. OpenAPI 문서화 기준

API 문서에는 성공 응답뿐 아니라 실패 응답도 포함해야 합니다.

# 예시입니다.responses:  '200':    description: Answer created  '400':    description: Invalid request  '429':    description: Rate limit exceeded  '500':    description: Internal server error

문서화할 항목은 다음과 같습니다.

# 예시입니다.[ ] request schema[ ] response schema[ ] error schema[ ] authentication[ ] rate limit[ ] retry policy[ ] idempotency key[ ] timeout expectation[ ] trace/request ID header

9. 실무 체크리스트

# 예시입니다.[ ] 모든 API 응답에 request_id가 있는가?[ ] error.code가 검색 가능한 고유 문자열인가?[ ] retryable 필드가 있는가?[ ] 429 응답에 Retry-After가 있는가?[ ] 상태 변경 API에 idempotency key를 지원하는가?[ ] liveness와 readiness를 분리했는가?[ ] OpenAPI 문서에 실패 응답이 포함되어 있는가?[ ] 로그에 민감정보가 남지 않는가?

10. Q&A

Q1. request_id와 trace_id는 둘 다 필요한가요?

작은 서비스에서는 request_id만으로 시작해도 됩니다. 하지만 서비스가 여러 컴포넌트로 나뉘면 trace_id가 필요합니다. request_id는 단일 요청 추적에, trace_id는 여러 span 연결에 유리합니다.

Q2. 모든 API에 idempotency key를 넣어야 하나요?

아닙니다. 상태를 변경하고 재시도 가능성이 있는 API부터 적용하면 됩니다. 문서 색인, 결제, 외부 이벤트 처리 같은 API가 우선순위입니다.

Q3. LLM provider 장애는 readiness 실패로 봐야 하나요?

대부분의 경우 별도 provider health metric으로 보는 편이 낫습니다. provider 장애 때문에 전체 API 서버를 내리는 것보다 fallback, provider routing, degraded response가 더 안전할 수 있습니다.

11. 참고자료와 불확실성

참고자료

불확실성

  • 실제 error code 체계는 조직의 API governance 기준에 맞춰야 합니다.
  • trace ID header는 사용하는 observability stack에 따라 세부 구현이 달라질 수 있습니다.

실무 적용 예시: 실패 응답을 먼저 정한 API

LLM 답변 생성 API를 만든다면 성공 응답보다 실패 응답을 먼저 정하는 편이 좋습니다. 사용자가 문서를 너무 크게 올렸는지, 권한이 없는 문서인지, provider timeout인지, schema validation 실패인지에 따라 재시도 가능 여부가 달라지기 때문입니다.

{  "error": {    "code": "CONTEXT_TOO_LARGE",    "message": "입력 문서가 현재 처리 한도를 넘었습니다.",    "retryable": false,    "request_id": "req_123",    "trace_id": "trace_456"  }}

비교 기준은 retryable입니다. rate limit은 일정 시간 뒤 재시도할 수 있지만, 권한 없음은 재시도해도 해결되지 않습니다. context 초과는 문서를 줄이거나 작업을 나눠야 합니다. 이 구분이 없으면 클라이언트는 모든 실패를 같은 alert으로 표시하고, 사용자는 무엇을 해야 하는지 알 수 없습니다.

실패 코드클라이언트 행동
RATE_LIMITEDbackoff 후 재시도
UNAUTHORIZED_DOCUMENT접근 권한 안내
CONTEXT_TOO_LARGE입력 축소 요청
PROVIDER_TIMEOUT재시도 또는 fallback

Failure responses and tracking IDs come before endpoints

This article summarizes how to design the LLM service's API in an operable form.

When you think of API design, URLs and HTTP methods usually come to mind. First decide on endpoints such asPOST /chatandGET /documents/{id}. However, from an operational perspective, there are things you need to decide before endpoints.

This is a failure response. Tracking ID. Whether retry is possible. Rate limit policy. It is based on health check and readiness.

Analysis date: 2026-05-12 Practice standard environment: FastAPI, Pydantic, PostgreSQL, OpenTelemetry Key reference materials: OpenAPI Specification, FastAPI Error Handling, W3C Trace Context, Google SRE Fact checking of official documents and operational interpretation of this article are separated in the main text.

Key takeaways

  • API contracts include not only success responses but also failure responses.
  • All requests must haverequest_idortrace_id.
  • The LLM API must document retry, timeout, and idempotency.
  • Health checks should separate process survival verification and dependency readiness.
  • The API document is not just a front-end specification, but also a document for operators and failure responders.

1. What is an operational API?

An operational API is not an API that only works well under normal circumstances. This is an API that allows the cause of a failure to be tracked, the caller to determine whether to retry, and the operator to check the status with indicators.

itemSimple APIOperational API
success responsereturn dataIncludes schema and version
failure responseString or arbitrary JSONstandard error envelope
traceabilityLog Searchrequest_id, trace_id
retryCaller judgmentspecify retryable
Restriction Policydoesn't existrate limit header
Check status/healthoneliveness/readiness separation

In LLM services, this difference is more important. This is because model call failures, schema validation failures, rate limits, and long delays often occur.

2. Failure responses come before success responses.

Most APIs, which make it difficult to respond to failures in practice, have different failure responses.

// This is an example JSON structure.{  "message": "failed"}

The cause cannot be determined from this response. I can't tell what the request is, if it's retryable, if it was sent incorrectly by the user, or if it's an internal server issue.

An operational failure response must include, at a minimum, the information below:

// This is an example JSON structure.{  "error": {    "code": "LLM_SCHEMA_VALIDATION_FAILED",    "message": "Model response did not match the expected schema.",    "retryable": true,    "details": {      "schema_version": "answer.v1"    }  },  "request_id": "req_01HX...",  "trace_id": "0af7651916cd43dd8448eb211c80319c"}

3. Error Response Envelope Design

The recommended error envelope structure is as follows.

fieldexplanation
error.codeUnique code retrievable from within the system
error.messageDescription that users or developers can understand
error.retryableWhether the client can retry
error.detailsAdditional information for debugging. Excluding sensitive information
request_idsingle request identifier
trace_iddistributed tracking identifier

One thing to be careful of is not to include the original text of the prompt, personal information, API key, or full text of internal documents indetails.

Security Caution LLM requests/responses may contain sensitive documents. You must first set a masking policy to avoid including the original text of prompts, document chunks, and user inputs in failure responses and logs.

4. trace ID and Request ID design

request_idis a single request identifier inside the service.trace_idis a distributed tracking identifier that cuts across multiple services and external calls.

# This is an example.Client→ API Server span→ Retrieval span→ LLM Provider span→ Validation span→ DB Save span

In the LLM service, a request is divided into several subtasks. So you need to connect a trace to see “where it’s slow.”

An example response header is as follows:

# This is an example.X-Request-Id: req_01HXABCtraceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

5. API that requires idempotency key

Not all APIs require an idempotency key. However, it is required for APIs that change state.

APIIs Dempotency Required?reason
GET /answers/{id}lownessread request
POST /questionsmiddlePrevents duplicate creation of the same question
POST /documents/{id}/indexheightAvoid duplicate indexes
POST /paymentsvery highPrevent duplicate payments

The Document Index API is a prime example. If the same document is indexed multiple times, vector rows may be duplicated and search quality may decrease.

# This is an example.POST /documents/doc_123/indexIdempotency-Key: doc_123:index:v4

The server stores this key in the task table, and returns the existing task status when the same key comes again.

6. Rate limit response design

The LLM API has both cost and provider limitations. Therefore, the rate limit is not just a security device but also a cost control device.

# This is an example.HTTP/1.1 429 Too Many RequestsX-RateLimit-Limit: 100X-RateLimit-Remaining: 0X-RateLimit-Reset: <unix_timestamp>Retry-After: 30

The 429 response must also include request_id and error code.

// This is an example JSON structure.{  "error": {    "code": "RATE_LIMIT_EXCEEDED",    "message": "Too many requests. Retry after 30 seconds.",    "retryable": true  },  "request_id": "req_01HX..."}

7. Health Check and Readiness

Having just one/healthis not enough.

endpointpurposeCheck for
/livezprocess survivalapplication process
/readyzTraffic can be receivedDB, Redis, Queue, config
/metricsIndicator collectionPrometheus, etc.

You should be careful about whether to add the LLM provider status to readiness. If the entire service is excluded from traffic just because the external model API is temporarily slow, the failure may actually increase. Usually, the provider status is viewed as a separate metric and a fallback policy is set.

8. OpenAPI documentation standards

Your API documentation should include failure responses as well as success responses.

# This is an example.responses:'200':description: Answer created'400':description: Invalid request'429':description: Rate limit exceeded'500':description: Internal server error

Items to document include:

# This is an example.[ ] request schema[ ] response schema[ ] error schema[ ] authentication[ ] rate limit[ ] retry policy[ ] idempotency key[ ] timeout expectation[ ] trace/request ID header

9. Practical checklist

# This is an example.[ ] Do all API responses have a request_id?[ ] Is error.code a unique, searchable string?[ ] Is there a retryable field?[ ] 429 Is there a Retry-After in the response?[ ] Is idempotency key supported in the state change API?[ ] Have you separated liveness and readiness?[ ] Does the OpenAPI document include a failure response?[ ] Is there no sensitive information left in the log?

10. Q&A

Q1. Are both request_id and trace_id needed?

For small services, you can start with just request_id. However, if the service is divided into multiple components, trace_id is required. request_id is advantageous for tracing a single request, trace_id is advantageous for concatenating multiple spans.

Q2. Do I need to include an idempotency key in all APIs?

no. You can change the state and apply it first to the API with the possibility of retrying. APIs such as document indexing, payments, and external event handling are priorities.

Q3. Should LLM provider failure be considered a failure in readiness?

In most cases, it is better to view it as a separate provider health metric. Fallback, provider routing, and degraded responses may be safer than taking down the entire API server due to provider failure.

11. References and uncertainty

References

uncertainty

  • The actual error code scheme should align with your organization's API governance standards.
  • The detailed implementation of the trace ID header may vary depending on the observability stack used.

Practical application example: API that sets the failure response first

If you are creating an LLM answer generation API, it is better to decide on failure responses before success responses. This is because whether a retry is possible depends on whether the user uploaded a document that is too large, a document without permission, a provider timeout, or schema validation failure.

{  "error": {    "code": "CONTEXT_TOO_LARGE",    "message": "The input document has exceeded the current processing limit.",    "retryable": false,    "request_id": "req_123",    "trace_id": "trace_456"  }}

The comparison standard isretryable. Rate limits can be retried after a certain period of time, but no permissions cannot be resolved by retrying. If the context is exceeded, the document must be shortened or the work divided. Without this distinction, the client would display all failures with the same alert, and the user wouldn't know what to do.

failure codeclient behavior
RATE_LIMITEDRetry after backoff
UNAUTHORIZED_DOCUMENTAccess permission information
CONTEXT_TOO_LARGERequest to reduce input
PROVIDER_TIMEOUTRetry or fallback

댓글

GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.

TOP