feat: WebSocket 설정 외부화 및 부하 제어 모니터링 엔드포인트 추가

Phase 4: 설정 외부화 및 모니터링
- WebSocketProperties: websocket.* 설정을 @ConfigurationProperties로 바인딩
  - query: 동시 제한, 세션 제한, 대기 큐 타임아웃
  - transport: 인바운드/아웃바운드 채널 스레드풀, 메시지 크기
  - backpressure: 버퍼 크기, 메시지 크기 제한
- WebSocketMonitoringController에 /api/websocket/load-control 엔드포인트 추가
  - 글로벌 동시 쿼리 수, 대기 큐 현황
  - 활성 쿼리 상세 (세션ID, 쿼리ID, 시작시간, 진행테이블, 청크수, 취소여부)
  - 메모리 사용률

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
HeungTak Lee 2026-02-06 13:45:41 +09:00
부모 7b7e283ea4
커밋 c92bf0e5ad
2개의 변경된 파일98개의 추가작업 그리고 0개의 파일을 삭제

파일 보기

@ -0,0 +1,52 @@
package gc.mda.signal_batch.global.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* WebSocket 부하 제어 관련 설정
* application-{profile}.yml의 websocket.* 설정을 바인딩
*/
@Data
@Component
@ConfigurationProperties(prefix = "websocket")
public class WebSocketProperties {
private QueryProperties query = new QueryProperties();
private TransportProperties transport = new TransportProperties();
private BackpressureProperties backpressure = new BackpressureProperties();
@Data
public static class QueryProperties {
/** 서버 전체 동시 실행 쿼리 상한 */
private int maxConcurrentGlobal = 30;
/** 세션당 동시 쿼리 상한 */
private int maxPerSession = 3;
/** 글로벌 대기 큐 타임아웃 (초) */
private int queueTimeoutSeconds = 30;
}
@Data
public static class TransportProperties {
private int inboundCorePoolSize = 10;
private int inboundMaxPoolSize = 20;
private int inboundQueueCapacity = 100;
private int outboundCorePoolSize = 20;
private int outboundMaxPoolSize = 40;
private int outboundQueueCapacity = 5000;
private int messageSizeLimitMb = 50;
private int sendBufferSizeLimitMb = 256;
private int sendTimeLimitSeconds = 120;
}
@Data
public static class BackpressureProperties {
/** 최대 대기 버퍼 크기 (MB) */
private long maxPendingBufferMb = 50;
/** 메시지당 최대 크기 (KB) */
private int maxMessageSizeKb = 1024;
/** 메시지당 최소 크기 (KB) */
private int minMessageSizeKb = 256;
}
}

파일 보기

@ -1,6 +1,7 @@
package gc.mda.signal_batch.monitoring.controller;
import gc.mda.signal_batch.monitoring.service.TrackStreamingMetrics;
import gc.mda.signal_batch.global.websocket.service.ActiveQueryManager;
import gc.mda.signal_batch.global.websocket.service.StompTrackStreamingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -13,6 +14,7 @@ import org.springframework.web.servlet.view.RedirectView;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@ -24,6 +26,7 @@ public class WebSocketMonitoringController {
private final TrackStreamingMetrics trackStreamingMetrics;
private final StompTrackStreamingService trackStreamingService;
private final ActiveQueryManager activeQueryManager;
/**
* WebSocket 스트리밍 현황 조회
@ -69,6 +72,49 @@ public class WebSocketMonitoringController {
return ResponseEntity.ok(result);
}
/**
* 부하 제어 현황 조회
*/
@GetMapping("/load-control")
@Operation(summary = "부하 제어 현황", description = "글로벌 동시 쿼리 제한, 대기 큐, 활성 쿼리 상세 정보를 조회합니다")
public ResponseEntity<Map<String, Object>> getLoadControlStatus() {
Map<String, Object> status = new HashMap<>();
// 글로벌 동시 쿼리 제한 상태
Map<String, Object> concurrency = new HashMap<>();
concurrency.put("globalActiveQueries", activeQueryManager.getGlobalActiveQueryCount());
concurrency.put("maxConcurrentGlobal", activeQueryManager.getMaxConcurrentGlobal());
concurrency.put("waitingInQueue", activeQueryManager.getWaitingCount());
concurrency.put("registeredQueries", activeQueryManager.getActiveQueryCount());
status.put("concurrency", concurrency);
// 활성 쿼리 상세
status.put("activeQueryDetails", activeQueryManager.getAllActiveQueries().entrySet().stream()
.map(e -> {
Map<String, Object> detail = new HashMap<>();
detail.put("sessionId", e.getKey());
detail.put("queryId", e.getValue().getQueryId());
detail.put("startTime", e.getValue().getStartTime().toString());
detail.put("currentTable", String.valueOf(e.getValue().getCurrentTable()));
detail.put("processedChunks", e.getValue().getProcessedChunks());
detail.put("cancelled", e.getValue().isCancelled());
return detail;
})
.collect(Collectors.toList()));
// 메모리 상태
Runtime runtime = Runtime.getRuntime();
Map<String, String> memory = new HashMap<>();
long usedMb = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
long maxMb = runtime.maxMemory() / (1024 * 1024);
memory.put("used", usedMb + " MB");
memory.put("max", maxMb + " MB");
memory.put("usage", String.format("%.1f%%", (double) usedMb / maxMb * 100));
status.put("memory", memory);
return ResponseEntity.ok(status);
}
/**
* WebSocket 테스트 페이지로 리다이렉트
*/