// utils/cache.js - 통합 캐싱 시스템 const NodeCache = require('node-cache'); // 메모리 캐시 (Redis가 없을 때 fallback) const memoryCache = new NodeCache({ stdTTL: 600, // 기본 10분 checkperiod: 120, // 2분마다 만료된 키 정리 useClones: false // 성능 향상을 위해 복사본 생성 안함 }); // Redis 클라이언트 (선택적) let redisClient = null; /** * Redis 연결 초기화 (선택적) */ const initRedis = async () => { try { const redis = require('redis'); redisClient = redis.createClient({ socket: { host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379, reconnectStrategy: (retries) => { if (retries > 10) { console.warn('Redis 재시도 횟수 초과. 메모리 캐시를 사용합니다.'); return false; // Redis 연결 포기 } return Math.min(retries * 100, 3000); } }, password: process.env.REDIS_PASSWORD || undefined, database: process.env.REDIS_DB || 0 }); redisClient.on('error', (err) => { console.warn('Redis 오류:', err.message); redisClient = null; // Redis 사용 중단, 메모리 캐시로 fallback }); redisClient.on('connect', () => { console.log('✅ Redis 캐시 연결 성공'); }); await redisClient.connect(); } catch (error) { console.warn('Redis 초기화 실패, 메모리 캐시 사용:', error.message); redisClient = null; } }; /** * 캐시에서 값 조회 */ const get = async (key) => { try { if (redisClient && redisClient.isOpen) { const value = await redisClient.get(key); return value ? JSON.parse(value) : null; } else { return memoryCache.get(key) || null; } } catch (error) { console.warn(`캐시 조회 오류 (${key}):`, error.message); return null; } }; /** * 캐시에 값 저장 */ const set = async (key, value, ttl = 600) => { try { if (redisClient && redisClient.isOpen) { await redisClient.setEx(key, ttl, JSON.stringify(value)); } else { memoryCache.set(key, value, ttl); } return true; } catch (error) { console.warn(`캐시 저장 오류 (${key}):`, error.message); return false; } }; /** * 캐시에서 값 삭제 */ const del = async (key) => { try { if (redisClient && redisClient.isOpen) { await redisClient.del(key); } else { memoryCache.del(key); } return true; } catch (error) { console.warn(`캐시 삭제 오류 (${key}):`, error.message); return false; } }; /** * 패턴으로 캐시 키 삭제 */ const delPattern = async (pattern) => { try { if (redisClient && redisClient.isOpen) { const keys = await redisClient.keys(pattern); if (keys.length > 0) { await redisClient.del(keys); } } else { const keys = memoryCache.keys(); const matchingKeys = keys.filter(key => { const regex = new RegExp(pattern.replace('*', '.*')); return regex.test(key); }); memoryCache.del(matchingKeys); } return true; } catch (error) { console.warn(`패턴 캐시 삭제 오류 (${pattern}):`, error.message); return false; } }; /** * 전체 캐시 초기화 */ const flush = async () => { try { if (redisClient && redisClient.isOpen) { await redisClient.flushDb(); } else { memoryCache.flushAll(); } return true; } catch (error) { console.warn('캐시 초기화 오류:', error.message); return false; } }; /** * 캐시 통계 조회 */ const getStats = () => { if (redisClient && redisClient.isOpen) { return { type: 'redis', connected: true, host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379 }; } else { const stats = memoryCache.getStats(); return { type: 'memory', connected: true, keys: stats.keys, hits: stats.hits, misses: stats.misses, hitRate: stats.hits / (stats.hits + stats.misses) || 0 }; } }; /** * 캐시 키 생성 헬퍼 */ const createKey = (prefix, ...parts) => { return `${prefix}:${parts.join(':')}`; }; /** * TTL 상수 정의 */ const TTL = { SHORT: 60, // 1분 MEDIUM: 300, // 5분 LONG: 600, // 10분 HOUR: 3600, // 1시간 DAY: 86400 // 24시간 }; /** * 캐시 미들웨어 생성기 */ const createCacheMiddleware = (keyGenerator, ttl = TTL.MEDIUM) => { return async (req, res, next) => { try { const cacheKey = typeof keyGenerator === 'function' ? keyGenerator(req) : keyGenerator; const cachedData = await get(cacheKey); if (cachedData) { console.log(`🎯 캐시 히트: ${cacheKey}`); return res.json(cachedData); } // 원본 res.json을 저장 const originalJson = res.json; // res.json을 오버라이드하여 응답을 캐시에 저장 res.json = function(data) { // 성공 응답만 캐시 if (res.statusCode >= 200 && res.statusCode < 300) { set(cacheKey, data, ttl).then(() => { console.log(`💾 캐시 저장: ${cacheKey}`); }); } // 원본 응답 실행 return originalJson.call(this, data); }; next(); } catch (error) { console.warn('캐시 미들웨어 오류:', error.message); next(); } }; }; /** * 캐시 무효화 헬퍼 */ const invalidateCache = { // 사용자 관련 캐시 무효화 user: async (userId) => { await delPattern(`user:${userId}:*`); await delPattern('users:*'); }, // 작업자 관련 캐시 무효화 worker: async (workerId) => { await delPattern(`worker:${workerId}:*`); await delPattern('workers:*'); }, // 프로젝트 관련 캐시 무효화 project: async (projectId) => { await delPattern(`project:${projectId}:*`); await delPattern('projects:*'); }, // 작업 관련 캐시 무효화 task: async (taskId) => { await delPattern(`task:${taskId}:*`); await delPattern('tasks:*'); }, // 일일 작업 보고서 관련 캐시 무효화 dailyWorkReport: async (date) => { await delPattern(`daily-work-report:${date}:*`); await delPattern('daily-work-reports:*'); }, // 전체 캐시 무효화 all: async () => { await flush(); } }; module.exports = { initRedis, get, set, del, delPattern, flush, getStats, createKey, TTL, createCacheMiddleware, invalidateCache };