Node.js

Node-cache 를 통한 API Response 개선 (feat. Events) (1)

ooin 2024. 2. 22. 18:10
반응형

 

오늘은 Node-cache 에 대해 알아보도록 하겠습니다.

목표는 데이터를 캐싱해 API의 Response 속도를 개선하는 것 입니다.

node-cache - npm (npmjs.com)

 

node-cache

Simple and fast NodeJS internal caching. Node internal in memory cache like memcached.. Latest version: 5.1.2, last published: 4 years ago. Start using node-cache in your project by running `npm i node-cache`. There are 2522 other projects in the npm regis

www.npmjs.com

먼저 Node-Cache 에 대해 알아보겠습니다.

공식 문서에서 설명하는 Node-cache는 위 내용과 같습니다. 내용을 번역해보면

간단하고 빠른 Node.js 내부 캐싱. memcached와 비슷하게 작동하는 set, get 및 delete 메서드를 갖춘 간단한 캐싱 모듈입니다. 키에는 만료 시간 (ttl)을 설정할 수 있으며, 만료되면 캐시에서 삭제됩니다. 모든 키는 단일 객체에 저장되므로 실제 제한은 약 1백만 개의 키 정도입니다.

간단하고 빠른 Node.js 내부 캐싱 모듈 정도로 이해할 수 있습니다.


주간 다운로드수가 굉장하다..!


어떻게 데이터를 캐싱하는지 코드를 좀 더 자세히 살펴보았습니다.

coffescript 로 개발된 오픈소스로 내용은 아래와 같습니다.

node-cache/_src/lib/node_cache.coffee at master · node-cache/node-cache (github.com)

일부를 살펴보면

//node_cache.coffee

clone = require( "clone" )
EventEmitter = require('events').EventEmitter

# generate superclass
module.exports = class NodeCache extends EventEmitter
	constructor: ( @options = {} )->
		super()

		@_initErrors()

		# container for cached data
		@data = {}

		# module options
		@options = Object.assign(
			# convert all elements to string
			forceString: false
			# used standard size for calculating value size
			objectValueSize: 80
			promiseValueSize: 80
			arrayValueSize: 40
			# standard time to live in seconds. 0 = infinity;
			stdTTL: 0
			# time in seconds to check all data and delete expired keys
			checkperiod: 600
			# en/disable cloning of variables. If `true` you'll get a copy of the cached variable. If `false` you'll save and get just the reference
			useClones: true
			# whether values should be deleted automatically at expiration
			deleteOnExpire: true
			# enable legacy callbacks
			enableLegacyCallbacks: false
			# max amount of keys that are being stored
			maxKeys: -1
		, @options )

		# generate functions with callbacks (legacy)
		if (@options.enableLegacyCallbacks)
			console.warn("WARNING! node-cache legacy callback support will drop in v6.x")
			[
				"get",
				"mget",
				"set",
				"del",
				"ttl",
				"getTtl",
				"keys",
				"has"
			].forEach((methodKey) =>
				# reference real function
				oldMethod = @[methodKey]
				@[methodKey] = (args..., cb) ->
					# return a callback if cb is defined and a function
					if (typeof cb is "function")
						try
							res = oldMethod(args...)
							cb(null, res)
						catch err
							cb(err)
					else
						return oldMethod(args..., cb)
					return
				return
			)

		# statistics container
		@stats =
			hits: 0
			misses: 0
			keys: 0
			ksize: 0
			vsize: 0

		# pre allocate valid keytypes array
		@validKeyTypes = ["string", "number"]

		# initalize checking period
		@_checkData()
		return

	# ## get
	#
	# get a cached key and change the stats
	#
	# **Parameters:**
	#
	# * `key` ( String | Number ): cache key
	#
	# **Example:**
	#
	#	myCache.get "myKey", ( err, val )
	#
	get: ( key )=>
		# handle invalid key types
		if (err = @_isInvalidKey( key ))?
			throw err

		# get data and increment stats
		if @data[ key ]? and @_check( key, @data[ key ] )
			@stats.hits++
			_ret = @_unwrap( @data[ key ] )
			# return data
			return _ret
		else
			# if not found return undefined
			@stats.misses++
			return undefined

좀 더 보기 편하게 자바스크립트 코드로 변경해보면

