Skip to content

Commit d79fbd2

Browse files
authored
Merge pull request #295 from prgrms-web-devcourse-final-project/feat/chat-cache
채팅 기록 redis 캐시
2 parents 4a4067a + 48a53c5 commit d79fbd2

File tree

7 files changed

+114
-18
lines changed

7 files changed

+114
-18
lines changed

backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatResponse;
77
import com.ai.lawyer.domain.chatbot.entity.History;
88
import com.ai.lawyer.domain.chatbot.repository.HistoryRepository;
9-
import com.ai.lawyer.domain.kafka.dto.ChatPostProcessEvent;
10-
import com.ai.lawyer.domain.kafka.dto.DocumentDto;
9+
import com.ai.lawyer.infrastructure.kafka.dto.ChatPostProcessEvent;
10+
import com.ai.lawyer.infrastructure.kafka.dto.DocumentDto;
1111
import com.ai.lawyer.domain.member.entity.Member;
1212
import com.ai.lawyer.domain.member.repositories.MemberRepository;
1313
import com.ai.lawyer.global.qdrant.service.QdrantService;

backend/src/main/java/com/ai/lawyer/domain/chatbot/service/HistoryService.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package com.ai.lawyer.domain.chatbot.service;
22

3-
import com.ai.lawyer.domain.chatbot.dto.ChatDto;
3+
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatHistoryDto;
44
import com.ai.lawyer.domain.chatbot.dto.HistoryDto;
55
import com.ai.lawyer.domain.chatbot.entity.Chat;
66
import com.ai.lawyer.domain.chatbot.entity.History;
77
import com.ai.lawyer.domain.chatbot.exception.HistoryNotFoundException;
88
import com.ai.lawyer.domain.chatbot.repository.HistoryRepository;
99
import com.ai.lawyer.domain.member.entity.Member;
1010
import com.ai.lawyer.domain.member.repositories.MemberRepository;
11+
import com.ai.lawyer.infrastructure.redis.service.ChatCacheService;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.http.ResponseEntity;
1314
import org.springframework.stereotype.Service;
@@ -20,6 +21,8 @@
2021
@RequiredArgsConstructor
2122
public class HistoryService {
2223

24+
private final ChatCacheService chatCacheService;
25+
2326
private final HistoryRepository historyRepository;
2427
private final MemberRepository memberRepository;
2528

@@ -54,20 +57,30 @@ public String deleteHistory(Long memberId, Long roomId) {
5457
}
5558

5659
@Transactional(readOnly = true)
57-
public ResponseEntity<List<ChatDto.ChatHistoryDto>> getChatHistory(Long memberId, Long roomId) {
60+
public ResponseEntity<List<ChatHistoryDto>> getChatHistory(Long memberId, Long roomId) {
5861

5962
Member member = memberRepository.findById(memberId).orElseThrow(
6063
() -> new IllegalArgumentException("존재하지 않는 회원입니다.")
6164
);
6265

63-
List<Chat> chats = historyRepository.findByHistoryIdAndMemberId(roomId, member).getChats();
64-
List<ChatDto.ChatHistoryDto> chatDtos = new ArrayList<>();
65-
66-
for (Chat chat : chats) {
67-
ChatDto.ChatHistoryDto dto = ChatDto.ChatHistoryDto.from(chat);
68-
chatDtos.add(dto);
66+
// 1. Redis 캐시에서 조회 (있으면 바로 반환)
67+
List<ChatHistoryDto> cached = chatCacheService.getChatHistory(roomId);
68+
if (!cached.isEmpty()) {
69+
return ResponseEntity.ok(cached);
6970
}
7071

72+
// 2. DB에서 조회 후 캐시에 저장
73+
History history = historyRepository.findByHistoryIdAndMemberId(roomId, member);
74+
List<Chat> chats = history.getChats();
75+
76+
// 엔티티 -> DTO 변환
77+
List<ChatHistoryDto> chatDtos = chats.stream()
78+
.map(ChatHistoryDto::from)
79+
.toList();
80+
81+
// DB 조회 결과를 Redis 캐시에 저장
82+
chatDtos.forEach(dto -> chatCacheService.cacheChatMessage(roomId, dto));
83+
7184
return ResponseEntity.ok(chatDtos);
7285

7386
}

backend/src/main/java/com/ai/lawyer/global/config/RedisConfig.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.ai.lawyer.global.config;
22

3+
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatHistoryDto;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.databind.SerializationFeature;
6+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
37
import lombok.extern.slf4j.Slf4j;
48
import org.springframework.beans.factory.annotation.Value;
59
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -10,6 +14,7 @@
1014
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
1115
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
1216
import org.springframework.data.redis.core.RedisTemplate;
17+
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
1318
import org.springframework.data.redis.serializer.StringRedisSerializer;
1419

1520
@Slf4j
@@ -53,4 +58,25 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisC
5358
log.info("=== RedisTemplate 설정 완료 (host={}, port={}) ===", redisHost, redisPort);
5459
return redisTemplate;
5560
}
61+
62+
@Bean
63+
public RedisTemplate<String, ChatHistoryDto> chatRedisTemplate(RedisConnectionFactory connectionFactory) {
64+
RedisTemplate<String, ChatHistoryDto> template = new RedisTemplate<>();
65+
template.setConnectionFactory(connectionFactory);
66+
67+
template.setKeySerializer(new StringRedisSerializer());
68+
69+
ObjectMapper mapper = new ObjectMapper();
70+
mapper.registerModule(new JavaTimeModule());
71+
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
72+
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper);
73+
74+
template.setValueSerializer(serializer);
75+
template.setHashValueSerializer(serializer);
76+
77+
template.afterPropertiesSet();
78+
return template;
79+
}
80+
81+
5682
}

