# 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<String, AtomicBoolean>) 추가
  - 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<SearchPolygon>
  - 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 |
