Skip to content

Commit 2b4e3fd

Browse files
committed
feature: redis cache class 추가와 jsdocs 추가
1 parent fbfb886 commit 2b4e3fd

File tree

2 files changed

+253
-13
lines changed

2 files changed

+253
-13
lines changed

src/modules/cache/cache.type.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,80 @@
1+
/**
2+
* 캐시 설정 옵션입니다.
3+
*
4+
* @property host 캐시 서버의 호스트명 또는 IP 주소
5+
* @property port 캐시 서버의 포트 번호
6+
* @property [password] 캐시 서버 인증 비밀번호(선택)
7+
* @property [db] 사용할 데이터베이스 인덱스(선택)
8+
* @property [keyPrefix] 모든 키에 붙일 접두사(선택)
9+
* @property [defaultTTL] 기본 만료 시간(초, 선택)
10+
*/
111
export interface CacheConfig {
212
host: string;
313
port: number;
414
password?: string;
515
db?: number;
616
keyPrefix?: string;
7-
maxSize?: number;
817
defaultTTL?: number;
9-
strategy?: 'lru' | 'ttl' | 'combined';
10-
}
11-
12-
export interface CacheMetadata {
13-
key: string;
14-
size: number;
15-
createdAt: number;
16-
lastAccessed: number;
17-
accessCount: number;
18-
ttl?: number;
19-
expiresAt?: number;
2018
}
2119

20+
/**
21+
* 캐시 서비스 인터페이스입니다.
22+
*/
2223
export interface ICache {
24+
/**
25+
* 키로부터 값을 가져옵니다.
26+
* @param key 값을 가져올 키
27+
* @returns 값을 반환하거나 없으면 null을 반환합니다.
28+
*/
2329
get<T>(key: string): Promise<T | null>;
30+
31+
/**
32+
* 값을 캐시에 저장합니다.
33+
* @param key 저장할 키
34+
* @param value 저장할 값
35+
* @param ttlSeconds 값의 만료 시간(초, 선택)
36+
*/
2437
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
38+
39+
/**
40+
* 키에 해당하는 값을 삭제합니다.
41+
* @param key 삭제할 키
42+
* @returns 삭제 성공 여부를 반환합니다.
43+
*/
2544
delete(key: string): Promise<boolean>;
45+
46+
/**
47+
* 키가 존재하는지 확인합니다.
48+
* @param key 확인할 키
49+
* @returns 존재하면 true, 아니면 false를 반환합니다.
50+
*/
2651
exists(key: string): Promise<boolean>;
52+
53+
/**
54+
* 캐시를 비웁니다. 패턴이 있으면 해당 키만 비웁니다.
55+
* @param pattern 비울 키의 패턴(선택)
56+
*/
2757
clear(pattern?: string): Promise<void>;
58+
59+
/**
60+
* 캐시에 저장된 항목 개수를 반환합니다.
61+
* @returns 항목 개수
62+
*/
2863
size(): Promise<number>;
29-
}
3064

65+
/**
66+
* 캐시 서버에 연결합니다.
67+
*/
68+
connect(): Promise<void>;
69+
70+
/**
71+
* 캐시 서버와 연결을 끊습니다.
72+
*/
73+
disconnect(): Promise<void>;
74+
75+
/**
76+
* 캐시 서버와 연결되어 있는지 확인합니다.
77+
* @returns 연결되어 있으면 true, 아니면 false
78+
*/
79+
isConnected(): boolean;
80+
}

