Blog / Java/Kotlin / Spring AI로 OpenAI 연동하기 : Kotlin + Spring Boot

Spring AI로 OpenAI 연동하기 : Kotlin + Spring Boot

개발했던 소스코드는 여기에

Spring AI는 Spring 생태계에서 AI 모델을 손쉽게 연동할 수 있도록 설계된 공식 프레임워크다. OpenAI, Anthropic, Ollama 등 다양한 LLM을 추상화된 인터페이스로 다룰 수 있다. Spring Boot 3.x + Kotlin 환경에서 Spring AI를 사용해 OpenAI ChatGPT API를 연동하는 전체 과정을 기록해둔다.


환경 정보
항목 버전
Spring Boot 3.5.7
Spring AI 1.0.3
Spring Cloud 2025.0.0
Kotlin 2.0.21
Java 21

1. 의존성 설정

Gradle (Kotlin DSL) 기준

kotlin
// build.gradle.kts

extra["springAiVersion"] = "1.0.3"
extra["springCloudVersion"] = "2025.0.0"

repositories {
    mavenCentral()
    maven { url = uri("https://repo.spring.io/milestone") }
    maven { url = uri("https://repo.spring.io/snapshot") }
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")

    // Spring AI - OpenAI 연동에 필요한 두 가지 의존성
    implementation("org.springframework.ai:spring-ai-openai")
    implementation("org.springframework.ai:spring-ai-starter-model-openai")
}

dependencyManagement {
    imports {
        mavenBom("org.springframework.ai:spring-ai-bom:${property("springAiVersion")}")
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}

핵심 포인트:

  • spring-ai-openai: Spring AI의 OpenAI 구현체 (ChatClient 등의 구현 포함)
  • spring-ai-starter-model-openai: Auto-configuration을 포함하는 스타터. application.yml에 API 키만 설정하면 ChatClient.Builder가 자동으로 빈 등록된다.
  • Spring AI는 정식 릴리스 전에는 milestone/snapshot 레포지터리를 통해 배포되었으므로, 사용하는 버전에 따라 레포지터리 설정을 확인해야 한다.

2. API 키 설정

application.yml에 OpenAI API 키와 사용할 모델을 지정한다.

yaml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}   # 환경변수로 주입 권장
      chat:
        options:
          model: gpt-4o            # 사용할 모델 지정
    chat:
      client:
        enabled: true              # ChatClient Auto-configuration 활성화

API 키는 절대 코드에 하드코딩하지 말고, 환경변수 또는 Spring Cloud Config 등으로 주입한다.


3. ChatClient Bean 설정

Spring AI의 핵심 인터페이스는 ChatClient다. spring-ai-starter-model-openai가 클래스패스에 있으면 ChatClient.Builder가 자동으로 등록된다. 이 Builder를 주입받아 ChatClient 빈을 만들면 된다.

kotlin
// config/ChatClientConfig.kt

package com.walter.config

import org.springframework.ai.chat.client.ChatClient
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class ChatClientConfig {

    @Bean
    fun openAiChatClient(builder: ChatClient.Builder): ChatClient {
        return builder.build()
    }
}

ChatClient.Builder는 기본 시스템 프롬프트, 옵션(temperature, maxTokens 등)을 빌더 단계에서 미리 설정할 수 있다. 예를 들어 시스템 역할을 고정하고 싶다면 다음과 같이 쓸 수 있다.

kotlin
@Bean
fun openAiChatClient(builder: ChatClient.Builder): ChatClient {
    return builder
        .defaultSystem("당신은 친절한 한국어 어시스턴트입니다.")
        .defaultOptions(
            OpenAiChatOptions.builder()
                .model("gpt-4o")
                .temperature(0.7)
                .build()
        )
        .build()
}

4. 서비스 레이어 구현

ChatClient를 주입받아 실제 API 호출 로직을 구현한다.

kotlin
// service/OpenAiService.kt

package com.walter.service

import com.walter.controller.dto.request.OpenAiChatResponse
import org.apache.commons.lang3.StringUtils
import org.springframework.ai.chat.client.ChatClient
import org.springframework.ai.chat.messages.UserMessage
import org.springframework.stereotype.Service

@Service
class OpenAiService(
    private val openAiChatClient: ChatClient
) {

    fun getChatCompletion(userComment: String): OpenAiChatResponse {
        val message = UserMessage.builder()
            .text(userComment)
            .build()

        val completion = openAiChatClient.prompt()
            .messages(listOf(message))
            .call()
            .content() ?: StringUtils.EMPTY

        return OpenAiChatResponse(completion)
    }
}

흐름 설명:

  1. UserMessage.builder().text(...) — 사용자 입력을 Message 객체로 만든다.
  2. chatClient.prompt().messages(...).call() — 메시지 리스트를 포함한 프롬프트를 구성하고 OpenAI API를 동기 호출한다.
  3. .content() — 응답에서 텍스트 컨텐츠를 추출한다. 응답이 없을 경우를 대비해 null 처리를 해준다.

