AI 챗봇 구현
진행 중인 프로젝트에서 AI를 활용한 대화형 챗봇을 구현했다.
단순히 텍스트 응답을 생성하는 수준을 넘어, 비동기 이벤트 흐름 기반의 실시간 응답 구조를 설계하는 것이 핵심 목표였다.
전체 설계 개요
가장 중요한 설계 목표는 비동기 메시징 기반의 실시간 응답 파이프라인을 구축하는 것이다.
사용자의 입력은 Redis Pub/Sub을 통해 비동기로 전달되고,
AI는 Groq API를 통해 자연어 응답을 생성한 뒤 WebSocket을 통해 클라이언트로 다시 전달한다.
사용자 입력
↓
RedisPublisher → chat:{userId}:input
↓
ChatMessageSubscriber → ChatbotService.reply()
↓
Groq API → 자연어 응답 생성
↓
RedisPublisher → chat:{userId}:output
↓
RedisSubscriber → /topic/chat/{userId} 브로드캐스트
↓
프론트엔드 WebSocket 수신
Redis Pub/Sub를 활용한 이유
Redis Pub/Sub을 사용한 이유는 AI 연산을 사용자 요청과 완전히 분리하기 위함이다.
AI 연산은 상대적으로 시간이 많이 걸리는 작업이기 때문에,
HTTP 요청-응답 구조로 처리하면 서버 자원을 지속적으로 점유하게 되어 성능 저하가 발생하기 쉽다.
Pub/Sub 구조를 적용하면 입력 이벤트 발생 → 메시지 발행 → 비동기 처리 → 결과 발행으로 흐름을 분리할 수 있다.
또한, Redis는 메모리 기반이므로 빠르고 가벼운 메시지 전달이 가능하다.
ChatMessageSubscriber
사용자의 입력은 `chat:{userId}:input` 채널로 발행되고,
이 채널을 구독 중인 `ChatMessageSubscriber`가 메시지를 받아 AI 호출을 트리거한다.
public class ChatMessageSubscriber implements MessageListener {
private final ChatbotService chatbotService;
private final RedisPublisher redisPublisher;
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel());
String body = new String(message.getBody());
log.info("[Redis 구독] channel={}, message={}", channel, body);
String[] parts = channel.split(":");
Long userId = Long.valueOf(parts[1]);
String aiResponse = chatbotService.reply(userId, body);
redisPublisher.publish("chat:" + userId + ":output", aiResponse);
}
}
이 과정을 통해 사용자의 요청은 HTTP 요청 없이 Redis 이벤트를 통해 처리된다.
이는 높은 처리량과 응답 지연 최소화를 동시에 달성하는 구조이다.
ChatbotService
챗봇은 대화 맥락을 유지하면서 자연스러운 응답을 생성한다.
또한, 사용자가 “승률”, “확률” 등의 특정 키워드를 포함한 요청을 보낼 경우,
챗봇은 DB에서 경기 전적을 조회해 통계 기반 데이터를 분석해 응답을 생성한다.
public String reply(Long userId, String userMessage) {
if (WINNING_RATE_KEYWORDS.stream().anyMatch(userMessage::contains)) {
return handleWinningRate(userMessage);
}
List<String> history = chatHistoryService.getRecentMessages(userId);
String historyText = String.join("\n", history);
String prompt = """
아래는 사용자와 챗봇 간의 최근 대화 내용입니다.
대화의 흐름을 이해하고, 사용자의 질문에 답변하세요.
대화 내역:
%s
사용자 요청: %s
응답 지침;
- 대화의 맥락을 유지하면서 자연스럽게 응답하세요.
- 경기, 팀 관련 데이터는 사실에 기반해 답변하세요.
- 불확실한 정보일 경우 추측하지 말고 "정보가 부족합니다."라고 답변하세요.
- 답변은 한국어로 작성하세요.
""".formatted(historyText, userMessage);
ChatResponse chatResponse = chatClient.prompt(prompt)
.system("너는 e스포츠 데이터 기반 AI 챗봇이다. 사용자와의 맥락을 기억한다.")
.call()
.chatResponse();
Generation result = chatResponse.getResult();
String aiAnswer = result.getOutput().getText();
chatHistoryService.addMessage(userId, "user", userMessage);
chatHistoryService.addMessage(userId, "assistant", aiAnswer);
return aiAnswer;
}
Redis 대화 히스토리
AI가 일관된 맥락을 유지하려면 과거 대화가 필요하다.
이를 위해 Redis List를 이용하여 사용자의 최근 20개 대화를 저장하고, 7일 후 자동 만료되도록 구성했다.
redisTemplate.opsForList().rightPush(key, json);
redisTemplate.opsForList().trim(key, -MAX_HISTORY, -1);
redisTemplate.expire(key, Duration.ofDays(7));
이 방식은 빠르면서도 메모리 낭비가 없고, 불필요한 데이터 누적을 방지한다.
WebSocket으로 결과 전달
AI의 응답은 RedisPublisher를 통해 다시 `chat:{userId}:output` 채널로 발행되고,
`ChatMessageOutputSubscriber`가 이를 수신해 WebSocket을 통해 클라이언트에 전달한다.
public class ChatMessageOutputSubscriber implements MessageListener {
private final SimpMessagingTemplate messagingTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel());
String body = new String(message.getBody());
log.info("[Redis 구독] channel={}, message={}", channel, body);
String[] parts = channel.split(":");
String userId = parts[1];
String destination = "/topic/chat/" + userId;
messagingTemplate.convertAndSend(destination, body);
}
}
프론트엔드는 `/topic/chat/{userId}`를 구독하여 실시간으로 응답을 받는다.
이를 통해 HTTP 없이 양방향 대화형 통신이 가능해진다.
전체 흐름 요약
[사용자 입력]
↓
RedisPublisher → chat:{userId}:input
↓
ChatMessageSubscriber
↓
ChatbotService.reply()
├─ Redis: 대화 히스토리 조회
├─ Groq AI 호출
├─ (선택) 승률 분석 수행
└─ Redis: 히스토리 저장
↓
RedisPublisher → chat:{userId}:output
↓
RedisSubscriber → /topic/chat/{userId} 전송
↓
프론트엔드 WebSocket으로 실시간 응답 표시
마치며
이번 구조는 Redis를 통해 입력과 출력을 분리하고, Groq AI를 통해 자연어 기반 응답을 생성하며,
WebSocket으로 실시간 소통이 가능한 대화형 환경을 구현했다.
실시간성과 확장성을 만족하면서, AI 기반 서비스의 백엔드 아키텍처를 한 단계 고도화할 수 있었다.
'TIL' 카테고리의 다른 글
| 11/6 (0) | 2025.11.06 |
|---|---|
| 11/5 (0) | 2025.11.05 |
| Spring Boot 성능 최적화 7 (0) | 2025.11.03 |
| Spring Boot 성능 최적화 6 (0) | 2025.10.31 |
| Spring Boot 성능 최적화 5 (0) | 2025.10.30 |