LLM의 답은 보통 자연어 문자열이다. 하지만 애플리케이션에서 쓰려면 String이 아니라 Java 객체여야 한다. Spring AI의 구조화 출력은 모델에 "이런 스키마로 답해"라고 지시하고, 돌아온 JSON을 자동으로 객체에 매핑까지 해 준다.
이 편에서 만드는 것
/structured/recipe(단일 객체) · /structured/movies(리스트) · /structured/facts(Map) 세 개의 엔드포인트.
flowchart LR
U["요청"] --> CC["ChatClient<br/>.entity(Recipe.class)"]
CC -->|① 스키마 지시 자동 첨부| LLM
LLM -->|② JSON 응답| CC
CC -->|③ JSON → 객체 파싱| O["Recipe 객체"]1. 단일 객체 매핑 — .entity(Class)
.call() 뒤에 .content() 대신 .entity(타입) 을 쓰면, Spring AI가 (1) 해당 타입의 JSON 스키마를 프롬프트에 자동으로 덧붙이고, (2) 응답 JSON을 그 타입으로 파싱해 돌려준다.
public Recipe recipe(String dish) {
return chatClient.prompt()
.user(dish + " 요리의 레시피를 알려줘.")
.call()
.entity(Recipe.class); // ← 스키마 지시 + 파싱을 자동 처리
}
// 매핑 대상은 평범한 record면 충분하다
public record Recipe(String name, List<String> ingredients, List<String> steps) {}curl -G "http://localhost:8080/structured/recipe" --data-urlencode "dish=계란말이"{
"name": "계란말이",
"ingredients": ["계란 4개", "소금 1/2 작은술", "후추 약간", "식용유"],
"steps": ["계란을 풀어 소금·후추로 간한다.", "팬에 부어 익으면 말아 준다.", "...써서 담아낸다."]
}record가 곧 스키마
필드 이름·타입이 그대로 LLM에게 전달할 스키마가 된다. 중첩 리스트(List<String>)도 자연스럽게 채워진다. 별도 어노테이션 없이 동작한다.
2. 리스트 매핑 — ParameterizedTypeReference
List<Movie> 같은 제네릭 타입은 타입 소거 때문에 List.class로 줄 수 없다. 대신 ParameterizedTypeReference로 정확한 타입을 알려 준다.
public List<Movie> recommendMovies(String genre) {
return chatClient.prompt()
.user(genre + " 장르 영화 3편을 추천해줘.")
.call()
.entity(new ParameterizedTypeReference<List<Movie>>() {});
}
public record Movie(String title, String director, int year) {}curl -G "http://localhost:8080/structured/movies" --data-urlencode "genre=SF"[
{"title": "Dune", "director": "Denis Villeneuve", "year": 2021},
{"title": "Inception", "director": "Christopher Nolan", "year": 2010},
{"title": "Blade Runner 2049", "director": "Denis Villeneuve", "year": 2017}
]3. 자유 구조 — Map
스키마를 미리 정하기 어려운 경우 Map<String, Object>로 받으면 LLM이 적절한 key-value를 채운다.
public Map<String, Object> facts(String topic) {
return chatClient.prompt()
.user(topic + "에 대한 핵심 정보를 key-value로 알려줘.")
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {});
}curl -G "http://localhost:8080/structured/facts" --data-urlencode "topic=대한민국"{ "capital": "서울", "population": 51780579, "currency": "원", "official_language": "한국어" }정리
받고 싶은 타입 | 쓰는 법 |
|---|---|
단일 객체 |
|
리스트/제네릭 |
|
자유 구조 |
|
LLM은 사실을 틀릴 수 있다
구조화 출력은 형식을 보장할 뿐 내용의 정확성까지 보장하지 않는다. 위 예시에서 영화 감독을 틀리게 답할 수도 있다(환각). 사실이 중요한 도메인이라면 다음 편의 Tool Calling이나 RAG로 실제 데이터를 물려줘야 한다.
➡️ 다음 편: Tool Calling