285 lines
7.6 KiB
TypeScript
285 lines
7.6 KiB
TypeScript
|
|
/**
|
||
|
|
* 🔥 성능 최적화: 데이터플로우 설정 캐싱 시스템
|
||
|
|
*
|
||
|
|
* 버튼별 제어관리 설정을 메모리에 캐시하여
|
||
|
|
* 1ms 수준의 즉시 응답을 제공합니다.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { ButtonDataflowConfig } from "@/types/screen";
|
||
|
|
import { apiClient } from "@/lib/api/client";
|
||
|
|
|
||
|
|
export interface CachedDataflowConfig {
|
||
|
|
config: ButtonDataflowConfig;
|
||
|
|
timestamp: number;
|
||
|
|
hits: number; // 캐시 히트 횟수 (통계용)
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface CacheMetrics {
|
||
|
|
totalRequests: number;
|
||
|
|
cacheHits: number;
|
||
|
|
cacheMisses: number;
|
||
|
|
hitRate: number; // 히트율 (%)
|
||
|
|
averageResponseTime: number; // 평균 응답 시간 (ms)
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 L1 메모리 캐시 (1ms 응답)
|
||
|
|
*
|
||
|
|
* - TTL: 5분 (300초)
|
||
|
|
* - 자동 만료 처리
|
||
|
|
* - 성능 지표 수집
|
||
|
|
*/
|
||
|
|
export class DataflowConfigCache {
|
||
|
|
private memoryCache = new Map<string, CachedDataflowConfig>();
|
||
|
|
private readonly TTL = 5 * 60 * 1000; // 5분 TTL
|
||
|
|
private metrics: CacheMetrics = {
|
||
|
|
totalRequests: 0,
|
||
|
|
cacheHits: 0,
|
||
|
|
cacheMisses: 0,
|
||
|
|
hitRate: 0,
|
||
|
|
averageResponseTime: 0,
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 버튼별 제어관리 설정 조회 (캐시 우선)
|
||
|
|
*/
|
||
|
|
async getConfig(buttonId: string): Promise<ButtonDataflowConfig | null> {
|
||
|
|
const startTime = performance.now();
|
||
|
|
this.metrics.totalRequests++;
|
||
|
|
|
||
|
|
const cacheKey = `button_dataflow_${buttonId}`;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// L1: 메모리 캐시 확인 (1ms)
|
||
|
|
if (this.memoryCache.has(cacheKey)) {
|
||
|
|
const cached = this.memoryCache.get(cacheKey)!;
|
||
|
|
|
||
|
|
// TTL 확인
|
||
|
|
if (Date.now() - cached.timestamp < this.TTL) {
|
||
|
|
cached.hits++;
|
||
|
|
this.metrics.cacheHits++;
|
||
|
|
this.updateHitRate();
|
||
|
|
|
||
|
|
const responseTime = performance.now() - startTime;
|
||
|
|
this.updateAverageResponseTime(responseTime);
|
||
|
|
|
||
|
|
console.log(`⚡ Cache hit: ${buttonId} (${responseTime.toFixed(2)}ms)`);
|
||
|
|
return cached.config;
|
||
|
|
} else {
|
||
|
|
// TTL 만료된 캐시 제거
|
||
|
|
this.memoryCache.delete(cacheKey);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// L2: 서버에서 로드 (100-300ms)
|
||
|
|
console.log(`🌐 Loading from server: ${buttonId}`);
|
||
|
|
const serverConfig = await this.loadFromServer(buttonId);
|
||
|
|
|
||
|
|
// 캐시에 저장
|
||
|
|
if (serverConfig) {
|
||
|
|
this.memoryCache.set(cacheKey, {
|
||
|
|
config: serverConfig,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
hits: 1,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
this.metrics.cacheMisses++;
|
||
|
|
this.updateHitRate();
|
||
|
|
|
||
|
|
const responseTime = performance.now() - startTime;
|
||
|
|
this.updateAverageResponseTime(responseTime);
|
||
|
|
|
||
|
|
console.log(`📡 Server response: ${buttonId} (${responseTime.toFixed(2)}ms)`);
|
||
|
|
return serverConfig;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`❌ Failed to get config for button ${buttonId}:`, error);
|
||
|
|
|
||
|
|
const responseTime = performance.now() - startTime;
|
||
|
|
this.updateAverageResponseTime(responseTime);
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 서버에서 설정 로드
|
||
|
|
*/
|
||
|
|
private async loadFromServer(buttonId: string): Promise<ButtonDataflowConfig | null> {
|
||
|
|
try {
|
||
|
|
const response = await apiClient.get(`/api/button-dataflow/config/${buttonId}`);
|
||
|
|
|
||
|
|
if (response.data.success) {
|
||
|
|
return response.data.data as ButtonDataflowConfig;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
} catch (error) {
|
||
|
|
// 404는 정상 상황 (설정이 없는 버튼)
|
||
|
|
if (error.response?.status === 404) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 설정 업데이트 (캐시 무효화)
|
||
|
|
*/
|
||
|
|
async updateConfig(buttonId: string, config: ButtonDataflowConfig): Promise<void> {
|
||
|
|
const cacheKey = `button_dataflow_${buttonId}`;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 서버에 저장
|
||
|
|
await apiClient.put(`/api/button-dataflow/config/${buttonId}`, config);
|
||
|
|
|
||
|
|
// 캐시 업데이트
|
||
|
|
this.memoryCache.set(cacheKey, {
|
||
|
|
config,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
hits: 0,
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(`💾 Config updated: ${buttonId}`);
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`❌ Failed to update config for button ${buttonId}:`, error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 특정 버튼 캐시 무효화
|
||
|
|
*/
|
||
|
|
invalidateCache(buttonId: string): void {
|
||
|
|
const cacheKey = `button_dataflow_${buttonId}`;
|
||
|
|
this.memoryCache.delete(cacheKey);
|
||
|
|
console.log(`🗑️ Cache invalidated: ${buttonId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 전체 캐시 무효화
|
||
|
|
*/
|
||
|
|
clearAllCache(): void {
|
||
|
|
this.memoryCache.clear();
|
||
|
|
console.log(`🗑️ All cache cleared`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 관계도별 캐시 무효화 (관계도가 수정된 경우)
|
||
|
|
*/
|
||
|
|
invalidateDiagramCache(diagramId: number): void {
|
||
|
|
let invalidatedCount = 0;
|
||
|
|
|
||
|
|
for (const [key, cached] of this.memoryCache.entries()) {
|
||
|
|
if (cached.config.selectedDiagramId === diagramId) {
|
||
|
|
this.memoryCache.delete(key);
|
||
|
|
invalidatedCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (invalidatedCount > 0) {
|
||
|
|
console.log(`🗑️ Invalidated ${invalidatedCount} caches for diagram ${diagramId}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 캐시 통계 조회
|
||
|
|
*/
|
||
|
|
getMetrics(): CacheMetrics {
|
||
|
|
return { ...this.metrics };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 상세 캐시 정보 조회 (디버깅용)
|
||
|
|
*/
|
||
|
|
getCacheInfo(): Array<{
|
||
|
|
buttonId: string;
|
||
|
|
config: ButtonDataflowConfig;
|
||
|
|
age: number; // 캐시된지 몇 분 경과
|
||
|
|
hits: number;
|
||
|
|
ttlRemaining: number; // 남은 TTL (초)
|
||
|
|
}> {
|
||
|
|
const now = Date.now();
|
||
|
|
const result: Array<any> = [];
|
||
|
|
|
||
|
|
for (const [key, cached] of this.memoryCache.entries()) {
|
||
|
|
const buttonId = key.replace("button_dataflow_", "");
|
||
|
|
const age = Math.floor((now - cached.timestamp) / 1000 / 60); // 분
|
||
|
|
const ttlRemaining = Math.max(0, Math.floor((this.TTL - (now - cached.timestamp)) / 1000)); // 초
|
||
|
|
|
||
|
|
result.push({
|
||
|
|
buttonId,
|
||
|
|
config: cached.config,
|
||
|
|
age,
|
||
|
|
hits: cached.hits,
|
||
|
|
ttlRemaining,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return result.sort((a, b) => b.hits - a.hits); // 히트 수 기준 내림차순
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🔥 TTL 만료된 캐시 정리 (주기적 호출)
|
||
|
|
*/
|
||
|
|
cleanupExpiredCache(): number {
|
||
|
|
const now = Date.now();
|
||
|
|
let cleanedCount = 0;
|
||
|
|
|
||
|
|
for (const [key, cached] of this.memoryCache.entries()) {
|
||
|
|
if (now - cached.timestamp >= this.TTL) {
|
||
|
|
this.memoryCache.delete(key);
|
||
|
|
cleanedCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cleanedCount > 0) {
|
||
|
|
console.log(`🧹 Cleaned up ${cleanedCount} expired cache entries`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return cleanedCount;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 히트율 업데이트
|
||
|
|
*/
|
||
|
|
private updateHitRate(): void {
|
||
|
|
this.metrics.hitRate =
|
||
|
|
this.metrics.totalRequests > 0 ? (this.metrics.cacheHits / this.metrics.totalRequests) * 100 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 평균 응답 시간 업데이트 (이동 평균)
|
||
|
|
*/
|
||
|
|
private updateAverageResponseTime(responseTime: number): void {
|
||
|
|
if (this.metrics.averageResponseTime === 0) {
|
||
|
|
this.metrics.averageResponseTime = responseTime;
|
||
|
|
} else {
|
||
|
|
// 이동 평균 (기존 90% + 새로운 값 10%)
|
||
|
|
this.metrics.averageResponseTime = this.metrics.averageResponseTime * 0.9 + responseTime * 0.1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 🔥 전역 싱글톤 인스턴스
|
||
|
|
export const dataflowConfigCache = new DataflowConfigCache();
|
||
|
|
|
||
|
|
// 🔥 5분마다 만료된 캐시 정리
|
||
|
|
if (typeof window !== "undefined") {
|
||
|
|
setInterval(
|
||
|
|
() => {
|
||
|
|
dataflowConfigCache.cleanupExpiredCache();
|
||
|
|
},
|
||
|
|
5 * 60 * 1000,
|
||
|
|
); // 5분
|
||
|
|
}
|
||
|
|
|
||
|
|
// 🔥 개발 모드에서 캐시 정보를 전역 객체에 노출
|
||
|
|
if (typeof window !== "undefined" && process.env.NODE_ENV === "development") {
|
||
|
|
(window as any).dataflowCache = {
|
||
|
|
getMetrics: () => dataflowConfigCache.getMetrics(),
|
||
|
|
getCacheInfo: () => dataflowConfigCache.getCacheInfo(),
|
||
|
|
clearCache: () => dataflowConfigCache.clearAllCache(),
|
||
|
|
};
|
||
|
|
}
|