Spring AI

멀티모달 - 이미지

지금까지는 텍스트만 주고받았다. 멀티모달(multimodal) 모델은 이미지·음성도 함께 다룬다. 이 편에서는 이미지의 두 방향 — 이미지를 읽는 입력(비전)과, 텍스트로 이미지를 만드는 생성 — 을 구현한다.

이 편에서 만드는 것

/image/describe(이미지 업로드 → 설명) · /image/generate(프롬프트 → 이미지) 두 개의 엔드포인트.


1. 이미지 입력 (비전)

gpt-4o 계열은 이미지를 보고 설명할 수 있다. ChatClient의 user(...)에서 텍스트와 함께 이미지를 미디어로 붙이면 된다. 여기서는 업로드된 파일(MultipartFile)을 받아 처리한다.

public String describe(byte[] imageBytes, String contentType, String question) {
    MimeType mimeType = (contentType != null)
            ? MimeType.valueOf(contentType)        // 예: image/png
            : MimeTypeUtils.IMAGE_PNG;
    return chatClient.prompt()
            .user(u -> u.text(question)
                    .media(mimeType, new ByteArrayResource(imageBytes)))  // 텍스트 + 이미지
            .call()
            .content();
}
@PostMapping("/image/describe")
public String describe(@RequestParam("file") MultipartFile file,
                       @RequestParam(defaultValue = "이 이미지를 한국어로 설명해줘.") String question)
        throws IOException {
    return imageService.describe(file.getBytes(), file.getContentType(), question);
}
curl -X POST -F "file=@apple.png" \
     "http://localhost:8080/image/describe?question=이 사진에 무엇이 있나요?"
# 사진에는 나무 테이블 위에 놓인 빨간 사과가 있습니다.

업로드 용량 제한

Spring Boot 기본 업로드 한도는 1MB라서 사진을 올리면 413 오류가 난다. application.yml에서 한도를 올린다.

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 15MB
flowchart LR
    F["이미지 파일"] --> M["Media(mimeType, bytes)"]
    Q["질문 텍스트"] --> P["user(text + media)"]
    M --> P --> LLM["gpt-4o (비전)"] --> A["텍스트 설명"]

2. 이미지 생성

OpenAiImageModel에 프롬프트를 주면 이미지를 만들어 준다. 모델은 gpt-image-1을 쓴다.

public byte[] generate(String prompt) {
    ImageResponse response = imageModel.call(new ImagePrompt(prompt,
            OpenAiImageOptions.builder()
                    .model("gpt-image-1")
                    .width(1024)
                    .height(1024)
                    .N(1)              // 생성 장수
                    .build()));
    // gpt-image-1은 URL이 아니라 base64(b64Json)로 결과를 준다
    String b64 = response.getResult().getOutput().getB64Json();
    return Base64.getDecoder().decode(b64);
}
@GetMapping(value = "/image/generate", produces = MediaType.IMAGE_PNG_VALUE)
public byte[] generate(@RequestParam String prompt) {
    return imageService.generate(prompt);   // PNG 바이트를 그대로 반환 → 브라우저에 표시
}
curl -G "http://localhost:8080/image/generate" \
     --data-urlencode "prompt=수채화 스타일의 우주를 나는 귀여운 고양이" \
     -o cat.png
# 1024x1024 PNG 저장

모델마다 응답 형태가 다르다

예전 dall-e-3는 결과를 URL로 주지만, gpt-image-1은 base64(b64Json) 로 준다. 그래서 getUrl()이 아니라 getB64Json()을 디코딩해 바이트로 다뤘다. 쓰는 모델의 응답 형태를 확인하고 맞춰야 한다.


정리

방향

모델

핵심 API

이미지 입력(비전)

gpt-4o 계열

user(u -> u.text(...).media(mime, resource))

이미지 생성

gpt-image-1

imageModel.call(new ImagePrompt(...)) → getB64Json()

비전 + Tool Calling 조합

이미지 입력은 Tool Calling과 묶으면 강력하다. 예를 들어 주차장 차량 번호판 사진을 읽어(비전) → 번호를 DB와 대조(도구) → 차단기를 여는(도구) 흐름을 만들 수 있다.

➡️ 다음 편: 멀티모달 - 음성

마지막 수정 2026년 10월 22일