Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
# ===========================================
# Copy this file to .env and fill in your actual values

ENVIRONMENT=development
DEBUG_MODE=true
LOG_LEVEL=INFO
LOG_FORMAT=json
DATA_PATH=../data
Comment on lines +6 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ENVIRONMENT vs ASPNETCORE_ENVIRONMENT 값 충돌

두 변수가 서로 다른 값을 갖고 있어 실제 실행 환경이 오인될 수 있습니다. 하나로 통일하세요.

-ASPNETCORE_ENVIRONMENT=Production
+ASPNETCORE_ENVIRONMENT=Development

(또는 ENVIRONMENT 키를 제거/주석 처리하고 ASPNETCORE_ENVIRONMENT만 사용)

Also applies to: 50-50

🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 7-7: [UnorderedKey] The DEBUG_MODE key should go before the ENVIRONMENT key

(UnorderedKey)


[warning] 9-9: [UnorderedKey] The LOG_FORMAT key should go before the LOG_LEVEL key

(UnorderedKey)


[warning] 10-10: [UnorderedKey] The DATA_PATH key should go before the DEBUG_MODE key

(UnorderedKey)

🤖 Prompt for AI Agents
In .env.example around lines 6 to 10 (and similarly at lines ~50), there is a
potential conflict between ENVIRONMENT and ASPNETCORE_ENVIRONMENT having
different values; pick one environment variable to be authoritative (preferably
ASPNETCORE_ENVIRONMENT for ASP.NET apps) and either remove or comment out the
duplicate ENVIRONMENT entry, or set both to the same value to avoid mismatched
runtime behavior; update the file so only the chosen key remains (or both match)
and add a brief comment explaining which variable the app reads.


# Database Configuration
DB_CONNECTION_STRING=Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

DB 연결 문자열은 따옴표로 감싸기

특수문자 포함 값은 인용이 안전합니다. dotenv-linter 경고도 해소됩니다.

-DB_CONNECTION_STRING=Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true
+DB_CONNECTION_STRING="Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DB_CONNECTION_STRING=Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true
DB_CONNECTION_STRING="Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true"
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 13-13: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
.env.example around line 13: the DB_CONNECTION_STRING value contains special
characters and should be wrapped in quotes to be safe and satisfy dotenv-linter;
update the line to enclose the entire connection string in double quotes (e.g.
DB_CONNECTION_STRING="Server=...;Password=...;...") so parsers and linters treat
it as a single quoted value.

DB_PASSWORD=YOUR_DB_PASSWORD

# Redis Configuration
REDIS_CONNECTION_STRING=host.docker.internal:6380

# Distributed System Configuration
DISTRIBUTED_MODE=false
SERVER_ID=

# External Services
LLM_BASE_URL=http://host.docker.internal:7930
MEMORY_BASE_URL=http://host.docker.internal:7940
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ _ReSharper*/
# Docker
**/Dockerfile.*
docker-compose.override.yml
.dockerignore.local

# Keep template files but ignore runtime files
!docker-compose.prod.yml
Expand Down
8 changes: 8 additions & 0 deletions ProjectVG.Api/Middleware/WebSocketMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ await socket.SendAsync(
WebSocketMessageType.Text,
true,
cancellationTokenSource.Token);

// 세션 하트비트 업데이트 (Redis TTL 갱신)
try {
await _webSocketService.UpdateSessionHeartbeatAsync(userId);
}
catch (Exception heartbeatEx) {
_logger.LogWarning(heartbeatEx, "세션 하트비트 업데이트 실패: {UserId}", userId);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ProjectVG.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
}

builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddApplicationServices();
builder.Services.AddApplicationServices(builder.Configuration);
builder.Services.AddDevelopmentCors();

// 부하테스트 환경에서 성능 모니터링 서비스 추가
Expand Down
20 changes: 20 additions & 0 deletions ProjectVG.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,25 @@
"JWT": {
"Issuer": "ProjectVG",
"Audience": "ProjectVG"
},
"DistributedSystem": {
"Enabled": true,
"ServerId": "api-server-001",
"HeartbeatIntervalSeconds": 30,
"CleanupIntervalMinutes": 5,
"ServerTimeoutMinutes": 2
},
Comment on lines +14 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

ServerId 고정값은 인스턴스 충돌 유발 — 인스턴스별 유일값 보장 필요

