오류 및 상태 코드

Max PrilutskiyCEO 겸 공동창업자6분 소요

개발 환경에서는 호출이 잘 작동합니다. 이제 프로덕션에서 실행될 예외 처리 블록을 작성할 차례입니다. 타사 API의 HTTP 오류는 그 자체만으로는 원인을 알기 어렵습니다. 빨간 상태 코드만 보일 뿐, 새벽 3시에 가장 절실한 질문에는 답이 없습니다. 내 요청이 문제일까, 키나 요금제가 문제일까, 아니면 상대 서버가 문제일까? 무엇을 재시도하고, 무엇을 사용자에게 알려야 할까요?

Lingo.dev는 일관된 오류 구조로 이 질문에 답합니다. 이 문서에서 다루는 엔드포인트의 모든 오류는 동기와 비동기 모두 동일한 JSON 객체로 반환되며, 상태 코드는 정해진 하나의 표를 따릅니다. 다만 /v2 경로의 플랫폼 API는 표준 문제 상세 정보 형식을 사용합니다. 플랫폼 API 오류를 참조하세요. 상태 코드는 단순한 표시가 아니라 행동 지침입니다. 요청을 수정할지, 키를 교체할지, 계정을 충전할지, 잠시 기다릴지, 재시도할지 알려 줍니다. 코드를 읽으면 다음 조치가 보입니다. 상태 코드에 따라 분기하는 오류 처리기 하나로 전체 API를 처리할 수 있습니다.

이 페이지의 내용

오류 형식#

2xx가 아닌 모든 응답은 본문 형식이 같습니다. 어떤 문제가 발생했는지 설명하는 message 필드 하나를 담은 JSON 객체입니다.

json
{
  "message": "Invalid API key"
}

규약은 이것이 전부입니다. 감싸고 있는 구조를 따로 풀어낼 필요도, 엔드포인트마다 오류 형식을 예외 처리할 필요도 없습니다. /process/localize의 400 응답과 작업 조회의 404 응답은 형식이 같고, 상태 코드와 message 텍스트만 다릅니다.

메시지 문구가 아닌 상태 코드로 분기하세요

일관되게 신뢰할 수 있는 신호는 HTTP 상태 코드입니다. 이를 기준으로 오류 처리를 분기하세요. message 문자열은 로그를 읽는 사람을 위한 설명이지, 기계가 판독할 오류 코드가 아닙니다. 정확한 문구가 일치하는지에 따라 처리하지 마세요.

상태 코드#

모든 응답은 일곱 가지 상태 코드로 구분됩니다. 여기서는 누가 해결해야 하는지에 따라 묶었습니다. 이 구분이 곧 재시도 정책이 되기 때문입니다.

요청에 처리할 수 없는 내용이 포함된 경우(무작정 재시도하지 말고 요청을 수정하세요):

상태의미
400 Bad Request요청 유효성 검사에 실패했습니다. 필드가 누락되었거나, 로캘이 유효하지 않거나, callbackUrl에 HTTPS 대신 HTTP가 사용되었거나, 페이로드 형식이 잘못된 경우입니다.
401 UnauthorizedX-API-Key 헤더가 없거나 유효하지 않습니다. 인증을 참조하세요.
403 Forbidden키는 유효하지만 요청한 리소스에 대한 접근 권한이 없습니다.
404 Not Found엔진, 작업, 작업 그룹 등의 리소스가 존재하지 않습니다.

조직이 계정 한도에 도달한 경우(결제에서 해결하세요):

상태의미
402 Payment Required조직이 크레딧 한도에 도달했습니다.
429 Too Many Requests조직이 일일 토큰 할당량에 도달했습니다. 한도를 높이려면 요금제를 업그레이드하세요.

서비스 측에서 문제가 발생한 경우(일시적 오류이므로 재시도하세요):

상태의미
500 Internal Server Error예기치 않은 오류입니다. 데이터베이스 오류가 발생했거나 엔진에 설정된 모든 모델에서 번역 호출이 실패한 경우입니다.

