[TIL] HW5 Refactoring

김시원·2023년 4월 26일
2

TIL

목록 보기
12/50

1. Redis로 Refresh Token 저장하기

Redis로 Refresh token을 클라우드에 저장하는 시도를 해보았다.
Redis 사용법 포스팅

리팩토링 전 login API

const redisClient = redis.createClient({
  url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
  legacyMode: true,
});
redisClient.on("connect", () => {
  console.info("Redis connected!");
});
redisClient.on("error", (err) => {
  console.error("Redis Client Error", err);
});
redisClient.connect().then();
const redisCli = redisClient.v4;
await redisCli.set(refreshToken, userId);

res.cookie("accessToken", `Bearer ${accessToken}`);
res.cookie("refreshToken", `Bearer ${refreshToken}`);
  • 로그인 페이지에서 accessToken과 refreshToken이 재발급되는 형식으로 구현해보았다.
    1) 먼저 redisClient를 만들어주고 연결을 시켜준다.
    2) set(key, value) 메서드를 사용해서 refreshToken을 key로, userId를 value로 Redis에 넣어준다.
    3) 쿠키 안에 access & refresh tokens을 담아준다.

리팩토링 전 auth-middleware

const redisClient = redis.createClient({
  url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
  legacyMode: true,
});
redisClient.on("connect", () => {
  console.info("Redis connected!");
});
redisClient.on("error", (err) => {
  console.error("Redis Client Error", err);
});
redisClient.connect().then(); // redis v4 연결 (비동기)
const redisCli = redisClient.v4;

if (!isRefreshTokenValid) {
  await redisCli.del(authRefreshToken);
  return res
    .status(419)
    .json({ message: "Refresh Token이 만료되었습니다." });
}
if (!isAccessTokenValid) {
  const accessTokenId = await redisCli.get(authRefreshToken);
  if (!accessTokenId)
    return res.status(419).json({
      message: "Refresh Token의 정보가 서버에 존재하지 않습니다.",
    });
    newAccessToken = createAccessToken(accessTokenId);
    res.cookie("accessToken", `Bearer ${newAccessToken}`);
    return res.json({ message: "Access Token을 새롭게 발급하였습니다. 다시 시도하십시오." });
}
const { userId } = getAccessTokenPayload(authAccessToken);

const user = await Users.findOne({ where: { userId } });
res.locals.user = user

next();
  • auth-middleware에서 access와 refresh token이 valid한지 확인해주고, access token이 만료되었다면 재발급해주었다.
    1) 먼저 redisClient를 만들어주고 연결을 시켜준다.
    2) token validation을 진행한다. 이때 refreshToken이 이미 만료되었다면 redis에서 해당 refreshToken 키와 값을 없애준다. Refresh token을 통해 redis에서 userId 정보를 가져오고, 이를 통해 Access token이 만료되었으면 재발급해준다.
    3) Access token을 통해 실제 우리가 필요한 값인 userId payload를 받아오고 이를 res.locals.user에 담아준다.
    4) 다음 미들웨어로 넘어간다.

이렇게 만들었을 때 일단 처음 redis와 connect을 하는 코드가 겹친다. 그래서 리팩토링을 해주었다.

RedisClient Class로 리팩토링

class RedisClient {
	constructor() {
		this.redisClient = redis.createClient({
			url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
			legacyMode: true,
		});

		this.redisConnected = false;
	}

	async initialize() {
		this.redisClient.on("connect", () => {
			this.redisConnected = true;
			console.info("Redis connected!");
		});
		this.redisClient.on("error", (err) => {
			console.error("Redis Client Error", err);
		});
		if (!this.redisConnected) this.redisClient.connect().then(); // redis v4 연결 (비동기)
	}

	setRefreshToken = async (refreshToken, userId) => {
		await this.initialize();
		await this.redisClient.set(refreshToken, userId);
	};

	getRefreshToken = async (refreshToken) => {
		await this.initialize();
		const token = await this.redisClient.get(refreshToken);
		return token;
	};

	deleteRefreshToken = async (refreshToken) => {
		await this.initialize();
		await this.redisClient.del(refreshToken);
	};
}
  • Redis와 관련된 함수들을 정의한 class를 선언해주었다. 이때 로그인을 했다가 다시 로그인 API를 호출하면 "throw new Error('Socket already opened'); Error: Socket already opened error" 에러가 났는데, 이는 재로그인을 했을 때 이미 redis가 connected되었지만 이를 다시 열려고 시도하는 것 때문에 발생하는 것이었다. 그래서 constructor 내부에 this.redisConnected라는 boolean 변수를 만들고, 이 값이 false일 때만 connect를 진행하는 식으로 에러를 고칠 수 있었다.
  • login API와 auth-middleware에서는 redisClient = new RedisClient();를 통해 인스턴스를 만들었고, 기존 redis 메서드들을 클래스에서 정의한 함수들로 대체해주었다.

3-layered Architecture 적용

1) Repository Layer (실제 Redis에 접근하는 layer): RedisClient Class를 users repository로 넣어주고, 객체를 이용해서 두 class를 export 해준다. module.exports = { UserRepository, RedisClientRepository };
2) Service Layer: RedisClientRepository를 import하고 관련 함수들을 정의해준다. 데이터 가공 과정이 필요한 경우 이 layer에서 해준다.
3) Controller Layer: RedisClientService를 import하고 UserController class 내에서 인스턴스를 만들어준다. redisClient = new RedisClientService(); 인스턴스를 통해 Redis의 메서드들을 해당 layer에서 사용한다.

2. joi module로 body 데이터 validation

리팩토링 전

if문으로 validation을 해주었다.

if (typeof nickname !== "string")
			throw new Error("412/닉네임의 형식이 일치하지 않습니다.");

if (password !== confirmedPassword)
  throw new Error("412/패스워드가 일치하지 않습니다.");

if (password.length < 4 || typeof password !== "string")
  throw new Error("412/패스워드 형식이 일치하지 않습니다.");

if (password.includes(nickname.toLowerCase()))
  throw new Error("412/패스워드에 닉네임이 포함되어 있습니다.");

const nickNameRegex = new RegExp("^[a-zA-z0-9]{3,}$", "g");
if (!nickNameRegex.test(nickname))
  throw new Error("412/닉네임의 형식이 일치하지 않습니다.");

리팩토링 후: joi 사용

const { nickname, password } = await signupSchema
			.validateAsync(req.body)
			.catch((error) => {
				console.error(error);
				throw new Error(`412/${error}`);
			});
  • joi.js 파일
signupSchema: Joi.object({
  nickname: Joi.string()
  .regex(/^[a-zA-Z0-9]{3,}$/)
  .messages({
    "string.base": "닉네임의 형식이 일치하지 않습니다.",
    "string.pattern.base": "닉네임의 형식이 일치하지 않습니다.",
    "string.empty": "닉네임의 형식이 일치하지 않습니다.",
    "string.min": "닉네임의 형식이 일치하지 않습니다.",
  }),
  password: Joi.string().min(4).required().messages({
    "string.base": "패스워드의 형식이 일치하지 않습니다.",
    "string.empty": "패스워드 형식이 일치하지 않습니다.",
    "string.min": "패스워드 형식이 일치하지 않습니다.",
  }),
  confirmedPassword: Joi.string().valid(Joi.ref("password")).required().messages({
    "string.base": "패스워드가 일치하지 않습니다.",
    "any.only": "패스워드가 일치하지 않습니다.",
    "string.empty": "패스워드가 일치하지 않습니다.",
  }),
}),

0개의 댓글