"api-server-001" 고정은 다중 인스턴스에서 충돌/세션 오염을 유발합니다. 권장:

  • ServerId를 환경변수로 주입(예: 컨테이너 인스턴스 ID/호스트명 기반).
  • 코드에서 미지정/공백일 때 머신명+GUID 등으로 자동 생성.

또한 간격/단위 정합성 확인:

  • HeartbeatIntervalSeconds=30, ServerTimeoutMinutes=2, CleanupIntervalMinutes=5 조합은 최대 5분까지 정리 지연이 발생합니다. 타임아웃(2분) ≤ 정리주기(5분)이면 지연이 큽니다. 정리주기를 더 짧게 권장합니다.

빠른 개선안(정리 주기 단축):

-    "CleanupIntervalMinutes": 5,
+    "CleanupIntervalMinutes": 1,

추가 권장(선택):

  • 단위 혼용(Seconds/Minutes)으로 혼동 여지 → 동일 단위로 통일하거나 옵션 명확화.
  • 환경/클러스터 분리를 위한 ChannelPrefix/Namespace 추가로 키/채널 충돌 방지.

중복 ServerId 사용 가능성 점검:


🏁 Script executed:

#!/bin/bash
rg -n -C2 '"ServerId"\s*:\s*"' --hidden

Length of output: 1942


ServerId 하드코딩 제거 — 인스턴스별 유일값 필요

ProjectVG.Api/appsettings.json에 "ServerId": "api-server-001"이 하드코딩되어 있어 다중 인스턴스에서 충돌/세션 오염 발생 가능. 문서에도 동일 예시가 존재함.

  • 조치: ServerId를 환경변수로 주입(컨테이너 인스턴스 ID/호스트명 또는 런타임 생성 GUID). 미지정/빈값일 경우 머신명+GUID로 자동 생성.
  • 타이밍 정합성: HeartbeatIntervalSeconds=30, ServerTimeoutMinutes=2, CleanupIntervalMinutes=5 조합은 정리 지연을 초래함 — Cleanup 주기 단축(예: 1분) 또는 단위 통일 권장.

빠른 개선안:

-    "CleanupIntervalMinutes": 5,
+    "CleanupIntervalMinutes": 1,

수정 위치: ProjectVG.Api/appsettings.json (약 lines 14–18); 문서 예시: docs/distributed-system/README.md, docs/architecture/chat-system-flow.md.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"DistributedSystem": {
"Enabled": true,
"ServerId": "api-server-001",
"HeartbeatIntervalSeconds": 30,
"CleanupIntervalMinutes": 5,
"ServerTimeoutMinutes": 2
},
"DistributedSystem": {
"Enabled": true,
"ServerId": "api-server-001",
"HeartbeatIntervalSeconds": 30,
"CleanupIntervalMinutes": 1,
"ServerTimeoutMinutes": 2
},
🤖 Prompt for AI Agents
In ProjectVG.Api/appsettings.json around lines 14 to 20, remove the hardcoded
"ServerId": "api-server-001" and instead read ServerId from an environment
variable (e.g., DISTRIBUTED_SERVER_ID); if the env var is missing or empty,
generate a fallback value at runtime using the machine hostname concatenated
with a GUID to guarantee per-instance uniqueness; update the configuration
loading logic to accept the env var and runtime fallback and persist no
hardcoded value in appsettings.json; also adjust CleanupIntervalMinutes to a
shorter/default value (e.g., 1) or unify timing units with
HeartbeatIntervalSeconds/ServerTimeoutMinutes to avoid delayed cleanup, and
update docs (docs/distributed-system/README.md and
docs/architecture/chat-system-flow.md) to show using the env var or
runtime-generated ServerId.

"LLM": {
"BaseUrl": "http://localhost:7930"
},
"MEMORY": {
"BaseUrl": "http://localhost:7940"
},
"TTS": {
"BaseUrl": "https://supertoneapi.com"
},
Comment on lines +21 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

외부 서비스 BaseUrl만으로는 운영 리스크 — 타임아웃/재시도 등 구성 노출 권장

LLM/MEMORY/TTS 섹션에 최소 다음을 함께 노출하면 운영 안정성이 향상됩니다:

  • 요청 타임아웃(초), 재시도/백오프, 회로차단(서킷브레이커) 스위치
  • 베이스 URL 트레일링 슬래시 규칙 통일(클라이언트에서 중복/누락 방지)
  • 인증 토큰/키는 비밀 주입으로 처리

