signal-batch/docs/implementation-progress.md
HeungTak Lee b4221c36fd feat: 다중 폴리곤 영역 탐색 REST API + STRtree 공간 인덱스 (Phase 8)
캐시 기반 인메모리 다중 폴리곤 영역 내 선박 탐색 API 구현.
JTS STRtree + PreparedGeometry로 고속 공간 검색, ANY/ALL/SEQUENTIAL 3가지 모드 지원.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 08:38:18 +09:00

242 lines
11 KiB
Markdown
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 |