Skip to content

Commit b26076b

Browse files
authored
Merge pull request #77 from uju-in/LIME-149-BE-chat-feat
[LIME-149] 채팅 기능 개선
2 parents b0873fa + 18ad8d6 commit b26076b

27 files changed

+1073
-67
lines changed

lime-api/src/main/java/com/programmers/lime/domains/chat/api/ChatController.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
import org.springframework.http.ResponseEntity;
44
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.ModelAttribute;
56
import org.springframework.web.bind.annotation.PathVariable;
67
import org.springframework.web.bind.annotation.RequestMapping;
78
import org.springframework.web.bind.annotation.RestController;
89

9-
import com.programmers.lime.domains.chat.api.dto.response.ChatGetResponse;
10+
import com.programmers.lime.domains.chat.api.dto.response.ChatGetByCursorResponse;
1011
import com.programmers.lime.domains.chat.application.ChatService;
11-
import com.programmers.lime.domains.chat.application.dto.response.ChatGetServiceResponse;
12+
import com.programmers.lime.domains.chat.application.dto.response.ChatGetCursorServiceResponse;
13+
import com.programmers.lime.global.cursor.CursorRequest;
1214

1315
import io.swagger.v3.oas.annotations.Operation;
1416
import io.swagger.v3.oas.annotations.tags.Tag;
17+
import jakarta.validation.Valid;
1518
import lombok.RequiredArgsConstructor;
1619

