JSON 모드도 스키마를 보장하지 않는다…AI 출력 검증의 순서

|작성자: QUASA 편집팀|5 분 소요
JSON 모드도 스키마를 보장하지 않는다…AI 출력 검증의 순서

AI가 반환한 JSON을 업무 시스템에 연결하려면 응답의 완료·거부 여부를 먼저 확인하고, 정상 본문만 파싱한 뒤 JSON Schema와 업무 규칙을 차례로 검증해야 한다. OpenAI의 구조화 출력 안내에 따르면 JSON 모드는 유효한 JSON을 생성하도록 하지만 지정한 스키마까지 보장하지 않는다. 지원되는 환경에서는 Structured Outputs로 생성 형식을 제한하고, 서버에서 최종 수용 여부를 결정한다.

JSON.parse가 성공했다는 사실은 문자열을 JSON 값으로 읽었다는 뜻이다. 그 값이 필요한 필드를 모두 갖춘 객체인지, 허용된 상태값을 담았는지, 실제 주문을 가리키는지는 별도로 확인해야 한다. 데이터베이스 저장이나 외부 API 호출은 이 검증을 모두 통과한 뒤에 실행한다.

본문을 읽기 전에 응답 상태를 확인한다

검증 함수에는 생성된 문자열뿐 아니라 API의 응답 상태도 전달해야 한다. 요청 자체가 실패했거나 생성이 중단됐거나 모델이 응답을 거부했다면, 남아 있는 본문을 정상 결과로 처리해서는 안 된다. 스트리밍으로 받은 조각도 완료 신호가 오기 전에는 저장 대상이 아니다.

Responses API라면 response.status가 completed인지 확인하고 출력 내용에 refusal 항목이 있는지 살핀다. incomplete의 사유가 max_output_tokens이면 JSON이 도중에 잘렸을 수 있으며, content_filter에 따른 중단도 성공 경로에서 분리해야 한다. 다른 API에서는 같은 역할을 하는 완료 상태와 거부 신호를 찾아 내부의 공통 오류 상태로 매핑하면 된다.

이 단계에서 completed, refusal, incomplete, request_error처럼 결과를 나누면 뒤의 파서가 처리할 입력이 분명해진다. 거부 문구를 JSON 객체로 억지로 바꾸거나 잘린 출력에 닫는 중괄호를 덧붙이면 원래 응답의 의미가 달라진다. 완료된 정상 본문을 확보하지 못했다면 파싱과 저장으로 진행하지 않는다.

필수 필드, null, 추가 속성을 계약에 적는다

스키마에는 애플리케이션이 실제로 사용하는 필드와 허용 값을 적어야 한다. JSON Schema의 객체 규칙에 따르면 properties에 필드 이름을 선언해도 required에 넣지 않으면 필수가 아니다. 정의하지 않은 추가 속성은 기본적으로 허용되므로, 이를 거부하려면 additionalProperties를 false로 지정한다.

가상의 주문 분류 결과가 order_id, status, note를 담는다고 가정하자. order_id와 status가 반드시 필요하다면 required에 두 이름을 넣고, status에는 허용할 문자열을 enum으로 지정한다. 예상하지 않은 필드를 받지 않으려면 객체의 additionalProperties를 false로 둔다. 중첩 객체가 있다면 내부 필드의 필수 여부와 추가 속성 허용 여부도 그 객체에서 따로 정한다.

필드 누락과 null은 서로 다른 상태다. order_id가 아예 없으면 required 위반이다. order_id가 null이면 필드는 존재하지만 문자열을 요구하는 type 조건에 어긋난다. 반대로 비어 있을 수 있는 note에는 string과 null을 함께 허용할 수 있다. 서비스가 값이 없는 경우와 속성 자체가 없는 경우를 다르게 처리한다면 스키마와 후속 코드에서도 그 차이를 유지해야 한다.

required는 존재 여부를, type은 값의 종류를, enum은 허용 목록을 검사한다. 빈 문자열을 거부해야 하거나 상태값 사이에 관계가 있다면 그 조건도 별도로 정해야 한다. 예컨대 status가 취소를 뜻할 때 note가 필요한지는 단일 필드의 자료형만으로 판단할 수 없으므로 서버의 업무 규칙에서 검사할 수 있다.

생성 단계의 제약과 서버 계약을 맞춘다

Structured Outputs를 사용할 수 있다면 모델에 전달하는 스키마로 생성 단계부터 필드 구조를 제한할 수 있다. 다만 이 기능이 지원하는 것은 JSON Schema의 일부이며, 엄격한 구조화 출력에서는 객체의 모든 속성을 required로 표시하고 additionalProperties를 false로 설정해야 한다. 먼저 요청에 넣을 스키마가 해당 API의 지원 범위에 맞는지 확인해야 한다.

