Skip to content

Commit bd8e3e8

Browse files
committed
feat: 여러 서버 인스턴스에서 실시간 마인드맵의 모든 사용자가 동일한 접속자 목록 Redis 저장
- Set 자료구조를 통한 중복 제거 - 간단한 Redis Key 사용(mindmap:{mapId}:users)
1 parent 0ff1219 commit bd8e3e8

File tree

1 file changed

+50
-24
lines changed

1 file changed

+50
-24
lines changed

src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapSseService.java

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package com.teamEWSN.gitdeun.mindmap.service;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
34
import com.fasterxml.jackson.databind.ObjectMapper;
45
import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails;
56
import com.teamEWSN.gitdeun.mindmap.dto.MindmapDetailResponseDto;
67
import com.teamEWSN.gitdeun.mindmap.dto.prompt.PromptPreviewResponseDto;
78
import lombok.RequiredArgsConstructor;
89
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.data.redis.core.RedisTemplate;
911
import org.springframework.stereotype.Service;
1012
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
1113

1214
import java.io.IOException;
13-
import java.util.List;
14-
import java.util.Map;
15+
import java.util.*;
1516
import java.util.concurrent.ConcurrentHashMap;
1617
import java.util.concurrent.CopyOnWriteArrayList;
18+
import java.util.concurrent.TimeUnit;
1719
import java.util.stream.Collectors;
1820

1921
@Slf4j
@@ -22,7 +24,7 @@
2224
public class MindmapSseService {
2325

2426
private final ObjectMapper objectMapper;
25-
27+
private final RedisTemplate<String, String> redisTemplate;
2628
private record SseConnection(Long userId, String nickname, String profileImage, SseEmitter emitter) {}
2729
private final Map<Long, List<SseConnection>> connectionsByMapId = new ConcurrentHashMap<>();
2830

@@ -45,19 +47,27 @@ public SseEmitter createConnection(Long mapId, CustomUserDetails userDetails) {
4547

4648
connectionsByMapId.computeIfAbsent(mapId, k -> new CopyOnWriteArrayList<>()).add(connection);
4749

50+
try {
51+
// Redis Set에 현재 접속자 정보 추가
52+
String redisKey = "mindmap:" + mapId + ":users";
53+
String userData = objectMapper.writeValueAsString(
54+
new ConnectedUserDto(userDetails.getId(), userDetails.getNickname(), userDetails.getProfileImage())
55+
);
56+
redisTemplate.opsForSet().add(redisKey, userData);
57+
58+
// Redis Set의 만료 시간을 설정하여 비정상 종료된 연결 처리
59+
redisTemplate.expire(redisKey, 1, TimeUnit.HOURS);
60+
61+
} catch (JsonProcessingException e) {
62+
log.error("사용자 정보 직렬화 실패", e);
63+
}
64+
4865
// 연결 종료 시 정리 (기존 로직 개선)
49-
emitter.onCompletion(() -> {
50-
removeConnection(mapId, connection);
51-
broadcastUserListUpdate(mapId);
52-
});
53-
emitter.onTimeout(() -> {
54-
removeConnection(mapId, connection);
55-
broadcastUserListUpdate(mapId);
56-
});
66+
emitter.onCompletion(() -> removeConnection(mapId, connection));
67+
emitter.onTimeout(() -> removeConnection(mapId, connection));
5768
emitter.onError(throwable -> {
5869
log.error("SSE 연결 오류 - 마인드맵 ID: {}, 사용자 ID: {}", mapId, userDetails.getId(), throwable);
5970
removeConnection(mapId, connection);
60-
broadcastUserListUpdate(mapId);
6171
});
6272

6373
// 연결 확인용 초기 메시지
@@ -158,14 +168,20 @@ private void sendToEmitter(SseEmitter emitter, Object data) {
158168
// 연결 종료 시 SseConnection 객체를 찾아 제거
159169
private void removeConnection(Long mapId, SseConnection connection) {
160170
List<SseConnection> connections = connectionsByMapId.get(mapId);
161-
if (connections != null) {
162-
connections.remove(connection);
163-
log.info("SSE 연결 해제 - 마인드맵 ID: {}, 사용자 ID: {}", mapId, connection.userId());
164-
if (connections.isEmpty()) {
165-
connectionsByMapId.remove(mapId);
166-
log.info("마인드맵 ID {} 의 모든 구독자 연결 종료", mapId);
171+
if (connections != null && connections.remove(connection)) {
172+
try {
173+
// Redis Set에서 접속 종료한 사용자 정보 제거
174+
String redisKey = "mindmap:" + mapId + ":users";
175+
String userData = objectMapper.writeValueAsString(
176+
new ConnectedUserDto(connection.userId(), connection.nickname(), connection.profileImage())
177+
);
178+
redisTemplate.opsForSet().remove(redisKey, userData);
179+
180+
} catch (JsonProcessingException e) {
181+
log.error("사용자 정보 직렬화 실패 (연결 종료)", e);
167182
}
168183
broadcastUserListUpdate(mapId);
184+
log.info("SSE 연결 해제 및 Redis 업데이트 - 마인드맵 ID: {}, 사용자 ID: {}", mapId, connection.userId());
169185
}
170186
}
171187

@@ -188,13 +204,23 @@ public record ConnectedUserDto(Long userId, String nickname, String profileImage
188204

189205
// 현재 접속 중인 사용자 목록을 반환하는 서비스 메서드
190206
public List<ConnectedUserDto> getConnectedUsers(Long mapId) {
191-
List<SseConnection> connections = connectionsByMapId.getOrDefault(mapId, new CopyOnWriteArrayList<>());
207+
String redisKey = "mindmap:" + mapId + ":users";
208+
Set<String> usersJson = redisTemplate.opsForSet().members(redisKey);
192209

193-
// 중복된 userId를 제거하고 DTO로 변환하여 반환 (여러 탭 접속 시 중복 방지)
194-
return connections.stream()
195-
.map(conn -> new ConnectedUserDto(conn.userId(), conn.nickname(), conn.profileImage()))
196-
.distinct() // userId 기준으로 중복 제거
210+
if (usersJson == null) {
211+
return Collections.emptyList();
212+
}
213+
214+
return usersJson.stream()
215+
.map(json -> {
216+
try {
217+
return objectMapper.readValue(json, ConnectedUserDto.class);
218+
} catch (JsonProcessingException e) {
219+
log.error("사용자 정보 역직렬화 실패", e);
220+
return null;
221+
}
222+
})
223+
.filter(Objects::nonNull)
197224
.collect(Collectors.toList());
198225
}
199-
200226
}

0 commit comments

Comments
 (0)