예시(권장 키; 실제 반영은 코드 지원 필요):

  • TimeoutSeconds, RetryCount, RetryBackoffMillis, CircuitBreakerEnabled

BaseUrl 사용부에서 슬래시 정규화/타임아웃이 적용되는지 확인:


🏁 Script executed:

#!/bin/bash
rg -n -C3 -i '(LLM|MEMORY|TTS).*BaseUrl|HttpClient|Timeout|Circuit|Retry' --type cs --hidden

Length of output: 35983


운영 리스크: LLM/MEMORY/TTS 외부 클라이언트 — 타임아웃/재시도/회로차단 구성 노출 및 BaseUrl 슬래시 정규화 필요

  • 검증 결과: ProjectVG.Infrastructure/InfrastructureServiceCollectionExtensions.cs에서 BaseAddress만 설정(client.BaseAddress = new Uri(...))하고 재시도/백오프/서킷브레이커/타임아웃 정책 미적용.
  • LLMClient는 생성자에서 _httpClient.Timeout = TimeSpan.FromSeconds(30)로 하드코딩됨 (ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs) — 타임아웃을 구성으로 이동 필요.
  • Memory/TTS 클라이언트에는 명시적 타임아웃·재시도·서킷 적용 없음 (ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs, ProjectVG.Infrastructure/Integrations/TextToSpeechClient/TextToSpeechClient.cs).
  • 경로 결합 불일치: LLMClient.PostAsync("api/v1/chat") (선행 슬래시 없음) vs Memory/TTS는 "/api/…" (선행 슬래시 있음) — BaseUrl에 경로/트레일링 슬래시가 포함되면 URI 결합 오류 가능. BaseUrl 정규화(슬래시 규칙 통일) 또는 클라이언트 경로 결합 규칙 통일 필요.
  • 권장 조치: appsettings.json에 TimeoutSeconds/RetryCount/RetryBackoffMillis/CircuitBreakerEnabled 등 노출, InfrastructureServiceCollectionExtensions.cs의 AddHttpClient에 Polly 기반 재시도·백오프·서킷·타임아웃 정책 추가(또는 HttpClientFactory 설정으로 중앙관리), LLMClient 내부 하드코딩 제거 및 BaseUrl 정규화/검증 로직 추가. 인증 키는 시크릿 주입(환경변수/시크릿스토어) 유지.

"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Database=ProjectVG;User Id=sa;Password=ProjectVG123!;TrustServerCertificate=true;MultipleActiveResultSets=true",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

심각: DB 자격증명(비밀번호) 커밋됨 — 즉시 제거 및 비밀 회수 필요

sa 계정 비밀번호가 평문으로 커밋되었습니다. 이는 치명적 보안사고로 이어질 수 있습니다. 기본 원칙: 비밀번호는 절대 저장소에 두지 말고 환경변수/비밀관리(User-Secrets, Vault 등)로 주입하세요.

권고 조치:

  • 비밀번호 교체(회수) 및 DB 사용자 최소권한 계정으로 전환.
  • 저장소에서 값 제거(Git 이력도 정리 필요).
  • 환경별 오버라이드(appsettings.{Environment}.json, 환경변수)로 주입.

즉시 적용 가능한 임시 수정(기본값에서 비밀번호 제거; 실제 값은 환경변수로 오버라이드):

-    "DefaultConnection": "Server=localhost,1433;Database=ProjectVG;User Id=sa;Password=ProjectVG123!;TrustServerCertificate=true;MultipleActiveResultSets=true",
+    "DefaultConnection": "Server=localhost,1433;Database=ProjectVG;TrustServerCertificate=true;MultipleActiveResultSets=true",

비밀 노출 여부 점검 스크립트:


🏁 Script executed:

#!/bin/bash
# 민감 문자열 및 연결문자열 유사 패턴 탐지
rg -n -C1 -i '(Password=|User Id=sa|ProjectVG123!)|DefaultConnection' --hidden

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for 'DefaultConnection' (fixed string)..."
rg -nF --hidden --no-ignore 'DefaultConnection' -S || true

echo
echo "Searching for 'Password=' (fixed string)..."
rg -nF --hidden --no-ignore 'Password=' -S || true

echo
echo "Searching for 'User Id=sa' (fixed string)..."
rg -nF --hidden --no-ignore 'User Id=sa' -S || true

