블로그 이전

지난번 개발한 세션의 구조적인 변경을 진행했다.
이유는 현재 사내 프레임워크는 config.js라는 object에 필요한 정보들(DB host, port 등 과 같은 정보들)을 필드로 가지게 하고, 필요한 곳에서 모듈로 가져와 참조하는 방식을 사용했다.

그 중 Redis client와 관련된 정보도 있었다.

문제는 여기서 발생한다.

나는 config.js에 어떤 세션 스토어를 사용할지 명시할 계획이었으므로 개발자가 레디스 스토어를 사용하게 된다면 config.js

const config = {
 	redis: {
   		socket: {
        	host: 127.0.0.1,
        	port: 6379
        }
    },
	sessionStore: RedisSessionStore
}

와 같이 적히게 되고 RedisSessionStore.js

constructor() {
  	super();
  	
  	this.#client = redis.createClient(config.redis);
}

와 같은 형태로 client를 생성하기 때문에 순환참조 문제가 발생한다.
이 외에도 많은 곳에서 config를 참조하게돼 구조적으로 좋지 않다고 생각했다.

일단 config.js 자체가 썩 좋아보이진 않았지만, 이미 만들어진 구조는 어쩔수 없다 생각하고 앞으로 추가할 기능에 대해선 조금 나은 구조를 갖는게 좋다 생각하여 당장 개발중인 세션에 관한 구조를 수정하였다.

config.js에 설정정보들을 적어두되 최대한 정보들을 주입받는 형식으로 진행하고자 했고 그러기 위해 세션 저장소를 생성, 주입 역할을 담당하는 객체를 만들어주기로 했다.

먼저 레디스 세션 저장소에 설정정보를 외부에서 주입받도록 수정이 필요하다.

RedisSessionStore

/**
 * @param {Object} option
 */
constructor(option) {
	super();

  	if (!RedisSessionStore.#instance) {
		RedisSessionStore.#instance = this;

		this.#client = redis.createClient(option);

		this.#client.on("connect", () => {
			console.log("Redis Client Connected!")
		});

		this.#client.on("error", (err) => {
			console.error("RedisSessionFactory Client Connect Error." + err)
		});

		this.#client.connect().then();
	}

	return RedisSessionStore.#instance;
}

이제 생성자를 통해 기본 설정 정보를 가져올 수 있다.

SessionStoreRegister


class SessionStoreRegister {

    /**
     * @type {SessionStore}
     */
    #sessionStore;

    constructor() {
        this.#sessionStore = new MemorySessionStore();
    }

    /**
     * @param {SessionStore} store
     * @return {SessionStoreRegister}
     */
    setStore = (store) => {
        if (!(store instanceof SessionStore)) throw new Error("Must use the SessionStore.");
        this.#sessionStore = store;

        return this;
    }

    /**
     * @return {SessionStore}
     */
    getStore = () => this.#sessionStore;

    /**
     * @param {number} expireTime
     * @return {SessionFactory}
     */
    setExpireTime = (expireTime) => {
        SessionStore.expireTime = expireTime;

        return this;
    }

}

일단 기본적으로 이 객체가 생성되면 MemorySessionStore를 생성해 저장소로 설정해둔다.

setStore()

setStore()를 통해 저장소가 주입되면, 먼저 SessionStore를 상속받아 생성된 인스턴스 변수인지 확인 후 세션 저장소를 변경해준다.

반환을 자기 자신으로 해준 이유는 연속적으로 함수를 호출해 추가적인 설정을 해줄 수 있게 하기 위함이다.

setExpireTime()

세션 저장소의 만료시간을 설정하는 함수이다.
기본적인 만료시간은 30분으로 SessionStore에 클래스 변수로 정의해뒀다.
기존엔 만료시간도 config.js에 설정하도록 하여 SessionStore가 참조하도록 하였지만, 이럴경우 순환참조 문제가 발생활 확률이 다분하고, 애플리케이션 로드 시점에서 config.js가 정의되지 않았을 때 참조해버리면 undefined가 참조될 확률이 다분하므로 주입받는 방식을 선택했다.

SessionStoreRegister객체는 config.js에 개발자가 미리 정해둔 함수를 재정의 해주면 해당 함수를 호출해 저장소를 주입받도록 하였다.

config.js

