Skip to content

Commit 0d5fd45

Browse files
committed
Refactor: WebSocketConstants로 키/상수 값 분리하여 중앙 관리
1 parent 6f808e1 commit 0d5fd45

File tree

4 files changed

+98
-69
lines changed

4 files changed

+98
-69
lines changed

src/main/java/com/back/global/config/WebSocketConfig.java renamed to src/main/java/com/back/global/websocket/config/WebSocketConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.back.global.config;
1+
package com.back.global.websocket.config;
22

33
import com.back.global.security.user.CustomUserDetails;
44
import com.back.global.security.jwt.JwtTokenProvider;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.back.global.websocket.config;
2+
3+
import java.time.Duration;
4+
5+
public final class WebSocketConstants {
6+
7+
private WebSocketConstants() {
8+
throw new AssertionError("상수 클래스는 인스턴스화할 수 없습니다.");
9+
}
10+
11+
// ===== TTL & Timeout 설정 =====
12+
13+
/**
14+
* WebSocket 세션 TTL (6분)
15+
* - Heartbeat로 연장됨
16+
*/
17+
public static final Duration SESSION_TTL = Duration.ofMinutes(6);
18+
19+
/**
20+
* Heartbeat 권장 간격 (5분)
21+
* - 클라이언트가 이 주기로 Heartbeat 전송 권장
22+
*/
23+
public static final Duration HEARTBEAT_INTERVAL = Duration.ofMinutes(5);
24+
25+
// ===== Redis Key 패턴 =====
26+
27+
/**
28+
* 사용자 세션 정보 저장 Key
29+
* - 패턴: ws:user:{userId}
30+
* - 값: WebSocketSessionInfo
31+
*/
32+
public static final String USER_SESSION_KEY_PREFIX = "ws:user:";
33+
34+
/**
35+
* 세션 → 사용자 매핑 Key
36+
* - 패턴: ws:session:{sessionId}
37+
* - 값: userId (Long)
38+
*/
39+
public static final String SESSION_USER_KEY_PREFIX = "ws:session:";
40+
41+
/**
42+
* 방별 참가자 목록 Key
43+
* - 패턴: ws:room:{roomId}:users
44+
* - 값: Set<userId>
45+
*/
46+
public static final String ROOM_USERS_KEY_PREFIX = "ws:room:";
47+
public static final String ROOM_USERS_KEY_SUFFIX = ":users";
48+
49+
// ===== Key 빌더 헬퍼 메서드 =====
50+
51+
public static String buildUserSessionKey(Long userId) {
52+
return USER_SESSION_KEY_PREFIX + userId;
53+
}
54+
55+
public static String buildSessionUserKey(String sessionId) {
56+
return SESSION_USER_KEY_PREFIX + sessionId;
57+
}
58+
59+
public static String buildRoomUsersKey(Long roomId) {
60+
return ROOM_USERS_KEY_PREFIX + roomId + ROOM_USERS_KEY_SUFFIX;
61+
}
62+
63+
public static String buildUserSessionKeyPattern() {
64+
return USER_SESSION_KEY_PREFIX + "*";
65+
}
66+
67+
// ===== API 응답용 =====
68+
69+
public static String getSessionTTLDescription() {
70+
return SESSION_TTL.toMinutes() + "분 (Heartbeat 방식)";
71+
}
72+
73+
public static String getHeartbeatIntervalDescription() {
74+
return HEARTBEAT_INTERVAL.toMinutes() + "분";
75+
}
76+
}

src/main/java/com/back/global/websocket/controller/WebSocketApiController.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.back.global.websocket.controller;
22

