# WebSocket 부하 제어 개선 — 구현 진행 상황 > 브랜치: `feat/websocket-load-control` > 시작일: 2026-02-06 --- ## DB 커넥션 풀 분배 설계 (총 250개) | DataSource | AS-IS | TO-BE | 비고 | |------------|-------|-------|------| | **Query** | 60 (min 10) | 120 (min 20) | WebSocket 스트리밍 + REST API 주 사용 | | **Collect** | 20 (min 5) | 80 (min 15) | 배치 Reader, 신호 수집 조회 | | **Batch** | 20 (min 10) | 30 (min 5) | Spring Batch 메타데이터 | | **예비** | - | 20 | 운영 여유분 | | **합계** | 100 | 250 | DB 서버 500 중 250 사용 | ### 글로벌 동시 쿼리 제한 산정 - Query 풀 120개 / 쿼리당 평균 3커넥션 = 40 - 보수적 적용: **30개** (REST API, 헬스체크 등에 여유분 확보) --- ## Phase 1 — 긴급 안정화 - [x] **1.1** 글로벌 동시 쿼리 제한 (Semaphore + Fair Queue) - ActiveQueryManager에 Fair Semaphore 기반 글로벌 동시 쿼리 제한 추가 (기본 30개) - @Async 메서드 내에서 슬롯 획득 (인바운드 채널 블로킹 방지 설계) - application-prod.yml에 websocket.query 설정 외부화 - 상태: **완료** (2026-02-06) - [x] **1.2** 쿼리 완료 시 리소스 반환 보장 - ChunkedTrackStreamingService finally 블록에 releaseQuery() + releaseQuerySlot() 추가 - StompTrackStreamingService finally 블록에 releaseQuery() + releaseQuerySlot() 추가 - 세션별 쿼리 카운트 감소 누락 수정 - 상태: **완료** (2026-02-06) - [x] **1.3** CachedThreadPool → 제한된 ThreadPoolExecutor 교체 - CancellableQueryManager: newCachedThreadPool → ThreadPoolExecutor(core:5, max:20, queue:100) - CallerRunsPolicy로 큐 포화 시 자연 백프레셔 - 상태: **완료** (2026-02-06) - [x] **1.4** DB 커넥션 풀 재분배 (prod) - Query: 60→120(min 20), Collect: 20→80(min 15), Batch: 20→30(min 5) - 총 230/250, 예비 20개 - 상태: **완료** (2026-02-06) --- ## Phase 2 — 취소 및 정리 로직 완성 - [x] **2.1** ChunkedTrackStreamingService 쿼리 취소 구현 - queryCancelFlags(ConcurrentHashMap) 추가 - streamChunkedTracks 시작 시 등록, 전략별 루프 전 확인, finally에서 정리 - cancelQuery()에 실제 플래그 설정 로직 구현 (기존 TODO 해소) - isQueryCancelled()에 취소 플래그 통합 확인 - 상태: **완료** (2026-02-06) - [x] **2.2** 쿼리 관리 시스템 통합 - StompTrackController의 중복 completeQuery() 제거 - 리소스 정리를 서비스 finally 블록에서 일괄 처리하도록 단일화 - 상태: **완료** (2026-02-06) --- ## Phase 3 — 백프레셔 고도화 - [x] **3.1** 콜백 기반 버퍼 추적 - CompletableFuture+Thread.sleep(100) → try-finally 즉시 감소로 전환 - 상태: **완료** (2026-02-06) - [x] **3.2** 적응형 전송 지연 - ChunkedTrack: 버퍼 사용률(%) 기반 4단계 적응형 지연 (10~200ms) - StompTrack: 큐 사용률 + 데이터 크기 복합 적응형 지연 - 상태: **완료** (2026-02-06) --- ## Phase 4 — 설정 외부화 및 모니터링 - [x] **4.1** WebSocketProperties 설정 클래스 - @ConfigurationProperties(prefix = "websocket")로 query/transport/backpressure 설정 바인딩 - 상태: **완료** (2026-02-06) - [x] **4.2** 모니터링 엔드포인트 - GET /api/websocket/load-control — 글로벌 동시 제한, 대기 큐, 활성 쿼리 상세, 메모리 - 상태: **완료** (2026-02-06) --- ## Phase 5 — 대기열 기반 쿼리 관리 + 타임아웃 최적화 - [x] **5.1** QueryStatusUpdate에 `queuePosition`, `totalInQueue` 필드 추가 - QUEUED 상태에서 대기열 순번 정보 전달 - 상태: **완료** (2026-02-06) - [x] **5.2** ActiveQueryManager 대기열 추적 구현 - ConcurrentLinkedQueue 기반 대기열 추적 - `tryAcquireQuerySlotImmediate()`: 즉시 슬롯 획득 - `waitForSlotWithQueue()`: 2초 간격 슬롯 대기 - `getQueuePosition()`, `getQueueSize()`: 순번 조회 - 상태: **완료** (2026-02-06) - [x] **5.3** ChunkedTrackStreamingService 거부 → 대기열 전환 - 즉시 슬롯 획득 실패 → QUEUED 상태 2초 간격 전송하며 최대 2분 대기 - 대기 중에도 순번/전체 큐 크기 안내 - 상태: **완료** (2026-02-06) - [x] **5.4** StompTrackStreamingService 동일 패턴 적용 - 상태: **완료** (2026-02-06) - [x] **5.5** WebSocketProperties에 SessionProperties 추가 - idleTimeoutMs, serverHeartbeatMs, clientHeartbeatMs, sockjsDisconnectDelayMs, sendTimeLimitSeconds - 기본값을 하드코딩에서 설정 바인딩으로 전환 - 상태: **완료** (2026-02-06) - [x] **5.6** WebSocketStompConfig 하드코딩 제거 - 모든 타임아웃/하트비트/풀 설정을 WebSocketProperties에서 주입 - 상태: **완료** (2026-02-06) - [x] **5.7** application-prod.yml 설정 변경 - Query 풀: 120 → 180, global: 30 → 60, max-per-session: 3 → 20 - Session idle: 60s → 15s, Heartbeat: 10s → 5s, SockJS disconnect: 30s → 5s - Send time limit: 120s → 30s - 상태: **완료** (2026-02-06) - [x] **5.8** AsyncConfig 스레드 풀 확장 - core: 15 → 40, max: 30 → 120, queue: 500 → 100, keepAlive: 40s → 30s - 상태: **완료** (2026-02-06) --- ## Phase 6 — 일일 데이터 인메모리 캐시 - [x] **6.1** DailyTrackCacheManager 신규 구현 - @Service + @Async 비동기 캐시 매니저 - ApplicationReadyEvent에서 비동기 워밍업 (D-1 → D-7 순차 로드) - 날짜별 CompactVesselTrack 캐시 (ConcurrentHashMap) - 뷰포트 필터링 지원, 다중 날짜 병합 조회 - splitQueryRange(): 요청 범위 → (캐시구간, DB구간, 오늘구간) 분리 - refreshAfterDailyJob(): 배치 완료 후 캐시 갱신 - 상태: **완료** (2026-02-06) - [x] **6.2** DailyTrackCacheProperties 신규 구현 - @ConfigurationProperties(prefix = "cache.daily-track") - enabled, retentionDays, maxMemoryGb, warmupAsync - 상태: **완료** (2026-02-06) - [x] **6.3** DailyAggregationJobConfig에 캐시 갱신 리스너 추가 - JobExecutionListener로 배치 완료 후 refreshAfterDailyJob() 호출 - 상태: **완료** (2026-02-06) - [x] **6.4** ChunkedTrackStreamingService 캐시 통합 - processDailyStrategy: 날짜별 캐시 히트 체크 → 메모리 조회 / DB 폴백 - processQueryInChunks: 동일 캐시 우선 패턴 - 상태: **완료** (2026-02-06) - [x] **6.5** StompTrackStreamingService 캐시 통합 - Daily 전략 처리 시 날짜별 캐시 히트 → 메모리 조회, 미스만 DB 처리 - 상태: **완료** (2026-02-06) - [x] **6.6** application-prod.yml 캐시 설정 추가 - cache.daily-track: enabled=true, retention-days=7, max-memory-gb=5, warmup-async=true - 상태: **완료** (2026-02-06) - [x] **6.7** WebSocketMonitoringController 캐시 모니터링 엔드포인트 - GET /api/websocket/daily-cache — 캐시 상태, 날짜별 선박수/메모리, 로드 시각 - 상태: **완료** (2026-02-06) --- ## Phase 7 — 통합선박(IntegrationVessel) 전용 DataSource - [x] **7.1** IntegrationVesselService 전용 DataSource 지원 - `vessel.integration.datasource.*` 설정으로 별도 DB 연결 가능 - `@PostConstruct`에서 전용 HikariDataSource 생성 (max 3, min 1) - 미설정 시 queryDataSource 자동 폴백 (기존 동작 유지) - `@PreDestroy`에서 전용 DataSource 정리 - 상태: **완료** (2026-02-07) - [x] **7.2** 설정 기반 테이블명 - `vessel.integration.table-name` 프로파일별 분리 - prod: `t_ship_integration_sub` (gis 스키마는 jdbc-url currentSchema) - dev/prod-mpr/local/query: `signal.t_ship_integration_sub` (기본값) - 상태: **완료** (2026-02-07) - [x] **7.3** 프로파일 호환성 유지 - prod: 별도 DB (10.188.141.230:5432/mdadb2 gis) → 전용 DataSource - dev/prod-mpr: queryDB signal 스키마 → queryDataSource 폴백 - local/query: integration 비활성화 (기본값) - 기존 DataSourceConfig 4개 파일 수정 없음 - 상태: **완료** (2026-02-07) --- ## Phase 8 — 다중 폴리곤 영역 탐색 REST API + 공간 인덱스 - [x] **8.1** DailyTrackCacheManager에 STRtree 공간 인덱스 추가 - DailyTrackData에 STRtree spatialIndex 필드 추가 (하위 호환 유지) - loadDay() 완료 후 자동 빌드 → build() 호출로 불변화 (동시성 안전) - getDailyTrackData(date), getCachedDateList() public 메서드 추가 - 추가 메모리: 선박당 ~100B × 50K/일 = ~5MB/일, 7일 ~35MB - 상태: **완료** (2026-02-07) - [x] **8.2** AreaSearchRequest / AreaSearchResponse DTO 생성 - AreaSearchRequest: startTime/endTime + SearchMode(ANY/ALL/SEQUENTIAL) + List - AreaSearchResponse: tracks(CompactVesselTrack[]) + hitDetails + summary - Swagger @Schema 상세 기입, 요청/응답 예시 포함 - 상태: **완료** (2026-02-07) - [x] **8.3** AreaSearchService 핵심 비즈니스 로직 구현 - JTS PreparedGeometry + STRtree 기반 고속 영역 탐색 - 다일 데이터 병합 → 단일 STRtree → 폴리곤별 후보 추출 → 정밀 PIP - ANY(합집합)/ALL(교집합)/SEQUENTIAL(순차 통과) 3가지 모드 - 캐시 미준비 시 CacheNotReadyException → 503 반환 - 상태: **완료** (2026-02-07) - [x] **8.4** AreaSearchController REST 엔드포인트 - POST /api/v2/tracks/area-search - @Tag("항적 조회 API V2") → 기존 GisControllerV2와 동일 Swagger 그룹 - 에러 핸들러: 400 (잘못된 폴리곤), 503 (캐시 미준비) - Swagger: @Operation, @ExampleObject 상세 문서화 - 상태: **완료** (2026-02-07) --- ## 커밋 이력 | 날짜 | Phase | 커밋 메시지 | 해시 | |------|-------|------------|------| | 2026-02-06 | 1.1+1.2 | feat: 글로벌 동시 쿼리 제한(Semaphore) 및 리소스 반환 보장 | 78ff307 | | 2026-02-06 | 1.3 | fix: CachedThreadPool → 제한된 ThreadPoolExecutor 교체 | 2191671 | | 2026-02-06 | 1.4 | perf: DB 커넥션 풀 재분배 (총 250개, prod) | 122a247 | | 2026-02-06 | 2.1 | feat: ChunkedTrackStreamingService 쿼리 취소 로직 구현 | e073007 | | 2026-02-06 | 2.2 | refactor: 쿼리 생명주기 관리 서비스 finally 블록으로 단일화 | 28908e1 | | 2026-02-06 | 3 | perf: 백프레셔 고도화 - 정확한 버퍼 추적 및 적응형 지연 | 7b7e283 | | 2026-02-06 | 4 | feat: WebSocket 설정 외부화 및 부하 제어 모니터링 엔드포인트 | c92bf0e | | 2026-02-06 | 5 | feat: 대기열 기반 쿼리 관리 및 타임아웃 최적화 | 7bd7bf5 | | 2026-02-06 | 6 | feat: 일일 데이터 인메모리 캐시 구현 | 03b14e6 | | 2026-02-06 | 5~6 | docs: Phase 5~6 구현 진행 문서 및 성능 보고서 업데이트 | dc586dd | | 2026-02-06 | 6 | fix: 캐시-DB 하이브리드 조회 시 뷰포트 2-pass 필터링 정합성 보장 | e9d5d36 |