본문 바로가기

AI 번역

Spring AI 소개

1. 개요 (Overview)

Spring Framework는 Spring AI 프로젝트를 통해 AI 생성 프롬프트의 강력한 기능을 공식적으로 지원하기 시작했습니다.
이 튜토리얼에서는 Spring Boot 애플리케이션에서 생성형 AI를 통합하는 방법을 자세히 소개하고, 필수적인 AI 개념을 익히겠습니다.

또한, Spring AI가 AI 모델과 어떻게 상호 작용하는지 이해하고, 이를 활용한 애플리케이션을 만들어 그 기능을 시연해 보겠습니다.


2. Spring AI의 주요 개념 (Spring AI Main Concepts)

본격적으로 시작하기 전에, 핵심 개념 및 용어를 살펴보겠습니다.

Spring AI는 처음에 자연어 입력을 처리하고 언어 출력을 생성하는 AI 모델을 중심으로 개발되었습니다.
이 프로젝트의 핵심 목표는 애플리케이션 내에서 생성형 AI API를 독립적인 구성 요소로 통합할 수 있도록 추상적인 인터페이스를 제공하는 것입니다.

그중 대표적인 추상화 인터페이스가 **AiClient**이며, 현재 OpenAIAzure OpenAI에 대한 두 가지 기본 구현을 지원합니다.

 

public interface AiClient {
    default String generate(String message);
    AiResponse generate(Prompt prompt);
}

 

**AiClient**는 생성 기능을 위한 두 가지 옵션을 제공합니다.

  1. 간단한 방식 → generate(String message)
    • 문자열(String)을 입력 및 출력으로 사용합니다.
    • Prompt 및 AiResponse 클래스를 사용할 때 발생하는 추가적인 복잡성을 피할 수 있습니다.

이제 두 방식의 차이를 자세히 살펴보겠습니다.

 

2.1. 고급 프롬프트(Advanced Prompt) 및 AiResponse

AI 분야에서 프롬프트(Prompt) 는 AI에게 제공되는 텍스트 메시지를 의미합니다.
이 메시지는 컨텍스트(맥락)와 질문으로 구성되며, AI 모델은 이를 기반으로 응답을 생성합니다.

Spring AI 프로젝트에서는 Prompt매개변수가 포함된 메시지(Messages)의 목록으로 표현됩니다.

 

public class Prompt {
    private final List<Message> messages;
    // constructors and utility methods 
}

public interface Message {
    String getContent();
    Map<String, Object> getProperties();
    MessageType getMessageType();
}

 

Prompt 를 사용하면 개발자가 텍스트 입력을 보다 정교하게 제어할 수 있습니다.

그중 대표적인 예가 프롬프트 템플릿(Prompt Template) 입니다.
이 템플릿은 미리 정의된 텍스트와 자리 표시자(Placeholder) 세트로 구성됩니다.

이후, Message 생성자를 통해 Map<String, Object> 값을 전달하여 자리 표시자를 실제 데이터로 채울 수 있습니다.

 

Tell me a {adjective} joke about {content}.

 

Message 인터페이스는 AI 모델이 처리할 수 있는 다양한 유형의 메시지에 대한 고급 정보를 포함합니다.

예를 들어, OpenAI 구현에서는 대화형 역할을 구분하며, 이는 MessageType을 통해 효과적으로 매핑됩니다.
다른 모델에서는 메시지 형식을 나타내거나, 특정한 사용자 지정 속성(Custom Properties) 을 반영할 수도 있습니다.

자세한 내용은 공식 문서를 참고하세요.

 

public class AiResponse {
    private final List<Generation> generations;
    // getters and setters
}

public class Generation {
    private final String text;
    private Map<String, Object> info;
}

 

AiResponse여러 개의 Generation 객체 목록으로 구성되며, 각 객체는 해당 프롬프트에 대한 출력 데이터를 포함합니다.

또한, Generation 객체AI 응답의 메타데이터 정보를 제공하여, AI가 생성한 내용에 대한 추가적인 컨텍스트를 확인할 수 있습니다.

그러나 Spring AI 프로젝트는 아직 베타 버전이므로, 모든 기능이 완전히 구현되거나 문서화되지 않았습니다.
진행 상황은 GitHub 저장소의 이슈(issues) 를 통해 확인할 수 있습니다.

 

3. Spring AI 프로젝트 시작하기 (Getting Started With the Spring AI Project)

우선, **AiClient**는 OpenAI 플랫폼과의 모든 통신을 위해 API 키가 필요합니다.
따라서, 먼저 API Keys 페이지에서 토큰을 생성해야 합니다.

Spring AI 프로젝트에서는 spring.ai.openai.api-key라는 설정 속성을 정의하고 있습니다.
이를 application.yml 파일에 설정하여 API 키를 구성할 수 있습니다.

 

spring:
  ai:
    openai.api-key: ${OPEN_AI_KEY}

 

다음 단계는 의존성 저장소(repository) 를 설정하는 것입니다.
Spring AI 프로젝트Spring Milestone Repository에 아티팩트를 제공합니다.

