Skip to content

Commit 15adfec

Browse files
committed
feat: 세션 검증 추가 및 분산 세션 전달 로직 수정
1 parent 625cc80 commit 15adfec

File tree

8 files changed

+94
-49
lines changed

8 files changed

+94
-49
lines changed

ProjectVG.Api/appsettings.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,23 @@
1212
"Audience": "ProjectVG"
1313
},
1414
"DistributedSystem": {
15-
"Enabled": false,
15+
"Enabled": true,
1616
"ServerId": "api-server-001",
1717
"HeartbeatIntervalSeconds": 30,
1818
"CleanupIntervalMinutes": 5,
1919
"ServerTimeoutMinutes": 2
20+
},
21+
"LLM": {
22+
"BaseUrl": "http://localhost:7930"
23+
},
24+
"MEMORY": {
25+
"BaseUrl": "http://localhost:7940"
26+
},
27+
"TTS": {
28+
"BaseUrl": "https://supertoneapi.com"
29+
},
30+
"ConnectionStrings": {
31+
"DefaultConnection": "Server=localhost,1433;Database=ProjectVG;User Id=sa;Password=ProjectVG123!;TrustServerCertificate=true;MultipleActiveResultSets=true",
32+
"Redis": "projectvg-redis:6379"
2033
}
2134
}

ProjectVG.Application/Services/Chat/ChatService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using ProjectVG.Application.Services.Chat.Processors;
88
using ProjectVG.Application.Services.Chat.Validators;
99
using ProjectVG.Application.Services.Conversation;
10+
using ProjectVG.Application.Services.WebSocket;
1011

