내일배움캠프

[내일배움캠프] Spring AI

munsik22 2026. 6. 9. 16:39

Spring AI 전체 개발 모델

Spring AI는 LLM을 호출하는 단순 래퍼가 아니라 Spring 애플리케이션에서 AI 모델과 외부 지식을 연결하기 위한 추상화 계층이다. 백엔드 개발자는 모델 제공자별 API를 직접 다루기보다 Spring AI의 다양한 구성 요소들을 조합해서 애플리케이션 구조를 만든다.

Spring Boot 애플리케이션 안에서 AI 기능이 연결되는 방식

디렉토리 구조 예시를 들자면 아래와 같다.

src/main/java/com/example/springai
├── chat
│   ├── ChatController.java
│   ├── ChatService.java
│   └── dto
├── prompt
│   └── PromptCatalog.java
├── structured
│   ├── InquiryAnalysisResponse.java
│   └── InquiryAnalysisService.java
├── rag
│   ├── DocumentIngestionService.java
│   ├── RagChatService.java
│   └── RagController.java
├── tool
│   ├── OrderTools.java
│   └── ToolChatService.java
└── agent
    └── AgentRouterService.java

백엔드 계층에서 AI를 배치하는 방법

AI 호출은 단순 외부 API 호출처럼 보이지만 실제로는 프롬프트 정책, 응답 형식, 실패 처리, 비용, 로그, 도구 권한, 문서 검색 품질이 함께 얽히기 때문에 별도의 AiService 계층으로 나누는 것이 유리하다.

  • 기본 요청 DTO
public record ChatRequest(String message) {
}
  • 기본 응답 DTO
public record ChatResponse(String answer) {
}
  • Controller 예시
@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @PostMapping
    public ChatResponse chat(@RequestBody ChatRequest request) {
        return new ChatResponse(chatService.ask(request.message()));
    }
}

 

AI 기능도 결국 HTTP 요청을 받고, 서비스 계층에서 처리하고, DTO로 응답하는 일반적인 Spring Boot의 응답 구조에 들어간다.

ChatClient 요청 시퀀스

ChatClient는 Spring AI에서 LLM과 대화하기 위한 대표적인 진입점이다. Spring 개발자가 WebClient나 RestClient처럼 사용할 수 있는 AI 호출 클라이언트라고 하면 아마도 이해하기 쉽다.

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String ask(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

 

실무에서는 ChatClient를 매번 새로 만들기보다 기본 시스템 프롬프트 또는 공통 Advisor를 붙인 Bean으로 구성해두는 경우가 많다.

@Configuration
public class AiClientConfig {

    @Bean
    ChatClient serviceChatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("""
                        당신은 Spring Boot 백엔드 개발자를 돕는 AI 어시스턴트입니다.
                        답변은 한국어로 작성하고, 모르는 내용은 추측하지 않습니다.
                        코드 예시는 설명 가능한 최소 단위로 제공합니다.
                        """)
                .build();
    }
}
@Service
public class GuideChatService {

    private final ChatClient serviceChatClient;

    public GuideChatService(ChatClient serviceChatClient) {
        this.serviceChatClient = serviceChatClient;
    }

    public String answer(String question) {
        return serviceChatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

프롬프트 엔지니어링 레이어

public class PromptCatalog {

    public static String customerSupportPrompt(String product, String question) {
        var template = new PromptTemplate("""
                당신은 {product} 서비스의 고객지원 담당자입니다.
                사용자의 질문을 읽고, 다음 기준으로 답변하세요.

                기준:
                - 확인되지 않은 정책은 단정하지 않는다.
                - 사용자가 바로 할 수 있는 다음 행동을 제안한다.
                - 답변은 5문장 이내로 작성한다.

                사용자 질문:
                {question}
                """);

        return template.render(Map.of(
                "product", product,
                "question", question
        ));
    }
}
@Service
public class PromptedChatService {

    private final ChatClient chatClient;

    public PromptedChatService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String answerSupportQuestion(String product, String question) {
        String prompt = PromptCatalog.customerSupportPrompt(product, question);

        return chatClient.prompt()
                .user(prompt)
                .call()
                .content();
    }
}

Structured Output 변환 흐름

  • AI 응답을 DTO로 받기
public record InquiryAnalysisResponse(
        String intent,
        String summary,
        String riskLevel,
        String nextAction
) {}
@Service
public class InquiryAnalysisService {

    private final ChatClient chatClient;

    public InquiryAnalysisService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public InquiryAnalysisResponse analyze(String message) {
        return chatClient.prompt()
                .system("""
                        고객 문의를 분석하는 분류기입니다.
                        intent는 BILLING, BUG, ACCOUNT, GENERAL 중 하나로 작성하세요.
                        riskLevel은 LOW, MEDIUM, HIGH 중 하나로 작성하세요.
                        nextAction은 담당자가 바로 실행할 수 있는 행동으로 작성하세요.
                        """)
                .user(message)
                .call()
                .entity(InquiryAnalysisResponse.class);
    }
}
  • 예상 응답 형태
{
  "intent": "BUG",
  "summary": "사용자가 로그인 후 결제 내역을 확인하지 못하고 있다.",
  "riskLevel": "MEDIUM",
  "nextAction": "계정 ID와 결제 시각을 확인한 뒤 결제 이력 API를 조회한다."
}
질문 대답
LLM 호출은 어디에 둬야 하는가? AI Service 계층
프롬프트는 어떻게 관리해야 하는가? 문자열 흩뿌리기가 아니라 템플릿과 정책으로 관리
응답은 어떻게 서비스 로직에 연결하는가? DTO 기반 Structured Output
DB/API 조회는 어떻게 시키는가? Tool Calling
Agent는 언제 필요한가? RAG와 Tool을 선택적으로 조합해야 할 때

[참고자료]

  • 내일배움캠프 Spring AI - RAG 강의자료