Spring AI 2.0: LLM 출력 신뢰도 2배 향상
LLM의 텍스트 기반 인터페이스는 소프트웨어 연동에 부적합하여, 구조화된 출력(Structured Output)으로 데이터 정합성 확보 필요성 대두
Spring AI 2.0은 Java Record를 이용한 타입 안전한 객체 반환 기능과 함께, 스키마 검증(Schema Validation) 및 프로바이더 네이티브 지원(Provider-Native Support) 기능 강화
기존 코드 호환성을 유지하며, 응답 유효성 검증(Response Validation) 및 요청 제약(Request Constraint)을 통해 LLM 출력의 신뢰도 향상
구조화 출력(Structured Output)의 필요성
LLM은 본질적으로 텍스트 입출력 모델이므로, 자연어(Natural Language) 인터페이스는 소프트웨어 시스템 간의 데이터 연동에 비효율적이다. 구조화된 출력(Structured Output)은 LLM이 특정 스키마(Schema)에 맞는 JSON 형태의 데이터를 생성하도록 유도하여, 애플리케이션 코드에서 타입 안전한 객체(Type-Safe Object)로 파싱하고 활용할 수 있게 한다. Spring AI는 `ChatClient.call().entity(...)` 메서드를 통해 이를 지원하며, Java Record를 사용하면 별도의 파싱 로직 없이 간결하게 구현 가능하다.
Spring AI 2.0의 자체 교정 기능 분석
Spring AI 2.0은 두 가지 새로운 기능을 통해 LLM 출력의 신뢰도를 높인다. 첫째, `validateSchema()`는 응답 스키마 유효성 검증(Response Schema Validation)을 수행하고, 실패 시 오류 메시지를 프롬프트에 추가하여 최대 3회까지 재시도하는 자체 교정 루프(Self-Correcting Loop)를 제공한다. 둘째, `useProviderStructuredOutput()`은 OpenAI, Anthropic 등 지원되는 LLM 프로바이더의 네이티브 구조화 출력 기능(Native Structured Output Feature)을 활용하여 API 레벨에서 스키마 준수를 강제한다. 이 기능들은 요청 제약(Request Constraint)과 응답 재시도(Response Retry)를 결합하여 데이터 형태 불일치(Shape Drift) 문제를 효과적으로 해결한다.
프로바이더 네이티브 구조화 출력의 제약 사항
프로바이더 네이티브 구조화 출력(`useProviderStructuredOutput()`)은 LLM 제공사 API 레벨에서 스키마 준수를 강제하지만, JSON 스키마(JSON Schema)의 부분적 지원이라는 한계가 있다. `$ref`, 중첩 배열, `allOf`/`anyOf`/`oneOf`, 정규 표현식, 재귀 타입 등은 지원되지 않는 경우가 많다. 또한, OpenAI는 최상위 배열(Top-level Array)을 지원하지 않아 리스트 반환 시 컨테이너 레코드(Container Record)로 감싸는 추가 작업이 필요하다. Ollama의 경우, 추론 과정에서 발생하는 내부 텍스트가 JSON 파싱을 방해할 수 있어 `validateSchema()`와의 조합이 권장된다.
커스텀 구조화 출력 컨버터(StructuredOutputConverter) 활용
기본 `BeanOutputConverter`는 엄격하게 JSON 형식만 처리하지만, LLM이 마크다운 코드 블록(Markdown Code Fence)으로 응답하는 경우 파싱에 실패한다. 이를 해결하기 위해 커스텀 `StructuredOutputConverter`를 구현할 수 있다. 예시에서는 마크다운을 제거하고 내부 JSON을 추출하는 `LenientJsonOutputConverter`를 제시하며, 이를 `.entity()`에 직접 전달하여 유연한 파싱이 가능하다. 이 커스텀 컨버터는 `getJsonSchema()` 메서드를 통해 기존의 스키마 검증 기능과도 호환된다.
제네릭 타입 및 응답 객체 처리
Spring AI 2.0은 `List`, `Map`과 같은 제네릭 타입(Generic Type)을 처리하기 위해 `ParameterizedTypeReference`를 지원한다. 또한, LLM 응답 객체뿐만 아니라 전체 `ChatResponse` 객체 (토큰 사용량, 메타데이터 등 포함)가 필요한 경우 `.responseEntity()` 메서드를 사용하여 `ResponseEntity`를 얻을 수 있다. 이는 관측 가능성(Observability) 확보 및 디버깅에 유용하며, `.entity()`와 동일한 오버로드(Overload) 및 스펙(Spec) 설정을 지원한다.