관리 메뉴

The Nirsa Way

[Spring AI] #1. LLM 호출 실습 시작 – 셋팅 및 전체 구조 먼저 실행해보기 본문

Development/Spring AI

[Spring AI] #1. LLM 호출 실습 시작 – 셋팅 및 전체 구조 먼저 실행해보기

KoreaNirsa 2025. 6. 24. 20:21
반응형
※ 인프런 강의 Spring AI 실전 가이드: RAG 챗봇 만들기를 실습하는 내용입니다.
※ 해당 강의 코드를 코틀린 → 자바로 언어를 바꿔 진행하기 때문에 일부 코드 및 구현부가 다를 수 있습니다.
※ 실습이지만 코드를 직접 까보는 내용을 기록하는 포스팅이므로 강의 내용과 상이할 수 있습니다.

[Spring AI] #1. LLM 호출 실습 시작 – 구조 먼저 실행해보기
[Spring AI] #2. AiConfig 분석 – OpenAI API 연결 설정 방법
[Spring AI] #3-1. ChatService 분석 – 메시지 생성과 모델 호출 흐름
[Spring AI] #3-2. ChatService 분석 – buildRequestPrompt()로 요청 준비하기
[Spring AI] #3-3. ChatService 분석 – internalCall()로 요청과 응답 재구성 흐름 파악하기
[Spring AI] #4. ChatController 분석 – 요청 처리와 응답 흐름 정리

 

 

Spring AI란?

Spring 개발자들을 위한 LLM 통합 도구로써 OpenAI, Anthropic, Google AI 등 다양한 AI 모델을 Spring 스타일로 쉽게 연동하고 활용할 수 있도록 지원하는 Spring 공식 프레임워크 입니다. 아래의 4가지 컴포넌트를 중심으로 구현할 수 있습니다.

  1. PromptTemplate
    ☞ 프롬프트를 템플릿으로 관리 가능
  2. ChatClient / EmbeddingClient 인터페이스
    ☞ LLM 서비스 호출을 추상화하여 관리
    ☞ ex. ChatClient : OpenAI와의 채팅, EmbeddingClient : 임베디 생성 등
  3. Service Provider 구현체
    ☞ 내부적으로 Open AI 등의 클라이언트를 구현하여 제공
  4. Spring Configuration 기반 설정
    ☞ application.yml에서 API Key, Model, Endpoint 설정 가능

그 외에 MCP, Function Calling 등 다양한 기능을 지원해줍니다. 직접 구현하지 않아도 프레임워크에 구현되어있는 기능을 기반으로 틀을 잡고 시작하기에 훨씬 빠르고 안정적으로 접근할 수 있는 방법을 제공합니다.

 


 

LLM(Large Language Model) 이란?

방대한 양의 텍스트 데이터를 학습하여 사람이 쓰는 자연어를 이해하고 생성이 가능한 AI 모델입니다. 이러한 특성으로 인해 다양한 질문과 프롬프트에 대해 응답을 생성할 수 있습니다. 대표적인 LLM 모델로는 GPT, Claude, Gemini 등이 있으며 번역, 요약, 코드 생성, 질의응답 등 다양한 언어 기반 작업에 폭넓게 활용되고 있습니다.

이러한 배경을 바탕으로 이번 포스팅에서는 Spring AI를 사용하여 LLM 활용하기, 즉 LLM 모델과 상호작용하여 질의를 하고 원하는 응답을 받아오는 구현 예제를 다룹니다.

위에서 언급했듯이 해당 포스팅은 인프런 강의 Spring AI 실전 가이드: RAG 챗봇 만들기 를 참고하여 실습중이며 해당 강의에서 사용되는 코틀린 대신 자바로 구현한 코드입니다. 무료 강의이기 때문에 관심이 있으신 분들은 해당 강의를 보시는것도 좋을 것 같습니다.

 


 

OpenAI Key 발급받기

https://platform.openai.com/api-keys 으로 이동하여 OpenAI Key를 발급 받습니다. (2025년 6월 21일 기준으로 $18 크레딧이 제공됩니다) 발급받은 API Key는 따로 저장하여 관리해주세요.

 

현재재 날짜 기준으로 크레딧을 지급해주지 않기 때문에 저는 그냥 학습비용이라 생각하고 5달러를 결제하여 사용했습니다. https://platform.openai.com/settings/organization/billing/overview 에서 카드 등록하여 진행하였습니다. 

추후 코드에서 구현할 때 mini 모델로 사용하실 경우 무료로 가능하기에 굳이 크레딧을 충전할 필요는 없을 것 같습니다. (2025년 6월 24일 기준)

 


 

build.gradle / application.properties / 패키지 구조

프로젝트는 스프링 부트로 생성하시면 되고 사용할 build.gradle은 아래와 같습니다. 스웨거를 통해 LLM을 호출하여 진행합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    implementation 'org.springframework.ai:spring-ai-openai:1.0.0-M6'
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M6'
    
    implementation 'org.projectlombok:lombok:1.18.38'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 

application.properties에 위에서 발급 받았던 API Key를 넣어주세요.

spring.ai.openai.api-key=API키

 

저의 경우 패키지 구조는 아래와 같이 진행합니다. 크게 config, controller, dto, service 4개의 패키지로 진행합니다.


 

kr.co.ai.config.AiConfig
package co.kr.ai.config;

import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {
    @Value("${spring.ai.openai.api-key}")
    private String apiKey;

    @Bean
    public OpenAiApi openAiApi() {
        return OpenAiApi.builder()
                .apiKey(apiKey)
                .build();
    }
}

 


 

kr.co.ai.config.ApiConfig
package co.kr.ai.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;

@Configuration
class OpenApiConfig {