echo
echo "Showing ProjectVG.Api/appsettings.json (if present):"
if [ -f ProjectVG.Api/appsettings.json ]; then
  nl -ba ProjectVG.Api/appsettings.json | sed -n '1,200p'
else
  echo "File not found: ProjectVG.Api/appsettings.json"
fi

Length of output: 3207


즉시 조치 필요: 평문 DB 비밀번호(sa:ProjectVG123!)가 커밋됨 — 즉시 제거·회수 및 이력 정리

다음 파일에서 평문 연결문자열 확인(수정 필요):

  • ProjectVG.Api/appsettings.json:31
  • ProjectVG.Api/appsettings.Development.json:11
  • ProjectVG.Infrastructure/Persistence/EfCore/Data/DesignTimeDbContextFactory.cs:38

권장 즉시 조치:

  • DB 비밀번호(해당 sa 계정) 즉시 교체 및 해당 자격증명 폐기 → 최소 권한 계정 사용.
  • 저장소에서 민감값 제거 후 Git 이력에서 완전 삭제(예: git filter-repo 또는 BFG). 단순 커밋 삭제만으로는 불충분.
  • 코드/설정에서 평문 제거 — 연결문자열은 환경변수 또는 시크릿스토어(User-Secrets/HashiCorp Vault/GitHub Secrets 등)로 주입.
  • DesignTimeDbContextFactory.cs 내 하드코딩된 연결문자열을 제거하고 구성(Configuration) 또는 환경변수로 대체.

임시 패치(앱설정에서 비밀번호 제거 — 실제 값은 환경변수로 주입):

-    "DefaultConnection": "Server=localhost,1433;Database=ProjectVG;User Id=sa;Password=ProjectVG123!;TrustServerCertificate=true;MultipleActiveResultSets=true",
+    "DefaultConnection": "Server=localhost,1433;Database=ProjectVG;TrustServerCertificate=true;MultipleActiveResultSets=true",

추가 확인: .env.example 및 문서에 예시 연결문자열 존재(민감값 포함 여부 점검). 모든 변경 후 CI/CD 및 배포 비밀도 갱신.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"DefaultConnection": "Server=localhost,1433;Database=ProjectVG;User Id=sa;Password=ProjectVG123!;TrustServerCertificate=true;MultipleActiveResultSets=true",
"DefaultConnection": "Server=localhost,1433;Database=ProjectVG;TrustServerCertificate=true;MultipleActiveResultSets=true",
🤖 Prompt for AI Agents
In ProjectVG.Api/appsettings.json around line 31 (and also review
ProjectVG.Api/appsettings.Development.json line 11 and
ProjectVG.Infrastructure/Persistence/EfCore/Data/DesignTimeDbContextFactory.cs
line 38), remove the hardcoded plaintext sa connection string and replace it
with a placeholder that reads the connection string (or just the DB password)
from an environment variable or secret store; update DesignTimeDbContextFactory
to obtain configuration from IConfiguration or
Environment.GetEnvironmentVariable rather than a hardcoded literal; rotate the
exposed sa credential immediately (create a new least-privilege DB user and
revoke/disable the compromised account) and ensure CI/CD secrets are updated;
then purge the secret from Git history using git filter-repo or BFG and verify
.env.example/docs contain no real secrets.

"Redis": "projectvg-redis:6379"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Redis 보안 설정 누락(인증/TLS) — 프로덕션 대비 강화 필요

현재 "projectvg-redis:6379"로 인증/암호화 없이 접속합니다. 운영환경에서는 최소한 다음을 보장하세요:

  • 인증 비밀번호 사용(password) 및 주입은 환경변수/비밀관리로.
  • TLS 사용(예: ssl=true 등 라이브러리 호환 옵션).
  • 타임아웃/재시도/풀 크기 옵션 튜닝.

개발/로컬은 유지하되, appsettings.Production.json 또는 환경변수로 보안 옵션을 강제하는 구성을 추가해 주세요.

비슷한 설정이 다른 파일에 하드코딩되지 않았는지 점검:


🏁 Script executed:

#!/bin/bash
rg -n -C2 -i 'redis.*(password|ssl|rediss|:6379)' --hidden

Length of output: 1835


Redis 보안(인증/TLS) 미설정 — 프로덕션 적용 전 즉시 수정 필요

문제: ProjectVG.Api/appsettings.json에 인증/암호화 없이 Redis가 하드코딩되어 있습니다:

"Redis": "projectvg-redis:6379"