const EventEmitter = require('events').EventEmitter;
const clone = require("clone");

module.exports = class NodeCache extends EventEmitter {
    constructor(options = {}) {
        super();

        this._initErrors();

        // container for cached data
        this.data = {};

        // module options
        this.options = Object.assign({
            // convert all elements to string
            forceString: false,
            // used standard size for calculating value size
            objectValueSize: 80,
            promiseValueSize: 80,
            arrayValueSize: 40,
            // standard time to live in seconds. 0 = infinity;
            stdTTL: 0,
            // time in seconds to check all data and delete expired keys
            checkperiod: 600,
            // en/disable cloning of variables. If `true` you'll get a copy of the cached variable. If `false` you'll save and get just the reference
            useClones: true,
            // whether values should be deleted automatically at expiration
            deleteOnExpire: true,
            // enable legacy callbacks
            enableLegacyCallbacks: false,
            // max amount of keys that are being stored
            maxKeys: -1
        }, options);

        // generate functions with callbacks (legacy)
        if (this.options.enableLegacyCallbacks) {
            console.warn("WARNING! node-cache legacy callback support will drop in v6.x");
            ["get", "mget", "set", "del", "ttl", "getTtl", "keys", "has"].forEach(methodKey => {
                // reference real function
                const oldMethod = this[methodKey];
                this[methodKey] = (...args) => {
                    // return a callback if cb is defined and a function
                    const cb = args[args.length - 1];
                    if (typeof cb === "function") {
                        try {
                            const res = oldMethod(...args);
                            cb(null, res);
                        } catch (err) {
                            cb(err);
                        }
                    } else {
                        return oldMethod(...args);
                    }
                };
            });
        }

        // statistics container
        this.stats = {
            hits: 0,
            misses: 0,
            keys: 0,
            ksize: 0,
            vsize: 0
        };

        // pre allocate valid keytypes array
        this.validKeyTypes = ["string", "number"];

        // initalize checking period
        this._checkData();
    }

    // get
    // get a cached key and change the stats
    //
    // Parameters:
    //
    // * `key` ( String | Number ): cache key
    //
    // Example:
    //
    // myCache.get("myKey", (err, val) => { ... });
    //
    get(key) {
        // handle invalid key types
        const err = this._isInvalidKey(key);
        if (err) {
            throw err;
        }

        // get data and increment stats
        if (this.data[key] && this._check(key, this.data[key])) {
            this.stats.hits++;
            const _ret = this._unwrap(this.data[key]);
            // return data
            return _ret;
        } else {
            // if not found return undefined
            this.stats.misses++;
            return undefined;
        }
    }
}

// ChatGPT가 변경해주었습니다. ChatGPT 짱짱

코드를 살펴보면

  1. NodeCache Class 는 Node.js EventEmitter 을 상속 받는 것을 알 수 있습니다.
  2. get 메소드의 경우 key 를 받아 this._unwrap 메서드로 data 에서 키를 가져옵니다.
    1. data 는 {} 로 선언되어 있고, _unwrap의 로직은 github에서 확인
  3. Emit을 통해 이벤트 발생

그렇다면 Events Module에 대해서도 알아야 할 것 같습니다.

Events Module

은 이벤트 기반 프로그래밍을 지원하는 핵심 모듈 중 하나입니다.

Events 모듈은 EvnetsEmitter 클래스를 통해 이벤트 처리를 구현합니다.

// Events 모듈을 가져옵니다
const EventEmitter = require('events');

// 이벤트 발생자 객체를 생성
const myEmitter = new EventEmitter();

// 이벤트 리스너를 등록
myEmitter.on('event', () => {
  console.log('이벤트가 발생했습니다!');
});

// 이벤트를 발생
myEmitter.emit('event');

//이벤트가 발생하여 등록되어 있던 콜백함수가 실행
//이벤트가 발생했습니다!

Events 모듈을 사용하면 비동기적으로 발생하는 이벤트를 강력하게 관리하고 처리할 수 있습니다.

Node-Cache를 활용해 데이터 캐싱처리를 진행해 보려고 했지만 소스코드를 살펴보다가 글이 길어졌네요. 다음 글에서 Node-Cache를 활용해 데이터 캐싱을 해보겠습니다. :)

반응형