์œ ์šฉํ•œ npm cache package ์†Œ๊ฐœ ๋ฐ ์ ์šฉ

youngki
  • #cacheable-lookup
  • #node-cache
  • #microservice
  • #fromm

๋“ค์–ด๊ฐ€๋ฉฐ

์ €๋Š” ์ด์ „์— ์ €ํฌ๊ฐ€ ์‚ฌ์šฉ์ค‘์ธ ๋ฐฑ์—”๋“œ์—์„œ ์„œ๋ฒ„๊ฐ„์— ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœ๋“œ๋ฆฐ ์  ์žˆ์Šต๋‹ˆ๋‹ค.
์ด ๋ชจ๋“ˆ์€ monorepo ์•ˆ์—์„œ ๋…๋ฆฝ package๋กœ ๋ถ„๋ฆฌ๋˜์–ด ๋”์šฑ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
์ด internal_api package์— ์ถ”๊ฐ€๋œ ๊ธฐ๋Šฅ์„ ์†Œ๊ฐœํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก ์„ ๋จผ์ € ๋ง์”€๋“œ๋ฆฌ๋ฉด, cache์— ๊ด€ํ•œ 2๊ฐ€์ง€ npm package๋ฅผ ์†Œ๊ฐœํ•˜๋Š” ๊ธ€์ด ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

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์—๊ฒŒ ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.
์ด์™€ ๊ด€๋ จ๋œ ๊ธ€์€ ๋‹ค์Œ์„ ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ €ํฌ๊ฐ€ ๊ฐœ๋ฐœํ•ด ์‚ฌ์šฉ์ค‘์ธ 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์— ๋”์šฑ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