조치(우선순위)

  • Redis 연결 문자열에서 비밀번호 사용(ACL) — 민감값은 환경변수 또는 시크릿 매니저로 주입.
  • TLS 활성화(예: rediss:// 또는 클라이언트 옵션 ssl=true 등) 및 클라이언트 라이브러리 설정 확인.
  • 프로덕션 전용 구성 파일(appsettings.Production.json) 또는 환경변수로 보안 옵션 강제.
  • 문서/배포 예시 업데이트(docs/deployment/ci-cd-setup.md의 REDIS_CONNECTION_STRING 예시 점검).
  • 코드 검증: ProjectVG.Infrastructure/Persistence/Session/RedisSessionStorage.cs가 안전하게 구성된 IConnectionMultiplexer를 사용하도록 확인(구성 주입 경로 점검).

ProjectVG.Api/appsettings.json, docs/deployment/ci-cd-setup.md, ProjectVG.Infrastructure/Persistence/Session/RedisSessionStorage.cs에서 우선 수정 필요.

🤖 Prompt for AI Agents
In ProjectVG.Api/appsettings.json around line 32, the Redis connection is
hardcoded without auth/TLS ("Redis": "projectvg-redis:6379"); update
configuration to require a secured connection string (use password/ACL and TLS
scheme like rediss:// or ssl=true) sourced from environment variables or a
secrets manager (move this value into appsettings.Production.json and/or read
from ASPNETCORE configuration for production), update
docs/deployment/ci-cd-setup.md to show REDIS_CONNECTION_STRING example with
credentials and TLS, and verify
ProjectVG.Infrastructure/Persistence/Session/RedisSessionStorage.cs uses the
injected connection string to configure IConnectionMultiplexer with password and
SSL options (fail fast if missing) so production always uses
authenticated+encrypted Redis.

}
}
36 changes: 30 additions & 6 deletions ProjectVG.Application/ApplicationServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using ProjectVG.Application.Services.Auth;
using ProjectVG.Application.Services.Character;
using ProjectVG.Application.Services.Chat;
Expand All @@ -12,12 +13,14 @@
using ProjectVG.Application.Services.Credit;
using ProjectVG.Application.Services.Users;
using ProjectVG.Application.Services.WebSocket;
using ProjectVG.Application.Services.MessageBroker;
using ProjectVG.Application.Services.Server;

namespace ProjectVG.Application
{
public static class ApplicationServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
// Auth Services
services.AddScoped<IAuthService, AuthService>();
Expand Down Expand Up @@ -69,13 +72,34 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
// Conversation Services
services.AddScoped<IConversationService, ConversationService>();

// Session Services
services.AddSingleton<IConnectionRegistry, ConnectionRegistry>();

// WebSocket Services
services.AddScoped<IWebSocketManager, WebSocketManager>();
// Distributed System Services
AddDistributedServices(services, configuration);

return services;
}

