sgcOpenAPI 2026.9.0: 파서 전면 개편

· 릴리스
Delphi용 OpenAPI 파서 및 코드 제너레이터 sgcOpenAPI 2026.9.0

sgcOpenAPI 2026.9.0은 이 제품이 지금까지 선보인 가장 큰 릴리스입니다. 이전 버전은 대부분의 튜토리얼이 보여주는 형태의 명세는 처리했지만, 그 밖의 모든 것에서는 조용히 품질이 떨어졌습니다. 이번에는 OpenAPI 3.0, 3.1, 3.2 명세와 실제로 공개된 문서를 기준으로 파서를 기능 하나하나 점검했고, 그 결과 새 기능 9개, 버그 수정 26건, 의도적인 호환성 변경 5건이 담겼습니다.

요약하면, 예전에는 컴파일되지 않는 코드를 만들어내거나 더 나쁘게는 컴파일은 되지만 잘못된 URL을 호출하던 명세에 대해서도 이제 생성된 클라이언트가 올바르게 동작합니다.

이제 파서가 무엇을 하지 못했는지 알려줍니다

예전 파서가 문제를 알리는 방법은 예외를 발생시키는 것 하나뿐이었고, 나머지는 모두 조용히 넘어가는 방식으로 처리했습니다. 생성하지 못한 오퍼레이션은 그냥 출력에 없었고, 있어야 할 메서드를 찾다가 없다는 사실을 그제야 알게 됐습니다.

이제 모든 문서는 Warnings 목록과 함께 돌아옵니다. 누락된 openapi 또는 info 멤버, JSON 타입이 잘못된 멤버, 생성하지 못한 오퍼레이션, 해석되지 않은 경로 항목 참조, 그리고 읽기는 하지만 아직 반영되지 않는 JSON Schema 키워드가 모두 여기에 기록됩니다. 목록은 읽을 때마다 비워지므로, 손에 들어오는 내용은 방금 파싱한 문서의 것입니다.

uses
  sgcOpenAPI_Classes, sgcOpenAPI_Parser_Client_Pascal;

var
  oParser: TsgcOpenAPI_Parser_Client_Pascal;
  i: Integer;
begin
  oParser := TsgcOpenAPI_Parser_Client_Pascal.Create;
  Try
    oParser.OpenAPIClassName := 'TPetStoreClient';
    oParser.OpenAPINamespace := 'PetStore';
    oParser.OutputFileName := 'PetStoreClient.pas';

    oParser.ReadFromFile('petstore.json');

    for i := 0 to oParser.Warnings.Count - 1 do
      Memo1.Lines.Add('warning: ' + oParser.Warnings[i]);

    oParser.SaveToFile('PetStoreClient.pas');
  Finally
    oParser.Free;
  End;
end;

저장하기 전에 OutputFileName을 지정하세요. Pascal 유닛은 선언된 이름이 파일의 기본 이름과 일치해야만 컴파일되는데, 예전 제너레이터는 입력 문서의 이름을 따서 유닛 이름을 지었습니다. 그래서 petstore.json에서 MyClient.pas를 생성하면 petstore라는 유닛이 만들어져 컴파일되지 않았습니다. 이제는 출력 이름이 우선합니다.

어떤 버전을 읽고 있는지 압니다

OpenAPI 3.0과 3.1은 이름이 같은 키워드를 두고 서로 다르게 규정하는데, 예전 파서는 모든 문서를 3.0으로 취급했습니다. 가장 분명한 사례는 exclusiveMinimum으로, 3.0에서는 minimum에 붙는 불리언 한정자이고 3.1에서는 그 자체가 숫자입니다. 한쪽을 다른 쪽으로 읽으면 경계값이 틀어집니다.

이제 버전은 다이얼렉트로 파싱되어 Dialect, DialectMajor, DialectMinor로 노출되며, 차이가 있는 키워드는 모두 자기 버전이 요구하는 방식대로 읽힙니다.

oParser.ReadFromFile('api.yaml');

case oParser.Dialect of
  oapiDialect30: ShowMessage('OpenAPI 3.0');
  oapiDialect31: ShowMessage('OpenAPI 3.1');
  oapiDialect32: ShowMessage('OpenAPI 3.2');
end;