const config = {
	redis: {
    	socket: {
        	host: "172.31.96.1",
        	port: 6379,
    	}
	},
	/**
 	 * @param {SessionStoreRegister} register
 	 * @return {SessionStoreRegister}
 	 */
	setSessionStore: function (register) {
		register.setStore(new RedisSessionStore(this.redis)
    		.setExpireTime(10)

		return register;
	}
}

setSessionStore()함수를 위와 같이 정의해주면 애플리케이션 로드 시점에서 저장소를 해당 함수를 호출하여 SessionStoreRegister 인스턴스 변수를 인자값으로 넣어 해당 변수에 설정 정보들을 주입 받는다.

이렇게 설정이 주입된 변수는 기존의 SessionFactory에게 설정이 추가된 세션 저장소를 전달해주면 된다.

SessionFactory

/**
 * @param {SessionStoreRegister} register
 */
constructor(register) {
    this.#sessionStore = register.getStore();
}

이렇게 하면 config.js 순환참조를 모두 걷어내고

이렇게 config.js에서 정의된 함수를 호출하고, SessionFactory에게 세션 저장소를 넘겨주기 까지의 흐름은 ConfigLoader라는 객체에서 제어하고자 했다.

ConfigLoader


class ConfigLoader {
  
  	sessionFactory;

    /**
     * @param {Express} app
     */
    loadConfig = (app) => {
        this.#loadSessionStore(app);
    }

    #loadSessionStore = (app) => {
        let storeRegister = new SessionStoreRegister();
        if (config.setSessionStore) storeRegister = config.setSessionStore(storeRegister);

		this.sessionFactory = new SessionFactory(storeRegister);

		app.use(sessionInjection);
    }
    
    sessionInjection = (req, res, next) => {
      
      	/**
         * @param {Request} req
      	 * @param {Response} res
      	 */
      	addGetSessionMethod = (req, res) => {
			/**
          	 * @param { boolean } [status=true]
          	 * @return {HttpSession}
          	 */
          	req.getSession = async (status = true) => {

              	let cookieSessionKey;
              	if (req.cookies) cookieSessionKey = req.cookies[config.sessionKey];

              	return await sessionFactory.getSession(cookieSessionKey, res, status);
          	}
      	}

      	/**
      	 * @param {Request} req
      	 * @param {Response} res
      	 */
      	addRemoveSessionMethod = (req, res) => {

          	req.removeSession = async () => {

              	let cookieSessionKey;
              	if (req.cookies) cookieSessionKey = req.cookies[config.sessionKey];

              	if (cookieSessionKey) await sessionFactory.removeSession(cookieSessionKey);
              	res.clearCookie(config.sessionKey);
          	}
      	}

      	addGetSessionMethod(req, res);
      	addRemoveSessionMethod(req, res);

      	next();
  	}

}

이렇게 config.js에 정의한 함수를 호출하고 세션 저장소 주입, 요청시 Request에 session과 관련된 함수 주입까지 모든걸 마쳤다.

사실 sessionInjection()처럼 지저분한 방식으로 구현하지 않고 다음 포스팅에 소개할 Interceptor라는 기능을 개발해 멋지게 해결했지만, 점진적으로 발전하는 과정을 기록하고자 만든 임시적인 방안이다.

이제 애플리케이션 로드 시점에서 ConfigLoader를 생성해 loadConfig()만 호출하면 모든게 완료이다.

ConfigLoader 호출

const configLoader = new ConfigLoader();

configLoader.loadConfig(app);

엄청나게 간단해졌다.
이제 필요한 설정 정보들은 ConfigLoader 객체에만 정의해주면 자동으로 불러오게 된다.

이번 작업을 하면서 확실히 객체지향의 장점을 많이 느꼈다. 현재 ConfigLoader는 개발자가 config.jssetSessionStore()를 정의해뒀는지 모른다. 그렇기 때문에 어쩔수 없이 config.js를 참조해 함수가 정의됐는지 체크 후 호출하게 했다.

내가 궁극적으로 하고자 한것은 개발자가 아무 설정을 하지 않고도 기본적인 설정 정보를 불러오는 객체를 만들어두고 이용자는 추가적인 설정 정보를 명시하는 추상 객체 상속받아 개발하면, 애플리케이션이 로드되면서 해당 객체를 호출해 설정을 불러오기만 하면 되는 그림을 원했다.

이때 애플리케이션은 개발자가 함수를 재정의 안했다 하더라도 부모 클래스의 함수를 호출하면 되기 때문에 전혀 문제가 되지 않게된다.

어쩔 수 없는 상황이기 때문에 최대한 흉내는 내봤지만 완전 만족하지는 못한것 같다.

그리고 확실히 개발자가 개발 시점에서 복잡하고 힘들면 확실히 이용자는 편하게 이용하게 되는거 같다.

이런 구조 설계 자체가 머리 아프고 복잡했지만 이 기능을 추가한 프레임워크에서 개발을 하니 개발시점에선 단순히 세션 저장소 생성, 설정정보에 추가 만 해줘도 금방금방 적용이 됐기 때문에 다른 회사 직원분들도 앞으로 이용하는 시점에선 편하게 사용할수 있다 믿는다.

profile
이용자가 아닌 개발자가 되자!

0개의 댓글