/// <summary>
/// 분산 시스템 관련 서비스 등록
/// </summary>
private static void AddDistributedServices(IServiceCollection services, IConfiguration configuration)
{
var distributedEnabled = configuration.GetValue<bool>("DistributedSystem:Enabled", false);

if (distributedEnabled)
{
// 분산 환경 서비스
services.AddSingleton<IMessageBroker, DistributedMessageBroker>();
services.AddSingleton<IWebSocketManager, DistributedWebSocketManager>();
}
else
{
// 단일 서버 환경 서비스
services.AddSingleton<IMessageBroker, LocalMessageBroker>();
services.AddSingleton<IWebSocketManager, WebSocketManager>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

치명적: DistributedMessageBroker ↔ DistributedWebSocketManager 생성자 순환 의존성으로 DI 해석 실패

현재 싱글톤 등록에서 다음 순환이 발생합니다:

  • DistributedMessageBroker → IWebSocketManager
  • DistributedWebSocketManager → IMessageBroker

MS.DI는 생성자 주입 순환을 허용하지 않아 런타임에 InvalidOperationException이 납니다.

권장 수정(간단/국소 변경):

  • DistributedWebSocketManager에서 IMessageBroker 직접 주입을 제거하고 IServiceProvider를 주입해 필요 시 지연 조회하도록 변경합니다. 그 후 분산 브로커인 경우에만 Subscribe/Unsubscribe를 호출하세요.

예시 (다른 파일 수정):

// DistributedWebSocketManager.cs
using Microsoft.Extensions.DependencyInjection; // GetRequiredService 용

public class DistributedWebSocketManager : IWebSocketManager
{
    private readonly ILogger<DistributedWebSocketManager> _logger;
    private readonly IConnectionRegistry _connectionRegistry;
    private readonly ISessionStorage _sessionStorage;
    private readonly IServiceProvider _serviceProvider; // 변경

    public DistributedWebSocketManager(
        ILogger<DistributedWebSocketManager> logger,
        IConnectionRegistry connectionRegistry,
        ISessionStorage sessionStorage,
        IServiceProvider serviceProvider) // 변경
    {
        _logger = logger;
        _connectionRegistry = connectionRegistry;
        _sessionStorage = sessionStorage;
        _serviceProvider = serviceProvider;
    }

    public async Task<string> ConnectAsync(string userId)
    {
        ...
        var broker = _serviceProvider.GetService<IMessageBroker>();
        if (broker is DistributedMessageBroker distributed)
        {
            await distributed.SubscribeToUserChannelAsync(userId);
        }
        return userId;
    }

    public async Task DisconnectAsync(string userId)
    {
        ...
        var broker = _serviceProvider.GetService<IMessageBroker>();
        if (broker is DistributedMessageBroker distributed)
        {
            await distributed.UnsubscribeFromUserChannelAsync(userId);
        }
    }
}

대안:

  • Lazy/Func 기반 지연 주입 또는 Subscribe/Unsubscribe 전용 인터페이스(예: IUserChannelSubscriber)를 도입해 의존 방향을 단방향으로 정리.

이 수정이 반영되면 본 파일의 DI 등록은 그대로 유지해도 순환이 해소됩니다.

🤖 Prompt for AI Agents
ProjectVG.Application/ApplicationServiceCollectionExtensions.cs lines 91-98: the
DI registrations trigger a constructor cycle between DistributedMessageBroker
and DistributedWebSocketManager (each injects the other's interface); to fix,
change DistributedWebSocketManager to stop taking IMessageBroker in its
constructor and instead accept IServiceProvider (or a
lazy/Func<IMessageBroker>), update its ConnectAsync/DisconnectAsync to resolve
IMessageBroker at call-time (e.g., serviceProvider.GetService<IMessageBroker>),
and only call Subscribe/Unsubscribe when the resolved broker is a
DistributedMessageBroker; this removes the circular constructor dependency while
keeping the service registrations unchanged.

}

// WebSocket 연결 관리
services.AddSingleton<IConnectionRegistry, ConnectionRegistry>();
}
}
}
89 changes: 89 additions & 0 deletions ProjectVG.Application/Models/MessageBroker/BrokerMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Text.Json;