401와 403는 비슷해 보이지만 서로 다른 문제입니다. 401는 호출자를 전혀 식별하지 못했다는 뜻이고, 403는 키를 식별했지만 접근이 허용되지 않는다는 뜻입니다. 401는 키 자체를 교체하거나 확인해야 해결되며, 403는 키의 접근 권한을 조정해야 해결됩니다.

재시도해야 할 오류#

오류 표를 꼼꼼히 살피는 연동 개발자가 가장 먼저 묻는 질문은 정작 표에 답이 없는 경우가 많습니다. 이 중 어떤 오류를 재시도해야 할까요? 위의 구분이 바로 그 답입니다.

  • 4xx — 무작정 재시도하지 마세요. 400, 401, 403, 404는 보낸 요청에 문제가 있음을 나타냅니다. 같은 요청을 다시 보내면 같은 오류가 발생합니다. 입력값, 키 또는 리소스 식별자를 수정한 다음 다시 보내세요.
  • 402와 429 — 잠시 멈추고 한도 문제를 해결하세요. 개별 요청에서 일시적으로 발생하는 오류가 아닙니다. 해당 한도가 바뀌기 전까지는 다음 요청도 같은 문제에 부딪힙니다. 짧은 간격으로 재시도를 반복하지 말고, 한도에 도달했음을 알린 뒤 충전이나 요금제 업그레이드로 해결하세요.
  • 500 — 대기 시간을 두고 재시도하세요. 실제로 일시적인 오류에 해당하는 유일한 유형입니다. 500는 해당 호출에서 설정된 모든 모델의 응답 시간이 초과되었다는 뜻일 수 있습니다. 재시도하면 정상적으로 작동하는 모델에서 처리될 수 있습니다. 지수 백오프를 적용하고 최대 재시도 횟수를 정하세요.

비동기 API는 결과를 다른 방식으로 전달합니다

이 재시도 정책은 직접 수행하는 동기 호출에 적용됩니다. 비동기 로컬라이제이션 API는 작업 결과를 상태 코드로 전달하지 않습니다. POST는 요청이 접수되면 202를 반환하고, 각 대상 로캘은 실행 상태가 유지되는 백그라운드 워크플로를 통해 독립된 작업으로 실행됩니다. 원래 호출의 상태 코드로 결과를 확인하는 대신, 작업을 폴링하거나 웹훅으로 결과를 받습니다. 비동기 작업 오류를 확인하는 곳을 참조하세요.

402와 429: 서로 다른 두 한도#

이 두 계정 수준 코드는 모두 "다 썼다"는 뜻처럼 보입니다. 하지만 둘을 혼동하면 잘못된 해결책을 찾게 됩니다. 서로 다른 한도인 만큼 해결 방법도 다릅니다.

  • 402 Payment Required — 조직이 크레딧 한도에 도달했습니다. 결제와 관련된 한도입니다. 조직의 결제 상태가 바뀌기 전까지 이후 호출도 계속 실패합니다.
  • 429 Too Many Requests — 조직이 일일 토큰 할당량에 도달했습니다. 시간이 지나면 초기화되는 사용량 한도이며, 요금제를 업그레이드하면 높일 수 있습니다.

오류 처리기에서 둘을 구분해야 하는 이유가 있습니다. 402는 사람이 결제 관련 조치를 취해야 해결되지만, 429는 할당량이 초기화될 때까지 기다리거나 업그레이드로 늘릴 수 있습니다. 둘 다 막연한 "결제 문제" 메시지로 처리하면 운영자가 실제로 어떤 조치를 취해야 하는지 알 수 없습니다.

402 응답 본문은 다른 모든 오류와 형식이 같습니다. 크레딧 한도 문제임을 알려 주는 것은 상태 코드입니다.

json
{
  "message": "Organization has reached its credit limit"
}

비동기 작업 오류를 확인하는 곳#

여기서 분명히 구분해야 할 경계가 있습니다. 이 경계를 넘어서면 상태 코드 처리기만으로는 대응할 수 없습니다.

