Skip to content

Commit

Permalink
Merge pull request #100 from jjikky/refactor/word-performance
Browse files Browse the repository at this point in the history
Refactor/word performance
  • Loading branch information
jjikky authored Oct 30, 2024
2 parents b6696f9 + ee6db68 commit 2c25060
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 8 deletions.
1 change: 1 addition & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const conf = require('./src/common/config/index');
const expressLoader = require('./src/common/modules/express');
const initDB = require('./src/common/modules/mongodb');
const { START_MESSAGE } = require('./src/common/constants/express');
require('./src/common/modules/cron/sync-redis-to-db');

const app = express();

Expand Down
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"moment": "^2.30.1",
"mongoose": "^8.4.3",
"morgan": "^1.10.0",
"node-cron": "^3.0.3",
"nodemon": "^3.1.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
Expand Down
38 changes: 38 additions & 0 deletions src/common/modules/cron/sync-redis-to-db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const cron = require('node-cron');
const redisClient = require('../redis');
const mongoose = require('mongoose');
const Word = require('../../../routes/word/word.model');

// NOTE : 매 정각마다 Redis 조회수를 DB에 반영
cron.schedule('0 * * * *', async () => {
try {
const cachedWordsRaw = await redisClient.sendCommand(['ZRANGE', 'popular_words', '0', '-1', 'WITHSCORES']);

const updates = [];
for (let i = 0; i < cachedWordsRaw.length; i += 2) {
const word = cachedWordsRaw[i];
const redisFreq = Number(cachedWordsRaw[i + 1]);

// NOTE : word 스키마의 조회수 증가 미들웨어를 우회하기 위해 Mongoose 대신 MongoDB 드라이버를 사용
const dbWord = await mongoose.connection.collection('words').findOne({ word }, { projection: { freq: 1 } });
const dbFreq = dbWord ? dbWord.freq : 0;

if (redisFreq !== dbFreq) {
updates.push({
updateOne: {
filter: { word },
update: { freq: redisFreq },
},
});
}
}
if (updates.length > 0) {
await Word.bulkWrite(updates);
console.log('✅ Redis 조회수를 DB에 성공적으로 동기화했습니다.');
} else {
console.log('ℹ️ 이미 동기화된 조회수입니다.');
}
} catch (error) {
console.error('❌ Redis와 DB 동기화 중 오류 발생:', error);
}
});
6 changes: 4 additions & 2 deletions src/common/modules/express/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ const addNonce = require('../../../middlewares/add-nonce');
const checkBlockedIp = require('../../../middlewares/check-blocked-ip');
const setContentSecurityPolicy = require('../../../middlewares/set-content-security-policy');
const setupCors = require('../../../middlewares/setup-cors');
const latencyLogger = require('../../../middlewares/latency-logger');

module.exports = expressLoader = (app) => {
module.exports = expressLoader = async (app) => {
passportConfig();

app.use(morgan('dev'));
app.use(helmet());

Expand All @@ -29,8 +31,8 @@ module.exports = expressLoader = (app) => {
app.use(cookieParser());

app.use(checkBlockedIp);

app.use(commonLimiter);
app.use(latencyLogger);
app.use(router);
app.use('/docs', swaggerUi.serve, swaggerUi.setup(specs, { explorer: true }));

Expand Down
10 changes: 10 additions & 0 deletions src/middlewares/latency-logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = (req, res, next) => {
const startTime = Date.now();

res.on('finish', () => {
const latency = Date.now() - startTime;
console.log(`API Latency for ${req.method} ${req.originalUrl}: ${latency}ms\n`);
});

next();
};
2 changes: 0 additions & 2 deletions src/routes/user/user.controller.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
const passport = require('passport');
const jwt = require('jsonwebtoken');
const config = require('../../common/config');
const redisClient = require('../../common/modules/redis');

const userService = require('./user.service');
const wordService = require('../word/word.service');
const sendResponse = require('../../common/utils/response-handler');
const ErrorMessage = require('../../common/constants/error-message');
const SuccessMessage = require('../../common/constants/success-message');
Expand Down
11 changes: 11 additions & 0 deletions src/routes/user/user.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ const userSchema = new mongoose.Schema(
{ timestamps: true }
);

userSchema.pre(/^find|update|save|remove|delete|count/, function (next) {
this._startTime = Date.now();
next();
});

userSchema.post(/^find|update|save|remove|delete|count/, function (result, next) {
const latency = Date.now() - this._startTime;
console.log(`[${this.mongooseCollection?.modelName}] ${this.op} query - ${latency}ms`);
next();
});

userSchema.pre('save', async function (next) {
try {
if ((this.isNew && !this.provider) || this.isModified('password')) {
Expand Down
24 changes: 22 additions & 2 deletions src/routes/word/word.model.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const mongoose = require('mongoose');
const redisClient = require('../../common/modules/redis');

const wordSchema = new mongoose.Schema(
{
Expand All @@ -12,8 +13,27 @@ const wordSchema = new mongoose.Schema(
{ timestamps: true }
);

wordSchema.pre(/^findOne/, async function (next) {
await this.model.updateOne(this.getQuery(), { $inc: { freq: 1 } });
wordSchema.index({ word: 1 });

wordSchema.post(/^findOne/, async function (doc) {
const word = typeof this.getQuery().word === 'string' ? this.getQuery().word : doc?.word;
if (!word) {
console.error('❌ Error: No valid word found for Redis update');
return;
}

await redisClient.sendCommand(['ZINCRBY', 'popular_words', '1', word]);
await redisClient.expire('popular_words', 7200);
});

wordSchema.pre(/^find|update|save|remove|delete|count/, function (next) {
this._startTime = Date.now();
next();
});

wordSchema.post(/^find|update|save|remove|delete|count/, function (result, next) {
const latency = Date.now() - this._startTime;
console.log(`[${this.mongooseCollection.modelName}] ${this.op} query - ${latency}ms`);
next();
});

Expand Down
20 changes: 18 additions & 2 deletions src/routes/word/word.repository.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
const Word = require('./word.model');
const redisClient = require('../../common/modules/redis');

exports.getSearchWords = async (searchTerm) => {
return await Word.findOne({ word: { $regex: `^${searchTerm}$`, $options: 'i' } });
};

exports.getRankWords = async () => {
const words = await Word.find().sort({ freq: -1 }).limit(10);
return words.map((word) => word.word);
const words = await redisClient.sendCommand(['ZREVRANGE', 'popular_words', '0', '9']);

if (words && words.length > 0) {
return words;
}

const dbWords = await Word.find().sort({ freq: -1 }).limit(10).select('word freq').lean();
const wordList = dbWords.map((word) => word.word);

await redisClient.sendCommand([
'ZADD',
'popular_words',
...dbWords.flatMap((word) => [String(word.freq), word.word]),
]);
await redisClient.expire('popular_words', 7200);

return wordList;
};

exports.getRelatedWords = async (searchTerm, limit) => {
Expand Down

0 comments on commit 2c25060

Please sign in to comment.