파이썬 API 200 응답이 성공이 아닐 때: 본문 검증 순서

파이썬 API가 HTTP 200을 반환해도 실제 업무 처리가 성공했다는 뜻은 아닙니다. HTTP 상태 확인, JSON 구조 확인, API 명세의 업무 상태와 필수 데이터 확인을 차례로 해야 성공·실패·빈 결과를 구분할 수 있습니다. response.json()이 동작했다는 사실도 JSON 문법을 읽었다는 의미일 뿐, 업무 결과가 정상이라는 뜻은 아닙니다.

HTTP 200과 업무 성공을 먼저 분리하기

HTTP 상태 코드는 통신 계층의 결과입니다. RFC 9110의 HTTP Semantics에서 200은 요청이 성공적으로 처리되었다는 HTTP 수준의 의미를 가지지만, 응답 본문이 어떤 업무 결과를 담는지는 메서드와 콘텐츠의 계약에 달려 있습니다. 서버가 요청을 받아 오류 설명을 JSON으로 돌려주면서도 HTTP 200을 사용할 수 있는 이유가 여기에 있습니다.

따라서 아래 세 가지를 서로 다른 질문으로 다루는 편이 안전합니다.

  1. 서버와 HTTP 통신이 정상적으로 끝났는가?
  2. 응답 본문을 기대한 형식으로 읽을 수 있는가?
  3. API가 표현한 업무 처리가 성공했고 필요한 결과가 있는가?

첫 번째 질문은 raise_for_status() 또는 예상 상태 코드 검사로 확인합니다. Requests 공식 Quickstart도 HTTP 오류 확인과 JSON 디코딩을 별도 단계로 설명합니다. 둘을 한 줄의 if response.status_code == 200으로 합치면 본문 오류를 놓치기 쉽습니다.

검증 순서: status code에서 필수 data까지

운영 코드에서는 다음 순서를 기본 뼈대로 삼을 수 있습니다. 단, 필드명과 성공값은 일반 규칙이 아니라 해당 API 명세에 맞춰 바꿔야 합니다.

1. HTTP 상태를 확인합니다

import requests


def request_api(url: str, payload: dict) -> dict:
    response = requests.post(url, json=payload, timeout=10)
    response.raise_for_status()
    return response.json()

raise_for_status()는 HTTP 오류를 먼저 예외로 드러냅니다. 다만 이 코드만으로는 HTTP 200 본문 안에 들어 있는 업무 오류를 걸러내지 못합니다. API가 성공 상태 코드를 별도로 정의한다면 예상 상태 코드도 함께 확인하고, 이 단계의 실패를 업무 성공으로 기록하지 않아야 합니다.

2. JSON과 최상위 구조를 확인합니다

응답이 항상 JSON이라는 보장이 없다면 디코딩 실패도 처리해야 합니다. 특히 본문이 없거나 JSON이 아닌 경우에는 response.json() 자체가 실패할 수 있습니다.

try:
    body = response.json()
except ValueError as exc:
    raise RuntimeError("API 응답이 유효한 JSON이 아닙니다.") from exc

if not isinstance(body, dict):
    raise RuntimeError("API 응답의 최상위 구조가 객체가 아닙니다.")

여기서 확인하는 것은 문법과 자료형입니다. JSON 객체라는 이유만으로 success: true나 data가 존재한다고 가정하면 안 됩니다.

3. API 명세의 업무 상태를 확인합니다

API마다 success, status, code의 이름과 성공값이 다릅니다. 임의로 모든 API에서 code == 0이 성공이라고 처리하지 말고, 해당 서비스의 응답 명세를 기준으로 조건을 작성해야 합니다.

예를 들어 명세가 success와 data를 필수로 정의한다면 다음처럼 명시적으로 확인할 수 있습니다.

def validate_business_result(body: dict) -> list:
    if body.get("success") is not True:
        code = body.get("code")
        message = body.get("message", "메시지 없음")
        raise RuntimeError(f"API 업무 오류: code={code}, message={message}")

    if "data" not in body:
        raise RuntimeError("성공 응답에 필수 data가 없습니다.")

    return body["data"]

