feat: L3 Daily 캐시 DP 사전 간소화 + 14일 확대

- CacheTrackSimplifier: simplifyDpOnly() (DP-only 간소화), recalculateSpeeds() (Haversine 속도 재계산) 추가
- DailyTrackCacheManager: loadDay() 시 DP 사전 간소화 적용 (tolerance=0.001, ~100m)
- Daily 캐시 retention 7→14일, maxMemory 6→10GB
- Query/Batch DataSource: work_mem 256MB, synchronous_commit off 세션 튜닝

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
htlee 2026-03-08 09:21:00 +09:00
부모 c3a2ac3dea
커밋 0a0109fa7e
3개의 변경된 파일117개의 추가작업 그리고 5개의 파일을 삭제

파일 보기

@ -117,6 +117,9 @@ public class CacheTrackSimplifier {
track.setPointCount(afterZoom);
// 간소화 속도 재계산 (포인트 거리/시간 기반)
recalculateSpeeds(track);
// 처음 5개 선박 상세 로그 (debug 레벨)
if (simplifiedCount < 5) {
log.debug("[CacheSimplify] vessel={} original={} -> DP={} -> distTime={} -> zoom={} (avg={} kn)",
@ -139,6 +142,43 @@ public class CacheTrackSimplifier {
return tracks;
}
// L3 캐시 저장용: DP-only 사전 간소화
/**
* DP(Douglas-Peucker) 적용하는 사전 간소화 (L3 캐시 저장용).
* 방향 변화를 보존하여 어선 조업 패턴(원형, ㄹ자) 유지.
* 거리/시간 필터는 적용하지 않아 직선 구간만 제거.
*/
public void simplifyDpOnly(List<CompactVesselTrack> tracks, double dpTolerance) {
if (tracks == null || tracks.isEmpty()) return;
long startTime = System.currentTimeMillis();
int totalOriginal = 0;
int totalAfter = 0;
int simplifiedCount = 0;
for (CompactVesselTrack track : tracks) {
if (track.getGeometry() == null || track.getGeometry().size() <= 2) continue;
int before = track.getGeometry().size();
totalOriginal += before;
applyDouglasPeucker(track, dpTolerance);
recalculateSpeeds(track);
track.setPointCount(track.getGeometry().size());
totalAfter += track.getGeometry().size();
simplifiedCount++;
}
long elapsed = System.currentTimeMillis() - startTime;
if (simplifiedCount > 0) {
double reduction = (1 - (double) totalAfter / totalOriginal) * 100;
log.info("[DpPreSimplify] {} tracks, {} -> {} pts ({}% 감소), {}ms",
simplifiedCount, totalOriginal, totalAfter, Math.round(reduction), elapsed);
}
}
// 1단계: Douglas-Peucker (ST_Simplify 대체)
private void applyDouglasPeucker(CompactVesselTrack track, double tolerance) {
@ -412,6 +452,55 @@ public class CacheTrackSimplifier {
if (sampledSpd != null) track.setSpeeds(sampledSpd);
}
// 간소화 속도 재계산
/**
* 간소화된 포인트 속도 재계산.
* 간소화 남은 포인트에 대해 인접 좌표 Haversine 거리/시간차로 계산.
*/
private void recalculateSpeeds(CompactVesselTrack track) {
List<double[]> geometry = track.getGeometry();
List<String> timestamps = track.getTimestamps();
if (geometry == null || geometry.size() < 2 ||
timestamps == null || timestamps.size() != geometry.size()) {
return;
}
int size = geometry.size();
List<Double> speeds = new ArrayList<>(size);
speeds.add(0.0); // 포인트는 이전 포인트가 없으므로 0
for (int i = 1; i < size; i++) {
double[] prev = geometry.get(i - 1);
double[] curr = geometry.get(i);
try {
long prevTs = parseEpochSeconds(timestamps.get(i - 1));
long currTs = parseEpochSeconds(timestamps.get(i));
double timeDiffHours = (currTs - prevTs) / 3600.0;
if (timeDiffHours > 0) {
double distNm = calculateDistance(prev[1], prev[0], curr[1], curr[0]);
speeds.add(distNm / timeDiffHours); // knots
} else {
speeds.add(0.0);
}
} catch (Exception e) {
speeds.add(0.0);
}
}
track.setSpeeds(speeds);
}
private long parseEpochSeconds(String tsStr) {
if (tsStr == null) throw new IllegalArgumentException("null timestamp");
if (tsStr.matches("\\d{10,}")) {
return Long.parseLong(tsStr);
}
return LocalDateTime.parse(tsStr, TIMESTAMP_FORMATTER)
.atZone(java.time.ZoneId.systemDefault())
.toEpochSecond();
}
// 거리 계산 (Haversine, 해리 단위)
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {

파일 보기

@ -40,9 +40,13 @@ public class DailyTrackCacheManager {
NOT_STARTED, LOADING, PARTIAL, READY, DISABLED
}
/** L3 사전 간소화 DP tolerance (~100m) — 항적 형상 유지하면서 직선 구간만 제거 */
private static final double L3_DP_TOLERANCE = 0.001;
private final DataSource queryDataSource;
private final DailyTrackCacheProperties cacheProperties;
private final TrackMemoryBudgetManager memoryBudgetManager;
private final CacheTrackSimplifier cacheTrackSimplifier;
// 날짜별 캐시 (D-1 ~ D-N)
private final ConcurrentHashMap<LocalDate, DailyTrackData> cache = new ConcurrentHashMap<>();
@ -56,10 +60,12 @@ public class DailyTrackCacheManager {
public DailyTrackCacheManager(
@Qualifier("queryDataSource") DataSource queryDataSource,
DailyTrackCacheProperties cacheProperties,
TrackMemoryBudgetManager memoryBudgetManager) {
TrackMemoryBudgetManager memoryBudgetManager,
CacheTrackSimplifier cacheTrackSimplifier) {
this.queryDataSource = queryDataSource;
this.cacheProperties = cacheProperties;
this.memoryBudgetManager = memoryBudgetManager;
this.cacheTrackSimplifier = cacheTrackSimplifier;
}
/**
@ -337,6 +343,23 @@ public class DailyTrackCacheManager {
estimatedMemory += tracks.size() * 200L; // 객체 오버헤드
// DP 사전 간소화: 직선 구간만 제거, 방향 변화(어선 조업 패턴) 보존
long memoryBeforeDp = estimatedMemory;
List<CompactVesselTrack> trackList = new ArrayList<>(tracks.values());
cacheTrackSimplifier.simplifyDpOnly(trackList, L3_DP_TOLERANCE);
// 간소화 메모리 재추정
estimatedMemory = trackList.stream()
.mapToLong(t -> t.getPointCount() * 40L)
.sum();
estimatedMemory += tracks.size() * 200L; // 객체 오버헤드
if (memoryBeforeDp > 0) {
long reduction = memoryBeforeDp > 0 ? Math.round((1 - (double) estimatedMemory / memoryBeforeDp) * 100) : 0;
log.info("[DailyLoadDay] {} DP pre-simplification: {}MB -> {}MB ({}% reduction, tolerance={})",
date, memoryBeforeDp / (1024 * 1024), estimatedMemory / (1024 * 1024), reduction, L3_DP_TOLERANCE);
}
// STRtree 공간 인덱스 빌드
STRtree spatialIndex = buildSpatialIndex(tracks);
estimatedMemory += tracks.size() * 100L; // 인덱스 오버헤드

파일 보기

@ -48,7 +48,7 @@ spring:
validation-timeout: 5000
leak-detection-threshold: 60000 # 커넥션 누수 감지 (60초)
# PostGIS 함수를 위해 public 스키마를 search_path에 명시적으로 추가
connection-init-sql: "SET TIME ZONE 'Asia/Seoul'; SET search_path TO signal, public, pg_catalog;"
connection-init-sql: "SET TIME ZONE 'Asia/Seoul'; SET search_path TO signal, public, pg_catalog; SET work_mem = '256MB'; SET synchronous_commit = 'off';"
statement-cache-size: 250
data-source-properties:
prepareThreshold: 3
@ -68,7 +68,7 @@ spring:
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000 # 커넥션 누수 감지 (60초)
connection-init-sql: "SET TIME ZONE 'Asia/Seoul'; SET search_path TO signal, public;"
connection-init-sql: "SET TIME ZONE 'Asia/Seoul'; SET search_path TO signal, public; SET synchronous_commit = 'off';"
# Request 크기 설정
servlet:
@ -283,8 +283,8 @@ app:
cache:
daily-track:
enabled: true
retention-days: 7 # D-1 ~ D-7 (오늘 제외)
max-memory-gb: 6 # 최대 6GB (일 평균 ~720MB × 7일 = ~5GB)
retention-days: 14 # D-1 ~ D-14 (2주, DP 간소화로 메모리 절감)
max-memory-gb: 10 # 최대 10GB (DP 간소화 후 일 ~400MB × 14일 ≈ 6GB + 여유)
warmup-async: true # 비동기 워밍업 (서버 시작 차단 없음)
# 항적 데이터 메모리 예산 (64GB JVM 기준)