48시간 현장 기록: 무료 모델이 유효한 JSON을 반환했고, 내 스키마는 아무것도 말해주지 않았다
요약
무료 모델 및 서버 환경에서 데이터 추출 배치 작업을 진행하며 발생한 문제점들을 기록했습니다. 특히, JSON 스키마의 구조적 유효성만으로는 비즈니스 로직상의 '빈 값' 실패를 감지할 수 없음을 발견했습니다. 최소 항목 제약 조건(`minItems: 1`)을 추가하는 것이 핵심 해결책입니다.
핵심 포인트
- 구조적 스키마는 모양만 증명하며, 의미(비즈니스 제약)를 담기 어렵다.
- 빈 배열이 반환되는 경우, 이를 실패로 간주하도록 명시적인 비즈니스 로직을 추가해야 한다.
- 재시도 루프나 래퍼가 환경 설정 오류(오타 등)까지 '일시적' 오류로 처리하면 시간 낭비가 크다.
- 무료 등급의 성능 수치는 보장된 것이 아니므로, 측정값을 신뢰할 때 주의해야 한다.
2일 전 저는 작은 야간 추출 작업을 배포했고, 종료 코드 0을 확인하고 기분 좋게 잠자리에 들었습니다. 다음 날 아침 다운스트림 보고서는 비어 있었고, 로그는 깨끗했으며, 데이터베이스의 모든 행에는 records: []가 있었습니다. 모델은 완벽하게 유효한 JSON을 반환했습니다. 제 스키마는 단지 빈 배열이 제가 사용할 수 없는 하나의 답변이라는 것을 알려주지 않았을 뿐입니다.
그래서 다음 48시간 동안의 현장 기록을 공유합니다: 제가 시도했던 것, 실제로 문제가 발생했던 부분, 그리고 망설임 없이 반복할 부분들입니다. 만약 여러분이 배치 작업을 무료 모델 등급과 무료 서버 인스턴스에 연결하고 있다면, 이것이 바로 여러분이 빠지게 될 구덩이의 모양입니다.
제가 실제로 요청한 것
작업 자체는 의도적으로 지루합니다. 작은 텍스트 블롭(text blobs) 배치를 읽고, 모델에게 줄 항목(id, amount, currency)을 추출하도록 요청하고, 결과를 검증하며, 블롭당 한 행을 작성합니다. 워커는 제가 제공하는 어떤 호스트에서 실행되며, 이번 실행의 호스트는 월 단위로 임대하는 박스라기보다는 무료 서버 옵션이었습니다.
저는 추출 호출에 MonkeyCode의 무료 모델 액세스를 사용했고, 워커 자체도 같은 제품이 제공하는 무료 서버 옵션에서 실행되었습니다. 공개합니다: 이 글은 MonkeyCode의 제품 홍보의 일환으로 작성되었습니다. 저는 의도적으로 할당량(quota), 토큰 상한선(token ceiling) 또는 지연 시간(latency) 수치를 인용하지 않는데, 왜냐하면 어젯밤 한 인스턴스에서 제가 측정한 것은 여러분이 얻을 것이라는 약속이 아니기 때문입니다.
그것이 첫 번째 교훈이며, 저에게는 약 6시간의 비용이 들었습니다: 무료 등급에서는 측정된 수치가 보장되는 수치가 아니다. 어쨌든 측정하되, 그 측정을 신뢰할 수 있는 상한선(ceiling)이 아니라 검증하는 하한선(floor)으로 취급해야 합니다.
제가 발견한 순서대로 문제가 발생했던 부분
3시간 차: 성공적인 빈 값
제 JSON 스키마는 "records": {"type": "array"}라고 되어 있었다. 빈 배열은 유효한 배열이다. 모델은 자신이 자신 있게 구문 분석할 수 없는 데이터 덩어리(blob)에 직면하자, 항목을 지어내기보다는 아무것도 반환하지 않는 합리적인 선택을 했다. 나는 '비어 있다는 것은 실패'라고 규정한 규칙을 작성한 적이 없었으므로, 아무것도 실패하지 않았다.
단 하나의 키워드 수정으로 해결할 수 있는 부분은 "minItems": 1이며, 이것이 이 글 전체에서 가장 가치가 높은 한 줄이다. 이제 내가 작성하는 모든 스키마에는 최소한 하나의 비즈니스 제약 조건(business constraint)을 포함한다. 왜냐하면 구조적 스키마는 모양만 증명할 뿐, 의미를 증명하지 못하기 때문이다.
11시간 차: 아무것도 고치지 못한 복구 루프
나의 첫 번째 반사 작용은 유효성 검사 실패 시 재시도하는 것이었다. 나는 백오프(backoff)가 적용된 세 번 시도 루프를 작성하여 400개의 데이터 덩어리(blob)에 걸쳐 실행했고, 똑같은 형식 오류 페이로드(malformed payload)가 세 번씩 돌아오는 것을 보며 창피할 정도로 많은 시간을 낭비했다. 콘텐츠 실패를 재시도하는 것은 재시도가 아니라 추가 단계가 붙은 동전 던지기나 다름없다.
19시간 차: 밤새 돌았을 설정 오타
오타가 난 모델 이름 때문에 모든 요청에서 4xx 오류가 발생했다. 나의 재시도 래퍼(retry wrapper)는 200이 아닌 모든 것을 일시적(transient) 오류로 처리했기 때문에, 작업은 즐겁게 백오프하고 몇 시간 동안 재시도를 했다. 잘못된 환경 변수(env var)를 고치는 백오프 스케줄은 없으며, 아침 7시에 그것을 발견할 이유도 없다.
27시간 차: 내 것이 아니었던 상자
작업 중간에 워커 프로세스(worker process)가 사라졌고 내가 기록을 추가하던 저널(journal)도 함께 사라졌다. 추출 호출 자체는 문제가 없었다. 호스트가 일시적(ephemeral)인 것은 무료 서버 옵션에서는 정상이며, 나는 다른 사람의 기계를 내 디스크처럼 취급하고 있었다.
내가 사용하는 실패 분류 체계 (Failure Taxonomy)
재시도 정책을 건드리기 전에, 어떤 종류의 실패를 보고 있는지 적어본다. 이 표가 전체 설계이다:
| Failure class | 어떤 현상인가 | 재시도가 도움이 되는가? | 조치 |
|---|---|---|---|
transport | 타임아웃, 연결 초기화(connection reset), 502/503 | 때때로 | 경계 백오프(Bounded backoff), 최대 3회 |
| ... | |||
| 제가 가장 중요하게 생각하는 규칙은 마지막 줄입니다. 설정 오류는 대시보드에서 진행 상황처럼 보이는 수천 개의 로그 기록된 재시도로 은근히 사라지는 것이 아니라, 첫 분 안에 실행을 중단시켜야 합니다. |
사전 점검(Pre-flight probe): 실제 작업 시작 40분 전
이것은 제가 야간 작업을 무료 모델 티어에 연결하기 전에 실행하는 테스트 계획입니다. 이것은 벤치마크가 아니라 계획이며, 여러분의 계정에서 직접 실행하도록 되어 있습니다.
- 동일한 요청 10개를 보내고 모두 스키마를 만족하는지 기록합니다.
- 의도적으로 모호한 데이터(blob) 하나를 보내 모델이 숫자를 지어내는지 또는 빈 리스트를 반환하는지 확인합니다.
- 크기가 큰 데이터(oversized blob) 하나를 보내 잘림(truncation), 거부(refusal), 또는 4xx 에러 중 어떤 것이 발생하는지 기록합니다.
- 의도적으로 잘못된 모델 이름이 포함된 요청을 보내 코드가 재시도하는 대신 멈추는지 확인합니다.
- 의심되는 유휴 시간(idle window)을 지나도록 대기한 후, 한 번 더 요청을 보냅니다.
- 작업 도중 워커를 중단시키고 제어할 수 있는 어딘가에 로그 파일이 남아 있는지 확인합니다.
이 점검은 한 번에 읽기에 충분히 작습니다:
# probe.py - 야간 작업을 무료 티어에 신뢰하기 전에 실행하세요.
import os, json, time, requests
...
실행해보고 관찰한 내용을 채우고, 그 숫자들이 여러분의 가정을 반박하도록 하세요. 제 것들은 저 자신의 몇 가지 가정들을 반박했습니다.
제가 반복할 작업 골격(worker skeleton)
두 번째 결과물은 모델 출력이 계약을 만족해야 할 때마다 재사용하는 루프입니다. 이것은 재시도하기 전에 분류하고, 상자가 삭제할 수 없는 곳에 상태를 유지합니다.
import json, time
RETRYABLE = {"transport", "rate_limited"}
...
수리(repair) 단계는 검증기(validator)의 오류 메시지와 문제가 된 페이로드(offending payload)를 포함하는 추가 호출이며, 정확히 한 번 시도합니다. 만약 그 시도가 실패하면, 해당 행은 크게 실패하며 제가 커피 마시면서 읽을 수 있는 격리 파일에 저장됩니다.
스키마 검사가 아닌 체크
구조가 진실은 아니다. 유효성 검사(validation)를 거친 후 나는 다음과 같이 조정한다: 추출된 금액들을 합산하여 원본 문서에서 이미 알고 있는 총액과 비교하는 것이다. 만약 그 차이가 통화별로 내가 설정한 허용 오차(tolerance)를 초과하면, 해당 행은 재시도되는 대신 격리(quarantine)된다. 이 단 하나의 비교 과정이 모든 스키마 키워드를 합친 것보다 더 많은 오류 행을 포착했으며, 모델 접근 자체가 전혀 필요 없다.
결과 템플릿
내 지연 시간 수치(latency numbers)를 마치 당신의 것처럼 붙여넣지는 않겠다. 대신, 여기는 내가 모든 프로브 실행 후에 채우는 표이다:
| 프로브 케이스 | 통과 여부? | 실패 클래스 | 취한 조치 |
|---|---|---|---|
| 동일하고 깨끗한 요청 10개 | |||
| ... | |||
만약 두 개 이상의 행이 schema 또는 semantic으로 반환된다면, 아직 배치(batch) 크기를 늘리지 마라. 먼저 계약(contract)을 수정해야 한다. 왜냐하면 무료 티어는 아무런 비용 없이 수천 개의 잘 구성되었지만 잘못된 답변을 기꺼이 제공할 것이기 때문이다. |
제한 사항 및 이 방법을 사용해서는 안 되는 경우
- 무료 서버 옵션을 영구 저장소(durable storage)로 간주할 수 없다. 저널(journal)과 결과를 외부 박스에 보관하거나, 재시작이 진행 상황을 앗아갈 것임을 받아들여야 한다.
- 이 워크플로우는 유효성 검사기(validator)와 조정 출처(reconciliation source)가 필요하다. 비교할 근거 진실(ground truth)이 없다면, 격리될 행은 보낼 곳이 없다.
- 만약 엄격한 지연 시간 SLO(Service Level Objectives)나 보장된 동시성(concurrency)이 필요하다면, 무료 티어는 잘못된 도구이며 아무리 많은 재시도 로직을 적용해도 해결할 수 없다.
- 네트워크 외부로 나갈 수 없는 규제 데이터는 유료든 무료든 호스팅되는 모델에 보내서는 안 된다.
- 만약 당신의 출력이 바이트 단위로 결정론적(byte-for-byte deterministic)이어야 한다면, 모델과 검증기(validator)를 결합해도 여전히 그 앞에 게이트가 있는 확률적 시스템일 뿐이다.
내가 반복할 것들
첫 실행 전에 비즈니스 제약 조건(business constraint)을 스키마에 작성하라. 재시도하기 전에 실패 유형을 분류하라. 구성 오류(config errors) 발생 시 실행을 중단하라. 저널은 호스트가 삭제할 수 없는 곳에 보관하라. 그리고 모델 이름을 변경할 때마다 프로브를 실행하라. 왜냐하면 고치기 가장 저렴한 버그는 19분째에 실패하는 것이 아니라, 첫 분에 실패하는 것이기 때문이다.
직접 프롬프트를 테스트해보고 싶다면, MonkeyCode의 무료 모델 액세스를 통해 그대로 실행할 수 있으며, 워커는 무료 서버 옵션에 두고 저널은 사용자의 디스크에 둘 수 있습니다. 400개의 블롭(blobs)을 맡기기 전에 결과표를 솔직하게 채워 넣으세요.
어제 아침의 평온함을 조용히 망친 마지막 '유효한' 응답은 무엇이었나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기