이 페이지의 상태 코드는 전송 수준의 코드입니다. API가 HTTP 요청을 접수하고 처리할 수 있었는지를 나타냅니다. 비동기 API의 202는 요청이 접수되었다는 뜻이지, 번역이 성공했다는 뜻이 아닙니다. 비동기 작업은 정상적으로 접수된 뒤에도 실행 도중 모델 응답 시간이 초과되면 실패할 수 있습니다. 이 실패는 원래 호출의 HTTP 상태 코드가 아니라 작업 자체에 기록됩니다.

따라서 비동기 작업의 실패는 이 표가 아닌 다음 세 곳에서 확인합니다.

  • 작업별 상태. 실패한 로캘의 작업에는 status: "failed"와 errorMessage가 기록됩니다. 작업 상태를 참조하세요.
  • 그룹 상태. 일부 로캘은 성공하고 나머지는 실패하면 그룹 상태는 partial로 표시됩니다. 성공한 로캘의 결과는 그대로 전달됩니다. 작업 그룹 추적을 참조하세요.
  • 웹훅 전달. 실패는 error 필드가 포함된 translation.failed 이벤트로 전달됩니다. 웹훅 전달을 참조하세요.

자주 놓치는 차이가 하나 더 있습니다. 필수적이지 않은 파이프라인 단계가 실패해도 작업이 실패하는 것은 아닙니다. 작업은 오류 대신 completed_with_warnings 상태와 단계별 경고를 남기고 완료됩니다. 이는 오류 코드가 아니라 파이프라인 실행 관측의 영역입니다. 파이프라인 실행 관측을 참조하세요.

플랫폼 API 오류#

https://api.lingo.dev/v2 경로의 플랫폼 API는 오류 응답 방식이 다릅니다. RFC 9457 문제 상세 정보를 application/problem+json 형식으로 반환합니다.

json
{
  "type": "https://lingo.dev/docs/api/errors/glossary-in-use",
  "status": 409,
  "title": "Glossary in use",
  "detail": "Glossary gls_1 cannot be deleted while 2 engines apply it. Detach it from those engines first.",
  "id": "gls_1",
  "engineCount": 2
}
  • type 는 문제를 식별합니다. 해당 문제의 필드와 이를 반환하는 엔드포인트를 안내하는 전용 페이지의 링크입니다. type를 기준으로 처리를 분기하세요.
  • status 는 HTTP 상태 코드를 그대로 담습니다.
  • title 는 같은 유형의 문제에 항상 동일하게 제공되는 짧은 요약입니다.
  • detail 는 이번에 어떤 일이 발생했고 다음에 무엇을 해야 하는지 설명합니다. 사람이 읽도록 작성된 내용이므로 파싱하지 말고 그대로 표시하세요.
  • 기타 멤버에는 위의 id와 engineCount처럼 이번 발생 건에 관한 구체적인 정보가 담깁니다. detail는 이 정보만으로 구성되므로, 클라이언트는 type와 이 필드들을 바탕으로 자체 문구를 원하는 언어로 표시할 수 있습니다.

유효성 검사에 실패한 요청은 400를 반환하고, errors 아래에 유효하지 않은 각 값을 { "path", "message" } 쌍으로 나열합니다. 모든 엔드포인트는 대기 시간을 두고 안전하게 재시도할 수 있는 service-unavailable(503)과 internal-server-error(500)도 반환할 수 있습니다.

각 상태 코드는 다음 기준에 따라 사용됩니다.

상태의미
401호출자가 식별되지 않았습니다.
402업그레이드나 충전으로 해소할 수 있는 요금제 또는 이용 권한 한도입니다.
403호출자는 식별되었지만 이 작업은 허용되지 않습니다.
404리소스가 존재하지 않거나 호출자가 볼 수 없습니다.
409아직 사용 중인 리소스에 대한 요청처럼, 요청이 현재 상태와 충돌합니다.
422요청 형식은 올바르지만 규칙에 따라 거부됩니다.
429시간이 지나면 초기화되는 한도입니다.
502상위 서비스 제공업체에서 오류가 발생했습니다.
503서비스 측의 일시적인 오류입니다. 대기 시간을 두고 재시도하세요.

다음 단계#

깔끔한 오류 처리기를 만들려면 연동 과정에서 가장 먼저 마주칠 두 가지부터 살펴보세요. 바로 인증과 비동기 작업의 결과를 확인하는 지점입니다.