본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 1. 31. · 9 Views
ChatClient 기초 완벽 가이드
Spring AI의 ChatClient를 활용하여 AI 챗봇을 구축하는 방법을 초급자 눈높이에서 설명합니다. 빌더 패턴부터 실전 챗봇 구현까지 실무 예제로 쉽게 배워봅시다.
목차
1. ChatClient 인터페이스 이해
2-3문장으로 상황 설정 김개발 씨는 회사에서 AI 챗봇 프로젝트를 맡게 되었습니다. "Spring AI를 사용하면 쉽다던데..." 막상 문서를 펼쳐보니 ChatClient라는 낯선 인터페이스가 눈에 들어왔습니다.
이게 도대체 무엇이고 왜 필요한 걸까요?
핵심 개념을 3-4문장으로 ChatClient는 Spring AI에서 제공하는 대화형 AI 모델과 통신하기 위한 핵심 인터페이스입니다. 마치 레스토랑에서 주문을 받아주는 웨이터처럼, 개발자와 AI 모델 사이에서 메시지를 주고받는 역할을 합니다.
이것을 제대로 이해하면 복잡한 AI 통신 로직을 간단하게 처리할 수 있습니다.
다음 코드를 살펴봅시다.
// ChatClient 기본 사용 예제
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class AiService {
private final ChatClient chatClient;
// ChatClient를 주입받습니다
public AiService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
// 간단한 질문-응답 처리
public String askQuestion(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
이북처럼 술술 읽히는 설명 김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 평소 Spring Boot로 REST API를 개발하던 그에게 갑자기 AI 챗봇 프로젝트가 떨어졌습니다.
"AI라니... 어렵지 않을까?" 걱정이 앞섰습니다.
팀장님이 다가와 안심시켜 주셨습니다. "걱정 마세요.
Spring AI의 ChatClient를 사용하면 생각보다 간단합니다. 기존에 RestTemplate이나 WebClient 사용해봤죠?
그것과 비슷합니다." ChatClient란 무엇인가 쉽게 비유하자면, ChatClient는 마치 통역사와 같습니다. 여러분이 한국어로 말하면 통역사가 영어로 번역해서 외국인에게 전달하고, 다시 외국인의 답변을 한국어로 번역해서 돌려주는 것처럼, ChatClient는 개발자의 요청을 AI 모델이 이해할 수 있는 형태로 변환하고, AI 모델의 응답을 다시 개발자가 사용하기 쉬운 형태로 바꿔줍니다.
더 정확히 말하면, ChatClient는 인터페이스입니다. 인터페이스라는 것은 약속이자 규격입니다.
이 약속을 따르면 OpenAI의 GPT든, Google의 Gemini든, 어떤 AI 모델이든 동일한 방식으로 대화할 수 있습니다. 왜 필요한가 ChatClient가 없던 시절을 상상해봅시다.
개발자들은 각 AI 모델의 API를 직접 호출해야 했습니다. OpenAI를 사용하면 OpenAI의 REST API 문서를 보고, JSON 요청을 만들고, HTTP 헤더를 설정하고, 에러 처리도 직접 해야 했습니다.
나중에 다른 AI 모델로 바꾸고 싶다면? 코드를 처음부터 다시 작성해야 했습니다.
더 큰 문제는 일관성 없는 코드였습니다. A 개발자는 자기 방식대로, B 개발자는 또 다른 방식으로 AI와 통신하는 코드를 작성했습니다.
유지보수할 때마다 혼란스러웠고, 버그도 자주 발생했습니다. 해결책의 등장 바로 이런 문제를 해결하기 위해 Spring AI 팀은 ChatClient를 만들었습니다.
ChatClient를 사용하면 표준화된 방식으로 모든 AI 모델과 대화할 수 있습니다. 또한 Spring의 의존성 주입을 그대로 활용할 수 있어, 테스트하기도 쉽고 유지보수도 편리합니다.
무엇보다 코드가 깔끔해진다는 큰 이점이 있습니다. 코드 분석 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 생성자 부분을 보면 ChatClient.Builder를 주입받고 있습니다. 이 Builder는 Spring이 자동으로 제공해주는데, 이미 필요한 설정(API 키, 모델 이름 등)이 다 들어있습니다.
builder.build()를 호출하면 실제 사용 가능한 ChatClient 인스턴스가 생성됩니다. askQuestion 메서드를 보면, prompt()로 대화를 시작합니다.
그다음 user(question)으로 사용자의 질문을 전달하고, call()로 실제 AI 모델을 호출합니다. 마지막으로 content()로 AI의 응답 텍스트만 추출합니다.
실무 활용 사례 실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 상담 챗봇을 개발한다고 가정해봅시다.
고객이 "주문 배송은 얼마나 걸리나요?"라고 물으면, ChatClient를 통해 AI 모델에게 질문을 전달하고, AI가 생성한 답변을 고객에게 보여주는 식입니다. 네이버, 카카오 같은 대형 IT 기업에서도 이런 패턴을 적극적으로 사용하고 있습니다.
주의사항 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 ChatClient를 직접 생성하려는 것입니다.
new ChatClient()처럼 말이죠. 하지만 ChatClient는 인터페이스이므로 직접 인스턴스화할 수 없습니다.
반드시 Builder를 통해 생성해야 합니다. 또 다른 실수는 응답을 기다리지 않는 것입니다.
AI 모델 호출은 네트워크 통신이므로 시간이 걸립니다. 비동기 처리를 고려하지 않으면 애플리케이션이 멈춘 것처럼 보일 수 있습니다.
정리 다시 김개발 씨의 이야기로 돌아가 봅시다. 팀장님의 설명을 들은 김개발 씨는 안도의 한숨을 쉬었습니다.
"생각보다 어렵지 않네요!" ChatClient를 제대로 이해하면 AI 기능을 Spring 애플리케이션에 자연스럽게 통합할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 실전 팁
- ChatClient.Builder는 반드시 Spring Bean으로 주입받으세요. 직접 생성하면 안 됩니다.
- 응답 시간이 길 수 있으므로 타임아웃 설정을 고려하세요.
2. ChatClient 빌더 패턴
2-3문장으로 상황 설정 김개발 씨가 ChatClient를 사용하다가 문득 궁금해졌습니다. "Builder라는 게 계속 나오는데, 이게 정확히 뭐지?" 선배 박시니어 씨가 옆에서 말했습니다.
"아, 빌더 패턴 모르세요? 이거 알아두면 정말 유용합니다."
핵심 개념을 3-4문장으로 빌더 패턴은 복잡한 객체를 단계별로 구성하는 디자인 패턴입니다. 마치 레고 블록을 하나씩 조립하듯이, 필요한 설정을 하나씩 추가하면서 최종 객체를 만듭니다.
ChatClient.Builder를 사용하면 기본 URL, 타임아웃, 기본 메시지 등을 유연하게 설정할 수 있습니다.
다음 코드를 살펴봅시다.
// Builder 패턴을 활용한 ChatClient 설정
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class CustomAiService {
private final ChatClient chatClient;
public CustomAiService(ChatClient.Builder builder) {
// 빌더로 단계별 설정을 추가합니다
this.chatClient = builder
.defaultSystem("당신은 친절한 고객 상담 AI입니다.")
.build();
}
public String getAdvice(String topic) {
return chatClient.prompt()
.user("다음 주제에 대해 조언해주세요: " + topic)
.call()
.content();
}
}
이북처럼 술술 읽히는 설명 김개발 씨는 첫 AI 챗봇 기능을 성공적으로 구현했습니다. 하지만 테스트를 하다 보니 아쉬운 점이 생겼습니다.
"AI가 가끔 너무 딱딱하게 대답하는데, 좀 더 친절하게 만들 수는 없을까?" 박시니어 씨가 모니터를 들여다보며 말했습니다. "빌더 패턴을 제대로 활용하면 됩니다.
defaultSystem 메서드로 AI의 성격을 미리 정의할 수 있어요." 빌더 패턴이란 빌더 패턴을 비유하자면, 마치 햄버거 가게에서 주문하는 것과 같습니다. "빵은 통밀로, 패티는 더블로, 치즈는 체다로, 소스는 마요네즈로..." 이렇게 하나씩 선택하면서 나만의 햄버거를 만드는 것처럼, 빌더 패턴도 필요한 옵션을 하나씩 추가하면서 객체를 만듭니다.
일반적인 생성자 방식이라면 new ChatClient(url, timeout, system, ...) 처럼 모든 파라미터를 한 번에 넘겨야 합니다. 파라미터가 많아지면 순서를 헷갈리기 쉽고, 필요 없는 값도 전부 넣어야 하는 불편함이 있습니다.
왜 빌더가 필요한가 빌더 패턴이 없던 시절의 코드를 상상해봅시다. ChatClient를 만들려면 여러 설정값이 필요합니다.
API 키, 모델 이름, 타임아웃, 재시도 횟수, 시스템 메시지 등등. 만약 이 모든 것을 생성자로 받는다면 어떻게 될까요?
new ChatClient(key, model, timeout, retry, system)처럼 매우 길어집니다. 더 심각한 문제는 선택적 파라미터 처리입니다.
어떤 경우에는 타임아웃만 설정하고 싶고, 어떤 경우에는 시스템 메시지만 설정하고 싶을 수 있습니다. 그러면 생성자를 여러 개 만들어야 할까요?
이것을 생성자 오버로딩 지옥이라고 부릅니다. 빌더 패턴의 등장 이런 문제를 우아하게 해결하는 것이 바로 빌더 패턴입니다.
빌더를 사용하면 메서드 체이닝 방식으로 필요한 설정만 골라서 추가할 수 있습니다. builder.defaultSystem(...).build() 처럼 점(.)으로 연결해서 쓰는 방식이죠.
가독성도 좋고, 어떤 값을 설정하는지 명확하게 알 수 있습니다. 또한 불변 객체를 만들기도 쉽습니다.
한 번 build()로 만들어진 ChatClient는 내부 상태가 변하지 않습니다. 멀티스레드 환경에서도 안전하게 사용할 수 있다는 뜻입니다.
코드 분석 위의 코드를 자세히 살펴보겠습니다. ChatClient.Builder builder를 생성자로 주입받는 것이 첫 번째 단계입니다.
이 Builder는 이미 application.properties에 설정된 값들(API 키 등)을 가지고 있습니다. 다음으로 builder.defaultSystem("당신은 친절한 고객 상담 AI입니다.")를 호출합니다.
이것은 시스템 메시지를 설정하는 부분입니다. 시스템 메시지는 AI의 역할이나 말투를 정의합니다.
마지막으로 build()를 호출하면 최종 ChatClient 객체가 완성됩니다. 이렇게 만들어진 chatClient는 이제 모든 대화에서 "친절한 고객 상담 AI" 페르소나를 유지합니다.
실무 활용 사례 실제 프로젝트에서는 더 다양한 설정을 추가합니다. 예를 들어 의료 상담 챗봇이라면 `defaultSystem("당신은 전문 의료 상담사입니다.
항상 정확하고 신중하게 답변하세요.")`처럼 설정할 수 있습니다. 금융 서비스라면 보안이 중요하므로 타임아웃을 짧게 설정하고, 에러 처리 로직도 추가할 수 있습니다.
많은 스타트업에서 고객 서비스 자동화를 위해 이런 패턴을 활용하고 있습니다. 주의사항 빌더를 사용할 때 주의할 점이 있습니다.
가장 흔한 실수는 build()를 호출하지 않는 것입니다. builder.defaultSystem(...)만 호출하고 build()를 빼먹으면 ChatClient가 아니라 Builder 객체가 반환됩니다.
반드시 마지막에 build()를 붙여야 합니다. 또 다른 주의사항은 동일한 Builder를 여러 번 사용하는 경우입니다.
Builder는 재사용 가능하지만, 각 build() 호출마다 새로운 ChatClient 인스턴스가 생성됩니다. 보통은 한 번만 build()하고 그 인스턴스를 계속 사용하는 것이 효율적입니다.
정리 박시니어 씨의 조언을 듣고 김개발 씨는 코드를 수정했습니다. "오, AI가 훨씬 친절하게 대답하네요!" 빌더 패턴의 위력을 실감한 순간이었습니다.
빌더 패턴을 제대로 활용하면 ChatClient를 프로젝트 요구사항에 맞게 유연하게 설정할 수 있습니다. 여러분도 다양한 옵션을 시도해 보세요.
실전 팁
💡 실전 팁
- defaultSystem은 AI의 성격을 결정하는 중요한 설정입니다. 프로젝트 성격에 맞게 잘 작성하세요.
- build()는 단 한 번만 호출하고, 생성된 ChatClient를 재사용하세요.
3. 기본 메시지 요청과 응답
2-3문장으로 상황 설정 드디어 김개발 씨는 실제 메시지를 주고받는 코드를 작성할 차례입니다. "prompt()로 시작하고, user()로 질문하고, call()로 호출한다고 했는데..." 손가락이 키보드 위에서 잠시 멈췄습니다.
각 메서드가 정확히 무슨 역할을 하는지 확실히 알고 싶었습니다.
핵심 개념을 3-4문장으로 메시지 요청과 응답은 ChatClient의 핵심 워크플로우입니다. prompt()로 대화 세션을 시작하고, user()로 사용자 메시지를 추가하며, call()로 AI를 호출하고, content()로 응답을 받습니다.
이 네 단계가 하나의 완전한 대화 사이클을 만듭니다.
다음 코드를 살펴봅시다.
// 기본 메시지 요청과 응답 예제
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class QuestionAnswerService {
private final ChatClient chatClient;
public QuestionAnswerService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String ask(String userQuestion) {
// 1. prompt(): 대화 시작
// 2. user(): 사용자 메시지 추가
// 3. call(): AI 모델 호출
// 4. content(): 응답 텍스트 추출
return chatClient.prompt()
.user(userQuestion)
.call()
.content();
}
}
이북처럼 술술 읽히는 설명 김개발 씨는 이제 본격적으로 AI와 대화하는 코드를 작성할 준비가 되었습니다. 팀장님이 넌지시 말씀하셨습니다.
"이제 실제로 질문을 던지고 답변을 받아봐요. 어렵지 않습니다." 하지만 김개발 씨는 여전히 궁금했습니다.
"prompt(), user(), call(), content()... 왜 이렇게 여러 단계로 나뉘어 있는 거지?
한 번에 할 수는 없나?" 메시지 요청의 구조 ChatClient의 메시지 요청 과정을 비유하자면, 마치 편지를 쓰는 것과 같습니다. 먼저 편지지를 꺼냅니다(prompt()).
그다음 내용을 씁니다(user()). 이제 우체통에 넣습니다(call()).
며칠 후 답장을 받아서 읽습니다(content()). 각 단계가 명확하게 분리되어 있어서, 중간에 필요하면 다른 작업도 끼워 넣을 수 있습니다.
이렇게 단계를 나눈 이유는 유연성 때문입니다. 때로는 user() 호출 전에 system() 메시지를 추가하고 싶을 수도 있고, call() 전에 설정을 변경하고 싶을 수도 있습니다.
왜 이렇게 설계되었나 만약 모든 것을 한 번에 하는 메서드가 있다면 어떨까요? chatClient.askAndGetAnswer(question) 같은 메서드가 있다면 간단해 보입니다.
하지만 실무에서는 다양한 요구사항이 생깁니다. "시스템 메시지도 추가하고 싶은데?" "응답을 스트리밍으로 받고 싶은데?" "JSON 형식으로 응답받고 싶은데?" 이런 경우마다 새로운 메서드를 만들어야 할까요?
Spring AI 팀은 빌더 패턴과 유사한 플루언트 API를 선택했습니다. 각 단계를 메서드로 분리하고, 필요한 것만 골라서 체이닝하는 방식입니다.
이렇게 하면 코드도 읽기 쉽고, 확장성도 좋습니다. 각 단계의 역할 네 가지 핵심 메서드를 하나씩 살펴보겠습니다.
**prompt()**는 새로운 대화 요청을 시작합니다. 이것을 호출하면 PromptRequestSpec이라는 객체가 반환되는데, 이 객체에 메시지를 추가할 수 있습니다.
마치 빈 캔버스를 받는 것과 같습니다. **user()**는 사용자 메시지를 추가합니다.
"자바 스프링이 뭔가요?" 같은 질문을 여기에 넣습니다. 이것이 AI에게 전달될 실제 질문입니다.
**call()**은 실제로 AI 모델을 호출합니다. 네트워크 요청이 발생하고, AI가 답변을 생성하는 시간 동안 대기합니다.
이 메서드가 반환하는 것은 ChatResponse 객체입니다. **content()**는 ChatResponse에서 텍스트만 추출합니다.
AI가 생성한 답변 중에서 순수한 문자열만 가져오는 것이죠. 코드 분석 위의 ask() 메서드를 단계별로 뜯어보겠습니다.
chatClient.prompt()가 실행되면 대화 요청 준비가 시작됩니다. 이 시점에서는 아직 아무것도 전송되지 않았습니다.
.user(userQuestion)이 호출되면 사용자의 질문이 요청 객체에 추가됩니다. 예를 들어 "오늘 날씨 어때?"라는 질문이 설정됩니다.
.call()이 실행되는 순간, 비로소 네트워크 요청이 발생합니다. OpenAI나 다른 AI 서비스로 HTTP 요청이 나가고, 응답을 기다립니다.
이 과정에서 1-2초 정도 걸릴 수 있습니다. 마지막으로 .content()가 호출되면 응답 객체에서 문자열만 추출됩니다.
이제 이 문자열을 사용자에게 보여주면 됩니다. 실무 활용 사례 실제 프로젝트에서는 이 패턴을 어떻게 활용할까요?
고객 FAQ 봇을 만든다고 가정해봅시다. 고객이 "반품은 어떻게 하나요?"라고 물으면, ask("반품은 어떻게 하나요?")를 호출합니다.
AI가 회사의 반품 정책을 바탕으로 답변을 생성하고, 그 내용을 고객에게 보여줍니다. 또 다른 예로, 코드 리뷰 봇이 있습니다.
개발자가 작성한 코드를 ask("이 코드의 문제점을 찾아주세요: " + code)처럼 전달하면, AI가 코드를 분석해서 개선점을 알려줍니다. 주의사항 이 과정에서 주의할 점이 있습니다.
call()은 블로킹 메서드입니다. 즉, AI의 응답이 올 때까지 스레드가 대기합니다.
만약 웹 애플리케이션에서 사용한다면, 요청 스레드가 블로킹되어 다른 요청을 처리하지 못할 수 있습니다. 트래픽이 많은 서비스라면 비동기 처리를 고려해야 합니다.
또한 content()는 null을 반환할 수 있습니다. AI가 응답을 생성하지 못하거나, 에러가 발생한 경우입니다.
따라서 null 체크를 꼭 해야 합니다. 정리 김개발 씨는 코드를 실행해 보았습니다.
"자바 스프링이 뭔가요?"라고 질문을 던지자, AI가 친절하게 답변을 생성했습니다. "오, 진짜 된다!" prompt, user, call, content라는 네 단계를 이해하면 ChatClient의 기본기를 마스터한 것입니다.
여러분도 다양한 질문을 시도해 보세요.
실전 팁
💡 실전 팁
- call()은 블로킹 메서드이므로 비동기 처리를 고려하세요.
- content() 결과는 null일 수 있으니 항상 체크하세요.
4. 시스템 메시지와 유저 메시지
2-3문장으로 상황 설정 김개발 씨가 챗봇을 테스트하다가 이상한 점을 발견했습니다. 같은 질문을 해도 AI의 답변 톤이 매번 달랐습니다.
"어떻게 하면 일관성 있는 답변을 받을 수 있을까?" 박시니어 씨가 옆에서 말했습니다. "시스템 메시지를 제대로 활용해야 합니다."
핵심 개념을 3-4문장으로 시스템 메시지는 AI의 역할과 행동 방식을 정의하는 메타 지시사항이고, 유저 메시지는 실제 사용자의 질문이나 요청입니다. 마치 연극 배우에게 "당신은 셰익스피어 시대의 귀족입니다"라고 역할을 설정하는 것이 시스템 메시지이고, "오늘 기분이 어떠세요?"라고 묻는 것이 유저 메시지입니다.
두 가지를 적절히 조합하면 원하는 스타일의 응답을 얻을 수 있습니다.
다음 코드를 살펴봅시다.
// 시스템 메시지와 유저 메시지를 함께 사용하는 예제
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class PersonalizedChatService {
private final ChatClient chatClient;
public PersonalizedChatService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String getCasualAnswer(String question) {
// 시스템 메시지로 AI의 말투를 설정합니다
return chatClient.prompt()
.system("당신은 친근한 10년차 개발자입니다. 반말로 편하게 답변하세요.")
.user(question) // 유저 메시지: 실제 질문
.call()
.content();
}
public String getFormalAnswer(String question) {
// 다른 시스템 메시지로 공손한 톤을 설정합니다
return chatClient.prompt()
.system("당신은 전문 기술 컨설턴트입니다. 존댓말로 정중하게 답변하세요.")
.user(question) // 동일한 질문이라도 답변 톤이 달라집니다
.call()
.content();
}
}
이북처럼 술술 읽히는 설명 김개발 씨는 고민에 빠졌습니다. 같은 "스프링부트가 뭐야?"라는 질문에 대해, 어떨 때는 AI가 친절하게 답하고, 어떨 때는 너무 딱딱하게 답했습니다.
일관성이 없었습니다. 박시니어 씨가 화면을 보더니 바로 문제를 찾아냈습니다.
"시스템 메시지를 안 쓰고 있네요. 그러니까 AI가 매번 다른 성격으로 답변하는 거예요." 시스템 메시지란 무엇인가 시스템 메시지를 비유하자면, 마치 배우에게 주는 대본 지시사항과 같습니다.
영화 촬영장을 상상해봅시다. 감독이 배우에게 "당신은 지금 슬픈 상황입니다.
눈물을 흘리며 연기하세요"라고 지시합니다. 이것이 바로 시스템 메시지입니다.
배우의 연기 스타일과 감정선을 미리 정의하는 것이죠. 반면 유저 메시지는 실제 대사입니다.
"사랑해"라는 대사를 말할 때, 시스템 메시지에 따라 슬프게 말할 수도 있고, 기쁘게 말할 수도 있습니다. 왜 구분해야 하나 시스템 메시지와 유저 메시지를 구분하지 않으면 어떻게 될까요?
모든 것을 유저 메시지로만 보내면, AI는 매번 컨텍스트 없이 답변합니다. "친절하게 답변해줘.
스프링부트가 뭐야?"처럼 매 질문마다 톤을 지시해야 합니다. 비효율적이고, 일관성도 떨어집니다.
더 큰 문제는 역할 혼동입니다. 유저가 "당신은 의사입니다"라고 유저 메시지로 보내면, AI는 이것을 질문인지 지시사항인지 헷갈려 할 수 있습니다.
명확한 역할 분리 Spring AI는 이 문제를 깔끔하게 해결했습니다. system() 메서드로 전달하는 메시지는 AI의 역할과 행동 규칙을 정의합니다.
이것은 모든 대화의 배경이 되며, 사용자에게는 보이지 않는 설정입니다. user() 메서드로 전달하는 메시지는 실제 질문이나 요청입니다.
사용자가 직접 입력한 내용이 여기에 들어갑니다. 이렇게 분리하면 AI는 자신의 역할을 명확히 이해하고, 일관성 있는 답변을 제공합니다.
코드 분석 위의 코드에서 두 메서드를 비교해봅시다. getCasualAnswer()를 보면, system() 메시지가 "반말로 편하게 답변하세요"입니다.
이제 user()로 전달되는 모든 질문은 반말 톤으로 답변됩니다. "스프링부트?
그거 자바 기반 웹 프레임워크야. 설정 간단하고 좋아." 이런 식으로 나옵니다.
반면 getFormalAnswer()는 system() 메시지가 "존댓말로 정중하게"입니다. 동일한 질문이라도 "스프링부트는 자바 기반의 웹 애플리케이션 프레임워크입니다.
설정이 간편하여 많은 개발자들이 선호합니다." 이렇게 공손한 톤으로 응답합니다. 실무 활용 사례 실제 서비스에서는 어떻게 활용할까요?
고객 상담 챗봇이라면 system("당신은 친절한 고객센터 상담원입니다. 항상 공손하고 해결책을 제시하세요.")처럼 설정합니다.
개발자 도우미 봇이라면 system("당신은 시니어 개발자입니다. 코드 예제와 함께 설명하세요.")로 설정할 수 있습니다.
어떤 스타트업은 캐릭터 AI를 만들 때 이 기능을 활용합니다. "당신은 장난기 많은 강아지입니다.
왈왈!"처럼 특정 페르소나를 부여해서 재미있는 대화를 만듭니다. 주의사항 시스템 메시지를 작성할 때 주의할 점이 있습니다.
너무 복잡한 지시사항은 피해야 합니다. "당신은 A이면서 B이고 C도 해야 하며..."처럼 여러 역할을 한 번에 부여하면 AI가 혼란스러워합니다.
명확하고 간단하게 한 가지 역할에 집중하세요. 또한 유저 메시지와 충돌하는 지시를 조심해야 합니다.
시스템 메시지에서 "항상 긍정적으로 답변하라"고 했는데, 유저가 "부정적인 면을 알려줘"라고 하면 AI가 어떻게 해야 할지 고민하게 됩니다. 정리 박시니어 씨의 조언대로 시스템 메시지를 추가하자, 김개발 씨의 챗봇은 훨씬 일관성 있는 답변을 하기 시작했습니다.
"이제 제대로 된 챗봇 같네요!" 시스템 메시지와 유저 메시지의 차이를 이해하고 적절히 활용하면, 프로젝트 요구사항에 딱 맞는 AI 어시스턴트를 만들 수 있습니다.
실전 팁
💡 실전 팁
- 시스템 메시지는 간단명료하게 한 가지 역할만 부여하세요.
- 프로젝트마다 시스템 메시지를 템플릿으로 만들어두면 재사용하기 편합니다.
5. 컨텍스트 관리
2-3문장으로 상황 설정 김개발 씨가 챗봇을 테스트하다가 또 다른 문제를 발견했습니다. "아까 뭐라고 했지?"라고 물으니 AI가 전혀 기억하지 못했습니다.
"AI는 기억력이 없나?" 팀장님이 설명해주셨습니다. "아뇨, 컨텍스트를 직접 관리해줘야 합니다."
핵심 개념을 3-4문장으로 컨텍스트 관리는 이전 대화 내용을 기억하고 활용하는 기능입니다. AI 모델 자체는 상태를 저장하지 않으므로, 개발자가 대화 히스토리를 직접 관리하고 매번 함께 전달해야 합니다.
마치 치매 환자에게 매번 지난 대화를 상기시켜주는 것처럼, AI에게도 이전 메시지들을 계속 보여줘야 맥락을 이해합니다.
다음 코드를 살펴봅시다.
// 컨텍스트를 관리하는 대화형 서비스
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class ConversationService {
private final ChatClient chatClient;
private final List<Message> conversationHistory;
public ConversationService(ChatClient.Builder builder) {
this.chatClient = builder.build();
this.conversationHistory = new ArrayList<>();
}
public String chat(String userInput) {
// 사용자 메시지를 히스토리에 추가
conversationHistory.add(new UserMessage(userInput));
// 전체 히스토리를 AI에게 전달
String response = chatClient.prompt()
.messages(conversationHistory)
.call()
.content();
// AI 응답도 히스토리에 추가
conversationHistory.add(new AssistantMessage(response));
return response;
}
}
이북처럼 술술 읽히는 설명 김개발 씨는 당황했습니다. 첫 질문에 "내 이름은 김철수야"라고 말했는데, 다음 질문에서 "내 이름이 뭐였지?"라고 물으니 AI가 모른다고 했습니다.
"방금 말했는데 벌써 잊어버렸어?" 팀장님이 웃으며 설명했습니다. "AI는 원래 그래요.
각 요청은 완전히 독립적입니다. 이전 대화를 기억시키려면 우리가 직접 저장했다가 다시 보여줘야 해요." AI의 무상태성 AI 모델의 특성을 이해하려면 **무상태(Stateless)**라는 개념을 알아야 합니다.
웹 개발에서 HTTP가 무상태 프로토콜이라는 것을 배웠을 겁니다. 서버는 이전 요청을 기억하지 못합니다.
그래서 쿠키나 세션을 써서 상태를 관리하죠. AI도 마찬가지입니다.
ChatGPT 같은 AI 모델은 매번 요청을 받을 때마다 처음 만난 것처럼 행동합니다. "안녕"이라고 인사했다가 다음에 "아까 뭐라고 했지?"라고 물으면, AI는 "아까"가 무엇인지 모릅니다.
이전 대화는 사라졌으니까요. 왜 이렇게 설계되었나 그렇다면 왜 AI는 기억력이 없을까요?
첫째, 확장성 때문입니다. AI 모델이 전 세계 수백만 명의 대화를 동시에 처리한다고 상상해보세요.
만약 모든 대화 히스토리를 AI 서버가 저장한다면, 메모리가 금방 터질 겁니다. 각 요청을 독립적으로 처리하면 서버는 훨씬 가벼워집니다.
둘째, 보안 때문입니다. AI 서비스는 여러 사용자가 공유합니다.
만약 AI가 모든 대화를 기억한다면, A 사용자의 민감한 정보가 B 사용자에게 노출될 위험이 있습니다. 무상태로 설계하면 이런 문제가 원천 차단됩니다.
컨텍스트 관리의 필요성 하지만 실제 챗봇을 만들려면 대화 맥락이 필수입니다. "오늘 날씨 어때?" "좋아" "그럼 산책할까?" 이런 대화는 이전 문맥을 기억해야 자연스럽습니다.
"좋아"가 무엇이 좋다는 건지, "산책"이 왜 나온 건지 알려면 앞의 대화를 봐야 합니다. 그래서 개발자가 직접 컨텍스트를 관리해야 합니다.
대화 히스토리를 리스트에 저장하고, 새 질문을 할 때마다 이전 대화들을 함께 전달하는 방식입니다. 코드 분석 위의 코드를 단계별로 살펴봅시다.
먼저 conversationHistory라는 리스트를 만듭니다. 이것이 대화 히스토리 저장소입니다.
chat() 메서드가 호출될 때마다 사용자의 입력이 UserMessage 객체로 변환되어 이 리스트에 추가됩니다. 다음으로 messages(conversationHistory)를 호출합니다.
이것은 지금까지의 모든 대화를 AI에게 보여주는 것입니다. AI는 전체 히스토리를 보고 맥락을 파악한 다음 응답합니다.
AI의 응답이 돌아오면, 이것도 AssistantMessage로 만들어서 히스토리에 추가합니다. 이렇게 해야 다음번에 AI가 자기가 무슨 말을 했는지 알 수 있습니다.
실무 활용 사례 실제 서비스에서는 어떻게 관리할까요? 웹 애플리케이션이라면 세션에 저장할 수 있습니다.
사용자마다 고유한 세션 ID가 있고, 그 세션에 conversationHistory를 저장합니다. 모바일 앱이라면 로컬 데이터베이스나 서버에 저장하고, 대화 시작할 때 불러옵니다.
긴 대화는 토큰 제한을 주의해야 합니다. AI 모델은 한 번에 처리할 수 있는 텍스트 양이 정해져 있습니다.
대화가 너무 길어지면 오래된 메시지는 잘라내고 최근 것만 유지하는 전략이 필요합니다. 주의사항 컨텍스트 관리에서 주의할 점이 있습니다.
메모리 누수를 조심해야 합니다. 대화가 끝났는데도 히스토리를 계속 들고 있으면 메모리가 낭비됩니다.
사용자가 로그아웃하거나 세션이 만료되면 히스토리도 삭제해야 합니다. 또한 민감한 정보가 히스토리에 남을 수 있습니다.
비밀번호, 카드번호 같은 것이 대화에 나오면, 이것이 로그에 기록되거나 다른 곳으로 전달될 수 있습니다. 민감한 데이터는 암호화하거나 마스킹 처리해야 합니다.
정리 팀장님의 설명을 듣고 김개발 씨는 대화 히스토리를 관리하는 코드를 추가했습니다. 이제 AI는 이전 대화를 기억하며 자연스럽게 대화했습니다.
"이제야 진짜 챗봇 같네요!" 컨텍스트 관리를 제대로 하면 사용자에게 훨씬 자연스러운 대화 경험을 제공할 수 있습니다. 여러분도 프로젝트에 맞는 히스토리 관리 전략을 세워보세요.
실전 팁
💡 실전 팁
- 대화 히스토리는 세션이나 데이터베이스에 저장하여 관리하세요.
- 토큰 제한을 고려하여 오래된 메시지는 주기적으로 정리하세요.
6. 간단한 챗봇 구현 실습
2-3문장으로 상황 설정 드디어 김개발 씨는 지금까지 배운 모든 것을 종합할 시간입니다. "실제로 작동하는 챗봇을 만들어볼까요?" 팀장님이 격려했습니다.
ChatClient, 빌더 패턴, 시스템 메시지, 컨텍스트 관리... 모든 개념을 하나로 합치는 순간입니다.
핵심 개념을 3-4문장으로 실전 챗봇 구현은 지금까지 배운 모든 요소를 통합하는 과정입니다. 시스템 메시지로 성격을 정의하고, 컨텍스트를 관리하며, 사용자 입력을 처리하고, 응답을 반환하는 완전한 워크플로우를 구축합니다.
이것이 바로 실무에서 사용하는 AI 챗봇의 기본 구조입니다.
다음 코드를 살펴봅시다.
// 완전한 기능을 가진 챗봇 서비스
import org.springframework.ai.chat.client.ChatClient;
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.messages.AssistantMessage;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
@Service
public class CompleteChatbotService {
private final ChatClient chatClient;
// 사용자별 대화 히스토리 관리
private final Map<String, List<Message>> userSessions;
public CompleteChatbotService(ChatClient.Builder builder) {
// 시스템 메시지를 미리 설정한 ChatClient 생성
this.chatClient = builder
.defaultSystem("당신은 친절한 프로그래밍 도우미입니다. " +
"초보자도 이해하기 쉽게 설명하고, " +
"필요하면 코드 예제를 제공하세요.")
.build();
this.userSessions = new HashMap<>();
}
public String chat(String userId, String userMessage) {
// 사용자별 세션 히스토리 가져오기 (없으면 새로 생성)
List<Message> history = userSessions.computeIfAbsent(
userId,
k -> new ArrayList<>()
);
// 사용자 메시지를 히스토리에 추가
history.add(new UserMessage(userMessage));
// 전체 히스토리와 함께 AI 호출
String response = chatClient.prompt()
.messages(history)
.call()
.content();
// AI 응답을 히스토리에 추가
history.add(new AssistantMessage(response));
return response;
}
// 대화 초기화 기능
public void resetConversation(String userId) {
userSessions.remove(userId);
}
}
이북처럼 술술 읽히는 설명 김개발 씨는 마침내 완전한 챗봇을 구현할 준비가 되었습니다. 지난 일주일 동안 ChatClient의 기초부터 시스템 메시지, 컨텍스트 관리까지 차근차근 배웠습니다.
이제 모든 퍼즐 조각을 맞출 차례입니다. "이번 주 금요일까지 프로토타입을 만들어야 하는데..." 김개발 씨는 약간 긴장했습니다.
하지만 팀장님은 자신 있게 말했습니다. "지금까지 잘 배웠으니 충분히 할 수 있어요." 실전 챗봇의 구조 실제로 사용할 수 있는 챗봇은 어떤 구조일까요?
마치 레스토랑 시스템과 비슷합니다. 주방장(ChatClient)이 있고, 주문서(메시지 히스토리)가 있으며, 웨이터(서비스 레이어)가 손님(사용자)과 주방 사이를 연결합니다.
각 손님마다 따로 주문서를 관리해야 하고, 주방장은 주문서를 보고 요리(응답)를 만듭니다. 핵심은 세 가지 레이어입니다.
ChatClient라는 AI 통신 레이어, 세션 관리 레이어, 그리고 비즈니스 로직 레이어. 이것들이 유기적으로 연결되어야 합니다.
각 컴포넌트의 역할 코드를 컴포넌트별로 나눠서 살펴봅시다. 먼저 ChatClient 설정 부분입니다.
생성자에서 builder.defaultSystem(...)을 호출하여 AI의 기본 성격을 정의합니다. "친절한 프로그래밍 도우미"라는 페르소나를 부여했고, "초보자도 이해하기 쉽게"라는 행동 지침을 추가했습니다.
이것이 모든 대화의 기본 톤을 결정합니다. 다음은 세션 관리 부분입니다.
Map<String, List<Message>>로 사용자별 대화 히스토리를 관리합니다. 웹 서비스라면 userId는 로그인 ID가 될 수 있고, 익명 서비스라면 세션 ID가 될 수 있습니다.
워크플로우 분석 chat() 메서드의 실행 흐름을 따라가 봅시다. 사용자 "user123"이 "스프링부트가 뭐야?"라고 질문했다고 가정합니다.
첫 번째로 computeIfAbsent()가 실행되며, user123의 히스토리를 찾습니다. 없으면 새 ArrayList를 만들어서 반환합니다.
두 번째로 사용자 메시지가 UserMessage 객체로 만들어져서 히스토리에 추가됩니다. 이제 히스토리에는 "스프링부트가 뭐야?"라는 메시지 하나가 들어있습니다.
세 번째로 chatClient.prompt().messages(history).call().content()가 실행됩니다. 전체 히스토리(지금은 1개)를 AI에게 전달하고, AI는 시스템 메시지를 고려하여 친절하게 답변합니다.
네 번째로 AI의 응답이 AssistantMessage로 히스토리에 추가됩니다. 이제 히스토리에는 UserMessage와 AssistantMessage 두 개가 있습니다.
다음번에 user123이 "그럼 어떻게 시작해?"라고 물으면, 히스토리에는 총 4개의 메시지가 쌓이게 됩니다. AI는 이 모든 맥락을 보고 답변합니다.
실무 활용 시나리오 이 코드를 실제 서비스에 적용한다면 어떻게 될까요? REST API 컨트롤러를 만들어서 /chat 엔드포인트를 노출합니다.
프론트엔드에서 사용자가 채팅창에 메시지를 입력하면, POST 요청으로 이 엔드포인트를 호출합니다. userId는 JWT 토큰에서 추출하고, userMessage는 요청 바디에서 가져옵니다.
고객 지원 시스템이라면, 시스템 메시지를 "당신은 회사의 고객센터 상담원입니다"로 바꾸고, 회사 정책이나 FAQ를 함께 제공할 수 있습니다. 교육 플랫폼이라면 "당신은 프로그래밍 강사입니다"로 설정하고, 학생의 학습 진도에 맞춰 설명할 수 있습니다.
확장 포인트 이 기본 구조를 어떻게 확장할 수 있을까요? 먼저 히스토리 제한을 추가할 수 있습니다.
대화가 100개를 넘어가면 가장 오래된 것부터 삭제하는 식입니다. if (history.size() > 100) { history.remove(0); } 같은 로직을 추가하면 됩니다.
다음으로 영속성을 추가할 수 있습니다. 지금은 메모리에만 저장되므로 서버를 재시작하면 모든 대화가 사라집니다.
데이터베이스에 저장하면 대화 히스토리를 영구 보존할 수 있습니다. 에러 처리도 중요합니다.
AI 호출이 실패하면 어떻게 할까요? try-catch로 예외를 잡아서 "죄송합니다.
일시적인 오류입니다"같은 친절한 메시지를 반환할 수 있습니다. 주의사항 실전 배포 전에 반드시 고려해야 할 점들이 있습니다.
동시성 문제입니다. 여러 사용자가 동시에 채팅하면 userSessions Map에 동시에 접근합니다.
ConcurrentHashMap을 사용하거나, 세션을 Redis 같은 외부 저장소로 옮기는 것이 안전합니다. 비용 관리도 중요합니다.
AI API는 호출 횟수나 토큰 수에 따라 과금됩니다. 무분별한 요청을 막기 위해 사용자당 일일 요청 제한을 두거나, 캐싱을 활용할 수 있습니다.
정리 김개발 씨는 모든 코드를 작성하고 테스트를 돌려봤습니다. "안녕?" "안녕하세요!" "스프링부트 알려줘" "스프링부트는..." 대화가 자연스럽게 이어졌습니다.
"드디어 완성이다!" 팀장님이 코드를 리뷰하며 칭찬했습니다. "잘했어요.
이제 실전에 투입할 수 있겠네요." 지금까지 배운 ChatClient의 모든 개념이 이 하나의 서비스에 녹아있습니다. 여러분도 이 코드를 기반으로 자신만의 챗봇을 만들어보세요.
시스템 메시지를 바꿔보고, 새로운 기능을 추가해보면서 더 깊이 이해할 수 있을 겁니다.
실전 팁
💡 실전 팁
- ConcurrentHashMap을 사용하여 동시성 문제를 해결하세요.
- 대화 히스토리는 주기적으로 정리하여 메모리와 비용을 절약하세요.
- 에러 처리를 추가하여 안정적인 서비스를 만드세요.
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
AI 에이전트의 Task Decomposition & Planning 완벽 가이드
AI 에이전트가 복잡한 작업을 어떻게 분해하고 계획하는지 알아봅니다. 작업 분해 전략부터 동적 재계획까지, 에이전트 개발의 핵심 개념을 실무 예제와 함께 쉽게 설명합니다.
에이전트 강화 미세조정 RFT 완벽 가이드
AI 에이전트가 스스로 학습하고 적응하는 강화 미세조정(RFT) 기법을 알아봅니다. 온라인/오프라인 학습부터 A/B 테스팅까지 실무에서 바로 적용할 수 있는 핵심 개념을 다룹니다.
Voice Clone 구현 완벽 가이드
음성 복제(Voice Clone) 기술을 활용하여 특정 화자의 목소리를 재현하는 방법을 알아봅니다. 참조 오디오 준비부터 실전 구현까지, 초급 개발자도 따라할 수 있도록 단계별로 설명합니다.
Qwen3-TTS 프로젝트 소개
알리바바가 공개한 최신 텍스트-음성 변환 프로젝트 Qwen3-TTS를 소개합니다. 음성 복제부터 10개 언어 지원까지, 차세대 TTS 기술의 핵심을 초급 개발자 눈높이에서 살펴봅니다.
AI 에이전트 패턴 완벽 가이드
LLM 기반 AI 에이전트를 프로덕션 환경에서 성공적으로 구축하기 위한 핵심 패턴들을 소개합니다. 튜토리얼과 실제 제품 사이의 간극을 메우고, 8가지 카테고리로 정리된 패턴들을 통해 실무에서 바로 적용할 수 있는 지식을 전달합니다.