src/modules/cache/redis.cache.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { createClient, RedisClientType } from 'redis';
2+
3+
import logger from '@/configs/logger.config';
4+
import { ICache, CacheConfig } from './cache.type';
5+
6+
export class RedisCache implements ICache {
7+
private client: RedisClientType;
8+
private connected: boolean = false;
9+
private keyPrefix: string;
10+
private defaultTTL: number;
11+
12+
constructor(config: CacheConfig) {
13+
this.keyPrefix = config.keyPrefix || 'vd2:cache:';
14+
this.defaultTTL = config.defaultTTL || 300;
15+
16+
this.client = createClient({
17+
socket: {
18+
host: config.host,
19+
port: config.port,
20+
},
21+
password: config.password,
22+
database: config.db || 0,
23+
});
24+
25+
this.setupEventHandlers();
26+
}
27+
28+
/**
29+
* Redis 클라이언트의 이벤트 핸들러를 설정합니다.
30+
* 에러, 연결, 연결 해제 시 상태를 변경하고 로그를 남깁니다.
31+
*
32+
* @private
33+
*/
34+
private setupEventHandlers(): void {
35+
this.client.on('error', (err) => {
36+
logger.error('Redis Client Error:', err);
37+
this.connected = false;
38+
});
39+
40+
this.client.on('connect', () => {
41+
logger.info('Redis Client Connected');
42+
this.connected = true;
43+
});
44+
45+
this.client.on('disconnect', () => {
46+
logger.warn('Redis Client Disconnected');
47+
this.connected = false;
48+
});
49+
}
50+
51+
/**
52+
* 주어진 키에 keyPrefix를 접두사로 붙여 전체 Redis 키를 생성합니다.
53+
*
54+
* @param key - 접두사가 붙을 원본 키 문자열
55+
* @returns keyPrefix가 포함된 전체 Redis 키 문자열
56+
*/
57+
private getFullKey(key: string): string {
58+
return `${this.keyPrefix}${key}`;
59+
}
60+
61+
async connect(): Promise<void> {
62+
try {
63+
if (!this.connected) {
64+
await this.client.connect();
65+
this.connected = true;
66+
logger.info('Redis cache connection established');
67+
}
68+
} catch (error) {
69+
logger.error('Failed to connect to Redis cache:', error);
70+
throw error;
71+
}
72+
}
73+
74+
async disconnect(): Promise<void> {
75+
try {
76+
if (this.connected) {
77+
await this.client.disconnect();
78+
this.connected = false;
79+
logger.info('Redis cache connection closed');
80+
}
81+
} catch (error) {
82+
logger.error('Failed to disconnect from Redis cache:', error);
83+
throw error;
84+
}
85+
}
86+
87+
isConnected(): boolean {
88+
return this.connected;
89+
}
90+
91+
async get<T>(key: string): Promise<T | null> {
92+
try {
93+
if (!this.connected) {
94+
logger.warn('Redis not connected, skipping cache get');
95+
return null;
96+
}
97+
98+
const value = await this.client.get(this.getFullKey(key));
99+
return value ? JSON.parse(value) : null;
100+
} catch (error) {
101+
logger.error(`Cache GET error for key ${key}:`, error);
102+
return null;
103+
}
104+
}
105+
106+
async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
107+
try {
108+
if (!this.connected) {
109+
logger.warn('Redis not connected, skipping cache set');
110+
return;
111+
}
112+
113+
const fullKey = this.getFullKey(key);
114+
const serializedValue = JSON.stringify(value);
115+
const ttl = ttlSeconds ?? this.defaultTTL;
116+
117+
if (ttl > 0) {
118+
await this.client.setEx(fullKey, ttl, serializedValue);
119+
} else {
120+
await this.client.set(fullKey, serializedValue);
121+
}
122+
} catch (error) {
123+
logger.error(`Cache SET error for key ${key}:`, error);
124+
// 캐시 오류 시에도 애플리케이션은 계속 동작
125+
}
126+
}
127+
128+
async delete(key: string): Promise<boolean> {
129+
try {
130+
if (!this.connected) {
131+
logger.warn('Redis not connected, skipping cache delete');
132+
return false;
133+
}
134+
135+
const result = await this.client.del(this.getFullKey(key));
136+
return result > 0;
137+
} catch (error) {
138+
logger.error(`Cache DELETE error for key ${key}:`, error);
139+
return false;
140+
}
141+
}
142+
143+
async exists(key: string): Promise<boolean> {
144+
try {
145+
if (!this.connected) {
146+
return false;
147+
}
148+
149+
const result = await this.client.exists(this.getFullKey(key));
150+
return result > 0;
151+
} catch (error) {
152+
logger.error(`Cache EXISTS error for key ${key}:`, error);
153+
return false;
154+
}
155+
}
156+
157+
async clear(pattern?: string): Promise<void> {
158+
try {
159+
if (!this.connected) {
160+
logger.warn('Redis not connected, skipping cache clear');
161+
return;
162+
}
163+
164+
const searchPattern = pattern
165+
? `${this.keyPrefix}${pattern}`
166+
: `${this.keyPrefix}*`;
167+
168+
const keys = await this.client.keys(searchPattern);
169+
if (keys.length > 0) {
170+
await this.client.del(keys);
171+
}
172+
} catch (error) {
173+
logger.error(`Cache CLEAR error for pattern ${pattern}:`, error);
174+
}
175+
}
176+
177+
async size(): Promise<number> {
178+
try {
179+
if (!this.connected) {
180+
return 0;
181+
}
182+
183+
const keys = await this.client.keys(`${this.keyPrefix}*`);
184+
return keys.length;
185+
} catch (error) {
186+
logger.error('Cache SIZE error:', error);
187+
return 0;
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)