legacy nodejs를 위한 ioredis wrapper

말랑배·2024년 2월 15일
0

코드쪼가리

목록 보기
3/3

현재 참여하고 있는 라이브 프로젝트는 상당히 오래된 프로젝트로
노드6.x를 사용하며 패키지 관리를 package.json으로 하지 않고,
node_modules를 통째로 git에서 관리하고 있었다.

개발에 제약이 커 노드 버전을 8.x로 올렸으나, 여전히 갈 길은 멀다.
다른 누군가 이런 상황을 겪을 일은 없겠지만,
node를 사용하며 proxy를 처음 사용 해 보기도 해서, 기록을 겸하는 목적으로 작은 코드 하나 공유한다.

global에 변수를 박아놓고 쓰는 건 상상도 못했는데 상용 서비스에서 쓰이고 있어 놀랐다.

redis.pool.js

const Redis = require('ioredis');
const instances = {};

process.on('SIGINT', () => {
  logger.log('[Redis] close all redis connection');
  Object.keys(instances).forEach(k => {
    try {
      let conn = instances[k];
      if (conn)
        conn.close();
    } catch (e) {
      logger.error(`[Redis] release connection error - `, e);
    }
  });
  process.exit(0);
});

function createProxy(redis) {
  return new Proxy(redis,
    {
      get(target, propKey, _receiver) {
        const origMethod = target[propKey];
        if (typeof origMethod === 'function') {
          return function (...args) {
            // 마지막 인자가 콜백 함수인지 확인
            let callback = args[args.length - 1];
            if (typeof callback === 'function') {
              logger.debug(`[Redis] execute : ${propKey}`, args.slice(0, -1));
              // 원래 콜백을 감싸는 새로운 콜백
              const loggingCallback = (err, result) => {
                if (err) {
                  logger.error(`[Redis] error   : ${propKey}`, err);
                } else {
                  logger.debug(`[Redis] result  : ${propKey}`, result);
                }
                // 원래 콜백 호출
                callback(err, result);
              };
              // 원래 콜백 대신 로깅 콜백 사용
              args[args.length - 1] = loggingCallback;
            } else {
              // promise 로 호출된 경우 콜백이 없다.
              // 이 커맨드 객체의 상세 정보를 모두 남기는 로그인 관계로 trace로 남긴다.
              logger.trace(`[Redis] command details: ${propKey}`, args);
            }
            return origMethod.apply(target, args);
          };
        } else {
          return origMethod;
        }
      },
    });
}

const getClient = (dbNumber) => {
  if (!instances[dbNumber]) {
    let options = loadOptions();
    let config = {
      host: options.host,
      port: options.port,
      username: options.username,
      password: options.password,
      commandTimeout: options.timeout,
      connectTimeout: 10000,
      keepAlive: 10000,
      db: dbNumber,
    }
    let redisProxy = createProxy(new Redis(config)); // 프록시를 적용하여 로깅 활성화

    redisProxy.on('connect', () => {
      logger.debug(`[Redis] client connected - ${JSON.stringify(config)}`);
    });

    redisProxy.on('error', (error) => {
      logger.error('[Redis] error - ', error);
    });

    redisProxy.on('close', () => {
      logger.debug('[Redis] connection closed');
    });

    instances[dbNumber] = redisProxy; // 프록시 인스턴스 저장
  }

  return instances[dbNumber];
}

function close() {
  Object.keys(instances).forEach(k => {
    if (instances[k])
      instances[k].quit((r) => logger.debug(`close redis[${k}] connection - `, r));
  })
}

function loadOptions() {
  return {
    host: global.CONFIG ? global.CONFIG.REDIS_STORE_HOST || '127.0.0.1' : '127.0.0.1',
    port: global.CONFIG ? global.CONFIG.REDIS_STORE_PORT || 6379 : 6379,
    username: global.CONFIG ? global.CONFIG.REDIS_STORE_USERNAME || null : null,
    password: global.CONFIG ? global.CONFIG.REDIS_STORE_PASSWORD || null : null,
    timeout: global.CONFIG ? global.CONFIG.REDIS_STORE_TIMEOUT || 3000 : 3000
  };
}

module.exports = {
  getClient: getClient,
  close: close
}

redis.client.js

const _ = require('underscore');
const ioredis = require('ioredis');
const redis = require('./redis.pool');

class Client {
    constructor(dbNumber) {
        /** @type {ioredis.RedisCommander} */
        this.client = redis.getClient(dbNumber || 0);
    }

    close() {
        if (this.client) {
            this.client.close();
        }
    }

    /**
     * 
     * @param {(err:object, instance: ioredis.RedisCommander)} callback 
     * @returns 
     */
    getInstance(callback) {
        const CODE = require('../common').CODE;
        if (!this.client) {
            return callback({ code: CODE.REDIS_ERROR, message: `[Redis] invalid connection` }, null);
        } else {
            return callback(null, this.client);
        }
    }

    command (...args) {
        const command = args[0];
        const callback = args[args.length - 1];
        if (typeof command != 'string' || typeof callback != 'function') {
            throw new Error(`[Redis] invalid usage - ${JSON.stringify(args)}`);
        } else if (!this.client) {
            const CODE = require('../common').CODE;
            return callback({ code: CODE.REDIS_ERROR, message: `[Redis] invalid connection` }, null);
        } else {
            return this.client[command].apply(this, args.slice(1));
        }
    }

    zpopmin (key, count = 1, callback) {
        this.getInstance((err, instance) => {
            if (err) return callback(err, null);
            instance.zpopmin(key, count, (err, reply) => {
                return callback(err, reply && reply.length > 0 ? _.object(_.chunk(reply, 2).map(p => [p[0], +p[1]])) : {});
            });
        });
    }
}

module.exports = Client
profile
털보 호소인

0개의 댓글