1720
@Tag(name = "chat", description = "채팅 API")
@@ -22,11 +25,15 @@ public class ChatController {
2225

2326
private final ChatService chatService;
2427

25-
@Operation(summary = "채팅 조회", description = "chatRoomId을 이용하여 채팅을 조회")
28+
@Operation(summary = "채팅 커서 조회", description = "chatRoomId을 이용하여 채팅을 조회")
2629
@GetMapping("/{chatRoomId}")
27-
public ResponseEntity<ChatGetResponse> getChatInfoLists(@PathVariable final Long chatRoomId) {
28-
ChatGetServiceResponse serviceResponse = chatService.getChatWithMemberList(chatRoomId);
29-
ChatGetResponse response = ChatGetResponse.from(serviceResponse);
30+
public ResponseEntity<ChatGetByCursorResponse> getChatInfoLists(
31+
@PathVariable final Long chatRoomId,
32+
@ModelAttribute @Valid final CursorRequest cursorRequest
33+
) {
34+
ChatGetCursorServiceResponse serviceResponse = chatService.getChatByCursor(chatRoomId,
35+
cursorRequest.toParameters());
36+
ChatGetByCursorResponse response = ChatGetByCursorResponse.from(serviceResponse);
3037

3138
return ResponseEntity.ok(response);
3239
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.programmers.lime.domains.chat.api.dto.response;
2+
3+
import java.util.List;
4+
5+
import com.programmers.lime.domains.chat.application.dto.response.ChatGetCursorServiceResponse;
6+
import com.programmers.lime.domains.chat.model.ChatSummary;
7+
8+
public record ChatGetByCursorResponse(
9+
String nextCursorId,
10+
List<ChatSummary> chatSummaries
11+
) {
12+
public static ChatGetByCursorResponse from(final ChatGetCursorServiceResponse response) {
13+
return new ChatGetByCursorResponse(
14+
response.cursorSummary().nextCursorId(),
15+
response.cursorSummary().summaries()
16+
);
17+
}
18+
19+
}

lime-api/src/main/java/com/programmers/lime/domains/chat/application/ChatService.java

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,32 @@
55
import java.time.ZoneId;
66
import java.util.List;
77

8+
import org.springframework.cache.annotation.Cacheable;
9+
import org.springframework.context.ApplicationEventPublisher;
810
import org.springframework.messaging.simp.SimpMessagingTemplate;
911
import org.springframework.stereotype.Service;
1012

11-
import com.programmers.lime.domains.chat.application.dto.response.ChatGetServiceResponse;
13+
import com.programmers.lime.common.cursor.CursorPageParameters;
14+
import com.programmers.lime.common.cursor.CursorSummary;
15+
import com.programmers.lime.common.cursor.CursorUtils;
16+
import com.programmers.lime.domains.chat.application.dto.response.ChatGetCursorServiceResponse;
1217
import com.programmers.lime.domains.chat.implementation.ChatAppender;
1318
import com.programmers.lime.domains.chat.implementation.ChatReader;
1419
import com.programmers.lime.domains.chat.model.ChatInfoWithMember;
20+
import com.programmers.lime.domains.chat.model.ChatSummary;
1521
import com.programmers.lime.domains.chat.model.ChatType;
1622
import com.programmers.lime.domains.chatroom.implementation.ChatRoomMemberReader;
1723
import com.programmers.lime.domains.member.domain.Member;
1824
import com.programmers.lime.domains.member.implementation.MemberReader;
1925
import com.programmers.lime.error.BusinessException;
2026
import com.programmers.lime.error.ErrorCode;
2127
import com.programmers.lime.global.config.security.SecurityUtils;
28+
import com.programmers.lime.global.event.chat.ChatAppendCacheEvent;
29+
import com.programmers.lime.redis.chat.ChatCursorCacheReader;
2230
import com.programmers.lime.redis.chat.ChatSessionRedisManager;
31+
import com.programmers.lime.redis.chat.model.ChatCursorCache;
32+
import com.programmers.lime.redis.chat.model.ChatCursorCacheResult;
33+
import com.programmers.lime.redis.chat.model.ChatCursorCacheStatus;
2334
import com.programmers.lime.redis.chat.model.ChatSessionInfo;
2435

2536
import lombok.RequiredArgsConstructor;
@@ -40,6 +51,12 @@ public class ChatService {
4051

4152
private final ChatRoomMemberReader chatRoomMemberReader;
4253

54+
private final static int DEFAULT_CURSOR_SIZE = 20;
55+
56+
private final ChatCursorCacheReader chatCursorCacheReader;
57+
58+
private final ApplicationEventPublisher eventPublisher;
59+
4360
public void sendMessage(final Long memberId, final String sessionId, final String message, final String timeSeq) {
4461
Member member = memberReader.read(memberId);
4562

@@ -110,20 +127,81 @@ public void sendExitMessageToChatRoom(final Long chatRoomId) {
110127
simpMessagingTemplate.convertAndSend("/subscribe/rooms/exit/" + chatRoomId, chatInfoWithMember);
111128
}
112129

113-
public ChatGetServiceResponse getChatWithMemberList(final Long chatRoomId) {
130+
@Cacheable(value = "chat", key = "#chatRoomId + '_' + #parameters.cursorId + '_' + #parameters.size")
131+
public ChatGetCursorServiceResponse getChatByCursor(
132+
final Long chatRoomId,
133+
final CursorPageParameters parameters
134+
) {
114135

115136
Long memberId = SecurityUtils.getCurrentMemberId();
116137

117138
if (!chatRoomMemberReader.existMemberByMemberIdAndRoomId(chatRoomId, memberId)) {
118139
throw new BusinessException(ErrorCode.CHATROOM_NOT_PERMISSION);
119140
}
120-
List<ChatInfoWithMember> chatInfoWithMembers = chatReader.readChatInfoLists(chatRoomId);
121141

122-
return new ChatGetServiceResponse(chatInfoWithMembers);
142+
CursorSummary<ChatSummary> summaries = readByCursorInCacheOrDb(chatRoomId, parameters);
143+
144+
return new ChatGetCursorServiceResponse(summaries);
145+
}
146+
147+
/*
148+
Todo: 컨트롤러 레이어와 비즈니스 서비스 레이어 모듈 분리 후 ChatCursorCacheManager 관련 코드 삭제 예정
149+
ChatCursorCacheManager 관련 코드는 구현 레이어 코드로 분리되어야 함
150+
*/
151+
private CursorSummary<ChatSummary> readByCursorInCacheOrDb(
152+
final Long chatRoomId,
153+
final CursorPageParameters parameters
154+
) {
155+
int pageSize = parameters.size() == null ? DEFAULT_CURSOR_SIZE : parameters.size();
156+
157+
ChatCursorCacheResult cursorRedisResult = chatCursorCacheReader.readByCursor(
158+
chatRoomId,
159+
parameters.cursorId(),
160+
pageSize
161+
);
162+
163+
if (cursorRedisResult.chatCursorCacheStatus() == ChatCursorCacheStatus.FAIL) {
164+
CursorSummary<ChatSummary> chatSummaryCursorSummary = chatReader.readByCursor(chatRoomId, parameters);
165+
166+
eventPublisher.publishEvent(
167+
ChatAppendCacheEvent.builder()
168+
.chatRoomId(chatRoomId)
169+
.startCursorId(parameters.cursorId())
170+
.summaries(chatSummaryCursorSummary.summaries())
171+
.requestSize(pageSize)
172+
.build()
173+
);
174+
175+
return chatSummaryCursorSummary;
176+
} else {
177+
return dataToSummary(cursorRedisResult);
178+
}
179+
}
180+
181+
private CursorSummary<ChatSummary> dataToSummary(final ChatCursorCacheResult cursorRedisResult) {
182+
List<ChatSummary> summaries = cursorRedisResult.chatCursorCacheList().stream()
183+
.map(this::getChatSummary)
184+
.toList();
185+
186+
return CursorUtils.getCursorSummaries(summaries);
123187
}
124188

125189
private LocalDateTime getCreatedAt(final String timeSeq) {
126190
long longTimeSeq = Long.parseLong(timeSeq);
127191
return LocalDateTime.ofInstant(Instant.ofEpochMilli(longTimeSeq), ZoneId.systemDefault());
128192
}
193+
194+
private ChatSummary getChatSummary(final ChatCursorCache data) {
195+
return new ChatSummary(
196+
data.cursorId(),
197+
data.chatId(),
198+
data.chatRoomId(),
199+
data.memberId(),
200+
data.nickname(),
201+
data.profileImage(),
202+
data.message(),
203+
LocalDateTime.parse(data.sendAt()),
204+
ChatType.valueOf(data.chatType())
205+
);
206+
}
129207
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.programmers.lime.domains.chat.application.dto.response;
2+
3+
import com.programmers.lime.common.cursor.CursorSummary;
4+
import com.programmers.lime.domains.chat.model.ChatSummary;
5+
6+
public record ChatGetCursorServiceResponse(
7+
CursorSummary<ChatSummary> cursorSummary
8+
) {
9+
}

lime-api/src/main/java/com/programmers/lime/global/cache/CacheType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
@RequiredArgsConstructor
88
public enum CacheType {
99

10-
REFRESH_TOKEN("refreshToken", 1209600, 10000);
10+
REFRESH_TOKEN("refreshToken", 1209600, 10000),
11+
CHAT_CACHE("chat", 24 * 3600, 500000);
1112

1213
private final String cacheName;
1314
private final int expireAfterWrite;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.programmers.lime.global.event.chat;
2+
3+
import java.util.List;
4+
5+
import com.programmers.lime.domains.chat.model.ChatSummary;
6+
7+
import lombok.Builder;
8+
9+
@Builder
10+
public record ChatAppendCacheEvent(
11+
Long chatRoomId,
12+
String startCursorId,
13+
List<ChatSummary> summaries,
14+
int requestSize
15+
) {
16+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.programmers.lime.global.event.chat;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import org.springframework.context.event.EventListener;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Component;
9+
10+
import com.programmers.lime.domains.chat.implementation.ChatReader;
11+
import com.programmers.lime.domains.chat.model.ChatSummary;
12+
import com.programmers.lime.domains.chatroom.model.ChatRoomInfo;
13+
import com.programmers.lime.redis.chat.ChatCursorCacheAppender;
14+
import com.programmers.lime.redis.chat.ChatCursorCacheUtil;
15+
import com.programmers.lime.redis.chat.model.ChatCursorCache;
16+
17+
import lombok.RequiredArgsConstructor;
18+
19+
@Component
20+
@RequiredArgsConstructor
21+
public class ChatAppendEventListener {
22+
23+
private final ChatCursorCacheAppender chatCursorCacheAppender;
24+
25+
private final ChatReader chatReader;
26+
27+
private static String getNextCursorId(
28+
final int requestSize,
29+
final List<ChatSummary> summaries,
30+
final int idx
31+
) {
32+
String nextCursorId = null;
33+
34+
if (idx + 1 < summaries.size()) {
35+
nextCursorId = summaries.get(idx + 1).cursorId();
36+
}
37+
38+
if (summaries.size() < requestSize && idx == summaries.size() - 1) {
39+
nextCursorId = ChatCursorCacheUtil.TAIL_CURSOR_ID;
40+
}
41+
42+
return nextCursorId;
43+
}
44+
45+
@Async
46+
@EventListener
47+
public void publishAppendCacheEvent(final ChatAppendCacheEvent chatAppendCacheEvent) {
48+
List<ChatSummary> summaries = chatAppendCacheEvent.summaries();
49+
List<ChatCursorCache> chatCursorCacheList = new ArrayList<>();
50+
for (int i = 0; i < summaries.size(); i++) {
51+
ChatSummary chatSummary = summaries.get(i);
52+
String nextCursorId = getNextCursorId(chatAppendCacheEvent.requestSize(), summaries, i);
53+
54+
chatCursorCacheList.add(getChatSummaryCacheData(chatSummary, nextCursorId));
55+
}
56+
57+
chatCursorCacheAppender.append(
58+
chatAppendCacheEvent.chatRoomId(),
59+
chatAppendCacheEvent.startCursorId(),
60+
chatCursorCacheList
61+
);
62+
}
63+
64+
@Async
65+
@EventListener
66+
public void publishChatInitCacheEvent(final ChatInitCacheEvent chatInitCacheEvent) {
67+
List<ChatRoomInfo> chatRoomInfos = chatInitCacheEvent.chatRoomInfos();
68+
69+
for (ChatRoomInfo chatRoomInfo : chatRoomInfos) {
70+
ChatSummary firstChat = chatReader.readFirstChat(chatRoomInfo.chatRoomId());
71+
ChatSummary lastChat = chatReader.readLastChat(chatRoomInfo.chatRoomId());
72+
73+
if (firstChat == null || lastChat == null) {
74+
continue;
75+
}
76+
77+
chatCursorCacheAppender.appendHeadNext(
78+
chatRoomInfo.chatRoomId(),
79+
getChatSummaryCacheData(firstChat, null)
80+
);
81+
82+
chatCursorCacheAppender.appendLastChatNext(
83+
chatRoomInfo.chatRoomId(),
84+
getChatSummaryCacheData(lastChat, null)
85+
);
86+
}
87+
}
88+
89+
private ChatCursorCache getChatSummaryCacheData(
90+
final ChatSummary chatSummary,
91+
final String nextCursorId
92+
) {
93+
return ChatCursorCache.builder()
94+
.cursorId(chatSummary.cursorId())
95+
.nextCursorId(nextCursorId)
96+
.chatId(chatSummary.chatId())
97+
.chatRoomId(chatSummary.chatRoomId())
98+
.memberId(chatSummary.memberId())
99+
.nickname(chatSummary.nickname())
100+
.profileImage(chatSummary.profileImage())
101+
.message(chatSummary.message())
102+
.sendAt(chatSummary.sendAt().toString())
103+
.chatType(chatSummary.chatType().name())
104+
.build();
105+
}
106+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.programmers.lime.global.event.chat;
2+
3+
import java.util.List;
4+
5+
import com.programmers.lime.domains.chatroom.model.ChatRoomInfo;
6+
7+
public record ChatInitCacheEvent(
8+
List<ChatRoomInfo> chatRoomInfos
9+
) {
10+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.programmers.lime.global.initializer;
2+
3+
import java.util.List;
4+
5+
import org.springframework.boot.ApplicationArguments;
6+
import org.springframework.boot.ApplicationRunner;
7+
import org.springframework.context.ApplicationEventPublisher;
8+
import org.springframework.stereotype.Component;
9+
10+
import com.programmers.lime.domains.chatroom.implementation.ChatRoomReader;
11+
import com.programmers.lime.domains.chatroom.model.ChatRoomInfo;
12+
import com.programmers.lime.global.event.chat.ChatInitCacheEvent;
13+
14+
import lombok.RequiredArgsConstructor;
15+
16+
@Component
17+
@RequiredArgsConstructor
18+
public class ChatCursorCacheInitializer implements ApplicationRunner {
19+
20+
private final ChatRoomReader chatRoomReader;
21+
22+
private final ApplicationEventPublisher eventPublisher;
23+
24+
@Override
25+
public void run(final ApplicationArguments args) {
26+
List<ChatRoomInfo> chatRoomInfos = chatRoomReader.readOpenChatRoomsByMemberId(null);
27+
28+
ChatInitCacheEvent chatInitCacheEvent = new ChatInitCacheEvent(chatRoomInfos);
29+
eventPublisher.publishEvent(chatInitCacheEvent);
30+
}
31+
}

0 commit comments

Comments
 (0)