1112
namespace ProjectVG.Application.Services.Chat
1213
{
@@ -18,6 +19,7 @@ public class ChatService : IChatService
1819

1920
private readonly IConversationService _conversationService;
2021
private readonly ICharacterService _characterService;
22+
private readonly IWebSocketManager _webSocketManager;
2123

2224
private readonly ChatRequestValidator _validator;
2325
private readonly MemoryContextPreprocessor _memoryPreprocessor;
@@ -36,6 +38,7 @@ public ChatService(
3638
ILogger<ChatService> logger,
3739
IConversationService conversationService,
3840
ICharacterService characterService,
41+
IWebSocketManager webSocketManager,
3942
ChatRequestValidator validator,
4043
MemoryContextPreprocessor memoryPreprocessor,
4144
ICostTrackingDecorator<UserInputAnalysisProcessor> inputProcessor,
@@ -52,6 +55,7 @@ ChatFailureHandler chatFailureHandler
5255

5356
_conversationService = conversationService;
5457
_characterService = characterService;
58+
_webSocketManager = webSocketManager;
5559
_validator = validator;
5660
_memoryPreprocessor = memoryPreprocessor;
5761
_inputProcessor = inputProcessor;
@@ -67,6 +71,8 @@ public async Task<ChatRequestResult> EnqueueChatRequestAsync(ChatRequestCommand
6771
_metricsService.StartChatMetrics(command.Id.ToString(), command.UserId.ToString(), command.CharacterId.ToString());
6872

6973
await _validator.ValidateAsync(command);
74+
_logger.LogDebug("[채팅서비스] 요청 검증 완료: UserId={UserId}, CharacterId={CharacterId}",
75+
command.UserId, command.CharacterId);
7076

7177
var preprocessContext = await PrepareChatRequestAsync(command);
7278

ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using ProjectVG.Application.Services.Users;
33
using ProjectVG.Application.Services.Character;
44
using ProjectVG.Application.Services.Credit;
5+
using ProjectVG.Application.Services.WebSocket;
56
using Microsoft.Extensions.Logging;
67
using ProjectVG.Application.Models.Chat;
78

@@ -10,6 +11,7 @@ namespace ProjectVG.Application.Services.Chat.Validators
1011
public class ChatRequestValidator
1112
{
1213
private readonly ISessionStorage _sessionStorage;
14+
private readonly IWebSocketManager _webSocketManager;
1315
private readonly IUserService _userService;
1416
private readonly ICharacterService _characterService;
1517
private readonly ICreditManagementService _tokenManagementService;
@@ -20,12 +22,14 @@ public class ChatRequestValidator
2022

2123
public ChatRequestValidator(
2224
ISessionStorage sessionStorage,
25+
IWebSocketManager webSocketManager,
2326
IUserService userService,
2427
ICharacterService characterService,
2528
ICreditManagementService tokenManagementService,
2629
ILogger<ChatRequestValidator> logger)
2730
{
2831
_sessionStorage = sessionStorage;
32+
_webSocketManager = webSocketManager;
2933
_userService = userService;
3034
_characterService = characterService;
3135
_tokenManagementService = tokenManagementService;
@@ -69,31 +73,35 @@ public async Task ValidateAsync(ChatRequestCommand command)
6973
}
7074

7175
/// <summary>
72-
/// 사용자 세션 유효성 검증
76+
/// 사용자 WebSocket 세션 유효성 검증
77+
/// 분산 환경에서 WebSocket 연결이 활성화되어 있는지 확인합니다.
7378
/// </summary>
7479
private async Task ValidateUserSessionAsync(Guid userId)
7580
{
76-
try {
77-
// 사용자 ID를 기반으로 세션 조회
78-
var userSessions = (await _sessionStorage
79-
.GetSessionsByUserIdAsync(userId.ToString()))
80-
.ToList();
81+
var userIdString = userId.ToString();
8182

82-
if (userSessions.Count == 0) {
83-
_logger.LogWarning("유효하지 않은 사용자 세션: {UserId}", userId);
84-
throw new ValidationException(ErrorCode.SESSION_EXPIRED, "세션이 만료되었습니다. 다시 로그인해 주세요.");
85-
}
83+
// 분산 WebSocket 매니저에서 실제 연결 상태 확인
84+
bool isWebSocketConnected = _webSocketManager.IsSessionActive(userIdString);
8685

87-
// 세션이 존재하면 로그 기록
88-
_logger.LogDebug("세션 검증 성공: {UserId}, 활성 세션 수: {SessionCount}", userId, userSessions.Count);
86+
if (!isWebSocketConnected)
87+
{
88+
_logger.LogWarning("WebSocket 세션이 연결되어 있지 않습니다: {UserId}", userId);
89+
throw new ValidationException(ErrorCode.WEBSOCKET_SESSION_REQUIRED,
90+
"채팅 요청을 처리하려면 WebSocket 연결이 필요합니다. 먼저 WebSocket에 연결해주세요.");
8991
}
90-
catch (ValidationException) {
91-
throw; // 검증 예외는 그대로 전파
92+
93+
_logger.LogDebug("WebSocket 세션 검증 성공: {UserId}", userId);
94+
95+
// 추가로 세션 하트비트 업데이트 (선택사항)
96+
try
97+
{
98+
await _webSocketManager.UpdateSessionHeartbeatAsync(userIdString);
99+
_logger.LogDebug("세션 하트비트 업데이트 완료: {UserId}", userId);
92100
}
93-
catch (Exception ex) {
94-
_logger.LogError(ex, "세션 검증 중 예상치 못한 오류: {UserId}", userId);
95-
// 세션 스토리지 오류 시에는 검증을 통과시키되 로그는 남김 (서비스 가용성 우선)
96-
_logger.LogWarning("세션 스토리지 오류로 인해 세션 검증을 건너뜁니다: {UserId}", userId);
101+
catch (Exception ex)
102+
{
103+
// 하트비트 업데이트 실패는 로그만 남기고 진행
104+
_logger.LogWarning(ex, "세션 하트비트 업데이트 실패 (계속 진행): {UserId}", userId);
97105
}
98106
}
99107
}

ProjectVG.Application/Services/MessageBroker/DistributedMessageBroker.cs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using ProjectVG.Application.Models.MessageBroker;
33
using ProjectVG.Application.Models.WebSocket;
44
using ProjectVG.Domain.Services.Server;
5-
using ProjectVG.Application.Services.WebSocket;
5+
using ProjectVG.Application.Services.Session;
66
using StackExchange.Redis;
77

88
namespace ProjectVG.Application.Services.MessageBroker
@@ -14,7 +14,7 @@ public class DistributedMessageBroker : IMessageBroker, IDisposable
1414
{
1515
private readonly IConnectionMultiplexer _redis;
1616
private readonly ISubscriber _subscriber;
17-
private readonly IWebSocketManager _webSocketManager;
17+
private readonly IConnectionRegistry _connectionRegistry;
1818
private readonly IServerRegistrationService _serverRegistration;
1919
private readonly ILogger<DistributedMessageBroker> _logger;
2020
private readonly string _serverId;
@@ -27,13 +27,13 @@ public class DistributedMessageBroker : IMessageBroker, IDisposable
2727

2828
public DistributedMessageBroker(
2929
IConnectionMultiplexer redis,
30-
IWebSocketManager webSocketManager,
30+
IConnectionRegistry connectionRegistry,
3131
IServerRegistrationService serverRegistration,
3232
ILogger<DistributedMessageBroker> logger)
3333
{
3434
_redis = redis;
3535
_subscriber = redis.GetSubscriber();
36-
_webSocketManager = webSocketManager;
36+
_connectionRegistry = connectionRegistry;
3737
_serverRegistration = serverRegistration;
3838
_logger = logger;
3939
_serverId = serverRegistration.GetServerId();
@@ -70,7 +70,7 @@ public async Task SendToUserAsync(string userId, object message)
7070
_logger.LogInformation("[분산브로커] SendToUserAsync 시작: UserId={UserId}, ServerId={ServerId}", userId, _serverId);
7171

7272
// 1. 먼저 로컬에 해당 사용자가 있는지 확인
73-
var isLocalActive = _webSocketManager.IsSessionActive(userId);
73+
var isLocalActive = _connectionRegistry.IsConnected(userId);
7474
_logger.LogInformation("[분산브로커] 로컬 세션 확인: UserId={UserId}, IsLocalActive={IsLocalActive}", userId, isLocalActive);
7575

7676
if (isLocalActive)
@@ -201,7 +201,7 @@ private async void OnUserMessageReceived(RedisChannel channel, RedisValue messag
201201
brokerMessage.TargetUserId, brokerMessage.SourceServerId, brokerMessage.MessageType);
202202

203203
// 로컬에서 해당 사용자가 연결되어 있는지 확인
204-
var isLocalActive = _webSocketManager.IsSessionActive(brokerMessage.TargetUserId);
204+
var isLocalActive = _connectionRegistry.IsConnected(brokerMessage.TargetUserId);
205205
_logger.LogInformation("[분산브로커] 로컬 세션 확인: TargetUserId={TargetUserId}, IsLocalActive={IsLocalActive}",
206206
brokerMessage.TargetUserId, isLocalActive);
207207

@@ -280,17 +280,28 @@ private async Task SendLocalMessage(string userId, object? message)
280280

281281
try
282282
{
283-
if (message is WebSocketMessage wsMessage)
283+
if (_connectionRegistry.TryGet(userId, out var connection) && connection != null)
284284
{
285-
await _webSocketManager.SendAsync(userId, wsMessage);
286-
_logger.LogInformation("[분산브로커] WebSocketMessage 전송 완료: UserId={UserId}, Type={Type}",
287-
userId, wsMessage.Type);
285+
string messageText;
286+
287+
if (message is WebSocketMessage wsMessage)
288+
{
289+
messageText = System.Text.Json.JsonSerializer.Serialize(wsMessage);
290+
_logger.LogInformation("[분산브로커] WebSocketMessage 전송 완료: UserId={UserId}, Type={Type}",
291+
userId, wsMessage.Type);
292+
}
293+
else
294+
{
295+
var wrappedMessage = new WebSocketMessage("message", message);
296+
messageText = System.Text.Json.JsonSerializer.Serialize(wrappedMessage);
297+
_logger.LogInformation("[분산브로커] 래핑된 메시지 전송 완료: UserId={UserId}", userId);
298+
}
299+
300+
await connection.SendTextAsync(messageText);
288301
}
289302
else
290303
{
291-
var wrappedMessage = new WebSocketMessage("message", message);
292-
await _webSocketManager.SendAsync(userId, wrappedMessage);
293-
_logger.LogInformation("[분산브로커] 래핑된 메시지 전송 완료: UserId={UserId}", userId);
304+
_logger.LogWarning("[분산브로커] 로컬 연결을 찾을 수 없음: UserId={UserId}", userId);
294305
}
295306
}
296307
catch (Exception ex)

ProjectVG.Application/Services/WebSocket/DistributedWebSocketManager.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.DependencyInjection;
23
using ProjectVG.Application.Models.WebSocket;
34
using ProjectVG.Application.Services.MessageBroker;
45
using ProjectVG.Application.Services.Session;
@@ -16,20 +17,18 @@ public class DistributedWebSocketManager : IWebSocketManager
1617
private readonly ILogger<DistributedWebSocketManager> _logger;
1718
private readonly IConnectionRegistry _connectionRegistry;
1819
private readonly ISessionStorage _sessionStorage;
19-
private readonly DistributedMessageBroker? _distributedBroker;
20+
private readonly IServiceProvider _serviceProvider;
2021

2122
public DistributedWebSocketManager(
2223
ILogger<DistributedWebSocketManager> logger,
2324
IConnectionRegistry connectionRegistry,
2425
ISessionStorage sessionStorage,
25-
IMessageBroker messageBroker)
26+
IServiceProvider serviceProvider)
2627
{
2728
_logger = logger;
2829
_connectionRegistry = connectionRegistry;
2930
_sessionStorage = sessionStorage;
30-
31-
// MessageBroker가 분산 브로커인지 확인
32-
_distributedBroker = messageBroker as DistributedMessageBroker;
31+
_serviceProvider = serviceProvider;
3332
}
3433

3534
public async Task<string> ConnectAsync(string userId)
@@ -43,18 +42,21 @@ await _sessionStorage.CreateAsync(new SessionInfo
4342
ConnectedAt = DateTime.UtcNow
4443
});
4544

46-
// 분산 환경인 경우 사용자 채널 구독
47-
if (_distributedBroker != null)
45+
// 분산 메시지 브로커에서 사용자 채널 구독
46+
var messageBroker = _serviceProvider.GetService<IMessageBroker>() as DistributedMessageBroker;
47+
if (messageBroker != null)
4848
{
4949
_logger.LogInformation("[분산WebSocket] 분산 브로커 채널 구독 시작: UserId={UserId}", userId);
50-
await _distributedBroker.SubscribeToUserChannelAsync(userId);
50+
await messageBroker.SubscribeToUserChannelAsync(userId);
5151
_logger.LogInformation("[분산WebSocket] 분산 사용자 채널 구독 완료: UserId={UserId}", userId);
5252
}
5353
else
5454
{
5555
_logger.LogWarning("[분산WebSocket] 분산 브로커가 null입니다: UserId={UserId}", userId);
5656
}
5757

58+
_logger.LogInformation("[분산WebSocket] 분산 WebSocket 세션 생성 완료: UserId={UserId}", userId);
59+
5860
return userId;
5961
}
6062

@@ -97,11 +99,12 @@ public async Task DisconnectAsync(string userId)
9799

98100
_connectionRegistry.Unregister(userId);
99101

100-
// 분산 환경인 경우 사용자 채널 구독 해제
101-
if (_distributedBroker != null)
102+
// 분산 메시지 브로커에서 사용자 채널 구독 해제
103+
var messageBroker = _serviceProvider.GetService<IMessageBroker>() as DistributedMessageBroker;
104+
if (messageBroker != null)
102105
{
103106
_logger.LogInformation("[분산WebSocket] 분산 브로커 채널 구독 해제 시작: UserId={UserId}", userId);
104-
await _distributedBroker.UnsubscribeFromUserChannelAsync(userId);
107+
await messageBroker.UnsubscribeFromUserChannelAsync(userId);
105108
_logger.LogInformation("[분산WebSocket] 분산 사용자 채널 구독 해제 완료: UserId={UserId}", userId);
106109
}
107110

ProjectVG.Common/Constants/ErrorCodes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public enum ErrorCode
8484
GUEST_ID_INVALID,
8585
PROVIDER_USER_ID_INVALID,
8686
SESSION_EXPIRED,
87+
WEBSOCKET_SESSION_REQUIRED,
8788
RATE_LIMIT_EXCEEDED,
8889
RESOURCE_QUOTA_EXCEEDED,
8990

@@ -179,6 +180,7 @@ public static class ErrorCodeExtensions
179180
{ ErrorCode.GUEST_ID_INVALID, "유효하지 않은 게스트 ID입니다" },
180181
{ ErrorCode.PROVIDER_USER_ID_INVALID, "유효하지 않은 제공자 사용자 ID입니다" },
181182
{ ErrorCode.SESSION_EXPIRED, "세션이 만료되었습니다" },
183+
{ ErrorCode.WEBSOCKET_SESSION_REQUIRED, "WebSocket 연결이 필요합니다" },
182184
{ ErrorCode.RATE_LIMIT_EXCEEDED, "요청 한도를 초과했습니다" },
183185
{ ErrorCode.RESOURCE_QUOTA_EXCEEDED, "리소스 할당량을 초과했습니다" },
184186

ProjectVG.Infrastructure/InfrastructureServiceCollectionExtensions.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti
3030
AddAuthServices(services, configuration);
3131
AddRedisServices(services, configuration);
3232
AddOAuth2Services(services, configuration);
33-
// AddDistributedSystemServices(services, configuration);
33+
AddDistributedSystemServices(services, configuration);
3434

3535
return services;
3636
}
@@ -212,7 +212,6 @@ private static void AddRedisServices(IServiceCollection services, IConfiguration
212212
}
213213
}
214214

215-
/*
216215
/// <summary>
217216
/// 분산 시스템 서비스
218217
/// </summary>
@@ -233,6 +232,5 @@ private static void AddDistributedSystemServices(IServiceCollection services, IC
233232
Console.WriteLine("단일 서버 모드");
234233
}
235234
}
236-
*/
237235
}
238236
}

docker-compose.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
21
name: projectvg-api-server
32

43
networks:
54
projectvg-network:
65
driver: bridge
6+
projectvg-external-db:
7+
external: true
8+
name: projectvg-database_projectvg-external-db
79

810
volumes:
911
projectvg-api-logs:
@@ -28,6 +30,8 @@ services:
2830
mem_limit: ${API_MEMORY_LIMIT:-1g}
2931
memswap_limit: ${API_MEMORY_LIMIT:-1g}
3032
environment:
33+
# 분산 시스템 설정
34+
- SERVER_ID=${SERVER_ID:-api-server-001}
3135
# 글로벌 환경 설정
3236
- ENVIRONMENT=${ENVIRONMENT:-development}
3337
- DEBUG_MODE=${DEBUG_MODE:-false}
@@ -57,12 +61,12 @@ services:
5761
- projectvg-api-logs:/app/logs
5862
networks:
5963
- projectvg-network
64+
- projectvg-external-db
6065
restart: unless-stopped
6166
extra_hosts:
6267
- "host.docker.internal:host-gateway"
6368
logging:
6469
driver: "json-file"
6570
options:
6671
max-size: "10m"
67-
max-file: "3"
68-
72+
max-file: "3"

0 commit comments

Comments
 (0)