따라서, 저장소 정의(repository definition) 를 추가해야 합니다.

 

<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <releases>
            <enabled>false</enabled>
        </releases>
    </repository>
</repositories>

 

그 후, open-ai-spring-boot-starter 의존성을 프로젝트에 추가할 준비가 완료됩니다.

 

<dependency>
    <groupId>org.springframework.experimental.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0-M1</version>
</dependency>

 

Spring AI 프로젝트는 현재 활발히 개발 중이므로, 최신 버전은 공식 GitHub 페이지에서 확인하는 것이 좋습니다.

이제 개념을 실제로 적용해보겠습니다.


4. Spring AI 실습 (Spring AI in Action)

간단한 REST API를 작성하여 Spring AI의 기능을 시연해보겠습니다. 이 API는 두 가지 엔드포인트를 제공하며, 각각 주제와 장르에 맞는 시를 반환합니다:

  • /ai/cathaiku — 기본 generate() 메서드를 구현하여 고양이에 관한 Haiku를 문자열로 반환합니다.
  • /ai/poetry?theme={{theme}}&genre={{genre}} — PromptTemplate 및 AiResponse 클래스의 기능을 시연합니다.

4.1. Spring Boot 애플리케이션에 AiClient 주입하기 (Injecting AiClient in Spring Boot Application)

간단하게 고양이 하이쿠 엔드포인트부터 시작해보겠습니다.

우리는 @RestController 어노테이션을 사용하여 PoetryController 클래스를 설정하고, GET 메서드를 매핑합니다:

 

@RestController
@RequestMapping("ai")
public class PoetryController {
    private final PoetryService poetryService;

    // constructor

    @GetMapping("/cathaiku")
    public ResponseEntity<String> generateHaiku(){
        return ResponseEntity.ok(poetryService.getCatHaiku());
    }
}

 

다음으로, DDD (Domain-Driven Design) 개념에 따라 서비스 계층(Service Layer) 에서 모든 도메인 로직을 정의합니다.
generate() 메서드를 호출하는 작업을 처리하기 위해, **AiClient**를 PoetryService에 주입하면 됩니다. 이제, **Haiku 생성 요청을 위한 String prompt**를 정의할 수 있습니다.

 

@Service
public class PoetryServiceImpl implements PoetryService {
    public static final String WRITE_ME_HAIKU_ABOUT_CAT = """
        Write me Haiku about cat,
        haiku should start with the word cat obligatory""";

    private final AiClient aiClient;

    // constructor

    @Override
    public String getCatHaiku() {
        return aiClient.generate(WRITE_ME_HAIKU_ABOUT_CAT);
    }
}

 

엔드포인트가 설정되어 요청을 받을 준비가 되었습니다. 요청이 들어오면, 응답으로 단순 문자열이 포함된 결과를 반환합니다.

 

Cat prowls in the night,
Whiskers twitch with keen delight,
Silent hunter's might.

 

지금까지는 잘 진행되었습니다. 하지만 현재 솔루션에는 몇 가지 단점이 있습니다. 우선, 단순 문자열로 응답을 반환하는 것은 REST 계약 관점에서 최선의 해결책이 아닙니다.

게다가, 항상 같은 프롬프트로 ChatGPT를 호출하는 데에는 큰 의미가 없습니다. 그래서 다음 단계로, themegenre와 같은 파라미터화된 값을 추가하려고 합니다. 이때 **PromptTemplate**이 가장 유용하게 사용될 것입니다.


4.2. PromptTemplate을 통한 구성 가능한 쿼리 (Configurable Queries With PromptTemplate)

PromptTemplate 은 본질적으로 **StringBuilder**와 **dictionary**의 조합처럼 작동합니다.
/cathaiku 엔드포인트와 유사하게, 우선 프롬프트의 기본 문자열을 정의합니다. 이번에는 **theme**과 **genre**와 같은 값을 **자리 표시자(placeholder)**로 정의하고, 이를 실제 값으로 채우는 방식입니다.

 

String promptString = """
    Write me {genre} poetry about {theme}
    """;
PromptTemplate promptTemplate = new PromptTemplate(promptString);
promptTemplate.add("genre", genre);
promptTemplate.add("theme", theme);

 

다음으로, 엔드포인트 출력을 표준화하고자 합니다. 이를 위해 간단한 PoetryDto 레코드 클래스를 도입할 것입니다. 이 클래스는 시의 제목(title), 이름(name), **장르(genre)**를 포함할 것입니다.

public record PoetryDto (String title, String poetry, String genre, String theme){}

 

PoetryDtoBeanOutputParser 클래스에 등록하는 추가 단계를 진행해보겠습니다. 이 클래스는 OpenAI API 출력을 직렬화하고 역직렬화하는 기능을 제공합니다. 이를 통해 PromptTemplate에서 사용할 때, 메시지를 DTO 객체로 직렬화할 수 있게 됩니다.

최종적으로, 우리의 생성 함수는 아래와 같이 구현될 것입니다:

 