backend/src/main/java/com/ai/lawyer/domain/kafka/consumer/ChatPostProcessingConsumer.java renamed to backend/src/main/java/com/ai/lawyer/infrastructure/kafka/consumer/ChatPostProcessingConsumer.java

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
package com.ai.lawyer.domain.kafka.consumer;
1+
package com.ai.lawyer.infrastructure.kafka.consumer;
22

3+
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatHistoryDto;
4+
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatLawDto;
5+
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatPrecedentDto;
36
import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.KeywordExtractionDto;
47
import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.TitleExtractionDto;
58
import com.ai.lawyer.domain.chatbot.entity.*;
69
import com.ai.lawyer.domain.chatbot.repository.*;
710
import com.ai.lawyer.domain.chatbot.service.KeywordService;
8-
import com.ai.lawyer.domain.kafka.dto.ChatPostProcessEvent;
9-
import com.ai.lawyer.domain.kafka.dto.DocumentDto;
11+
import com.ai.lawyer.infrastructure.kafka.dto.ChatPostProcessEvent;
12+
import com.ai.lawyer.infrastructure.kafka.dto.DocumentDto;
13+
import com.ai.lawyer.infrastructure.redis.service.ChatCacheService;
1014
import lombok.RequiredArgsConstructor;
1115
import lombok.extern.slf4j.Slf4j;
1216
import org.springframework.ai.chat.memory.ChatMemory;
@@ -19,6 +23,7 @@
1923
import org.springframework.stereotype.Service;
2024
import org.springframework.transaction.annotation.Transactional;
2125

26+
import java.util.ArrayList;
2227
import java.util.List;
2328
import java.util.stream.Collectors;
2429