namespace ProjectVG.Application.Models.MessageBroker
{
public class BrokerMessage
{
public string MessageId { get; set; } = Guid.NewGuid().ToString();
public string MessageType { get; set; } = string.Empty;
public string? TargetUserId { get; set; }
public string? TargetServerId { get; set; }
public string? SourceServerId { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string Payload { get; set; } = string.Empty;
public Dictionary<string, string> Headers { get; set; } = new();

public static BrokerMessage CreateUserMessage(string userId, object payload, string? sourceServerId = null)
{
return new BrokerMessage
{
MessageType = "user_message",
TargetUserId = userId,
SourceServerId = sourceServerId,
Payload = JsonSerializer.Serialize(payload),
Headers = new Dictionary<string, string>
{
["content-type"] = "application/json"
}
};
}

public static BrokerMessage CreateServerMessage(string serverId, object payload, string? sourceServerId = null)
{
return new BrokerMessage
{
MessageType = "server_message",
TargetServerId = serverId,
SourceServerId = sourceServerId,
Payload = JsonSerializer.Serialize(payload),
Headers = new Dictionary<string, string>
{
["content-type"] = "application/json"
}
};
}

public static BrokerMessage CreateBroadcastMessage(object payload, string? sourceServerId = null)
{
return new BrokerMessage
{
MessageType = "broadcast_message",
SourceServerId = sourceServerId,
Payload = JsonSerializer.Serialize(payload),
Headers = new Dictionary<string, string>
{
["content-type"] = "application/json"
}
};
}

public T? DeserializePayload<T>()
{
try
{
return JsonSerializer.Deserialize<T>(Payload);
}
catch
{
return default;
}
}

public string ToJson()
{
return JsonSerializer.Serialize(this);
}

public static BrokerMessage? FromJson(string json)
{
try
{
return JsonSerializer.Deserialize<BrokerMessage>(json);
}
catch
{
return null;
}
}
}
}
41 changes: 41 additions & 0 deletions ProjectVG.Application/Models/Server/ServerInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace ProjectVG.Application.Models.Server
{
public class ServerInfo
{
public string ServerId { get; set; } = string.Empty;
public DateTime StartedAt { get; set; }
public DateTime LastHeartbeat { get; set; }
public int ActiveConnections { get; set; }
public string Status { get; set; } = "healthy";
public string? Environment { get; set; }
public string? Version { get; set; }

public ServerInfo()
{
}

public ServerInfo(string serverId)
{
ServerId = serverId;
StartedAt = DateTime.UtcNow;
LastHeartbeat = DateTime.UtcNow;
ActiveConnections = 0;
Status = "healthy";
}

public void UpdateHeartbeat()
{
LastHeartbeat = DateTime.UtcNow;
}

public void UpdateConnectionCount(int count)
{
ActiveConnections = count;
}

public bool IsHealthy(TimeSpan timeout)
{
return DateTime.UtcNow - LastHeartbeat < timeout;
}
}
}
6 changes: 6 additions & 0 deletions ProjectVG.Application/Services/Chat/ChatService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using ProjectVG.Application.Services.Chat.Processors;
using ProjectVG.Application.Services.Chat.Validators;
using ProjectVG.Application.Services.Conversation;
using ProjectVG.Application.Services.WebSocket;

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

private readonly IConversationService _conversationService;
private readonly ICharacterService _characterService;
private readonly IWebSocketManager _webSocketManager;

private readonly ChatRequestValidator _validator;
private readonly MemoryContextPreprocessor _memoryPreprocessor;
Expand All @@ -36,6 +38,7 @@ public ChatService(
ILogger<ChatService> logger,
IConversationService conversationService,
ICharacterService characterService,
IWebSocketManager webSocketManager,
ChatRequestValidator validator,
MemoryContextPreprocessor memoryPreprocessor,
ICostTrackingDecorator<UserInputAnalysisProcessor> inputProcessor,
Expand All @@ -52,6 +55,7 @@ ChatFailureHandler chatFailureHandler

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

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

var preprocessContext = await PrepareChatRequestAsync(command);

Expand Down
17 changes: 12 additions & 5 deletions ProjectVG.Application/Services/Chat/Handlers/ChatSuccessHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@
using ProjectVG.Application.Models.WebSocket;
using ProjectVG.Application.Services.WebSocket;
using ProjectVG.Application.Services.Credit;
using ProjectVG.Application.Services.MessageBroker;


namespace ProjectVG.Application.Services.Chat.Handlers
{
public class ChatSuccessHandler
{
private readonly ILogger<ChatSuccessHandler> _logger;
private readonly IWebSocketManager _webSocketService;
private readonly IMessageBroker _messageBroker;
private readonly ICreditManagementService _tokenManagementService;

public ChatSuccessHandler(
ILogger<ChatSuccessHandler> logger,
IWebSocketManager webSocketService,
IMessageBroker messageBroker,
ICreditManagementService tokenManagementService)
{
_logger = logger;
_webSocketService = webSocketService;
_messageBroker = messageBroker;
_tokenManagementService = tokenManagementService;
}

Expand Down Expand Up @@ -61,8 +62,14 @@ public async Task HandleAsync(ChatProcessContext context)
var message = ChatProcessResultMessage.FromSegment(segment, requestId)
.WithCreditInfo(tokensUsed, tokensRemaining);
var wsMessage = new WebSocketMessage("chat", message);

await _webSocketService.SendAsync(userId, wsMessage);

_logger.LogInformation("[메시지브로커] 사용자에게 메시지 전송 시작: UserId={UserId}, MessageType={MessageType}, SegmentOrder={Order}, BrokerType={BrokerType}",
userId, wsMessage.Type, segment.Order, _messageBroker.IsDistributed ? "Distributed" : "Local");

await _messageBroker.SendToUserAsync(userId, wsMessage);

_logger.LogInformation("[메시지브로커] 사용자에게 메시지 전송 완료: UserId={UserId}, MessageType={MessageType}, SegmentOrder={Order}",
userId, wsMessage.Type, segment.Order);
}
catch (Exception ex)
{
Expand Down
Loading
Loading