프로젝트에서 AI 이미지 생성 기능을 구현하게 되면서 AI를 프로젝트에 넣고자 했고, 다음과 같은 이유로 Spring AI를 선택하게 되었다.
기술의 파편화가 생각보다 많은 비용을 초래한다. AI를 프로젝트에 넣어야 하는데 파이선이라는 다른 기술을 기술 풀에 포함시키는 것은 단순히 다른 언어 하나가 들어온 것 이상의 많은 끔찍한 비용을 초래할 수 있기 때문에, 같은 기술 풀 내에서 요구사항의 구현이 가능하다면 그렇게 하려고 한다.
SpringAI를 도입하게 되면서 공부한 내용과 소감을 기록하려 한다.
❤️🔥 Spring AI 란?
Spring AI는 GPT와 같은 여러 AI 모델을 Spring 애플리케이션에 통합하기 위한 추상화와 구현을 제공하는 솔루션이다. 이를 이용해 Spring 기반 프로젝트에서 AI 모델을 쉽게 활용할 수 있다.
https://docs.spring.io/spring-ai/reference/
Introduction :: Spring AI Reference
Support for all major Vector Database providers such as Apache Cassandra, Azure Cosmos DB, Azure Vector Search, Chroma, Elasticsearch, GemFire, Milvus, MongoDB Atlas, Neo4j, OpenSearch, Oracle, PostgreSQL/PGVector, PineCone, Qdrant, Redis, SAP Hana, Typese
docs.spring.io
구성 요소
1. Prompt
Prompt는 메시지(Message)의 리스트와 ChatOptions(채팅 모델 옵션)을 캡슐화한 클래스이다.
Message는 다음과 같은 종류가 있다.
-System: AI의 행동과 반응 스타일을 안내하고, AI가 입력을 해석하고 응답하는 방법에 대한 매개변수 또는 규칙을 설정한다. (보통 프롬프트라고 하면 이걸 말한다.)
- User: 사용자의 입력. 즉 AI에 대한 질문, 명령 또는 진술을 나타낸다.
- Assistant: 사용자 입력에 대한 AI의 응답. AI의 이전 응답을 추적함으로써 이전 컨텍스트를 기억하고 알맞은 응답을 낼 수 있다.
- Tool/Function: 도구 호출 지원 메시지에 응답하여 추가 정보를 반환하는 데 중점을 둔다. 사용자가 요청한 특정 작업(계산, 데이터 검색 등)이나 명령을 수행할 때 사용된다.
2. ChatClient
ChatClient에서는 Prompt를 GPT AI의 API에 맞게 입력 명령들을 변환하고, 채팅 옵션들을 병합한 뒤 GPT에 API 요청을 보내게 된다. 그리고 그 응답을 ChatResponse로 받을 수 있게 한다.
3. ChatResponse
ChatResponse는 채팅 응답에 대한 메타데이터(ChatResponseMetadata), Generation을 가지는 응답 객체이다.
ChatResponseMetadata는 AI 모델에 대한 RateLimit(API 호출 제한량), Usage(사용량) 등을 확인할 수 있다.
Generation 클래스는 모델 결과를 확장하여 보조 메시지(AssistantMessage) 응답과 관련된 메타데이터를 나타낸다.
GPT에 요청을 보낼 때는 ChatClient에서 UserMessage를 사용하고 (사람이 보낸 것이므로)
응답을 받을 때는 AssistantMessage가 Generation 안에 있다.
동작 흐름
1. 사용자가 Prompt를 통해 메시지 리스트(`List<Message>`)와 옵션(`ChatOptions`)을 포함한 요청을 보낸다. 이때 Prompt 객체는 대화의 컨텍스트와 AI 모델의 동작 방식을 정의하는 역할을 한다.
2. `ChatClient`는 User Request를 AI 모델에 UserMessage로 전달한다.
3. AI 모델은 UserMessage를 기반으로 첫 번째 AssistantMessage(응답)를 생성한다. 이 응답은 ChatClient로 전달된다.
4. ChatClient는 AI 모델이 제공한 응답을 기반으로 특정 함수 호출이 필요한지 판단한다. 만약 추가적인 외부 데이터나 작업이 필요하면 FunctionCallback을 통해 API 호출 또는 외부 함수 호출을 진행한다.
5. FunctionCallback이 외부 API나 함수 호출을 통해 필요한 데이터를 받아온다. 이 데이터를 통해 AI 모델의 요청을 충족시키고, ChatClient로 응답을 반환하여 대화의 흐름을 이어간다.
6. ChatClient는 FunctionCallback에서 얻은 데이터를 Tool 메시지 형태로 AI 모델에 다시 전달한다.
7. AI 모델은 Tool 메시지(추가 정보)를 기반으로 최종 AssistantMessage(응답)을 생성한다. 이 메시지가 사용자의 질문에 대한 최종 답변이 될 수 있다.
8. 마지막으로 ChatClient는 AI 모델의 응답을 ChatResponse 객체로 변환하여 사용자에게 전달한다.
`사용자 요청` -> `AI 응답` -> `외부 데이터 통합` -> `최종 응답`의 순서로 정리할 수 있다.
❤️🔥 적용기
서비스 구조
코드
1. 의존성 추가
`build.gradle`
...
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
ext {
set('springAiVersion', "1.0.0-M1")
}
dependencies {
// OpenAI
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
...
}
dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
}
}
...
참고
https://docs.spring.io/spring-ai/reference/getting-started.html
https://docs.spring.io/spring-ai/reference/api/image/openai-image.html
2. properties로 API key 세팅
`application.yml`
spring:
ai:
openai:
api-key: # 여기에 발급 받은 OpenAI API Key 값을 입력. 외부(깃허브 등) 노출 금지!!
image:
options:
model: DALL_E_3
response-format: b64_json
quality: hd
DALL_E_3란?
DALL-E 3는 OpenAI에서 2023년 9월에 출시한 프롬프트로 AI 일러스트 이미지를 생성하는 모델이다. Text-to-Image 즉, 텍스트로 이미지를 만들 수 있다.
`OpenAiImageProperties.java`
import org.springframework.ai.openai.api.OpenAiImageApi;
@ConfigurationProperties(prefix = "spring.ai.openai")
record OpenAiImageProperties (
String apiKey,
Image image
) {
record Image (
Options options
) {}
record Options (
OpenAiImageApi.ImageModel model,
String responseFormat,
String quality
) {}
}
@ConfigurationProperties 어노테이션을 사용하려면 Application.java에 @ConfigurationPropertiesScan 을 붙여야 한다!
3. OpenAI Image Generation API 사용을 위한 빈 주입
`OpenAiConfig.java`
@Configuration
@RequiredArgsConstructor
public class OpenAIConfig {
private final OpenAiImageProperties openAiImageProperties;
/**
* OpenAI 이미지 API 를 위한 Bean 을 생성하는 메서드
*
* @return OpenAiImageApi 객체
*/
@Bean
public OpenAiImageApi openAiImageApi() {
return new OpenAiImageApi(openAiImageProperties.apiKey());
}
}
4. 이미지 생성
`UniverseService.java`
@Service
@RequiredArgsConstructor
public class UniverseService {
private final ImageGenerator imageGenerator;
/**
* OpenAI API 를 이용해 Universe 파일 이미지를 생성하는 메서드
*
* @param universeSaveRequest 파일 생성 요청을 담은 DTO
* @return 생성된 이미지의 URL 을 ResponseEntity 로 반환
*/
@Transactional
public ResponseEntity<String> generateUniverseImage(UniverseSaveRequest universeSaveRequest) {
String prompt = PromptUtils.createPrompt(universeSaveRequest);
String imageBase64 = imageGenerator.createImage(prompt);
return ResponseEntity.ok(imageBase64);
}
}
`PromptUtils.java`
public class PromptUtils {
/**
* UniverseFileRequestDto 를 기반으로 작품 세계관 일러스트레이션 이미지를 위한 프롬프트를 생성하는 메서드
*
* @param universeSaveRequest 작품 정보가 담긴 DTO
* @return 생성된 작품 세계관 일러스트레이션 프롬프트
*/
public static String createPrompt(UniverseSaveRequest universeSaveRequest) {
return String.format(
"다음 정보에 따라 작품의 표지 일러스트레이션 이미지를 만드세요. 작품의 분위기와 주요 내용을 시각적으로 표현해야 합니다. 다음은 작품의 정보입니다 " +
"작품 제목: [%s] \n" +
"작품 설명: [%s] \n" +
"이미지는 캐릭터, 배경, 주요 사건, 또는 작품의 분위기를 시각적으로 나타내야 합니다. \n" +
"이미지 스타일: 전반적으로 현실적이면서도 작품의 장르와 분류에 맞는 예술적 디테일을 적용하세요. \n" +
"이미지는 주로 밝고 선명한 색감을 사용하되, 장르나 분류에 따라 어두운 색상도 적절히 혼합하세요. \n\n" +
"이미지는 하나이고, 제작된 표지 이미지는 독자의 관심을 끌 수 있도록 세밀하고 몰입감 있게 표현해주세요.",
universeSaveRequest.getName(),
universeSaveRequest.getDescription()
);
}
}
`ImageGenerator.java`
@Component
@RequiredArgsConstructor
public class ImageGenerator {
private final ImageModel imageModel;
private final OpenAiImageProperties openAiImageProperties;
/**
* OpenAI API 를 이용해 이미지를 생성하는 메서드
*
* @param prompt 생성할 이미지에 대한 설명
* @return 생성된 이미지의 Base64 인코딩 문자열
*/
public String createImage(String prompt) {
ImageMessage imageMessage = new ImageMessage(prompt, 1.0f);
OpenAiImageOptions imageOptions = imageOptions();
ImagePrompt imagePrompt = new ImagePrompt(imageMessage, imageOptions);
ImageResponse response = imageModel.call(imagePrompt);
if (response.getResults() != null && !response.getResults().isEmpty()) {
return response.getResult().getOutput().getB64Json();
} else {
throw new RuntimeException();
}
}
private OpenAiImageOptions imageOptions() {
return OpenAiImageOptions.builder()
.withModel(openAiImageProperties.image().options().model().getValue())
.withResponseFormat(openAiImageProperties.image().options().responseFormat())
.withQuality(openAiImageProperties.image().options().quality())
.build();
}
}
5. 응답 결과
❤️🔥 개인적인 소감
1. Java 환경에서도 AI를 유연하게 활용할 수 있었다.
이전에는 주로 파이썬을 사용하여 AI를 활용했다면, 이제는 Spring AI를 통해 Java 환경에서도 AI를 유연하게 활용 가능하다.
2. 스프링의 추상화를 활용하여 간편하게 AI 모델을 사용할 수 있었다.
직접 URL을 호출하여 AI 모델을 사용하는 것보다 Spring AI를 통해 더 간편하게 요청을 보낼 수 있고, 코드도 더 간결해서 개발 생산성을 높일 수 있다. Gemini, ChatGPT 등 여러 AI 모델들을 통함하여 사용하기에도 편리하다.
3. JSON 응답을 파싱하는 과정이 생략되어 비교적 간편했다.
이전에는 JSON 응답에서 필요한 정보를 직접 추출하는 번거로움이 있었지만, Spring AI를 사용하면 정해진 결과값만 반환되어 JSON 응답을 파싱하는 과정이 생략된다.
'✍️ 개발 기록' 카테고리의 다른 글
[👀 Owing] OpenAI ChatGPT로 프롬프트 생성하기 (feat. 프롬프트 작성팁) (0) | 2024.11.12 |
---|---|
[👀 Owing] OpenAI 이미지 생성 후 S3에 안전하게 저장하기 (feat. AWS S3에 Base64 이미지 저장) (0) | 2024.11.08 |
[👀 Owing] PostgreSQL 도입기 (feat. MySQL과의 차이) (0) | 2024.11.05 |
[👀 Owing] Java Record 도입기 ☕️ (0) | 2024.11.05 |
[👀 Owing] 멀티모듈 - 모듈 간 순환참조 이슈 해결 과정 💥 (0) | 2024.11.05 |