๊ฐ์
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ์์ฒญ ๊ณผ์ ์ ์ดํด๋ณด๋ฉด, ๋ค์๊ณผ ๊ฐ์ ์์๋ก ์งํ๋๋ค.
middleware
-guard
-interceptor
-endpoint
-service layer
-database layer
๋ฐ์ดํฐ๋ฒ ์ด์ค๊น์ง ๊ฐ๋ ๊ณผ์ ์ ์๋นํ ๊ธด ํธ์ด๋ค(์ฌ์ค pipe, filter ๊น์ง ํฌํจํ๋ฉด ๋ ๊ธธ๋ค). ๋๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋๋ฐ ์ด๋ฌํ ๊ณผ์ ์ ๊ณ์ ๋ฐ๋ณตํ๋ ๊ฒ์ ๋ถํ์ํ ์ผ์ด๋ค.
์ด๋ฒ ํฌ์คํธ์์๋ interceptor
layer์์ ์บ์๋ฅผ ์ด์ฉํด ์ต๊ทผ์ ์กฐํ๋ ๋ฐ์ดํฐ๋ฅผ ๋ ๋น ๋ฅด๊ฒ ์๋ตํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๊ณ ์ ํ๋ค.
1. ๋ด๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ caching
CacheInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| @Injectable()
export class CacheInterceptor implements NestInterceptor {
protected allowedMethods = ['GET'];
constructor(
@Inject(CACHE_MANAGER) protected readonly cacheManager: any,
@Inject(REFLECTOR) protected readonly reflector: any,
) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const key = this.trackBy(context); // ์บ์ key๋ฅผ ๊ตฌ์ฑ, ์บ์ํ ์ ์๋ ๊ฒฝ์ฐ falsy๊ฐ์ ๋ฐํ
if (!key) return next.handle(); // ์บ์ํ ์ ์๋ ๊ฒฝ์ฐ ๋น์ฆ๋์ค๋ก์ง์ ์ฒ๋ฆฌ
try {
const value = await this.cacheManager.get(key); // ์บ์๋ ๋ฐ์ดํฐ๋ฅผ ํ์ธ
if (!isNil(value)) return of(value); // ์บ์๋ ๋ฐ์ดํฐ๊ฐ ์๋ ๊ฒฝ์ฐ ์บ์๋ ๋ฐ์ดํฐ๋ฅผ ์๋ต
// ์บ์๋ ๋ฐ์ดํฐ๊ฐ ์๋ ๊ฒฝ์ฐ
// @CahceTTL() ๋ฐ์ฝ๋ ์ดํฐ๋ก ์์ฑํ ์บ์ TTL(Time To Live) ๋ฉํ๋ฐ์ดํฐ ์กฐํ
const ttlValueOrFactory = this.reflector.get(CACHE_TTL_METADATA, context.getHandler()) ?? null;
const ttl = isFunction(ttlValueOrFactory) ? await ttlValueOrFactory(context) : ttlValueOrFactory;
return next.handle().pipe(
tap(response => {
// ์๋ตํ๋ฉฐ ๋์์, ์๋ต ๋ฐ์ดํฐ ์บ์ฑ ์ฒ๋ฆฌ ์งํ(์
๋ ฅํ TTL ๋งํผ)
const args = isNil(ttl) ? [key, response] : [key, response, { ttl }];
this.cacheManager.set(...args);
}),
);
} catch {
return next.handle(); // ์บ์ ์ฒ๋ฆฌ, ์บ์ ์กฐํ ๊ณผ์ ์์ ์ค๋ฅ ๋ฐ์์ ๊ฒฝ์ฐ ๋น์ฆ๋์ค๋ก์ง์ ๊ทธ๋๋ก ์ฒ๋ฆฌ
}
}
}
|
CacheInterceptor๋ ์ด๋ค ์ปจํธ๋กค๋ฌ์์ cache๋ฅผ ์ฌ์ฉํ๊ฒ ํ๊ณ ์ถ์๋, interceptor๊ฐ ํด๋น cache ๊ฐ์ ๋ฆฌํดํ ์ ์์์ง ์์์ง ํ๋จํ๋ค.
์ ์ฝ๋๋ ๋ค์์ ๋ด์ฉ์ ํฌํจํ๋ค.
trackBy
๋ฉ์๋๋ฅผ ํตํด ์บ์์ฒ๋ฆฌํ ์ ์๋ ์์ฒญ์ธ์ง ํ์ธํ๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก GET ์๋ํฌ์ธํธ์ ๋ํด์๋ง ์ฒ๋ฆฌํ๋๋ก ๊ตฌ์ฑ๋์ด ์๋ค.
@CacheKey()
๋ฐ์ฝ๋ ์ดํฐ๋ก ์บ์ ๊ฐ์ ์ค์ ํ์ง ์์๋ค๋ฉด, default cache ๊ฐ์ ์์ฒญ url์ด๋ค. trackBy๋ฅผ cacheKey๋ฅผ ์ด์ฉํ๋๋ก overriding ํ ์ ์๋ค. ํด๋น ์์ ๋ ์๋์์.
cache key๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ, cache manager๋ฅผ ํตํด ํด๋น cache key๋ฅผ ๊ฐ์ง ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ๋ ์ง ํ์ธํ๋ค.
์ผ์นํ๋ cache data๊ฐ ์๋ค๋ฉด, @CacheKey()
/ @CacheTTL()
์ ํตํด ์
๋ ฅํ metadata๋ฅผ ์กฐํํ๋ค.
ํด๋น cache data๋ฅผ RxJs ๋ฅผ ์ด์ฉํด ์ฒ๋ฆฌํ๋ค.
CacheInterceptor Customizing
๋ค์๊ณผ ๊ฐ์ด ๋ด์ฅ๋ CacheInterceptor๋ฅผ
์ด์ฉํด ์บ์ฑ์ ์ฒ๋ฆฌํ๋ค๋ฉด, /posts/
์ /posts?search=title3
๋ฅผ ๋๊ฐ์ด ์ฒ๋ฆฌํ ๊ฒ์ด๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
| @UseInterceptors(CacheInterceptor)
@CacheKey(GET_POSTS_CACHE_KEY)
@CacheTTL(120)
@Get()
async getPosts(
@Query('search') search: string,
@Query() { offset, limit, startId }: PaginationParams
) {
if (search) {
return this.postsService.searchForPosts(search, offset, limit, startId);
}
return this.postsService.getAllPosts(offset, limit, startId);
}
|
๋ฐ๋ผ์ ์ฐ๋ฆฌ๋ CacheInterceptor๋ฅผ
ํ์ฅํด์ ์ฌ์ฉํด์ผ ํ๋ค. ๋ค์ ์ฝ๋๋ ์ฐ๋ฆฌ๊ฐ ๋ง์ฝ @CacheKey๋ฅผ ํตํด ์บ์ ํค๋ฅผ ์ ๋ฌํ์ง ์๋๋ค๋ฉด, CacheInterceptor๊ฐ cache๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก ๋์ํ๊ณ , ์บ์ ํค๋ฅผ ์ ๋ฌํ์๋ค๋ฉด, ์๋ก์ด ์บ์ํค๋ฅผ ์์ฑํด ์บ์๋ฅผ ์์ฑํ ๊ฒ์ด๋ค. (e.g POSTS_CACHE-null / POSTS_CACHE-search=hello)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import { CACHE_KEY_METADATA, CacheInterceptor, ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext): string | undefined {
const cacheKey = this.reflector.get(
CACHE_KEY_METADATA,
context.getHandler(),
);
if (cacheKey) {
const request = context.switchToHttp().getRequest();
return `${cacheKey}-${request._parsedUrl.query}`;
}
return super.trackBy(context);
}
}
|
@CacheKey() : ํน์ cache key๋ฅผ ์ง์ ํ๊ธฐ ์ํ ๋ฐ์ฝ๋ ์ดํฐ
@CacheTTL() : ํด๋น cache key์ TTL์ ์ง์ ํ๊ธฐ ์ํ ๋ฐ์ฝ๋ ์ดํฐ
์บ์ ๋ฌดํจํ
์บ์๋ฅผ ๋ฌดํ์ ์ผ๋ก ๊ธธ๊ฒ ๊ฐ์ ธ๊ฐ๋ฉด ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ๋ ์๋๋ฅผ ๋น ๋ฅด๊ฒ ํ ์ ์์ผ๋๊น ์ข์๊ฒ ์๋๊ฐ? ๋ผ๋ ์๊ฐ๋ ๋ค ์ ์์ง๋ง, ๋ฐ์ดํฐ๋ ๊ณ์ ๋ณํ๊ธฐ ๋๋ฌธ์, ๊ทธ๋ ์ง๋ง๋ ์๋๋ค. ์ฐ๋ฆฌ๋ ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์ถ๊ฐ, ์ญ์ , ์
๋ฐ์ดํธ ๋ ๋ ํด๋น cache key๋ฅผ ๊ฐ์ง cache ๊ฐ์ ๋ฌดํจํํ๊ณ , ์๋ก์ด ์บ์ ๋ฐ์ดํฐ๋ฅผ ์์ฑํด์ผ ํ๋ค.
์บ์ ๋ฌดํจํ๋ ๋ณดํต ์๋น์ค layer์์ ๊ตฌํํ๊ฒ ๋๋๋ฐ, ์ด๋ ๋ช๊ฐ์ง ๋ฌธ์ ์ ์ ๊ฐ์ง๋ค.
- ์บ์๋ฅผ ๋ฌดํจํํ๋ ๋์ผํ ์ฝ๋๊ฐ ๋ฐ๋ณต๋จ
- ์บ์ ๊ด๋ฆฌ๋ ์ฃผ์ ๋ก์ง์ด ์๋๋ผ ๋ถ๊ฐ์ ์ธ ๋ก์ง์ ๊ฐ๊น์
๋ฐ๋ผ์ ์บ์ ๋ฌดํจํ๋ฅผ ์ํ ์์
์ ์์ ๊ฐ์ด CacheInterceptor๋ฅผ
์์๋ฐ์ ๋ณ๋์ Interceptor ๋ด๋ถ์์ ๊ตฌํํด ์ค ์ ์๋ค. ํด๋น ๋ธ๋ก๊ทธ ์ฐธ๊ณ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| // src/core/httpcache.interceptor.ts
import {
CacheInterceptor,
CallHandler,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
import { Cluster } from 'ioredis';
import { Observable, tap } from 'rxjs';
@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
private readonly CACHE_EVICT_METHODS = [
'POST', 'PATCH', 'PUT', 'DELETE'
];
async intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Promise<Observable<any>> {
const req = context.switchToHttp().getRequest<Request>();
if (this.CACHE_EVICT_METHODS.includes(req.method)) {
// ์บ์ ๋ฌดํจํ ์ฒ๋ฆฌ
return next.handle().pipe(tap(() => this._clearCaches(req.originalUrl)));
}
// ๊ธฐ์กด ์บ์ฑ ์ฒ๋ฆฌ
return super.intercept(context, next);
}
/**
* @param cacheKeys ์ญ์ ํ ์บ์ ํค ๋ชฉ๋ก
*/
private async _clearCaches(cacheKeys: string[]): Promise<boolean> {
const client: Cluster = await this.cacheManager.store.getClient();
const redisNodes = client.nodes();
const result2 = await Promise.all(
redisNodes.map(async (redis) => {
const _keys = await Promise.all(
cacheKeys.map((cacheKey) => redis.keys(`*${cacheKey}*`)),
);
const keys = _keys.flat();
return Promise.all(keys.map((key) => !!this.cacheManager.del(key)));
}),
);
return result2.flat().every((r) => !!r);
}
}
|
์ ์ฒด ์ฝ๋๋ ๋ค์์์ ํ์ธํ ์ ์๋ค.
์ฝ๋
2. Redis๋ฅผ ์ด์ฉํ caching