@Override
public PoetryDto getPoetryByGenreAndTheme(String genre, String theme) {
    BeanOutputParser<PoetryDto> poetryDtoBeanOutputParser = new BeanOutputParser<>(PoetryDto.class);

    String promptString = """
        Write me {genre} poetry about {theme}
        {format}
    """;

    PromptTemplate promptTemplate = new PromptTemplate(promptString);
    promptTemplate.add("genre", genre);
    promptTemplate.add("theme", theme);
    promptTemplate.add("format", poetryDtoBeanOutputParser.getFormat());
    promptTemplate.setOutputParser(poetryDtoBeanOutputParser);

    AiResponse response = aiClient.generate(promptTemplate.create());

    return poetryDtoBeanOutputParser.parse(response.getGeneration().getText());
}

 

이제 클라이언트가 받는 응답은 훨씬 더 잘 구성되어 있으며, 더 중요한 점은 REST API 표준과 모범 사례에 부합한다는 것입니다.

 

{
    "title": "Dancing Flames",
    "poetry": "In the depths of night, flames dance with grace,
       Their golden tongues lick the air with fiery embrace.
       A symphony of warmth, a mesmerizing sight,
       In their flickering glow, shadows take flight.
       Oh, flames so vibrant, so full of life,
       Burning with passion, banishing all strife.
       They consume with ardor, yet do not destroy,
       A paradox of power, a delicate ploy.
       They whisper secrets, untold and untamed,
       Their radiant hues, a kaleidoscope unnamed.
       In their gentle crackling, stories unfold,
       Of ancient tales and legends untold.
       Flames ignite the heart, awakening desire,
       They fuel the soul, setting it on fire.
       With every flicker, they kindle a spark,
       Guiding us through the darkness, lighting up the dark.
       So let us gather 'round, bask in their warm embrace,
       For in the realm of flames, magic finds its place.
       In their ethereal dance, we find solace and release,
       And in their eternal glow, our spirits find peace.",
    "genre": "Liric",
    "theme": "Flames"
}

 

5. Error Handling

Spring AI 프로젝트는 OpenAI 오류를 OpenAiHttpException 클래스를 통해 추상화하여 처리합니다. 다만, 오류 유형별로 클래스를 개별적으로 매핑하는 방식은 제공하지 않습니다. 하지만 이러한 추상화 덕분에, **RestControllerAdvice**를 사용하여 모든 예외를 하나의 핸들러에서 처리할 수 있습니다.

아래 코드는 Spring 6 FrameworkProblemDetail 표준을 사용하여 오류를 표준화된 방식으로 처리하는 예시입니다. ProblemDetail 표준에 대해 더 자세히 알고 싶다면, Spring 공식 문서를 참조하세요.

 

@RestControllerAdvice
public class ExceptionTranslator extends ResponseEntityExceptionHandler {
    public static final String OPEN_AI_CLIENT_RAISED_EXCEPTION = "Open AI client raised exception";

    @ExceptionHandler(OpenAiHttpException.class)
    ProblemDetail handleOpenAiHttpException(OpenAiHttpException ex) {
        HttpStatus status = Optional
          .ofNullable(HttpStatus.resolve(ex.statusCode))
          .orElse(HttpStatus.BAD_REQUEST);
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
        problemDetail.setTitle(OPEN_AI_CLIENT_RAISED_EXCEPTION);
        return problemDetail;
    }
}

 

이제 OpenAI API 응답에 오류가 포함된 경우, 이를 처리하는 방법을 구현할 수 있습니다.

 

{
    "type": "about:blank",
    "title": "Open AI client raised exception",
    "status": 401,
    "detail": "Incorrect API key provided: sk-XG6GW***************************************wlmi. 
       You can find your API key at https://platform.openai.com/account/api-keys.",
    "instance": "/ai/cathaiku"
}

 

가능한 모든 예외 상태의 완전한 목록은 공식 문서 페이지에 있습니다.

 

6. 결론


이 글에서는 Spring AI 프로젝트와 그 기능을 REST API의 맥락에서 살펴보았습니다. 이 글이 작성될 당시, spring-ai-starter는 여전히 활발히 개발 중이었고 스냅샷 버전으로 제공되고 있었지만, Spring Boot 애플리케이션에 생성적 AI 통합을 위한 신뢰할 수 있는 인터페이스를 제공했습니다.

이 글에서는 Spring AI의 기본 및 고급 통합을 다루었으며, AiClient가 내부적으로 어떻게 작동하는지에 대해서도 설명했습니다. 개념 증명을 위해 우리는 기본적인 REST 애플리케이션을 구현했으며, 그 애플리케이션은 시를 생성합니다. 기본적인 생성 엔드포인트 예시와 함께, 우리는 Spring AI의 고급 기능인 PromptTemplate, AiResponse, BeanOutputParser를 사용한 샘플을 제공했습니다. 또한 오류 처리 기능도 구현했습니다.

전체 예제는 GitHub에서 확인할 수 있습니다.

 

 

출처 : https://www.baeldung.com/spring-ai

'AI 번역' 카테고리의 다른 글

Spring AI에서 구조화된 출력 가이드  (0) 2025.03.27
목차 spring-ai  (0) 2025.03.20