여기에 더해 3.1은 webhooks, jsonSchemaDialect, components.pathItems, 라이선스 identifier, mutualTLS 보안 스킴, ["string","null"]처럼 배열로 선언된 타입, 그리고 단순 불리언으로 선언된 스키마를 가져옵니다. 모두 지원합니다. 코드 제너레이터가 아직 처리하지 않는 JSON Schema 2020-12 키워드는 모델로 읽어 들인 다음 Warnings를 통해 알려주므로, 빠진 부분이 보이지 않는 대신 눈에 드러납니다.

3.2에서는 파서가 query 오퍼레이션과 additionalOperations 맵을 지원합니다. 둘 중 하나를 선언한 경로는 이제 그에 맞는 메서드를 생성하며, X-HTTP-Method-Override 헤더를 담은 POST로 전송됩니다.

경로 수준 파라미터

대부분의 사용자가 체감할 수정입니다. 명세는 파라미터를 그 아래 모든 오퍼레이션마다 반복하는 대신 경로 항목에 한 번만 선언할 수 있게 해주며, 이는 명세가 권장하는 방식이자 공개된 문서 대부분이 쓰는 방식입니다. 예전 파서는 그 파라미터를 읽고 나서 버렸습니다.

생성된 메서드는 인자를 하나도 받지 않았고, 요청은 URL에 플레이스홀더가 그대로 남은 채, 말 그대로 /pets/{petId}로 나갔습니다. 첫 호출이 404로 돌아오기 전까지는 제대로 동작하는 클라이언트처럼 보였습니다.

조합되는 스키마

조합은 예전 파서에서 가장 취약한 부분이었고, 모든 갈래가 저마다 다른 방식으로 잘못돼 있었습니다. allOf는 여러 기반 스키마 중 마지막 하나만 남기고 나머지의 멤버를 버렸습니다. oneOf는 모든 갈래를 하나의 클래스로 합쳐 중복 필드를 만들어냈습니다. anyOf는 아예 처리되지 않고 문자열로 해석됐습니다. propertiesadditionalProperties를 함께 선언한 스키마는 프로퍼티를 전부 잃었습니다.

이제 네 가지 모두 문서가 기술한 그대로 생성됩니다. 인라인 오브젝트 스키마도 문자열로 격하되지 않고 자체 클래스를 갖게 되며, items는 완전한 스키마로 읽히므로 인라인 오브젝트의 배열, 열거형의 배열, 중첩 배열이 각각 올바른 타입으로 생성됩니다.

서버가 기대하는 값을 담는 열거형

예전에는 생성된 열거형 테이블에 전송 값 대신 정리된 Pascal 식별자가 들어 있어서, allow-allallowall로, json-filejsonfile로 나갔습니다. 그런 열거형으로 만든 요청은 모두 거부됐습니다.

이제 테이블은 실제 값을 담고, 명세의 선언 순서가 보존되며, 정수 열거형에도 테이블이 생기고, 추가로 Unknown 멤버가 생성되어 서버가 나중에 추가한 값이 목록의 첫 멤버로 조용히 디코딩되는 일이 없습니다.

프로퍼티 이름은 반대 방향에서 같은 처리를 받습니다. property, class, string, function 같은 Delphi 예약어를 이름으로 쓴 스키마 프로퍼티나, Namename처럼 대소문자만 다른 두 프로퍼티는 예전에는 컴파일되지 않는 유닛을 만들어냈습니다. 이제 프로퍼티는 이름이 바뀌고 전송 이름은 JSONName 어트리뷰트로 보존되므로, 직렬화는 여전히 문서와 일치합니다.

한 번만 선언한 것까지 포함한 응답

default 응답과 범위 응답 2XX, 4XX, 5XX는 조용히 버려졌습니다. 오류를 default로만 선언하는 API는, 흔한 경우인데, 타입이 지정된 오류가 전혀 없는 클라이언트를 만들어냈습니다. 이제는 읽어 들입니다. 성공 응답이 여러 개 선언되면 가장 낮은 것을 사용하고, 한 오퍼레이션이 여러 미디어 타입을 제공하면 application/json을 우선합니다.

전송되는 파라미터

이제 생성된 클라이언트는 쿠키 파라미터와 OpenAPI 파라미터 직렬화 규칙 전체를 지원합니다. matrix, label, simple, form, spaceDelimited, pipeDelimited, deepObject를 각각 explodeallowReserved와 함께 지원합니다. 새로 추가된 AddArrayAddObject 메서드로 필요할 때 구조화된 값을 직접 만들 수 있습니다.