JSON-RPC를 사용하는 API라면 일반적인 success 필드 대신 JSON-RPC 2.0 명세의 result와 error 계약을 따라야 합니다. 성공 응답과 오류 응답의 구분 방식이 다르므로, API 형식을 섞어서 검사하지 않는 것이 중요합니다.

4. 필수 데이터와 빈 결과를 업무 규칙으로 구분합니다

data가 있다고 해서 내용까지 유효한 것은 아닙니다. 목록 조회에서 빈 배열은 “정상적인 0건”일 수도 있고, upstream 장애나 필터 조건 오류로 결과를 받지 못한 것일 수도 있습니다. 이 판단은 OpenAPI Specification처럼 응답 구조를 정의한 명세와 업무 규칙을 함께 봐야 합니다.

예를 들면 다음처럼 결과의 의미를 호출 목적에 맞게 나눕니다.

def classify_items(items: list, *, empty_is_valid: bool) -> str:
    if items:
        return "success_with_data"
    if empty_is_valid:
        return "success_empty"
    return "review_required"

조회 화면에서 “조건에 맞는 항목 없음”이 정상이라면 빈 배열을 성공으로 기록할 수 있습니다. 반대로 동기화 대상 목록이 반드시 있어야 하는 작업이라면 빈 배열을 그대로 완료 처리하지 말고 원인 확인 대상으로 남겨야 합니다. 배열의 길이만 보고 공통적으로 성공 또는 실패를 정하면 안 됩니다.

기록 기준을 성공·실패·보류로 나누기

자동화에서는 예외를 던지는 것만큼 결과를 어떻게 기록할지도 중요합니다. 운영 로그나 저장소에는 최소한 HTTP 상태, 본문 업무 코드, 메시지, 결과 판정, 원본 응답 식별 정보를 남기는 구조가 유용합니다. 민감한 토큰이나 개인정보가 응답에 들어 있다면 원문 전체를 그대로 기록하지 말고 필요한 필드만 남겨야 합니다.

  • 성공: HTTP와 JSON 구조가 기대에 맞고, API 명세상 성공이며, 필수 데이터가 업무 규칙을 만족한 경우
  • 실패: HTTP 오류, JSON 디코딩 실패, 필수 필드 누락, 명세상 업무 오류인 경우
  • 보류 또는 확인 필요: 빈 결과의 의미가 명세만으로 결정되지 않거나, 부분 결과 여부를 판단할 정보가 부족한 경우

본문 code가 실패를 나타낼 때 무조건 재시도하는 것도 위험합니다. 재시도 가능 여부는 오류가 일시적인지, 요청이 중복 실행되어도 안전한지, API가 멱등성을 보장하는지에 달려 있습니다. 이 글의 범위에서는 재시도 정책을 정하지 않고, 먼저 실패 상태를 정확히 기록한 뒤 해당 API 계약을 확인하는 데 집중하는 편이 맞습니다.

적용 전 확인할 체크리스트

실제 API에 연결하기 전 다음 질문에 답이 있어야 합니다.

  • HTTP 오류를 어떤 상태 코드와 예외로 분류하는가?
  • 정상 JSON의 최상위 자료형은 객체인가, 배열인가?
  • 업무 성공을 나타내는 필드와 값은 무엇인가?
  • message와 오류 상세 정보는 어디에 있는가?
  • 성공 시 반드시 있어야 하는 data의 타입과 필드는 무엇인가?
  • 빈 배열은 정상적인 0건인가, 확인이 필요한 결과인가?
  • 부분 결과와 중복 실행을 어떻게 구분할 것인가?

결론적으로 status_code == 200은 검증의 끝이 아니라 첫 관문입니다. HTTP 계층, JSON 구조, API 업무 상태, 필수 데이터와 빈 결과의 의미를 이 순서로 확인하면 “응답은 왔지만 작업은 실패한” 상황을 성공으로 저장할 가능성을 줄일 수 있습니다.

Leave a Comment