
์ ์ฉํ npm cache package ์๊ฐ ๋ฐ ์ ์ฉ

- #cacheable-lookup
- #node-cache
- #microservice
- #fromm
๋ค์ด๊ฐ๋ฉฐ
์ ๋ ์ด์ ์ ์ ํฌ๊ฐ ์ฌ์ฉ์ค์ธ ๋ฐฑ์๋์์ ์๋ฒ๊ฐ์ ํต์ ํ๋ ๋ฐฉ๋ฒ์ ์๊ฐ๋๋ฆฐ ์ ์์ต๋๋ค.
์ด ๋ชจ๋์ monorepo ์์์ ๋
๋ฆฝ package๋ก ๋ถ๋ฆฌ๋์ด ๋์ฑ ๋ฐ์ ํ๊ณ ์์ต๋๋ค.
์ด internal_api package์ ์ถ๊ฐ๋ ๊ธฐ๋ฅ์ ์๊ฐํ๊ณ ์ ํฉ๋๋ค.
๊ฒฐ๋ก ์ ๋จผ์ ๋ง์๋๋ฆฌ๋ฉด, cache์ ๊ดํ 2๊ฐ์ง npm package๋ฅผ ์๊ฐํ๋ ๊ธ์ด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
- DNS๋ฅผ ์ํ cacheable-lookup
- data๋ฅผ ์ํ in-memory node-cache
1. ๋น๋ฒํ DNS ์๋ฌ
์ ํฌ๋ lambda์ ECS fargate๋ก ์๋ฒ๊ฐ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
๋ฐฑ์๋ ์๋ฒ์์ ์ธ๋ถ๋ ๋ด๋ถ๋ ๋ ๋ค๋ฅธ ์๋ฒ์ ์์ฒญํ๋ ๊ฒฝ์ฐ, ๋๋ถ๋ถ์ ๋์ ์๋ฒ๊ฐ ์ ํด์ ธ ์๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ,
ํนํ, ํ๋ฒ ์คํ๋๋ฉด ์ค๋ ๋ ์๋ ECS์์ ์ข
์ข
DNS ์๋ฌ๊ฐ ๋ฐ์ํ๊ณค ํ์ต๋๋ค.
Error: getaddrinfo EAI_AGAIN account-api.frommyarti.com
ย ย ย ย at AxiosError.from (/app/node_modules/axios/dist/node/axios.cjs:836:14)
ย ย ย ย at RedirectableRequest.handleRequestError (/app/node_modules/axios/dist/node/axios.cjs:3086:25)
ย ย ย ย at RedirectableRequest.emit (node:events:513:28)
ย ย ย ย at eventHandlers.(/app/node_modules/follow-redirects/index.js:38:24)
ย ย ย ย at ClientRequest.emit (node:events:513:28)
ย ย ย ย at TLSSocket.socketErrorListener (node:_http_client:502:9)
ย ย ย ย at TLSSocket.emit (node:events:513:28)
ย ย ย ย at emitErrorNT (node:internal/streams/destroy:151:8)
ย ย ย ย at emitErrorCloseNT (node:internal/streams/destroy:116:3)
ย ย ย ย at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
ย ย ย ย at Axios.request (/app/node_modules/axios/dist/node/axios.cjs:3876:41)
ย ย ย ย at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
ย ย ย ย at async AccountExternal.getAccountUser (/app/apps/server/dist/src/user/account/account.external.js:110:40)
ย ย ย ย at async Promise.all (index 0)
ย ย ย ย at async FanService.getFanProfile (/app/apps/server/dist/src/user/fan/fan.service.js:54:36)
์ด์ ๊ด๋ จํด์ ๊ตฌ๊ธ๋ง์ ํด๋ณด์๋ฉด, getaddrinfo Error in Node.js ๋ผ๋ ์ ๋ชฉ์ผ๋ก ๋ง์ ๊ธ์ ๋ณผ ์ ์์ต๋๋ค.
Node.js์์ DNS issue๊ฐ ์ข
์ข
๋ฐ์ํ๋ ์ด์ ๋ libuv threadpool์ด 4๊ฐ ์ฐ๋ ๋๋ง ์ฌ์ฉํ๋ ๊ฒ๊ณผ ๊ด๋ จ์ด ์์ต๋๋ค.
libuv์์ OS์ getaddrinfo() ํธ์ถ์ ๋๊ธฐ์ ์ผ๋ก ์ํ๋๊ณ , Node๋ DNS๋ฅผ cachingํ์ง ์์ผ๋ฉฐ, OS์๊ฒ ์์ํฉ๋๋ค.
์ด์ ๊ด๋ จ๋ ๊ธ์ ๋ค์์ ์ฐธ๊ณ ํ์๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
- https://httptoolkit.com/blog/configuring-nodejs-dns/
- https://medium.com/swlh/solving-node-dns-issues-and-other-things-5051d8526cac
์ ํฌ๊ฐ ๊ฐ๋ฐํด ์ฌ์ฉ์ค์ธ monorepo internal_api package์ ์๋์ ๊ฐ์ด cacheable-lookup
๋ฅผ ์ถ๊ฐํจ์ผ๋ก์จ, ์๋น์ค ๋ก์ง์ ๊ฑด๋ค์ง ์์ผ๋ฉด์, ์์ฝ๊ฒ ์์ ์ฑ์ ๋์ผ ์ ์์์ต๋๋ค.
์๋๋ ์ ํฌ master branch์์ ์ถ์ถํ patch ์
๋๋ค. ์๋ฒ ๋ด๋ถ์์ axios๋ฅผ ์์ฃผ ์ฌ์ฉํ์ ๋ค๋ฉด, cacheable-lookup
๋ฅผ ์ฌ์ฉํด ๋ณด์๊ธธ ์ถ์ฒํฉ๋๋ค.
Subject: [PATCH] [BETT-2457] ๋น๋ฒํ DNS ์๋ฌ ๊ฐ์ (#1541)
===================================================================
+import { Agent as HttpAgent } from 'http';
+import { Agent as HttpsAgent } from 'https';
+
import { Injectable } from '@nestjs/common';
import axios, { AxiosInstance } from 'axios';
import { ConfigService } from '@nestjs/config';
+import CacheableLookup from '@esm2cjs/cacheable-lookup';
import { createSignature } from './signature.util';
@@ -15,7 +19,15 @@
this.#INTERNAL_API_ORIGIN = this.configService.getOrThrow('internalApi.origin', { infer: true });
this.#INTERNAL_API_ORIGIN_SIGNATURE = createSignature(internalApiSecretKey, this.#INTERNAL_API_ORIGIN);
- this.#axiosInstance = axios.create();
+
+ const cacheableLookup = new CacheableLookup();
+ const httpAgent = new HttpAgent();
+ const httpsAgent = new HttpsAgent();
+
+ cacheableLookup.install(httpAgent);
+ cacheableLookup.install(httpsAgent);
+
+ this.#axiosInstance = axios.create({ httpAgent, httpsAgent });
this.#axiosInstance.interceptors.request.use(config => {
config.headers['origin'] = this.#INTERNAL_API_ORIGIN;
NestJS HttpModule์๋ ๋ค์๊ณผ ๊ฐ์ด ์ ์ฉํ๋ฉด ๋ฉ๋๋ค.
HttpModule.registerAsync({
useFactory: () => {
const cacheableLookup = new CacheableLookup();
const httpAgent = new HttpAgent();
const httpsAgent = new HttpsAgent();
cacheableLookup.install(httpAgent);
cacheableLookup.install(httpsAgent);
return {
httpAgent,
httpsAgent
};
}
}),
๊ฐ์ ์ดํ, DNS ์๋ฌ(EAI_AGAIN, ENOTFOUND)๋ฅผ ๋ณด์ง ๋ชปํ์ต๋๋ค.
2. node-cache๋ฅผ ํตํ ์์ฐจ์ service logic ์์ฑ
internal_api package๋ฅผ ์ฌ์ฉํ๋ service ์ฝ๋
// internal_api ๊ฒฐ๊ณผ ์บ์ฑ
const cacheConfig: LocalCacheConfig = { contextId: uuidv4() };
const merged = await mediaPosts.reduce(async (accPromise, mediaPost) => {
const acc = await accPromise;
const isVisible = await this.#checkVisibility(level, userId, mediaPost, cacheConfig);
const responsePost = new GetMediaPostListResponsePost(mediaPost, isVisible, language);
return [...acc, responsePost];
}, Promise.resolve([]));
Artist article์ธ mediaPosts๋ฅผ labelId๋ก ์ฝ์ด ์์ ๋, ๊ฐ mediaPost๋ฅผ ํด๋น fan์ด ๋ณผ ์ ์๋์ง ํ์ธํ๋ ๋ก์ง์
๋๋ค.
membership์ ๊ฐ์
์ ๋ฌด, message ๊ตฌ๋
์ฌ๋ถ์ ๋ฐ๋ผ ๊ฐ mediaPost์ visibility๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
๋ณต์์ mediaPosts๋ฅผ ์์ฐจ์ ์ผ๋ก, ๊ฐ mediaPost ๋ง๋ค ์ค์ ๋์ด ์๋ ํด๋น article์ ๊ณต์ ๋ฒ์(์ ์ฒด ํ์ฉ, membership ๊ฐ์
, message ๊ตฌ๋
๋ฑ)์ fan์ membership ๊ฐ์
, message ๊ตฌ๋
์ฌ๋ถ์ ๋ฐ๋ผ visibility๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
๊ทธ๋ฐ๋ฐ article์ ๊ณต์ ๋ฒ์์ fan์ membership ๊ฐ์
, message ๊ตฌ๋
์ฌ๋ถ ์กฐํฉ์ด ๋ง์ ๊ฒฝ์ฐ์ ์๊ฐ ์๋๋ผ, mediaPosts๋ฅผ ์ํํ๋ ๊ณผ์ ์์ ๋์ผํ ์์ฒญ์ ์ฌ๋ฌ ๋ฒ ํ ์ ์๊ฒ ๋ฉ๋๋ค.
์ด๋ฐ ๊ฒฝ์ฐ, ์ด๋ฒ ์๋น์ค ์์ฒญ์์ ๋ฐ์ํ๋ ๋ชจ๋ article์ ๊ณต์ ๋ฒ์์ fan์ membership ๊ฐ์
, message ๊ตฌ๋
์ฌ๋ถ ์กฐํฉ์ ๋จผ์ ์ถ์ถํ๊ณ , ์๋น์ค ์กฐ์ง์ ๋ณ์์ ์ ์ฅํด ์ฌ์ฉํด๋ ๋์ง๋ง,
์ฝ๋๋ ๋ณต์กํด์ง๊ณ , ์ฝ๋์์ ์ฌ์ฉํ๋ ๊ณณ๊ณผ ๋ฉ๋ฆฌ ๋จ์ด์ง ์ ํ์์
์ด, ์๊ฐ์ด ์ง๋ ์ฝ๋๋ฅผ ๋ณผ ๋, ์๋น์ค ๋ก์ง์ ์ดํดํ๋๋ฐ ์ด๋ ค์์ ์ค ์ ์์ต๋๋ค.
๊ทธ๋์ ์ ์ฐจ์ ์ผ๋ก ํ์ํ ์์ ์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ ์ฝ๋๋ฅผ ์์ฑํด ์ฝ๋ ์ดํด๋๋ฅผ ๋์ด๋ฉด์, ๋์ผํ ์์ฒญ์ผ๋ก ๋ฐ์ํ ์ ์๋ ์ฑ๋ฅ์์ ๋ถ์ด์ต๋ ์ ๊ฑฐํ๋ ๋ฐฉ๋ฒ์ ์ฐพ๊ฒ๋์์ต๋๋ค.
์ ํฌ๊ฐ ๊ฐ๋ฐํด ์ฌ์ฉํด ์ค๊ณ ์๋ monorepo internal_api package์ cacheConfig
option์ ์ถ๊ฐํ์์ต๋๋ค.
export type LocalCacheConfig = {
contextId: string;
ttl?: number; // milliseconds
};
ttl์ ์ค ์๋ ์๊ณ , ์์ ์๋น์ค ์ฝ๋์ฒ๋ผ contextId: uuidv4()
๋ก ์ฌ์ฉํ๋ฉด, ์๋ฒ๊ฐ ๋ฐ์ ๊ฐ ์์ฒญ๋ง๋ค ์ ํจํ cache๋ก ์ฌ์ฉํ๊ณ ,
contextId: userId
๋ก ์ฌ์ฉํ๋ฉด, ๋ค๋ฅธ ์์ฒญ์์๋ ์ด์ฉ๊ฐ๋ฅํ cache๊ฐ ๋ฉ๋๋ค.
withCache()
๋ฅผ ์ถ๊ฐ ์ ์ฉํ monorepo internal_api package
async isAvailableMembership({ userId, artiGroupId }: { userId: string; artiGroupId: string }, cacheConfig?: LocalCacheConfig): Promise<boolean> {
const cacheKeyMsg = `isAvailableMembership:${userId}:${artiGroupId}`;
const internalAxiosOperation = async () => {
const response = await this.internalAxiosService.instance
.post<MembershipInternalResponse<boolean>>(`${this.#FROMM_MEMBERSHIP_API_DOMAIN}/v2/internal/membershipUsers/available`, { artiGroupId, userId })
.catch(({ response }) => this.#throwLoggingError(response.status, response.statusText, response.data.errorType ?? 'INTERNAL_SERVER_ERROR', cacheKeyMsg));
if (!response.data.success) {
this.#throwLoggingError(HttpStatus.INTERNAL_SERVER_ERROR, response.statusText, response.data.errorType, cacheKeyMsg);
}
return response.data.data;
};
return cacheConfig ? this.withCache(cacheKeyMsg, internalAxiosOperation, cacheConfig) : internalAxiosOperation();
}
import { Injectable } from '@nestjs/common';
import NodeCache from 'node-cache';
import { LocalCacheConfig } from './types/local_cache_config.type';
@Injectable()
export abstract class CacheableService {
protected readonly localCache: NodeCache;
protected readonly DEFAULT_CACHE_TTL = 60 * 1000; // 1 minute in milliseconds
protected constructor() {
this.localCache = new NodeCache({
checkperiod: 60, // automatic delete check interval in seconds
useClones: false // true: deep copy
});
}
protected async withCache<InternalResponseType>(key: string, internalAxiosOperation: () => Promise<InternalResponseType>, cacheConfig: LocalCacheConfig): Promise<InternalResponseType> {
const cacheKey = `${cacheConfig.contextId}:${key}`;
const cachedResult = this.localCache.get<InternalResponseType>(cacheKey);
if (cachedResult !== undefined) {
return cachedResult;
}
const responseData = await internalAxiosOperation();
const ttl = cacheConfig.ttl ?? this.DEFAULT_CACHE_TTL;
this.localCache.set(cacheKey, responseData, ttl / 1000);
return responseData;
}
}
๋ง์น๋ฉฐ
npm cache package๋ฅผ ์ถ๊ฐ๋ก ๋์
ํ์ฌ, ํ๋์ ๋๋ฉ์ธ ์๋ฒ์์ ์์กดํ๊ณ ์๋ ์ธ๋ถ์ ๋๋ฉ์ธ ์ด๋ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ค ์์ ์ ์ผ๋ก ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์์ด,
์๋น์ค ๋ก์ง ๊ตฌํ์, ๊ตฌํํ๋ ค๋ feature์ ๋์ฑ ์ง์คํ ์ ์๊ฒ ๋์์ต๋๋ค.
๊ฐ์ฌํฉ๋๋ค.