// query parameters built with the serialization style the document declares
oRequest.AddArray('tags', ['dog', 'cat'], True);   // explode
oRequest.AddObject('filter', ['color', 'red', 'size', 'M']);

외부 참조

여러 파일로 나뉜 명세는 거의 동작하지 않았습니다. ./common.yaml#/components/schemas/Error처럼 JSON 포인터 프래그먼트를 담은 참조는 해석되지 않았습니다. 서로를 참조하는 두 파일은 파서를 크래시시켰습니다. 하위 문서 안의 상대 참조는 자기 파일이 아니라 루트 문서를 기준으로 해석됐습니다. 기본 이름이 같은 외부 파일 두 개는 서로를 덮어썼고, 메인 문서에 속한 스키마를 대체해 버릴 수도 있었습니다. 그리고 참조 체인은 정확히 한 단계만 따라갔습니다.

이 모든 것이 수정됐고, 한 가지는 의도적으로 더 엄격해졌습니다. 예전에는 외부 참조가 ../../../credentials.json을 포함해 머신의 어떤 파일이든 읽어서 그 내용을 생성된 유닛에 복사할 수 있었습니다. 이제 외부 참조는 메인 문서의 디렉터리 안으로 제한됩니다. 구성상 정말로 그 바깥에 접근해야 한다면, 제한을 명시적으로 해제합니다.

uses
  sgcOpenAPI_Bundle;

begin
  // off by default: references may not leave the folder of the main document
  sgcOpenAPIAllowRefsOutsideRoot := True;
end;

완전한 UTF-8은 아닌 파일

RFC 8259는 JSON 문서가 UTF-8이라고 규정하지만, 공개된 명세 중에는 그렇지 않은 것이 많습니다. 바이트 순서 표시가 있는 파일은 ASCII 밖의 문자가 하나라도 들어 있는 순간 UTF-8 오류로 거부됐고, 중국어나 일본어 텍스트는 조용히 물음표로 바뀌었습니다.

이제 유효한 UTF-8이 아닌 문서는 실패하는 대신 경고를 기록하면서 Windows-1252로 읽습니다. 바이트 순서 표시가 있는 UTF-16 파일도 올바르게 읽습니다. 생성된 파일은 명시적인 인코딩으로 기록되며, 대상 인코딩으로 표현할 수 없는 문자는 조용히 물음표가 되는 대신 보고됩니다.

빌드 스크립트에 넣을 수 있는 명령줄

이제 명령줄은 종료 코드를 설정합니다. 성공은 0, 실패는 종류에 따라 1에서 7까지이므로, 빌드 단계에서 생성이 제대로 됐는지 판단할 수 있습니다. 오류 메시지는 항상 표준 오류로 나가고, -l 스위치는 이제 진행 상황 로깅 전용입니다.

sgcOpenAPI -i petstore.json -o PetStoreClient.pas -c TPetStoreClient
if errorlevel 1 (
  echo OpenAPI generation failed with exit code %errorlevel%
  exit /b %errorlevel%
)

명령줄 버그 세 가지도 함께 해결됐습니다. 문서에 나온 -output 스위치는 현재 디렉터리의 utput이라는 파일에 유닛을 썼고, 메시지가 억제돼 있어서 실행은 여전히 성공한 것처럼 보였습니다. 콘솔이 연결되지 않은 채 도구를 실행하면, 예약 작업이나 빌드 에이전트가 딱 그런 경우인데, 아무 일도 일어나지 않았고 기존 출력 리디렉션은 버려졌습니다. 그리고 활성화되지 않은 머신에서 -h는 사용법 대신 라이선스 오류를 출력했고, -m이나 -a에 잘못된 값을 줘도 조용히 받아들였으며, 알 수 없는 스위치는 무시됐습니다.

이번 릴리스에 새로 추가된 -r(또는 -remote)은 YAML 또는 Swagger 2.0 문서를 converter.swagger.io의 공개 변환기를 통해 변환합니다. 여러분의 문서를 제3자에게 보내기 때문에 기본값은 꺼짐이며, 알고 나서 켜는 기능입니다.

Swagger 2.0 변환 자체에도 짚어둘 만한 문제가 두 가지 있었습니다. 모든 숫자가 문자열이 돼서, 숫자형 default는 컴파일되지 않는 유닛을 만들어냈고 변환된 문서는 유효한 OpenAPI 3.0이 아니었습니다. 그리고 Swagger 2.0의 discriminator는 거기서 단순 문자열인데, 잘못된 타입 캐스트로 전체 파싱을 중단시켰습니다.

