@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
513 lines (456 loc) • 12.5 kB
text/typescript
import {
CacheConfig,
CacheLevel,
CacheProvider,
CacheEntry,
CacheMetrics,
CacheStats,
InvalidationRule,
CacheStatus,
ContentType,
CacheStrategy,
} from "./types";
export interface CacheManagerOptions {
levels: CacheLevel[];
defaultTTL: number;
maxMemoryUsage: number; // MB
compressionEnabled: boolean;
metricsEnabled: boolean;
debugMode: boolean;
}
export class CacheManager {
private configs: Map<CacheLevel, CacheConfig> = new Map();
private providers: Map<CacheLevel, CacheProvider> = new Map();
private metrics: CacheMetrics;
private invalidationRules: InvalidationRule[] = [];
private options: CacheManagerOptions;
constructor(options: CacheManagerOptions) {
this.options = options;
this.metrics = this.initializeMetrics();
this.setupDefaultConfigs();
}
/**
* 캐시 설정 초기화
*/
private setupDefaultConfigs(): void {
// 브라우저 캐시 설정
this.configs.set("browser", {
level: "browser",
provider: "browser",
strategy: "ttl",
ttl: 3600, // 1시간
maxSize: 50, // MB
compression: true,
encryption: false,
tags: ["static-assets", "components"],
headers: {
"Cache-Control": "public, max-age=3600",
ETag: "auto-generated",
},
invalidationRules: [
{
id: "version-change",
strategy: "version-based",
trigger: "deployment",
scope: "global",
priority: "high",
conditions: ["version_update"],
},
],
});
// CDN 캐시 설정
this.configs.set("cdn", {
level: "cdn",
provider: "cloudflare",
strategy: "ttl",
ttl: 86400, // 24시간
maxSize: 1000, // MB
compression: true,
encryption: false,
tags: ["static-assets", "images", "css", "js"],
headers: {
"Cache-Control": "public, max-age=86400",
Vary: "Accept-Encoding",
},
invalidationRules: [
{
id: "asset-update",
strategy: "tag-based",
trigger: "content_update",
scope: "tagged",
priority: "medium",
conditions: ["asset_change", "content_publish"],
},
],
});
// 서버 캐시 설정
this.configs.set("server", {
level: "server",
provider: "redis",
strategy: "lru",
ttl: 1800, // 30분
maxSize: 500, // MB
compression: true,
encryption: true,
tags: ["api-responses", "component-data", "templates"],
headers: {},
invalidationRules: [
{
id: "content-update",
strategy: "dependency-based",
trigger: "data_change",
scope: "related",
priority: "high",
conditions: ["content_edit", "component_update"],
},
],
});
}
/**
* 메트릭 초기화
*/
private initializeMetrics(): CacheMetrics {
return {
hitRate: 0,
missRate: 0,
totalRequests: 0,
totalHits: 0,
totalMisses: 0,
averageResponseTime: 0,
memoryUsage: 0,
diskUsage: 0,
networkTraffic: 0,
errors: 0,
evictions: 0,
compressionRatio: 0,
lastUpdated: new Date(),
};
}
/**
* 캐시에서 데이터 조회
*/
async get<T>(
key: string,
contentType: ContentType,
options?: {
levels?: CacheLevel[];
fallback?: () => Promise<T>;
}
): Promise<{ data: T | null; status: CacheStatus; level?: CacheLevel }> {
const startTime = Date.now();
const levels = options?.levels || this.options.levels;
// 캐시 레벨별로 순차 조회
for (const level of levels) {
try {
const config = this.configs.get(level);
if (!config) continue;
const entry = await this.getFromLevel<T>(key, level);
if (entry && this.isValidEntry(entry)) {
this.updateMetrics("hit", Date.now() - startTime, level);
return {
data: entry.data,
status: "hit",
level,
};
}
} catch (error) {
console.error(`캐시 조회 오류 (${level}):`, error);
this.metrics.errors++;
}
}
// 캐시 미스 처리
this.updateMetrics("miss", Date.now() - startTime);
if (options?.fallback) {
try {
const data = await options.fallback();
// 폴백 데이터를 캐시에 저장
await this.set(key, data, contentType);
return { data, status: "miss" };
} catch (error) {
console.error("폴백 함수 실행 오류:", error);
return { data: null, status: "error" };
}
}
return { data: null, status: "miss" };
}
/**
* 캐시에 데이터 저장
*/
async set<T>(
key: string,
data: T,
contentType: ContentType,
options?: {
ttl?: number;
levels?: CacheLevel[];
tags?: string[];
}
): Promise<boolean> {
const levels = options?.levels || this.options.levels;
let success = true;
for (const level of levels) {
try {
const config = this.configs.get(level);
if (!config) continue;
const entry: CacheEntry<T> = {
key,
data,
contentType,
level,
ttl: options?.ttl || config.ttl,
tags: options?.tags || config.tags,
createdAt: new Date(),
expiresAt: new Date(Date.now() + (options?.ttl || config.ttl) * 1000),
hits: 0,
lastAccessed: new Date(),
size: this.calculateSize(data),
compressed: config.compression,
metadata: {
version: "1.0",
checksum: this.calculateChecksum(data),
},
};
await this.setToLevel(entry, level);
} catch (error) {
console.error(`캐시 저장 오류 (${level}):`, error);
success = false;
this.metrics.errors++;
}
}
return success;
}
/**
* 캐시 무효화
*/
async invalidate(
pattern: string | string[],
options?: {
levels?: CacheLevel[];
strategy?: "key" | "tag" | "pattern";
}
): Promise<number> {
const levels = options?.levels || this.options.levels;
const strategy = options?.strategy || "key";
let invalidatedCount = 0;
for (const level of levels) {
try {
const count = await this.invalidateFromLevel(pattern, level, strategy);
invalidatedCount += count;
} catch (error) {
console.error(`캐시 무효화 오류 (${level}):`, error);
this.metrics.errors++;
}
}
return invalidatedCount;
}
/**
* 캐시 통계 조회
*/
getStats(): CacheStats {
const levelStats = new Map<CacheLevel, any>();
for (const level of this.options.levels) {
levelStats.set(level, {
hitRate: this.calculateHitRate(level),
memoryUsage: this.getMemoryUsage(level),
entryCount: this.getEntryCount(level),
averageSize: this.getAverageSize(level),
});
}
return {
overall: this.metrics,
byLevel: levelStats,
topKeys: this.getTopKeys(),
recentActivity: this.getRecentActivity(),
errors: this.getRecentErrors(),
};
}
/**
* 캐시 워밍 (사전 로딩)
*/
async warmUp(
keys: Array<{
key: string;
contentType: ContentType;
loader: () => Promise<any>;
}>
): Promise<number> {
let warmedCount = 0;
await Promise.allSettled(
keys.map(async ({ key, contentType, loader }) => {
try {
const data = await loader();
await this.set(key, data, contentType);
warmedCount++;
} catch (error) {
console.error(`캐시 워밍 오류 (${key}):`, error);
}
})
);
return warmedCount;
}
/**
* 캐시 클리어
*/
async clear(levels?: CacheLevel[]): Promise<boolean> {
const targetLevels = levels || this.options.levels;
let success = true;
for (const level of targetLevels) {
try {
await this.clearLevel(level);
} catch (error) {
console.error(`캐시 클리어 오류 (${level}):`, error);
success = false;
}
}
// 메트릭 리셋
if (!levels || levels.length === this.options.levels.length) {
this.metrics = this.initializeMetrics();
}
return success;
}
/**
* 헬스 체크
*/
async healthCheck(): Promise<{
healthy: boolean;
levels: Map<CacheLevel, boolean>;
issues: string[];
}> {
const levelHealth = new Map<CacheLevel, boolean>();
const issues: string[] = [];
let overallHealthy = true;
for (const level of this.options.levels) {
try {
const isHealthy = await this.checkLevelHealth(level);
levelHealth.set(level, isHealthy);
if (!isHealthy) {
overallHealthy = false;
issues.push(`${level} 캐시 레벨에 문제가 있습니다`);
}
} catch (error) {
levelHealth.set(level, false);
overallHealthy = false;
issues.push(`${level} 헬스 체크 실패: ${error}`);
}
}
// 메모리 사용량 체크
if (this.metrics.memoryUsage > this.options.maxMemoryUsage * 0.9) {
overallHealthy = false;
issues.push("메모리 사용량이 임계값에 근접했습니다");
}
// 에러율 체크
const errorRate =
this.metrics.errors / Math.max(this.metrics.totalRequests, 1);
if (errorRate > 0.05) {
// 5% 이상
overallHealthy = false;
issues.push("에러율이 높습니다");
}
return {
healthy: overallHealthy,
levels: levelHealth,
issues,
};
}
// Private helper methods
private async getFromLevel<T>(
key: string,
level: CacheLevel
): Promise<CacheEntry<T> | null> {
// 실제 구현은 provider별로 다름
return null;
}
private async setToLevel<T>(
entry: CacheEntry<T>,
level: CacheLevel
): Promise<void> {
// 실제 구현은 provider별로 다름
}
private async invalidateFromLevel(
pattern: string | string[],
level: CacheLevel,
strategy: string
): Promise<number> {
// 실제 구현은 provider별로 다름
return 0;
}
private async clearLevel(level: CacheLevel): Promise<void> {
// 실제 구현은 provider별로 다름
}
private async checkLevelHealth(level: CacheLevel): Promise<boolean> {
// 실제 구현은 provider별로 다름
return true;
}
private isValidEntry<T>(entry: CacheEntry<T>): boolean {
return entry.expiresAt > new Date();
}
private updateMetrics(
type: "hit" | "miss",
responseTime: number,
level?: CacheLevel
): void {
this.metrics.totalRequests++;
if (type === "hit") {
this.metrics.totalHits++;
} else {
this.metrics.totalMisses++;
}
this.metrics.hitRate = this.metrics.totalHits / this.metrics.totalRequests;
this.metrics.missRate =
this.metrics.totalMisses / this.metrics.totalRequests;
this.metrics.averageResponseTime =
(this.metrics.averageResponseTime + responseTime) / 2;
this.metrics.lastUpdated = new Date();
}
private calculateSize(data: any): number {
return JSON.stringify(data).length;
}
private calculateChecksum(data: any): string {
// 간단한 체크섬 계산
const str = JSON.stringify(data);
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // 32bit 정수로 변환
}
return hash.toString(16);
}
private calculateHitRate(level: CacheLevel): number {
// 레벨별 히트율 계산
return 0;
}
private getMemoryUsage(level: CacheLevel): number {
// 레벨별 메모리 사용량
return 0;
}
private getEntryCount(level: CacheLevel): number {
// 레벨별 엔트리 수
return 0;
}
private getAverageSize(level: CacheLevel): number {
// 레벨별 평균 크기
return 0;
}
private getTopKeys(): Array<{ key: string; hits: number }> {
// 인기 키 목록
return [];
}
private getRecentActivity(): Array<{
timestamp: Date;
action: string;
key: string;
}> {
// 최근 활동 로그
return [];
}
private getRecentErrors(): Array<{
timestamp: Date;
error: string;
level: CacheLevel;
}> {
// 최근 에러 로그
return [];
}
}
export default CacheManager;