|
13 | 13 |
|
14 | 14 | ## 1. 채팅 시스템 |
15 | 15 |
|
16 | | -### 이중 프로토콜: WebSocket + HTTP REST API |
| 16 | +### HTTP-WebSocket Bridge 패턴: 비동기 장시간 작업 처리 |
17 | 17 |
|
18 | | -**설명**: 실시간 WebSocket 통신과 HTTP REST API를 모두 지원하는 하이브리드 채팅 시스템 |
| 18 | +**설명**: HTTP 요청으로 채팅을 시작하고 WebSocket으로 결과를 전송하는 비동기 아키텍처. 장시간 소요되는 LLM 처리를 논블로킹 방식으로 구현하여 클라이언트 타임아웃을 방지합니다. |
| 19 | + |
| 20 | +**아키텍처 플로우**: |
| 21 | +``` |
| 22 | +Client ──HTTP POST──→ ChatController ──Enqueue──→ Background Processing |
| 23 | + ↑ ↓ |
| 24 | + └──WebSocket Push────← WebSocketManager ←──Result────┘ |
| 25 | +``` |
19 | 26 |
|
20 | 27 | **구현 위치**: |
21 | | -- **WebSocket 미들웨어**: [`ProjectVG.Api/Middleware/WebSocketMiddleware.cs`](../../ProjectVG.Api/Middleware/WebSocketMiddleware.cs) |
22 | | -- **HTTP 채팅 컨트롤러**: [`ProjectVG.Api/Controllers/ChatController.cs`](../../ProjectVG.Api/Controllers/ChatController.cs) |
23 | | -- **WebSocket 매니저**: [`ProjectVG.Application/Services/WebSocket/WebSocketManager.cs`](../../ProjectVG.Application/Services/WebSocket/WebSocketManager.cs) |
| 28 | +- **HTTP 진입점**: [`ProjectVG.Api/Controllers/ChatController.cs`](../../ProjectVG.Api/Controllers/ChatController.cs) |
| 29 | +- **WebSocket 결과 전송**: [`ProjectVG.Application/Services/WebSocket/WebSocketManager.cs`](../../ProjectVG.Application/Services/WebSocket/WebSocketManager.cs) |
| 30 | +- **비동기 처리 오케스트레이션**: [`ProjectVG.Application/Services/Chat/ChatService.cs`](../../ProjectVG.Application/Services/Chat/ChatService.cs) |
24 | 31 |
|
25 | 32 | **핵심 코드**: |
26 | 33 | ```csharp |
27 | | -// WebSocket 연결 처리 |
28 | | -public async Task InvokeAsync(HttpContext context) |
29 | | -{ |
30 | | - if (context.Request.Path == "/ws" && context.WebSockets.IsWebSocketRequest) |
31 | | - { |
32 | | - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); |
33 | | - var userId = await AuthenticateWebSocketAsync(context); |
34 | | - |
35 | | - if (userId.HasValue) |
36 | | - { |
37 | | - await _webSocketManager.AddConnectionAsync(userId.Value, webSocket); |
38 | | - await HandleWebSocketCommunication(webSocket, userId.Value); |
39 | | - } |
40 | | - } |
41 | | -} |
42 | | - |
43 | | -// HTTP 채팅 처리 |
| 34 | +// HTTP로 요청 접수 (즉시 응답) |
44 | 35 | [HttpPost] |
45 | 36 | [JwtAuthentication] |
46 | 37 | public async Task<IActionResult> ProcessChat([FromBody] ChatRequest request) |
47 | 38 | { |
48 | | - var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; |
49 | | - var command = new ChatRequestCommand(userGuid, request.CharacterId, request.Message); |
50 | | - var result = await _chatService.EnqueueChatRequestAsync(command); |
51 | | - return Ok(result); |
52 | | -} |
53 | | -``` |
54 | | - |
55 | | -### 메시지 관리: User/Assistant/System 역할 기반 대화 |
56 | | - |
57 | | -**설명**: 대화의 각 메시지를 User, Assistant, System 역할로 구분하여 관리하는 시스템 |
| 39 | + var command = new ChatRequestCommand(userId, request.CharacterId, request.Message); |
58 | 40 |
|
59 | | -**구현 위치**: |
60 | | -- **대화 엔티티**: [`ProjectVG.Domain/Entities/Conversation/ConversationHistory.cs`](../../ProjectVG.Domain/Entities/Conversation/ConversationHistory.cs) |
61 | | -- **채팅 역할**: [`ProjectVG.Domain/Entities/Conversation/ChatRole.cs`](../../ProjectVG.Domain/Entities/Conversation/ChatRole.cs) |
62 | | -- **대화 서비스**: [`ProjectVG.Application/Services/Conversation/ConversationService.cs`](../../ProjectVG.Application/Services/Conversation/ConversationService.cs) |
63 | | - |
64 | | -**핵심 코드**: |
65 | | -```csharp |
66 | | -public static class ChatRole |
67 | | -{ |
68 | | - public const string User = "User"; |
69 | | - public const string Assistant = "Assistant"; |
70 | | - public const string System = "System"; |
| 41 | + // 백그라운드 처리 시작 (논블로킹) |
| 42 | + var result = await _chatService.EnqueueChatRequestAsync(command); |
71 | 43 |
|
72 | | - public static bool IsValid(string role) => |
73 | | - role == User || role == Assistant || role == System; |
| 44 | + // 즉시 Accepted 응답 반환 |
| 45 | + return Ok(new { status = "ACCEPTED", sessionId = result.SessionId }); |
74 | 46 | } |
75 | 47 |
|
76 | | -// 대화 히스토리 엔티티 |
77 | | -public class ConversationHistory : BaseEntity |
| 48 | +// 백그라운드에서 비동기 처리 후 WebSocket으로 결과 전송 |
| 49 | +public async Task<ChatRequestResult> EnqueueChatRequestAsync(ChatRequestCommand command) |
78 | 50 | { |
79 | | - [Required] |
80 | | - [RegularExpression("^(User|Assistant|System)$")] |
81 | | - public string Role { get; set; } = string.Empty; |
| 51 | + await _validator.ValidateAsync(command); |
| 52 | + var context = await PrepareChatRequestAsync(command); |
82 | 53 |
|
83 | | - [Required] |
84 | | - [StringLength(10000)] |
85 | | - public string Content { get; set; } = string.Empty; |
| 54 | + // 백그라운드 태스크로 장시간 작업 실행 |
| 55 | + _ = Task.Run(async () => await ProcessChatRequestInternalAsync(context)); |
86 | 56 |
|
87 | | - public Guid? ConversationId { get; set; } // 대화 세션 그룹핑 |
| 57 | + return ChatRequestResult.Accepted(command.Id.ToString(), command.UserId, command.CharacterId); |
88 | 58 | } |
89 | 59 | ``` |
90 | 60 |
|
91 | | -### 외부 서비스 연동: LLM, Memory, TTS 서비스 통합 |
| 61 | +### ChatService 오케스트레이션 패턴: 복합 서비스 조율 |
| 62 | + |
| 63 | +**설명**: ChatService는 Facade 패턴을 구현하여 채팅 처리에 필요한 여러 서비스들을 단일 인터페이스로 추상화합니다. 비용 추적 데코레이터를 통해 LLM과 TTS 비용을 자동으로 추적합니다. |
92 | 64 |
|
93 | | -**설명**: LLM(언어모델), Memory(벡터 메모리), TTS(음성 합성) 외부 서비스와의 통합 |
| 65 | +**실제 처리 플로우**: |
| 66 | +``` |
| 67 | +1. 전처리 단계: |
| 68 | + - Validation: ChatRequestValidator |
| 69 | + - Input Analysis: UserInputAnalysisProcessor (+ Cost Tracking) |
| 70 | + - Action Processing: UserInputActionProcessor |
| 71 | + - Memory Context: MemoryContextPreprocessor |
| 72 | + - Character/Conversation Info |
| 73 | +
|
| 74 | +2. 백그라운드 처리: |
| 75 | + - LLM Processing: ChatLLMProcessor (+ Cost Tracking) |
| 76 | + - TTS Processing: ChatTTSProcessor (+ Cost Tracking) |
| 77 | + - Success Handling: ChatSuccessHandler |
| 78 | + - Result Persistence: ChatResultProcessor |
| 79 | +``` |
94 | 80 |
|
95 | 81 | **구현 위치**: |
96 | | -- **LLM 클라이언트**: [`ProjectVG.Infrastructure/Integrations/LLMClient/`](../../ProjectVG.Infrastructure/Integrations/LLMClient/) |
97 | | -- **Memory 클라이언트**: [`ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs`](../../ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs) |
98 | | -- **TTS 클라이언트**: [`ProjectVG.Infrastructure/Integrations/TextToSpeechClient/`](../../ProjectVG.Infrastructure/Integrations/TextToSpeechClient/) |
| 82 | +- **메인 오케스트레이터**: [`ProjectVG.Application/Services/Chat/ChatService.cs`](../../ProjectVG.Application/Services/Chat/ChatService.cs) |
| 83 | +- **비용 추적 데코레이터**: [`ProjectVG.Application/Services/Chat/CostTracking/`](../../ProjectVG.Application/Services/Chat/CostTracking/) |
99 | 84 |
|
100 | 85 | **핵심 코드**: |
101 | 86 | ```csharp |
102 | | -// LLM 서비스 호출 |
103 | | -public async Task<LLMResponse> CreateTextResponseAsync(string systemMessage, string userMessage, List<History> conversationHistory) |
| 87 | +// Facade 패턴으로 채팅 처리 추상화 |
| 88 | +public async Task<ChatRequestResult> EnqueueChatRequestAsync(ChatRequestCommand command) |
104 | 89 | { |
105 | | - var request = new LLMRequest |
106 | | - { |
107 | | - SystemMessage = systemMessage, |
108 | | - UserMessage = userMessage, |
109 | | - ConversationHistory = conversationHistory, |
110 | | - Model = "gpt-4o-mini", |
111 | | - MaxTokens = 1000 |
112 | | - }; |
| 90 | + // 1. 즉시 검증 |
| 91 | + await _validator.ValidateAsync(command); |
| 92 | + |
| 93 | + // 2. 전처리 (context 준비) |
| 94 | + var preprocessContext = await PrepareChatRequestAsync(command); |
| 95 | + |
| 96 | + // 3. 백그라운드에서 비동기 처리 |
| 97 | + _ = Task.Run(async () => { |
| 98 | + await ProcessChatRequestInternalAsync(preprocessContext); |
| 99 | + }); |
113 | 100 |
|
114 | | - var response = await _httpClient.PostAsJsonAsync("/api/v1/chat", request); |
115 | | - return await response.Content.ReadFromJsonAsync<LLMResponse>(); |
| 101 | + // 4. 즉시 Accepted 응답 |
| 102 | + return ChatRequestResult.Accepted(command.Id.ToString(), command.UserId, command.CharacterId); |
116 | 103 | } |
117 | 104 |
|
118 | | -// Memory 서비스 연동 |
119 | | -public async Task<MemoryInsertResponse> InsertAutoAsync(MemoryInsertRequest request) |
| 105 | +// 실제 백그라운드 처리 |
| 106 | +private async Task ProcessChatRequestInternalAsync(ChatProcessContext context) |
120 | 107 | { |
121 | | - var response = await _httpClient.PostAsync("/api/memory", content); |
122 | | - return MapInsertResponse(response); |
| 108 | + try { |
| 109 | + await _llmProcessor.ProcessAsync(context); // 비용 추적 있음 |
| 110 | + await _ttsProcessor.ProcessAsync(context); // 비용 추적 있음 |
| 111 | +
|
| 112 | + var successHandler = scope.ServiceProvider.GetRequiredService<ChatSuccessHandler>(); |
| 113 | + var resultProcessor = scope.ServiceProvider.GetRequiredService<ChatResultProcessor>(); |
| 114 | + |
| 115 | + await successHandler.HandleAsync(context); |
| 116 | + await resultProcessor.PersistResultsAsync(context); |
| 117 | + } |
| 118 | + catch (Exception) { |
| 119 | + var failureHandler = scope.ServiceProvider.GetRequiredService<ChatFailureHandler>(); |
| 120 | + await failureHandler.HandleAsync(context); |
| 121 | + } |
| 122 | + finally { |
| 123 | + _metricsService.EndChatMetrics(); |
| 124 | + } |
123 | 125 | } |
124 | 126 | ``` |
125 | 127 |
|
126 | | -### 페이지네이션: 대화 기록 조회 |
| 128 | +### WebSocket 세션 관리: 실시간 결과 전송 |
127 | 129 |
|
128 | | -**설명**: 대화 기록의 효율적인 페이지네이션 조회 시스템 |
| 130 | +**설명**: WebSocket을 통한 실시간 채팅 결과 전송 시스템. 클라이언트가 `/ws` 엔드포인트로 연결하면 채팅 처리 완료 시 결과를 자동으로 수신합니다. |
129 | 131 |
|
130 | 132 | **구현 위치**: |
131 | | -- **대화 리포지토리**: [`ProjectVG.Infrastructure/Persistence/Repositories/Conversation/SqlServerConversationRepository.cs`](../../ProjectVG.Infrastructure/Persistence/Repositories/Conversation/SqlServerConversationRepository.cs) |
132 | | -- **대화 컨트롤러**: [`ProjectVG.Api/Controllers/ConversationController.cs`](../../ProjectVG.Api/Controllers/ConversationController.cs) |
| 133 | +- **WebSocket 미들웨어**: [`ProjectVG.Api/Middleware/WebSocketMiddleware.cs`](../../ProjectVG.Api/Middleware/WebSocketMiddleware.cs) |
| 134 | +- **WebSocket 매니저**: [`ProjectVG.Application/Services/WebSocket/WebSocketManager.cs`](../../ProjectVG.Application/Services/WebSocket/WebSocketManager.cs) |
| 135 | +- **연결 관리**: [`ProjectVG.Infrastructure/Realtime/WebSocketConnection/WebSocketClientConnection.cs`](../../ProjectVG.Infrastructure/Realtime/WebSocketConnection/WebSocketClientConnection.cs) |
133 | 136 |
|
134 | 137 | **핵심 코드**: |
135 | 138 | ```csharp |
136 | | -// 페이지네이션 조회 |
137 | | -public async Task<IEnumerable<ConversationHistory>> GetConversationHistoryAsync( |
138 | | - Guid userId, Guid characterId, int page = 1, int pageSize = 10) |
| 139 | +// WebSocket 연결 처리 (미들웨어) |
| 140 | +if (context.Request.Path == "/ws" && context.WebSockets.IsWebSocketRequest) |
139 | 141 | { |
140 | | - return await _context.ConversationHistories |
141 | | - .Where(c => c.UserId == userId && c.CharacterId == characterId) |
142 | | - .OrderByDescending(c => c.CreatedAt) |
143 | | - .Skip((page - 1) * pageSize) |
144 | | - .Take(pageSize) |
145 | | - .ToListAsync(); |
| 142 | + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); |
| 143 | + var userId = await AuthenticateWebSocketAsync(context); |
| 144 | + |
| 145 | + if (userId.HasValue) |
| 146 | + { |
| 147 | + await _webSocketManager.AddConnectionAsync(userId.Value, webSocket); |
| 148 | + await HandleWebSocketCommunication(webSocket, userId.Value); |
| 149 | + } |
146 | 150 | } |
147 | 151 |
|
148 | | -// API 엔드포인트 |
149 | | -[HttpGet("{characterId}")] |
150 | | -[JwtAuthentication] |
151 | | -public async Task<ActionResult<ConversationHistoryListResponse>> GetConversationHistory( |
152 | | - Guid characterId, [FromQuery] GetConversationHistoryRequest request) |
| 152 | +// 채팅 결과 WebSocket으로 전송 |
| 153 | +public async Task SendChatResultAsync(Guid userId, ChatResult result) |
153 | 154 | { |
154 | | - var conversations = await _conversationService.GetConversationHistoryAsync( |
155 | | - userId.Value, characterId, request.Page, request.PageSize); |
| 155 | + var connection = _connections.GetValueOrDefault(userId); |
| 156 | + if (connection != null) |
| 157 | + { |
| 158 | + var message = JsonSerializer.Serialize(new WebSocketMessage |
| 159 | + { |
| 160 | + Type = "chat_result", |
| 161 | + Data = result |
| 162 | + }); |
156 | 163 |
|
157 | | - return Ok(new ConversationHistoryListResponse { Conversations = conversations }); |
| 164 | + await connection.SendAsync(message); |
| 165 | + } |
158 | 166 | } |
159 | 167 | ``` |
160 | 168 |
|
|
0 commit comments