호환성이 깨지는 변경

다섯 가지 변경은 단순한 업그레이드가 아니라 여러분의 결정을 필요로 합니다.

이제 생성된 클라이언트는 서버 인증서를 검증합니다. 예전에는 검증하지 않았고, 이는 중간자가 제시한 인증서를 포함해 어떤 인증서든 받아들였다는 뜻입니다. 자체 서명 엔드포인트나 테스트 엔드포인트에 접속하려면 의도적으로 꺼야 합니다.

oClient.TLSOptions.VerifyCertificate := False;   // test endpoints only
// certificates are trusted through the OpenSSL default paths, so a machine
// with no certificate store configured needs an explicit root
oClient.TLSOptions.RootCertFile := 'cacert.pem';

요청 본문은 UTF-8입니다. RFC 8259가 요구하는 대로입니다. 이제 클래스는 빈 문자열을 생략하는 대신 "field": ""로 직렬화합니다. Null 값은 따로 제어합니다.

oClient.JSONIgnoreEmptyStrings := True;  // previous output
oClient.JSONIgnoreNullValues := True;    // default

응답은 더 이상 여러분이 제공한 ResponseStream을 해제하지 않습니다. 예전 동작을 원하면 OwnsResponseStream을 True로 설정하세요. 클라이언트의 OnResponse, OnError, OnCancel 핸들러 안에서 그 클라이언트를 해제하면, 이제 멈춰 버리는 대신 명확한 오류가 발생합니다.

명령줄 스위치 값은 -name value 또는 -name:value 형태로 써야 합니다. -x"GET /pets"처럼 구분자 없이 붙여 쓰는 형태는 더 이상 허용되지 않습니다. 바로 그 형태 때문에 -x-xml처럼 x로 시작하는 다른 스위치와 매칭되기도 했습니다.

배열로 선언된 파라미터는 배열로 생성됩니다. 예전에는 문자열로 생성됐으므로, 해당 오퍼레이션에서는 생성된 메서드의 시그니처가 바뀝니다.

그 밖의 모든 것

나머지 수정은 직접 당해봐야 알아차리는 종류입니다. 예를 들어 "properties": []처럼 예상과 다른 JSON 타입의 멤버는, 건너뛰는 대신 잘못된 타입 캐스트로 파싱을 중단시켰습니다. default가 없는 integer 타입 스키마에는 0이 기본값으로 주어졌고, 값이 하나뿐인 열거형은 상수로 취급되어 생성된 메서드에서 파라미터가 통째로 사라졌습니다. 같은 문서를 두 번 읽으면 모든 경로, 태그, 서버, 스키마가 중복됐습니다. 경로들 사이에 놓인 x-tagGroups 같은 명세 확장은 경로인 것처럼 읽혔습니다. enum, required, tags는 쉼표로 구분된 텍스트 헬퍼로 파싱돼서, 쉼표나 JSON 이스케이프를 담은 값은 쪼개지거나 손상됐습니다. 여러 스킴을 나열한 보안 요구 사항은 첫 번째만 남겨서, 전부 충족해야 한다는 요구를 잃어버렸습니다. info.contactinfo.license는 결코 참이 될 수 없는 조건 검사 때문에 아예 읽히지 않았습니다. 변수가 여러 개인 서버 URL은 잘못된 값을 치환했고 리스트 인덱스 오류를 낼 수도 있었습니다. 명세를 번들링하면 백업도 메시지도 없이 입력 파일을 덮어썼고, 문서에서 타이포그래픽 아포스트로피를 모두 지워 버렸습니다. 그리고 C:\My Specs\처럼 공백이 들어간 경로에 저장된 명세는 외부 참조를 해석하지 못했습니다.

받아보기

sgcOpenAPI 2026.9.0은 지금 바로 사용할 수 있으며, 전체 소스 코드와 1년간의 업데이트가 함께 제공됩니다. Delphi 7부터 Delphi 13 Florence까지, 그리고 이에 대응하는 C++ Builder 버전을 지원합니다.

제품 페이지 · 평가판 다운로드 · 변경 이력

질문이나 의견이 있으신가요? 문의해 주세요, 코드를 직접 작성한 사람에게서 답변을 받으실 수 있습니다.