일반적인 JSON Schema에서 선택 필드로 표현할 값을 엄격한 구조화 출력에서는 필수 필드로 두고 null을 허용하는 방식으로 표현할 수 있다. 따라서 서버가 기대하는 ‘필드가 없음’과 모델이 반환한 ‘필드는 있지만 값이 null임’을 무심코 같은 상태로 취급하면 안 된다. 두 상태를 하나로 합칠 필요가 있는 서비스라면 변환 규칙을 명시하고 변환된 값도 다시 검증한다.

필드 설명에는 값의 의미를 적고, 자료형과 허용 목록은 가능하면 스키마에 둔다. 가상 예시에서 status가 paid 또는 cancelled여야 한다면 설명 문장에만 기대지 말고 enum으로 지정한다. 형식에 맞는 paid라는 값이 실제 주문의 결제 상태와 일치하는지는 주문 데이터와 대조해야 한다.

서버에서 파싱, 스키마 검증, 업무 검증을 실행한다

완료된 정상 본문에는 JSON.parse를 적용한다. 문법 오류가 발생하면 parse_error로 분류하고, 파싱에는 성공했지만 필드 계약을 어긴 값은 schema_error로 분류한다. 파싱된 값이 객체가 아닌 배열이나 문자열이어도 스키마의 최상위 type 조건에서 걸러야 한다.

JavaScript에서 사용할 수 있는 Ajv 검증 API는 스키마를 검증 함수로 컴파일하고 데이터에 적용하며, 실패한 검사의 오류 정보를 제공한다. 배포한 스키마를 미리 컴파일해 각 응답에 적용하면 요청마다 계약을 새로 만들 필요가 없다. 스키마를 바꿀 때에는 버전을 함께 관리해야 어떤 계약에서 오류가 났는지 추적하기 쉽다.

앞의 가상 주문 분류 결과를 처리한다면 코드의 분기 순서는 다음과 같다. 필드 이름과 오류 상태는 설명용이며 실제 구현에서는 서비스가 승인한 계약에 맞춰 정한다.

  1. 응답이 완료되지 않았거나 refusal이 있으면 해당 상태를 반환한다. 본문을 파싱하거나 저장하지 않는다.
  2. 정상 본문에 JSON.parse를 적용한다. 예외가 발생하면 parse_error를 반환하고 이후 단계를 중단한다.
  3. 미리 컴파일한 validate(data)를 실행한다. 결과가 false라면 validate.errors의 경로와 위반 규칙을 복사해 schema_error로 반환한다.
  4. 검증을 통과한 값에 주문 존재 여부, 허용되는 상태 변경인지 여부, 요청자의 권한 같은 업무 규칙을 적용한다. 이 검사까지 통과한 값만 저장하거나 다음 API에 전달한다.

오류 정보는 검증 함수를 다시 호출하면 갱신될 수 있으므로 필요한 내용은 실패 직후에 확보한다. 추가 속성이나 잘못된 자료형을 조용히 삭제·변환하도록 설정했다면 원래 출력의 오류가 가려질 수도 있다. 값의 정규화가 필요한 서비스에서는 변환 규칙을 따로 정의하고, 변환 결과를 다시 검증하는 편이 판단 과정을 분명하게 만든다.

오류 기록에는 전체 응답을 무조건 복사하기보다 오류 종류와 필드 경로, 요청 식별자, 스키마 버전을 우선 남긴다. 원문 보관이 필요한 경우에는 서비스의 데이터 보관 정책에 따른다. 이런 기록은 같은 필드가 반복해서 누락되는지, 파싱 실패가 특정 요청에서만 나타나는지 구분하는 데 쓰인다.

실패 원인에 따라 재시도를 제한한다

일시적인 요청 실패나 출력 길이 제한으로 중단된 생성은 재요청 후보가 될 수 있다. 재시도 횟수에는 상한을 두고, 매번 응답 상태부터 다시 검사해야 한다. 저장이나 결제처럼 중복 실행의 영향이 큰 후속 작업은 검증이 끝나기 전에 시작하지 않는다.

스키마 위반이 발생하면 먼저 어떤 경로에서 required, type, enum 또는 추가 속성 규칙을 어겼는지 확인한다. 재생성을 허용하더라도 같은 요청을 끝없이 반복하지 않도록 제한해야 한다. 같은 위반이 계속 나타난다면 모델 출력뿐 아니라 요청 스키마, 필드 설명, 서버 검증 계약이 서로 맞는지도 살펴야 한다.

거부 응답과 콘텐츠 필터에 따른 중단은 형식을 고치기 위한 재시도와 구분해 호출자에게 전달한다. 파싱이나 스키마 검증에 실패한 값을 빈 객체로 바꿔 성공 데이터처럼 저장해서도 안 된다. 운영 전에는 정상 객체와 함께 필수 필드 누락, null, 추가 속성, 허용 목록 밖의 값, 잘린 JSON, 거부 응답을 각각 넣어 모든 실패 경로가 저장 단계 전에 멈추는지 확인한다.

함께 읽기:

공유:

뉴스레터 구독

최신 Web3, AI, 암호화폐 뉴스를 이메일로 받아보세요.

0