33
import com.back.global.common.dto.RsData;
4+
import com.back.global.websocket.config.WebSocketConstants;
45
import com.back.global.websocket.service.WebSocketSessionManager;
56
import io.swagger.v3.oas.annotations.Operation;
67
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -29,8 +30,8 @@ public ResponseEntity<RsData<Map<String, Object>>> healthCheck() {
2930
data.put("service", "WebSocket");
3031
data.put("status", "running");
3132
data.put("timestamp", LocalDateTime.now());
32-
data.put("sessionTTL", "10분 (Heartbeat 방식)");
33-
data.put("heartbeatInterval", "5분");
33+
data.put("sessionTTL", WebSocketConstants.getSessionTTLDescription());
34+
data.put("heartbeatInterval", WebSocketConstants.getHeartbeatIntervalDescription());
3435
data.put("totalOnlineUsers", sessionManager.getTotalOnlineUserCount());
3536
data.put("endpoints", Map.of(
3637
"websocket", "/ws",
@@ -53,8 +54,8 @@ public ResponseEntity<RsData<Map<String, Object>>> getConnectionInfo() {
5354
connectionInfo.put("websocketUrl", "/ws");
5455
connectionInfo.put("sockjsSupport", true);
5556
connectionInfo.put("stompVersion", "1.2");
56-
connectionInfo.put("heartbeatInterval", "5분");
57-
connectionInfo.put("sessionTTL", "10분");
57+
connectionInfo.put("heartbeatInterval", WebSocketConstants.getHeartbeatIntervalDescription());
58+
connectionInfo.put("sessionTTL", WebSocketConstants.getSessionTTLDescription());
5859
connectionInfo.put("subscribeTopics", Map.of(
5960
"roomChat", "/topic/rooms/{roomId}/chat",
6061
"privateMessage", "/user/queue/messages",

src/main/java/com/back/global/websocket/store/RedisSessionStore.java

Lines changed: 16 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22

33
import com.back.global.exception.CustomException;
44
import com.back.global.exception.ErrorCode;
5+
import com.back.global.websocket.config.WebSocketConstants;
56
import com.back.global.websocket.dto.WebSocketSessionInfo;
67
import com.fasterxml.jackson.databind.DeserializationFeature;
78
import com.fasterxml.jackson.databind.ObjectMapper;
89
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
9-
import lombok.RequiredArgsConstructor;
1010
import lombok.extern.slf4j.Slf4j;
1111
import org.springframework.data.redis.core.RedisTemplate;
1212
import org.springframework.stereotype.Component;
1313

14-
import java.time.Duration;
1514
import java.util.LinkedHashMap;
1615
import java.util.Set;
1716
import java.util.stream.Collectors;
@@ -30,61 +29,44 @@ public class RedisSessionStore {
3029
private final RedisTemplate<String, Object> redisTemplate;
3130
private final ObjectMapper objectMapper;
3231

33-
// Redis Key 패턴
34-
private static final String USER_SESSION_KEY = "ws:user:{}";
35-
private static final String SESSION_USER_KEY = "ws:session:{}";
36-
private static final String ROOM_USERS_KEY = "ws:room:{}:users";
37-
38-
// TTL 설정
39-
private static final Duration SESSION_TTL = Duration.ofMinutes(6);
40-
41-
// 생성자에서 ObjectMapper 설정
4232
public RedisSessionStore(RedisTemplate<String, Object> redisTemplate) {
4333
this.redisTemplate = redisTemplate;
44-
45-
// ObjectMapper 초기화 및 설정
4634
this.objectMapper = new ObjectMapper();
4735
this.objectMapper.registerModule(new JavaTimeModule());
4836
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
4937
}
5038

51-
// ============= 세션 정보 저장/조회/삭제 =============
52-
53-
// 사용자 세션 정보 저장
5439
public void saveUserSession(Long userId, WebSocketSessionInfo sessionInfo) {
5540
try {
56-
String userKey = buildUserSessionKey(userId);
57-
redisTemplate.opsForValue().set(userKey, sessionInfo, SESSION_TTL);
41+
String userKey = WebSocketConstants.buildUserSessionKey(userId);
42+
redisTemplate.opsForValue().set(userKey, sessionInfo, WebSocketConstants.SESSION_TTL);
5843
log.debug("사용자 세션 정보 저장 완료 - userId: {}", userId);
5944
} catch (Exception e) {
6045
log.error("사용자 세션 정보 저장 실패 - userId: {}", userId, e);
6146
throw new CustomException(ErrorCode.WS_REDIS_ERROR);
6247
}
6348
}
6449

65-
// 세션ID → 사용자ID 매핑 저장
6650
public void saveSessionUserMapping(String sessionId, Long userId) {
6751
try {
68-
String sessionKey = buildSessionUserKey(sessionId);
69-
redisTemplate.opsForValue().set(sessionKey, userId, SESSION_TTL);
52+
String sessionKey = WebSocketConstants.buildSessionUserKey(sessionId);
53+
redisTemplate.opsForValue().set(sessionKey, userId, WebSocketConstants.SESSION_TTL);
7054
log.debug("세션-사용자 매핑 저장 완료 - sessionId: {}", sessionId);
7155
} catch (Exception e) {
7256
log.error("세션-사용자 매핑 저장 실패 - sessionId: {}", sessionId, e);
7357
throw new CustomException(ErrorCode.WS_REDIS_ERROR);
7458
}
7559
}
7660

77-
// 사용자 세션 정보 조회
7861
public WebSocketSessionInfo getUserSession(Long userId) {
7962
try {
80-
String userKey = buildUserSessionKey(userId);
63+
String userKey = WebSocketConstants.buildUserSessionKey(userId);
8164
Object value = redisTemplate.opsForValue().get(userKey);
8265

8366
if (value == null) {
8467
return null;
8568
}
8669

87-
// LinkedHashMap으로 역직렬화된 경우 변환
8870
if (value instanceof LinkedHashMap || !(value instanceof WebSocketSessionInfo)) {
8971
return objectMapper.convertValue(value, WebSocketSessionInfo.class);
9072
}
@@ -97,10 +79,9 @@ public WebSocketSessionInfo getUserSession(Long userId) {
9779
}
9880
}
9981

100-
// 세션ID로 사용자ID 조회
10182
public Long getUserIdBySession(String sessionId) {
10283
try {
103-
String sessionKey = buildSessionUserKey(sessionId);
84+
String sessionKey = WebSocketConstants.buildSessionUserKey(sessionId);
10485
Object value = redisTemplate.opsForValue().get(sessionKey);
10586

10687
if (value == null) {
@@ -115,10 +96,9 @@ public Long getUserIdBySession(String sessionId) {
11596
}
11697
}
11798

118-
// 사용자 세션 정보 삭제
11999
public void deleteUserSession(Long userId) {
120100
try {
121-
String userKey = buildUserSessionKey(userId);
101+
String userKey = WebSocketConstants.buildUserSessionKey(userId);
122102
redisTemplate.delete(userKey);
123103
log.debug("사용자 세션 정보 삭제 완료 - userId: {}", userId);
124104
} catch (Exception e) {
@@ -127,10 +107,9 @@ public void deleteUserSession(Long userId) {
127107
}
128108
}
129109

130-
// 세션-사용자 매핑 삭제
131110
public void deleteSessionUserMapping(String sessionId) {
132111
try {
133-
String sessionKey = buildSessionUserKey(sessionId);
112+
String sessionKey = WebSocketConstants.buildSessionUserKey(sessionId);
134113
redisTemplate.delete(sessionKey);
135114
log.debug("세션-사용자 매핑 삭제 완료 - sessionId: {}", sessionId);
136115
} catch (Exception e) {
@@ -139,36 +118,31 @@ public void deleteSessionUserMapping(String sessionId) {
139118
}
140119
}
141120

142-
// 사용자 세션 존재 여부 확인
143121
public boolean existsUserSession(Long userId) {
144122
try {
145-
String userKey = buildUserSessionKey(userId);
123+
String userKey = WebSocketConstants.buildUserSessionKey(userId);
146124
return Boolean.TRUE.equals(redisTemplate.hasKey(userKey));
147125
} catch (Exception e) {
148126
log.error("사용자 세션 존재 여부 확인 실패 - userId: {}", userId, e);
149127
throw new CustomException(ErrorCode.WS_REDIS_ERROR);
150128
}
151129
}
152130

153-
// ============= 방 참가자 관리 =============
154-
155-
// 방에 사용자 추가
156131
public void addUserToRoom(Long roomId, Long userId) {
157132
try {
158-
String roomUsersKey = buildRoomUsersKey(roomId);
133+
String roomUsersKey = WebSocketConstants.buildRoomUsersKey(roomId);
159134
redisTemplate.opsForSet().add(roomUsersKey, userId);
160-
redisTemplate.expire(roomUsersKey, SESSION_TTL);
135+
redisTemplate.expire(roomUsersKey, WebSocketConstants.SESSION_TTL);
161136
log.debug("방에 사용자 추가 완료 - roomId: {}, userId: {}", roomId, userId);
162137
} catch (Exception e) {
163138
log.error("방에 사용자 추가 실패 - roomId: {}, userId: {}", roomId, userId, e);
164139
throw new CustomException(ErrorCode.WS_REDIS_ERROR);
165140
}
166141
}
167142

168-
// 방에서 사용자 제거
169143
public void removeUserFromRoom(Long roomId, Long userId) {
170144
try {
171-
String roomUsersKey = buildRoomUsersKey(roomId);
145+
String roomUsersKey = WebSocketConstants.buildRoomUsersKey(roomId);
172146
redisTemplate.opsForSet().remove(roomUsersKey, userId);
173147
log.debug("방에서 사용자 제거 완료 - roomId: {}, userId: {}", roomId, userId);
174148
} catch (Exception e) {
@@ -177,10 +151,9 @@ public void removeUserFromRoom(Long roomId, Long userId) {
177151
}
178152
}
179153

180-
// 방의 사용자 목록 조회
181154
public Set<Long> getRoomUsers(Long roomId) {
182155
try {
183-
String roomUsersKey = buildRoomUsersKey(roomId);
156+
String roomUsersKey = WebSocketConstants.buildRoomUsersKey(roomId);
184157
Set<Object> userIds = redisTemplate.opsForSet().members(roomUsersKey);
185158

186159
if (userIds != null) {
@@ -196,10 +169,9 @@ public Set<Long> getRoomUsers(Long roomId) {
196169
}
197170
}
198171

199-
// 방의 사용자 수 조회
200172
public long getRoomUserCount(Long roomId) {
201173
try {
202-
String roomUsersKey = buildRoomUsersKey(roomId);
174+
String roomUsersKey = WebSocketConstants.buildRoomUsersKey(roomId);
203175
Long count = redisTemplate.opsForSet().size(roomUsersKey);
204176
return count != null ? count : 0;
205177
} catch (Exception e) {
@@ -208,36 +180,16 @@ public long getRoomUserCount(Long roomId) {
208180
}
209181
}
210182

211-
// ============= 전체 통계 =============
212-
213-
// 전체 온라인 사용자 수 조회
214183
public long getTotalOnlineUserCount() {
215184
try {
216-
Set<String> userKeys = redisTemplate.keys(buildUserSessionKey("*"));
185+
Set<String> userKeys = redisTemplate.keys(WebSocketConstants.buildUserSessionKeyPattern());
217186
return userKeys != null ? userKeys.size() : 0;
218187
} catch (Exception e) {
219188
log.error("전체 온라인 사용자 수 조회 실패", e);
220189
return 0;
221190
}
222191
}
223192

224-
// ============= Key 생성 헬퍼 =============
225-
226-
private String buildUserSessionKey(Object userId) {
227-
return USER_SESSION_KEY.replace("{}", userId.toString());
228-
}
229-
230-
private String buildSessionUserKey(String sessionId) {
231-
return SESSION_USER_KEY.replace("{}", sessionId);
232-
}
233-
234-
private String buildRoomUsersKey(Long roomId) {
235-
return ROOM_USERS_KEY.replace("{}", roomId.toString());
236-
}
237-
238-
// ============= 타입 변환 헬퍼 =============
239-
240-
// Object를 Long으로 안전하게 변환
241193
private Long convertToLong(Object obj) {
242194
if (obj instanceof Long) {
243195
return (Long) obj;

0 commit comments

Comments
 (0)