    @Bean
    public OpenAPI springOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring AI Tutorial API")
                        .version("1.0")
                        .description("Spring AI를 활용한 챗봇 API"));
    }
}

 


 

kr.co.ai.controller.ChatController
package co.kr.ai.controller;

import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import co.kr.ai.dto.ChatRequest;
import co.kr.ai.dto.Response;
import co.kr.ai.service.ChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/v1/chat")
@RequiredArgsConstructor
@Tag(name = "Chat API", description = "OpenAI API를 통한 채팅 기능")
public class ChatController {
    private final ChatService chatService;
    private static final Logger logger = LoggerFactory.getLogger(ChatController.class);

    @Operation(
            summary = "LLM 채팅 메시지 전송",
            description = "사용자의 메시지를 받아 OpenAI API를 통해 응답을 생성합니다.",
            responses = {
                @ApiResponse(
                    responseCode = "200",
                    description = "LLM 응답 성공",
                    content = @Content(schema = @Schema(implementation = ApiResponse.class))
                ),
                @ApiResponse(responseCode = "400", description = "잘못된 요청"),
                @ApiResponse(responseCode = "500", description = "서버 오류")
            }
        )
        @PostMapping("/query")
    public ResponseEntity<Response<Map<String, Object>>> sendMessage(
            @Parameter(description = "채팅 요청 객체", required = true)
            @RequestBody ChatRequest request) {

        logger.info("Chat API 요청 받음: model={}", request.getModel());

        if (request.getQuery() == null || request.getQuery().isBlank()) {
            logger.warn("빈 질의가 요청됨");
            return ResponseEntity.badRequest().body(
                    new Response<>(false, null, "질의가 비어있습니다.")
            );
        }

        try {
            String systemMessage = "You are a helpful AI assistant.";

            var response = chatService.openAiChat(
                    request.getQuery(),
                    systemMessage,
                    request.getModel()
            );

            logger.debug("LLM 응답 생성: {}", response);

            if (response != null) {
                return ResponseEntity.ok(
                        new Response<>(true, Map.of("answer", response.getResult().getOutput().getText()), null)
                );
            } else {
                logger.error("LLM 응답 생성 실패");
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
                        new Response<>(false, null, "LLM 응답 생성 중 오류 발생")
                );
            }
        } catch (Exception e) {
            logger.error("Chat API 처리 중 오류 발생", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
                    new Response<>(false, null, e.getMessage() != null ? e.getMessage() : "알 수 없는 오류 발생")
            );
        }
    }
}

 


 

kr.co.ai.service.ChatService
package co.kr.ai.service;

import java.util.Arrays;
import java.util.List;

import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class ChatService {

    private final OpenAiApi openAiApi;

    /**
     * OpenAI 챗 API를 이용하여 응답을 생성합니다.
     *
     * @param userInput     사용자 입력 메시지
     * @param systemMessage 시스템 프롬프트
     * @param model         사용할 LLM 모델명
     * @return ChatResponse 객체 (실패 시 null)
     */
    public ChatResponse openAiChat(String userInput, String systemMessage, String model) {
        log.debug("OpenAI 챗 호출 시작 - 모델: {}", model);

        try {
            // 메시지 구성
            List<Message> messages = Arrays.asList(
                    new SystemMessage(systemMessage),
                    new UserMessage(userInput)
            );

            // 챗 옵션 구성
            OpenAiChatOptions chatOptions = OpenAiChatOptions.builder()
            		.model(model)
            	    .temperature(0.7d)
            	    .build();

            // 프롬프트 생성
            Prompt prompt = new Prompt(messages, chatOptions);

            // 챗 모델 생성 및 호출
            OpenAiChatModel chatModel = OpenAiChatModel.builder()
                    .openAiApi(openAiApi)
                    .build();

            return chatModel.call(prompt);

        } catch (Exception e) {
            log.error("OpenAI 챗 호출 중 오류 발생: {}", e.getMessage(), e);
            return null;
        }
    }
}

 


 

kr.co.ai.dto.ChatResponse
package co.kr.ai.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@Schema(description = "채팅 요청 데이터 모델")
public class ChatRequest {

    @Schema(description = "사용자 질문", example = "안녕하세요")
    private String query;

    @Schema(description = "사용할 LLM 모델", example = "gpt-3.5-turbo", defaultValue = "gpt-3.5-turbo")
    private String model = "gpt-3.5-turbo";
}

 


 

kr.co.ai.dto.Response
package co.kr.ai.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@AllArgsConstructor
@Schema(description = "API 응답 포맷")
public class Response<T> {

    @Schema(description = "요청 처리 성공 여부")
    private boolean success;

    @Schema(description = "성공 응답 데이터")
    private T data;

    @Schema(description = "실패 오류 메시지")
    private String error;
}

 


 

LLM 호출해보기

서버를 실행하신 후 스웨거 페이지로 이동하여 LLM을 호출 해보시면 됩니다. 저의 경우 기본 포트 8080을 사용중이기에 http://localhost:8080/swagger-ui/index.html 를 입력하면 아래와 같이 스웨거 문서를 확인하실 수 있습니다.

POST를 펼친 후 Try it out을 눌러 요청을 준비합니다.

query는 질문이라고 생각하시면 되며, 모델은 gpt 등 ai 모델이라고 보시면 됩니다. 크레딧 없이 무료로 사용하시는 분들은 mini 모델(gpt-4o-mini 등)을 작성하시면 됩니다. 저는 "현재 시간 가장 핫한 언어를 추천해줘" 라고 질문하였으며 gpt-3.5-turbo 모델을 사용하였습니다.

아래와 같이 응답까지 확인을 할 수 있습니다.

반응형