Spring AI의 ChatClient fluent API는 시스템 메시지, 유저 메시지, 어시스턴트 메시지를 자유롭게 조합할 수 있다.

kotlin
// 시스템 + 유저 메시지를 함께 전달하는 예시
val completion = openAiChatClient.prompt()
    .system("당신은 코드 리뷰 전문가입니다.")
    .user(userComment)
    .call()
    .content()

5. REST API 레이어 구현

DTO와 컨트롤러를 구성해 외부에 HTTP API로 노출한다.

DTO 정의
kotlin
// controller/dto/request/OpenAiChatRequest.kt
data class OpenAiChatRequest(
    val prompt: String
)

// controller/dto/request/OpenAiChatResponse.kt
data class OpenAiChatResponse(
    val completion: String
)
공통 응답 래퍼
kotlin
// controller/dto/Rest.kt
import java.time.LocalDateTime

data class Rest<T>(
    val timeStamp: LocalDateTime,
    val data: T
) {
    companion object {
        fun <T> ok(data: T): Rest<T> = Rest(LocalDateTime.now(), data)
    }
}
컨트롤러
kotlin
// controller/OpenAiController.kt

package com.walter.controller

import com.walter.controller.dto.Rest
import com.walter.controller.dto.request.OpenAiChatRequest
import com.walter.controller.dto.request.OpenAiChatResponse
import com.walter.service.OpenAiService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class OpenAiController(
    private val openAiService: OpenAiService
) {

    @PostMapping("/open-ai/request")
    fun getOpenAiResponse(
        @RequestBody openAiChatRequest: OpenAiChatRequest
    ): Rest<OpenAiChatResponse> {
        val response = openAiService.getChatCompletion(openAiChatRequest.prompt)
        return Rest.ok(response)
    }
}

6. 실행 및 테스트
애플리케이션 실행
bash
# 환경변수로 API 키를 주입하면서 실행
OPENAI_API_KEY=sk-... ./gradlew bootRun
API 호출 예시
bash
curl -X POST http://localhost:8080/open-ai/request \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Spring AI가 무엇인지 한 문장으로 설명해줘"}'
응답 예시
json
{
  "timeStamp": "2026-03-19T10:30:00",
  "data": {
    "completion": "Spring AI는 Spring 생태계에서 AI 언어 모델과 쉽게 통합할 수 있도록 지원하는 공식 프레임워크입니다."
  }
}

7. Spring AI의 핵심 추상화 이해

Spring AI가 강력한 이유는 모델에 독립적인 추상화 레이어를 제공한다는 점이다.

text
ChatClient (추상화 인터페이스)
    ├── OpenAI ChatClient (spring-ai-openai)
    ├── Anthropic ChatClient (spring-ai-anthropic)
    ├── Ollama ChatClient (spring-ai-ollama)
    └── ...

OpenAiService의 코드는 ChatClient만 알고 있다. 즉, OpenAI에서 Anthropic Claude로 바꾸고 싶다면 의존성과 설정만 교체하면 서비스 코드를 수정할 필요가 없다.

Spring AI 개념 역할
ChatClient LLM과 대화하는 핵심 인터페이스
ChatClient.Builder ChatClient 생성 빌더. Auto-configuration이 제공
Prompt 메시지 목록 + 옵션을 묶은 요청 객체
Message SystemMessage, UserMessage, AssistantMessage
ChatResponse LLM의 전체 응답 (메타데이터 포함)

8. 스트리밍 응답 처리

call() 대신 stream()을 사용하면 SSE(Server-Sent Events) 방식의 스트리밍 응답을 처리할 수 있다.

kotlin
fun getChatCompletionStream(userComment: String): Flux<String> {
    return openAiChatClient.prompt()
        .user(userComment)
        .stream()
        .content()
}

컨트롤러에서는 MediaType.TEXT_EVENT_STREAM_VALUE로 반환하면 된다.

kotlin
@GetMapping("/open-ai/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun streamResponse(@RequestParam prompt: String): Flux<String> {
    return openAiService.getChatCompletionStream(prompt)
}

마무리

Spring AI는 복잡한 OpenAI SDK 설정을 추상화하고, Spring의 의존성 주입 및 Auto-configuration 메커니즘 위에서 동작하기 때문에 기존 Spring 개발자라면 매우 낮은 학습 비용으로 LLM을 연동할 수 있다.

이 프로젝트처럼 핵심 구조는 단순하다.

  1. 의존성 추가 (spring-ai-starter-model-openai)
  2. API 키 설정 (application.yml)
  3. ChatClient 빈 등록 (Builder 주입받아 build())
  4. 서비스에서 ChatClient 사용 (prompt().user(...).call().content())

이 네 단계만으로 운영 가능한 LLM 연동 API를 만들 수 있다. 이후에는 RAG(Retrieval-Augmented Generation), Function Calling, Vector Store 연동 등 Spring AI의 고급 기능으로 확장할 수 있다.


참고
Written by
author
풍우래기

여행을 좋아하는 집돌이 개발자입니다.

블로그에 새로운 글이 발행되었습니다.