Skip to content
Snippets Groups Projects
Commit 93d36c2b authored by 세현 임's avatar 세현 임
Browse files

[#23] 스케줄 로직 리팩토링

parents b00d1dc8 ab89042f
No related branches found
No related tags found
2 merge requests!42[#25] 배포코드 master브랜치로 이동,!36[#23] 스케줄 로직 리팩토링
// controllers/scheduleController.js
const ScheduleService = require('../services/scheduleService');
const ScheduleRequestDTO = require('../dtos/ScheduleRequestDTO');
const performanceMonitor = require('../utils/performanceMonitor');
class scheduleController {
/**
......@@ -21,20 +22,20 @@ class scheduleController {
*/
async createSchedule(req, res) {
try {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('create');
return await performanceMonitor.measureAsync('createSchedule', async () => {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('create');
const schedule = await ScheduleService.createSchedules({
userId,
...validatedData
});
const schedule = await ScheduleService.createSchedules({
userId,
...validatedData
});
return res.status(201).json({
success: true,
data: {
schedule
}
return res.status(201).json({
success: true,
data: { schedule }
});
});
} catch (error) {
return res.status(400).json({
......@@ -61,17 +62,17 @@ class scheduleController {
*/
async updateSchedules(req, res) {
try {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('bulk_update');
return await performanceMonitor.measureAsync('updateSchedules', async () => {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('bulk_update');
const updatedSchedule = await ScheduleService.updateSchedules(userId, validatedData);
return res.status(200).json({
success: true,
data: {
schedule: updatedSchedule
}
const updatedSchedule = await ScheduleService.updateSchedules(userId, validatedData);
return res.status(200).json({
success: true,
data: { schedule: updatedSchedule }
});
});
} catch (error) {
if (error.message === 'Schedule not found') {
......@@ -104,17 +105,20 @@ class scheduleController {
*/
async deleteSchedules(req, res) {
try {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('bulk_delete');
const result = await ScheduleService.deleteSchedules(userId, validatedData.title);
return await performanceMonitor.measureAsync('deleteSchedules', async () => {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('bulk_delete');
return res.status(200).json({
success: true,
data: {
message: 'Schedule successfully deleted',
deletedCount: result.deletedCount
}
const result = await ScheduleService.deleteSchedules(userId, validatedData.title);
return res.status(200).json({
success: true,
data: {
message: 'Schedule successfully deleted',
deletedCount: result.deletedCount
}
});
});
} catch (error) {
return res.status(404).json({
......@@ -132,14 +136,14 @@ class scheduleController {
*/
async getAllSchedules(req, res) {
try {
const userId = req.user.id;
const schedules = await ScheduleService.getAllSchedules(userId);
return await performanceMonitor.measureAsync('getAllSchedules', async () => {
const userId = req.user.id;
const schedules = await ScheduleService.getAllSchedules(userId);
return res.status(200).json({
success: true,
data: {
schedules
}
return res.status(200).json({
success: true,
data: { schedules }
});
});
} catch (error) {
return res.status(500).json({
......@@ -159,18 +163,18 @@ class scheduleController {
*/
async getScheduleByTimeIdx(req, res) {
try {
const { time_idx } = req.params;
const userId = req.user.id;
return await performanceMonitor.measureAsync('getScheduleByTimeIdx', async () => {
const { time_idx } = req.params;
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO({ time_idx: parseInt(time_idx, 10) });
const validatedData = scheduleRequestDTO.validate('get_by_time_idx');
const scheduleRequestDTO = new ScheduleRequestDTO({ time_idx: parseInt(time_idx, 10) });
const validatedData = scheduleRequestDTO.validate('get_by_time_idx');
const schedule = await ScheduleService.getScheduleByTimeIdx(userId, validatedData.time_idx);
const schedule = await ScheduleService.getScheduleByTimeIdx(userId, validatedData.time_idx);
return res.status(200).json({
success: true,
data: {
schedule
}
return res.status(200).json({
success: true,
data: { schedule }
});
});
} catch (error) {
if (error.message === 'Schedule not found') {
......
// routes/performanceRoute.js
const express = require('express');
const router = express.Router();
const performanceMonitor = require('../utils/performanceMonitor');
router.get('/stats', (req, res) => {
const stats = performanceMonitor.getAllStats();
res.json({
success: true,
data: { stats }
});
});
module.exports = router;
// services/performance.test.js
require('dotenv').config();
const { Op } = require('sequelize');
const ScheduleService = require('./scheduleService');
const sequelize = require('../config/sequelize');
const Schedule = require('../models/schedule');
class PerformanceTester {
constructor() {
this.testUserIds = [1, 2, 3, 4, 5]; // 5명의 테스트 유저만 사용
this.results = {
operations: {
createSchedules: [],
getAllSchedules: [],
updateSchedules: [],
deleteSchedules: []
},
summary: {}
};
}
async setup() {
try {
await sequelize.authenticate();
console.log('Database connection established successfully.');
await Schedule.destroy({ where: {}, force: true });
console.log('Test data cleaned successfully.');
console.log('Using existing user IDs:', this.testUserIds);
} catch (error) {
console.error('Setup failed:', error);
throw error;
}
}
async runLoadTest() {
console.log('Starting simplified test...');
const testSchedules = this.testUserIds.map((userId, i) => ({
userId,
title: `Test Schedule ${i}`,
is_fixed: true,
time_indices: [i * 2, i * 2 + 1]
}));
console.log('Test schedules:', testSchedules);
const transaction = await sequelize.transaction();
try {
// Create 테스트
console.log('\nTesting createSchedules...');
const createdSchedules = [];
for (const schedule of testSchedules) {
const result = await this.measureOperation('createSchedules', async () => {
const created = await ScheduleService.createSchedules(schedule, transaction);
console.log(`Created schedule for user ${schedule.userId}`);
return created;
});
if (result) createdSchedules.push(result);
}
await transaction.commit();
// 생성된 스케줄 확인
const verifySchedules = await Schedule.findAll({
where: {
user_id: { [Op.in]: this.testUserIds }
},
raw: true
});
console.log('\nVerified schedules:', verifySchedules);
// GetAll 테스트
console.log('\nTesting getAllSchedules...');
for (const userId of this.testUserIds) {
await this.measureOperation('getAllSchedules', async () => {
return await ScheduleService.getAllSchedules(userId);
});
}
// Update 테스트
console.log('\nTesting updateSchedules...');
for (const schedule of createdSchedules) {
await this.measureOperation('updateSchedules', async () => {
return await ScheduleService.updateSchedules(schedule.user_id, {
originalTitle: schedule.title,
title: `Updated ${schedule.title}`,
is_fixed: schedule.is_fixed,
time_indices: schedule.time_indices
});
});
}
// Delete 테스트
console.log('\nTesting deleteSchedules...');
const deleteTransaction = await sequelize.transaction();
try {
for (const schedule of createdSchedules) {
await this.measureOperation('deleteSchedules', async () => {
return await ScheduleService.deleteSchedules(
schedule.user_id,
`Updated ${schedule.title}`,
deleteTransaction
);
});
}
await deleteTransaction.commit();
} catch (error) {
await deleteTransaction.rollback();
throw error;
}
} catch (error) {
await transaction.rollback();
throw error;
}
this.analyzePerfResults();
}
async measureOperation(name, operation) {
const start = process.hrtime.bigint();
try {
const result = await operation();
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000;
this.results.operations[name].push({ success: true, duration });
return result;
} catch (error) {
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000;
this.results.operations[name].push({
success: false,
duration,
error: error.message
});
console.error(`Error in ${name}:`, error.message);
return null;
}
}
analyzePerfResults() {
Object.entries(this.results.operations).forEach(([operation, results]) => {
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
if (successful.length > 0) {
const durations = successful.map(r => r.duration);
this.results.summary[operation] = {
totalRequests: results.length,
successCount: successful.length,
failCount: failed.length,
avgDuration: durations.reduce((a, b) => a + b, 0) / successful.length,
minDuration: Math.min(...durations),
maxDuration: Math.max(...durations),
p95: this.calculatePercentile(durations, 95),
p99: this.calculatePercentile(durations, 99)
};
}
});
this.printResults();
}
calculatePercentile(array, percentile) {
const sorted = array.sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index];
}
printResults() {
console.log('\n=== Performance Test Results ===');
Object.entries(this.results.summary).forEach(([operation, stats]) => {
console.log(`\n${operation}:`);
console.log(`Total Requests: ${stats.totalRequests}`);
console.log(`Success Rate: ${((stats.successCount / stats.totalRequests) * 100).toFixed(2)}%`);
console.log(`Average Duration: ${stats.avgDuration.toFixed(2)}ms`);
console.log(`Min Duration: ${stats.minDuration.toFixed(2)}ms`);
console.log(`Max Duration: ${stats.maxDuration.toFixed(2)}ms`);
console.log(`95th Percentile: ${stats.p95.toFixed(2)}ms`);
console.log(`99th Percentile: ${stats.p99.toFixed(2)}ms`);
});
}
async cleanup() {
try {
await Schedule.destroy({ where: {}, force: true });
console.log('Cleanup completed successfully.');
} catch (error) {
console.error('Cleanup failed:', error);
}
}
}
async function runTests() {
const tester = new PerformanceTester();
try {
await tester.setup();
console.log('Starting performance tests...');
await tester.runLoadTest();
} catch (error) {
console.error('Test failed:', error);
} finally {
await sequelize.close();
}
}
runTests();
\ No newline at end of file
......@@ -10,34 +10,46 @@ class ScheduleService {
* @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가
*/
async createSchedules({ userId, title, is_fixed, time_indices }, transaction = null) {
// 중복 검사
for (const time_idx of time_indices) {
const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction);
if (overlap) {
throw new Error(`Schedule overlaps at time_idx ${time_idx}`);
}
const overlaps = await Schedule.findAll({
where: {
user_id: userId,
time_idx: {
[Op.in]: time_indices
}
},
transaction
});
if (overlaps.length > 0) {
throw new Error(`Schedule overlaps at time_idx ${overlaps[0].time_idx}`);
}
const createdSchedules = await Promise.all(
time_indices.map(time_idx =>
Schedule.create({
user_id: userId,
title,
time_idx,
is_fixed
}, { transaction })
)
);
return {
id: createdSchedules[0].id,
const scheduleData = time_indices.map(time_idx => ({
user_id: userId,
title,
is_fixed,
time_indices,
createdAt: createdSchedules[0].createdAt,
updatedAt: createdSchedules[0].updatedAt
};
time_idx,
is_fixed
}));
try {
const createdSchedules = await Schedule.bulkCreate(scheduleData, {
transaction,
returning: true,
validate: true
});
return {
id: createdSchedules[0].id,
user_id: userId,
title,
is_fixed,
time_indices,
createdAt: createdSchedules[0].createdAt,
updatedAt: createdSchedules[0].updatedAt
};
} catch (error) {
throw new Error(`Failed to bulk create schedules: ${error.message}`);
}
}
async getAllSchedules(userId) {
......@@ -55,80 +67,100 @@ class ScheduleService {
async updateSchedules(userId, updates, transaction = null) {
const { originalTitle, title, is_fixed, time_indices } = updates;
// 기존 스케줄 조회
const existingSchedules = await Schedule.findAll({
where: {
user_id: userId,
title: originalTitle
},
transaction
});
if (existingSchedules.length === 0) {
throw new Error('Schedule not found');
}
const existingTimeIndices = existingSchedules.map(s => s.time_idx); // 기존 시간대
const toDelete = existingTimeIndices.filter(idx => !time_indices.includes(idx)); // 삭제할 시간대
const toAdd = time_indices.filter(idx => !existingTimeIndices.includes(idx)); // 추가할 시간대
const t = transaction || await sequelize.transaction();
try {
// 삭제
if (toDelete.length > 0) {
await Schedule.destroy({
// 기존 스케줄 조회
const [existingSchedule, existingSchedules] = await Promise.all([
Schedule.findOne({
where: {
user_id: userId,
title: originalTitle,
time_idx: {
[Op.in]: toDelete
}
title: originalTitle
},
transaction: t
});
}
// 제목, 고정/유동 업데이트
await Schedule.update(
{
title,
is_fixed
},
{
}),
Schedule.findAll({
attributes: ['time_idx'],
where: {
user_id: userId,
title: originalTitle
},
transaction: t
}
})
]);
if (!existingSchedule) {
throw new Error('Schedule not found');
}
const existingTimeIndices = existingSchedules.map(s => s.time_idx);
const toDelete = existingTimeIndices.filter(idx => !time_indices.includes(idx));
const toAdd = time_indices.filter(idx => !existingTimeIndices.includes(idx));
// 벌크 연산
const operations = [];
// 삭제 연산
if (toDelete.length > 0) {
operations.push(
Schedule.destroy({
where: {
user_id: userId,
title: originalTitle,
time_idx: {
[Op.in]: toDelete
}
},
transaction: t
})
);
}
// 업데이트 연산
operations.push(
Schedule.update(
{ title, is_fixed },
{
where: {
user_id: userId,
title: originalTitle
},
transaction: t
}
)
);
// 새로운 time_indices 추가
// 생성 연산
if (toAdd.length > 0) {
await Promise.all(
toAdd.map(time_idx =>
Schedule.create({
operations.push(
Schedule.bulkCreate(
toAdd.map(time_idx => ({
user_id: userId,
title,
time_idx,
is_fixed
}, { transaction: t })
})),
{
transaction: t,
validate: true
}
)
);
}
await Promise.all(operations); // 병렬 처리
if (!transaction) {
await t.commit();
}
return {
id: existingSchedules[0].id,
id: existingSchedule.id,
user_id: userId,
title,
is_fixed,
time_indices,
createdAt: existingSchedules[0].createdAt,
createdAt: existingSchedule.createdAt,
updatedAt: new Date()
};
......@@ -157,25 +189,19 @@ class ScheduleService {
*/
async getScheduleByTimeIdx(userId, time_idx) {
// 해당 time_idx의 스케줄 찾기
const schedule = await Schedule.findOne({
where: { user_id: userId, time_idx }
});
if (!schedule) {
throw new Error('Schedule not found');
}
// 같은 제목의 모든 스케줄 찾기
const relatedSchedules = await Schedule.findAll({
const schedules = await Schedule.findAll({
where: {
user_id: userId,
title: schedule.title,
is_fixed: schedule.is_fixed
title: {
[Op.in]: sequelize.literal(
`(SELECT title FROM Schedules WHERE user_id = ${userId} AND time_idx = ${time_idx})`
)
}
},
order: [['time_idx', 'ASC']]
});
return ScheduleResponseDTO.groupSchedules(relatedSchedules)[0];
return ScheduleResponseDTO.groupSchedules(schedules)[0];
}
async getAllSchedules(userId) {
......
// utils/performanceMonitor.js
const { performance, PerformanceObserver } = require('perf_hooks');
class PerformanceMonitor {
constructor() {
this.measurements = new Map();
// 성능 관찰자 설정
this.observer = new PerformanceObserver((items) => {
items.getEntries().forEach((entry) => {
const measurements = this.measurements.get(entry.name) || [];
measurements.push(entry.duration);
this.measurements.set(entry.name, measurements);
console.log(`Performance Measurement - ${entry.name}: ${entry.duration}ms`);
});
});
this.observer.observe({ entryTypes: ['measure'] });
}
async measureAsync(name, fn) {
const start = performance.now();
try {
return await fn();
} finally {
const duration = performance.now() - start;
performance.measure(name, {
start,
duration,
detail: { timestamp: new Date().toISOString() }
});
}
}
getStats(name) {
const measurements = this.measurements.get(name) || [];
if (measurements.length === 0) return null;
const sum = measurements.reduce((a, b) => a + b, 0);
const avg = sum / measurements.length;
const min = Math.min(...measurements);
const max = Math.max(...measurements);
return {
count: measurements.length,
average: avg,
min: min,
max: max,
total: sum
};
}
getAllStats() {
const stats = {};
for (const [name, measurements] of this.measurements.entries()) {
stats[name] = this.getStats(name);
}
return stats;
}
}
module.exports = new PerformanceMonitor();
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment