structured output 을 우리는 언제 쓰고 언제 안 쓰는가
structured output은 결제·정산 파이프라인의 안정성을 높이지만, 복잡한 추론이 필요한 곳에선 오히려 품질을 깎는다. HEDVION이 실제 운영에서 정착시킨 적용·비적용 기준을 공유한다.
결제·정산 파이프라인에서 파싱 오류는 단순한 버그가 아니다
HEDVION은 하루에도 수백 건의 결제 이벤트를 처리하고, 그 결과를 여러 정산 파이프라인으로 흘려보낸다. LLM을 결제 분류기나 이상 탐지 보조 도구로 쓰기 시작했을 때 처음 마주친 문제는 기술적인 것이 아니라 신뢰의 문제였다. 모델이 오늘은 {"action": "refund", "amount": 15000}을 반환하고, 내일은 {"type": "환불", "금액": "15,000원"}을 반환한다면, 이 응답을 받는 정산 코드는 언제 터질지 모르는 시한폭탄이 된다.
실제로 structured output을 도입하기 전, 자유 형식 JSON 응답의 파싱 실패율은 약 23% 수준이었다. 요청 1,000건 중 2030건이 런타임 오류를 냈다는 뜻이다. 금액이 관계된 파이프라인에서 이 숫자는 단순한 에러율이 아니다 — 그 건들은 모두 수동 검토 대기열로 빠지고, 팀원 누군가의 아침을 갉아먹는다. structured output 도입 후 이 실패율은 사실상 0에 수렴했다. 스키마 불일치로 인한 오류가 모델 레이어에서 차단되기 때문이다.
structured output이 명확하게 맞는 세 가지 상황
다운스트림 시스템이 특정 형태를 기대할 때가 첫 번째다. 우리가 운영하는 정산 자동화 파이프라인의 경우, LLM 응답이 PostgreSQL 테이블에 직접 삽입되거나 내부 정산 API로 전달된다. 이때 스키마를 미리 고정하면 코드 전체가 안정된다. required 필드가 누락되거나 타입이 다르면 모델 레이어에서 즉시 재시도하므로, 파싱 실패가 DB나 API 레이어까지 전파되지 않는다. 사후 방어 코드를 잔뜩 쓰는 대신, 구조 보장을 입력 단계에서 해결한다.
필드 존재 여부로 분기하는 로직이 두 번째다. 예를 들어 "이 거래에 dispute_flag 필드가 true이면 분쟁 처리 큐로 보내고, 없으면 정상 처리로 넘긴다"는 로직에서, 스키마가 dispute_flag: boolean | null로 보장되면 조건 분기가 명확해진다. 필드가 아예 빠지거나, 오타로 dispite_flag가 되는 케이스 — 이런 엣지가 structured output으로 사라진다. 세 번째는 배치 처리에서 응답 일관성이 필요할 때다. 우리는 월말 정산 시 수천 건의 거래 내역을 LLM으로 카테고리 분류한다. 각 응답이 동일한 구조를 가지면 집계, 오류율 계산, 이상치 탐지가 한결 단순해진다. 배치 크기가 커질수록 구조 불일치 하나가 전체 집계를 망가뜨릴 위험도 커지기 때문에, 여기서 structured output은 선택이 아닌 필수에 가깝다.
structured output이 오히려 방해가 되는 경우 — 수치로 본 트레이드오프
가장 흔히 마주치는 문제는 추론 과정이 출력 품질에 직접 영향을 줄 때다. 우리가 이상 거래 탐지 보조 도구를 만들 때 경험한 일이다. 처음에는 응답을 {"is_suspicious": true, "reason": "...", "confidence": 0.87} 형태로 강제했다. 결과는 나쁘지 않았지만, 자유 형식으로 모델이 "이 거래 패턴이 의심스러운 이유를 단계적으로 분석하라"는 지시를 받았을 때와 비교하면 탐지 정확도가 약 8~10%p 떨어졌다. 스키마가 모델의 사고 흐름을 압축했기 때문이다.
이 문제는 복합 신호를 종합해야 하는 케이스에서 특히 두드러진다. "금액은 정상 범위지만, 수취인 계좌가 최근 3일간 처음 등장했고, IP가 평소와 다르다"는 상황을 평가할 때, 모델이 자연어로 사고하고 결론을 내리는 방식이 스키마에 맞춰 즉시 필드를 채우는 방식보다 실제로 더 정확했다. 우리 해결책은 두 단계로 나누는 것이었다: 1단계에서 자유 형식으로 추론하게 하고, 2단계에서 그 추론을 structured output으로 요약하게 했다. 레이턴시는 약 40% 늘었지만 탐지 정확도는 자유 형식 수준을 유지했다 — 이 트레이드오프가 정당화되는 케이스인지는 각 팀이 직접 측정해야 한다.
탐색적 단계에서 스키마를 너무 일찍 고정하는 것도 흔한 실수다. 우리가 자동 이의제기 처리 기능을 프로토타이핑할 때, 초반에 스키마를 고정했다가 모델이 자연스럽게 뽑아내는 유용한 컨텍스트 정보를 놓쳤다는 걸 뒤늦게 깨달았다. 나중에 스키마를 수정하는 건 가능하지만, 이미 그 필드를 읽는 코드가 여러 곳에 생겨 있어서 비용이 컸다. 스키마는 패턴이 보일 때 정의하는 것이다 — 탐색 시작 시점에 정의하면 발견을 막는다.
우리가 정착한 결정 프레임워크
지금 우리 팀은 새 LLM 호출을 설계할 때 두 가지 질문을 먼저 한다.
"이 출력을 읽는 코드가 있는가?" — 있다면 structured output을 기본으로 고려한다. DB 삽입, API 전달, 조건 분기가 모두 여기 해당한다. 인간이 읽는 텍스트이고 그게 최종 목적지라면, 구조화할 이유가 없다. 불필요한 구조는 코드 복잡도만 올린다.
"응답 품질이 추론 과정에 의존하는가?" — 단순 분류나 필드 추출이라면 structured output이 품질을 해치지 않는다. 복잡한 판단, 다중 신호 종합, 근거 있는 추론이 필요하다면 자유 형식 1단계 + 구조화 2단계를 검토한다. 스키마 설계 원칙도 정해두었다: 필드는 가능한 한 좁게, 선택적 필드는 명시적으로 nullable로 선언한다. "혹시 몰라서" 추가한 필드는 나중에 제거하기 훨씬 어렵다 — 그 필드를 읽는 코드가 어딘가에 이미 생겨 있기 때문이다. 우리는 이걸 한 번 겪은 뒤 스키마 PR에 "이 필드를 실제로 소비하는 코드가 명시되어 있는가?" 체크를 추가했다.
바로 써먹을 수 있는 실행 시사점
결제·정산·자동화 파이프라인에서 LLM을 쓰는 팀이라면 지금 당장 확인할 것들이다.
1. 현재 LLM 호출 목록을 분류하라. 각 호출의 출력이 (a) 코드가 읽는가, (b) 사람이 읽는가를 나눈다. (a)인데 structured output이 미적용된 호출에 즉시 도입을 검토한다.
2. 파싱 실패율을 측정하라. 자유 형식 JSON 응답을 파싱하는 코드가 있다면, 지금 실패율이 얼마인지 로그에서 확인한다. 1%를 넘으면 structured output 전환의 ROI가 크다.
3. 품질이 중요한 추론 호출에는 두 단계를 검토하라. 자유 형식 추론 → structured output 요약. 레이턴시 트레이드오프를 실제로 측정하고, 품질 향상이 그 비용을 정당화하는지 확인한다. 직관이 아니라 숫자로.
4. 스키마는 좁게 시작하고, 나중에 넓혀라. 처음부터 모든 필드를 담으려 하지 않는다. 실제로 소비되는 필드만 required로, 나머지는 nullable로. 스키마 변경은 항상 하위 호환성을 먼저 확인한다.
5. 탐색 단계와 운영 단계를 구분하라. 프로토타입에서는 자유 형식으로 모델이 무엇을 뽑아내는지 먼저 관찰한다. 패턴이 안정되면 그때 스키마를 정의한다. 이 순서를 바꾸면 나중에 스키마를 뜯어고치는 비용이 생긴다.
structured output은 우리에게 결제 파이프라인의 안정성을 높여준 도구이지, 모든 LLM 호출에 붙이는 기본값이 아니다. 언제 쓰고 언제 안 쓰는지를 팀이 명확히 공유하는 것 — 그게 이 도구를 제대로 쓰는 방법이다.
— by slecs
* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.