@@ -28,6 +33,8 @@
2833
public class ChatPostProcessingConsumer {
2934

3035
private final KeywordService keywordService;
36+
private final ChatCacheService chatCacheService;
37+
3138
private final HistoryRepository historyRepository;
3239
private final ChatRepository chatRepository;
3340
private final KeywordRankRepository keywordRankRepository;
@@ -59,7 +66,7 @@ public void consume(ChatPostProcessEvent event) {
5966
// 2. 채팅방 제목 설정 / 및 필터
6067
setHistoryTitle(event.getUserMessage(), history, event.getChatResponse());
6168

62-
// 3. 채팅 기록 저장
69+
// 3. 채팅 기록 저장 및 Redis 캐시 저장
6370
saveChatWithDocuments(history, MessageType.USER, event.getUserMessage(), event.getSimilarCaseDocuments(), event.getSimilarLawDocuments());
6471
saveChatWithDocuments(history, MessageType.ASSISTANT, event.getChatResponse(), event.getSimilarCaseDocuments(), event.getSimilarLawDocuments());
6572

@@ -99,6 +106,10 @@ private void extractAndUpdateKeywordRanks(String message) {
99106
}
100107

101108
private void saveChatWithDocuments(History history, MessageType type, String message, List<DocumentDto> similarCaseDocuments, List<DocumentDto> similarLawDocuments) {
109+
110+
List<ChatPrecedent> chatPrecedents = new ArrayList<>();
111+
List<ChatLaw> chatLaws = new ArrayList<>();
112+
102113
Chat chat = chatRepository.save(Chat.builder()
103114
.historyId(history)
104115
.type(type)
@@ -108,7 +119,7 @@ private void saveChatWithDocuments(History history, MessageType type, String mes
108119
// Ai 메시지가 저장될 때 관련 문서 저장
109120
if (type == MessageType.ASSISTANT) {
110121
if (similarCaseDocuments != null && !similarCaseDocuments.isEmpty()) {
111-
List<ChatPrecedent> chatPrecedents = similarCaseDocuments.stream()
122+
chatPrecedents = similarCaseDocuments.stream()
112123
.map(doc -> ChatPrecedent.builder()
113124
.chatId(chat)
114125
.precedentContent(doc.getText())
@@ -120,7 +131,7 @@ private void saveChatWithDocuments(History history, MessageType type, String mes
120131
}
121132

122133
if (similarLawDocuments != null && !similarLawDocuments.isEmpty()) {
123-
List<ChatLaw> chatLaws = similarLawDocuments.stream()
134+
chatLaws = similarLawDocuments.stream()
124135
.map(doc -> ChatLaw.builder()
125136
.chatId(chat)
126137
.content(doc.getText())
@@ -130,5 +141,16 @@ private void saveChatWithDocuments(History history, MessageType type, String mes
130141
chatLawRepository.saveAll(chatLaws);
131142
}
132143
}
144+
145+
// Redis 캐시에 DTO 저장
146+
ChatHistoryDto dto = ChatHistoryDto.builder()
147+
.type(type.toString())
148+
.message(message)
149+
.createdAt(chat.getCreatedAt())
150+
.precedent(chatPrecedents.isEmpty() ? null : ChatPrecedentDto.from(chatPrecedents.get(0)))
151+
.law(chatLaws.isEmpty() ? null : ChatLawDto.from(chatLaws.get(0)))
152+
.build();
153+
154+
chatCacheService.cacheChatMessage(history.getHistoryId(), dto);
133155
}
134156
}

backend/src/main/java/com/ai/lawyer/domain/kafka/dto/ChatPostProcessEvent.java renamed to backend/src/main/java/com/ai/lawyer/infrastructure/kafka/dto/ChatPostProcessEvent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.ai.lawyer.domain.kafka.dto;
1+
package com.ai.lawyer.infrastructure.kafka.dto;
22

33
import lombok.AllArgsConstructor;
44
import lombok.Data;

backend/src/main/java/com/ai/lawyer/domain/kafka/dto/DocumentDto.java renamed to backend/src/main/java/com/ai/lawyer/infrastructure/kafka/dto/DocumentDto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.ai.lawyer.domain.kafka.dto;
1+
package com.ai.lawyer.infrastructure.kafka.dto;
22

33
import lombok.AllArgsConstructor;
44
import lombok.Data;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.ai.lawyer.infrastructure.redis.service;
2+
3+
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatHistoryDto;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.data.redis.core.RedisTemplate;
6+
import org.springframework.stereotype.Service;
7+
8+
import java.time.Duration;
9+
import java.util.List;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class ChatCacheService {
14+
15+
private final RedisTemplate<String, ChatHistoryDto> chatRedisTemplate; // 새 템플릿 사용
16+
private static final String CHAT_HISTORY_KEY_PREFIX = "chat:history:";
17+
18+
// 채팅 메시지 캐싱 (24시간)
19+
public void cacheChatMessage(Long roomId, ChatHistoryDto chatHistory) {
20+
String key = CHAT_HISTORY_KEY_PREFIX + roomId;
21+
chatRedisTemplate.opsForList().rightPush(key, chatHistory);
22+
chatRedisTemplate.expire(key, Duration.ofHours(24));
23+
}
24+
25+
public List<ChatHistoryDto> getChatHistory(Long roomId) {
26+
String key = CHAT_HISTORY_KEY_PREFIX + roomId;
27+
List<ChatHistoryDto> cachedList = chatRedisTemplate.opsForList().range(key, 0, -1);
28+
return cachedList == null ? List.of() : cachedList;
29+
}
30+
31+
public void clearChatHistory(Long roomId) {
32+
chatRedisTemplate.delete(CHAT_HISTORY_KEY_PREFIX + roomId);
33+
}
34+
35+
}

0 commit comments

Comments
 (0)