๋์ฉ๋ ํธ๋ํฝ์ ๊ฒฌ๋๋ ๊ณ ์ฑ๋ฅ ์น์๋ฒ ๊ตฌํํ๊ธฐ
Node.js๋ ๋จ์ผ ์ค๋ ๋ ์ด๋ฒคํธ ๋ฃจํ ๊ธฐ๋ฐ์ผ๋ก ๋์ํฉ๋๋ค. ๋ฐ๋ผ์ ์ฌํ๊น์ง๋ ์ฑ๊ธ ์ค๋ ๋์์ ์ฒ๋ฆฌ๋๋ HTTP ์น ์๋ฒ๋ฅผ ๋ค๋ฃจ์๋๋ฐ์. ๊ทธ๋ฐ๋ฐ ๋ง์ฝ ์์ฒญ์ ์๊ฐ ๊ธ์ฆํ๋ค๋ฉด, ๋ฉํฐ ์ฝ์ด CPU ํ๊ฒฝ์์ ํ๋์ ์ค๋ ๋๋ง ์ฌ์ฉํด์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ๋ค์ ์์ฝ๊ฒ ๋๊ปด์ง ์ ์์ต๋๋ค.
๋ง์ฝ ๋ฉํฐ ์ค๋ ๋๋ฅผ ํ์ฉํด์ ์ปค๋ฒํ ์ ์๋ ์์ญ์ด๋ผ๋ฉด ๊ตณ์ด ์ธ์คํด์ค๋ฅผ ์ํ ํ์ฅํ ํ์๋ ์๊ธฐ์ ์๋ ์์์ ์ต๋ํ ํจ์จ์ ์ผ๋ก ํ์ฉํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
๋ฐ๋ผ์ ์น ์๋ฒ๋ฅผ ๊ฐ๊ฐ ๋ฉํฐ ํ๋ก์ธ์ค/ ๋ฉํฐ ์ค๋ ๋ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํด๋ณด๊ณ ๊ฐ ๋ฐฉ์์ ๋น๊ต ๋ถ์ํ๊ณ ์ ํ์์ต๋๋ค.
1. cluster ๋ชจ๋์ ํ์ฉํด CPU ์ฝ์ด ์๋งํผ ์์ปค ํ๋ก์ธ์ค๋ฅผ ์์ฑ
2. Worker Threads ๊ธฐ๋ฐ ์์ ์ฒ๋ฆฌ
์ ๋ฉํฐ ํ๋ก์ธ์ค/์ค๋ ๋๊ฐ ํ์ํ ๊น?
Node.js์ ๋จ์ผ ์ค๋ ๋ ๋ชจ๋ธ์ I/O ์์ ์์๋ ๋ฐ์ด๋ ์ฑ๋ฅ์ ๋ณด์ด์ง๋ง, ๋ค์๊ณผ ๊ฐ์ ์ํฉ์์๋ ํ๊ณ๊ฐ ์์ต๋๋ค.
- CPU ์ง์ฝ์ ์์ : ์ด๋ฏธ์ง ์ฒ๋ฆฌ, ์ํธํ, ๋ฐ์ดํฐ ๋ณํ ๋ฑ
- ๋ฉํฐ์ฝ์ด ํ์ฉ: 8์ฝ์ด CPU์์ 1์ฝ์ด๋ง ์ฌ์ฉํ๋ ๊ฒ์ ๋ญ๋น
- ์ฅ์ ๊ฒฉ๋ฆฌ: ํ๋์ ํ๋ก์ธ์ค๊ฐ ์ฃฝ์ด๋ ์๋น์ค๋ ๊ณ์ ์ด์๋์ด์ผ ํจ
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ ๊ฐ์ง ๋๊ตฌ๋ฅผ ํ์ฉํ ์ ์์ต๋๋ค.
- Cluster ๋ชจ๋: ์ฌ๋ฌ ํ๋ก์ธ์ค๋ฅผ ์์ฑํด ํฌํธ๋ฅผ ๊ณต์
- Worker Threads: ๊ฐ์ ํ๋ก์ธ์ค ๋ด์์ ๋ณ๋ ์ค๋ ๋๋ก ๋ฌด๊ฑฐ์ด ์์ ์ฒ๋ฆฌ
Part 1: Cluster๋ก ํ๋ก์ธ์ค ๋ณ๋ ฌํํ๊ธฐ
๊ธฐ๋ณธ ๊ฐ๋
Cluster ๋ชจ๋์ Master-Worker ์ํคํ ์ฒ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
- Master: ์์ปค๋ค์ ์์ฑํ๊ณ ๊ด๋ฆฌ
- Workers: ์ค์ HTTP ์์ฒญ์ ์ฒ๋ฆฌ
ํต์ฌ์ ๋ชจ๋ ์์ปค๊ฐ ๊ฐ์ ํฌํธ๋ฅผ ๊ณต์ ํ๋ฉด์๋ OS ๋ ๋ฒจ์์ ๋ก๋๋ฐธ๋ฐ์ฑ์ด ๋๋ค๋ ์ ์ ๋๋ค.
์ค์ ๊ตฌํ: net ๋ชจ๋ ๊ธฐ๋ฐ Cluster ์๋ฒ
๋ณธ ํ๋ก์ ํธ์์๋ webserver/server.js์ Cluster ๋ชจ๋๊ฐ ์ด๋ฏธ ๊ตฌํ๋์ด ์์ต๋๋ค.
// webserver/server.js (์ค์ ๊ตฌํ๋ ์ฝ๋)
import cluster from 'cluster';
import os from 'os';
import net from 'net';
// Cluster ๋ชจ๋ ์ค์ (ํ๊ฒฝ๋ณ์ ๋๋ ๋ช
๋ นํ ์ธ์๋ก ์ ์ด)
const USE_CLUSTER = process.env.USE_CLUSTER === 'true' || process.argv.includes('--cluster');
const NUM_WORKERS = process.env.NUM_WORKERS ? parseInt(process.env.NUM_WORKERS) : os.cpus().length;
console.log(`๐ง Cluster ๋ชจ๋: ${USE_CLUSTER ? 'ํ์ฑํ' : '๋นํ์ฑํ'}`);
// HTTP ์๋ฒ ์์ฑ ํจ์
function createServer() {
const server = net.createServer((socket) => {
// TCP ์์ผ์ ์ง์ ๋ค๋ฃจ๋ ์์ net ๋ชจ๋ ๊ธฐ๋ฐ ๊ตฌํ
// ... HTTP ์์ฒญ ํ์ฑ ๋ฐ ์๋ต ์ฒ๋ฆฌ ๋ก์ง
});
return server;
}
// Cluster ๋ชจ๋์ ๋ฐ๋ฅธ ์๋ฒ ์์
if (USE_CLUSTER && cluster.isPrimary) {
console.log(`๐ Cluster ๋ชจ๋ ์์: ${NUM_WORKERS}๊ฐ ์์ปค ํ๋ก์ธ์ค ์์ฑ`);
console.log(`๐ CPU ์ฝ์ด ์: ${os.cpus().length}`);
// ์์ปค ํ๋ก์ธ์ค ์์ฑ
for (let i = 0; i < NUM_WORKERS; i++) {
const worker = cluster.fork();
console.log(`๐ง ์์ปค ํ๋ก์ธ์ค ์์ฑ๋จ: PID ${worker.process.pid}`);
}
// ์์ปค ํ๋ก์ธ์ค ์ข
๋ฃ ์ ์๋ ์ฌ์์
cluster.on('exit', (worker, code, signal) => {
console.log(
`๐ ์์ปค ํ๋ก์ธ์ค ์ข
๋ฃ: PID ${worker.process.pid} (์ฝ๋: ${code}, ์๊ทธ๋: ${signal})`
);
if (!worker.exitedAfterDisconnect) {
console.log('์์ปค ํ๋ก์ธ์ค ์ฌ์์ ์ค...');
const newWorker = cluster.fork();
console.log(`๐ง ์ ์์ปค ํ๋ก์ธ์ค ์์ฑ๋จ: PID ${newWorker.process.pid}`);
}
});
// ๋ง์คํฐ ํ๋ก์ธ์ค Graceful Shutdown
process.on('SIGINT', () => {
console.log('\n๐ ๋ง์คํฐ ํ๋ก์ธ์ค ์ข
๋ฃ ์ ํธ ์์ ');
console.log('๋ชจ๋ ์์ปค ํ๋ก์ธ์ค์ ์ข
๋ฃ ์ ํธ ์ ์ก...');
for (const id in cluster.workers) {
cluster.workers[id].kill();
}
setTimeout(() => {
console.log('๋ง์คํฐ ํ๋ก์ธ์ค ์ข
๋ฃ');
process.exit(0);
}, 2000);
});
} else {
// ๋จ์ผ ํ๋ก์ธ์ค ๋ชจ๋ ๋๋ ์์ปค ํ๋ก์ธ์ค
const server = createServer();
const PORT = 80;
const HOST = '::'; // IPv6 + IPv4 ๋ชจ๋ ์์ฉ
server.listen(PORT, HOST, () => {
const mode = USE_CLUSTER ? `์์ปค ํ๋ก์ธ์ค (PID: ${process.pid})` : '๋จ์ผ ํ๋ก์ธ์ค';
console.log(`๐ Codestargram Web Server running on http://${HOST}:${PORT} [${mode}]`);
});
}
์คํ ๋ฐฉ๋ฒ
# ๋จ์ผ ํ๋ก์ธ์ค ๋ชจ๋ (๊ธฐ๋ณธ๊ฐ)
npm start
# Cluster ๋ชจ๋ ํ์ฑํ (CPU ์ฝ์ด ์๋งํผ ์์ปค ์์ฑ)
USE_CLUSTER=true npm start
# ๋๋
node server.js --cluster
# ์์ปค ์ ์ง์
NUM_WORKERS=4 USE_CLUSTER=true npm start
์ฃผ์ ํฌ์ธํธ
- ์๋ ์ฌ์์: ์์ปค๊ฐ ์ฃฝ์ผ๋ฉด ์ฆ์ ์๋ก์ด ์์ปค๋ฅผ ์์ฑ
- Zero Downtime: ํ ์์ปค๊ฐ ์ฃฝ์ด๋ ๋๋จธ์ง ์์ปค๋ค์ด ์์ฒญ ์ฒ๋ฆฌ
- ๋ชจ๋ํฐ๋ง: Master๊ฐ ์์ปค๋ค์ ์ํ๋ฅผ ์ถ์
Part 2: Worker Threads๋ก CPU ์์ ๋ถ๋ฆฌํ๊ธฐ
์ธ์ ์ฌ์ฉํ ๊น?
- ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง/์์ถ
- ๋์ฉ๋ ํ์ผ ํ์ฑ (CSV, JSON)
- ์ํธํ/๋ณตํธํ ์ฐ์ฐ
- ๋ณต์กํ ๋ฐ์ดํฐ ๋ณํ
Worker Thread ๊ตฌํ ์์
// backend/src/workers/image-processor.ts
import { Worker } from 'worker_threads';
import path from 'path';
export interface ImageProcessTask {
imagePath: string;
targetWidth: number;
targetHeight: number;
}
export function processImageAsync(task: ImageProcessTask): Promise<string> {
return new Promise((resolve, reject) => {
const worker = new Worker(path.join(__dirname, 'image-worker.js'), {
workerData: task
});
worker.on('message', (result) => {
resolve(result.outputPath);
});
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
});
}
// backend/src/workers/image-worker.js
import { parentPort, workerData } from 'worker_threads';
import sharp from 'sharp';
import path from 'path';
const { imagePath, targetWidth, targetHeight } = workerData;
(async () => {
try {
const outputPath = path.join(
path.dirname(imagePath),
`resized_${path.basename(imagePath)}`
);
await sharp(imagePath)
.resize(targetWidth, targetHeight, {
fit: 'cover',
position: 'center'
})
.toFile(outputPath);
parentPort?.postMessage({ outputPath });
} catch (error) {
throw error;
}
})();
๊ฒ์๊ธ ์์ฑ API์ ์ ์ฉํ๊ธฐ
ํ์ฌ ํ๋ก์ ํธ์ ๊ฒ์๊ธ ์์ฑ ๋ก์ง(backend/src/routes/postRoutes.ts:224-382)์ Worker Thread๋ฅผ ํตํฉํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
// backend/src/routes/postRoutes.ts
import { processImageAsync } from '../workers/image-processor.js';
async function createPostWithMultipart(request: HttpRequest): Promise<HttpResponse> {
try {
const multipartData = request.multipartData;
// ... ๊ธฐ์กด ๊ฒ์ฆ ๋ก์ง ...
let savedImageUrl = null;
if (multipartData.files.length > 0) {
const imageFile = multipartData.files.find((file) => file.name === 'image');
if (imageFile) {
// ์๋ณธ ํ์ผ ์ ์ฅ
const saveResult = await saveUploadedFile(imageFile.data, imageFile.filename);
// Worker Thread๋ก ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง (๋น๋๊ธฐ, ๋ฉ์ธ ์ค๋ ๋ ์ฐจ๋จ ์์)
const processedPath = await processImageAsync({
imagePath: saveResult.path,
targetWidth: 800,
targetHeight: 600
});
savedImageUrl = `/uploads/${path.basename(processedPath)}`;
console.log(`โ
์ด๋ฏธ์ง ์ฒ๋ฆฌ ์๋ฃ: ${processedPath}`);
}
}
// ๊ฒ์๊ธ ์์ฑ
const newPost = {
id: (postIdCounter++).toString(),
userId: 'anonymous',
username: 'Anonymous',
content: multipartData.fields.content,
imageUrl: savedImageUrl,
likes: 0,
comments: [],
createdAt: new Date().toISOString(),
};
mockPosts.push(newPost);
return {
statusCode: 201,
contentType: 'application/json',
body: JSON.stringify({
success: true,
message: '๊ฒ์๊ธ์ด ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ต๋๋ค.',
data: newPost,
timestamp: new Date().toISOString(),
}),
};
} catch (error) {
console.error('Post creation error:', error);
// ... ์๋ฌ ์ฒ๋ฆฌ ...
}
}
Part 3: Worker Pool ํจํด์ผ๋ก ์ฑ๋ฅ ์ต์ ํ
๋งค๋ฒ Worker๋ฅผ ์์ฑํ๋ฉด ์ค๋ฒํค๋๊ฐ ๋ฐ์ํฉ๋๋ค. Worker Pool์ ๊ตฌํํด ์ฌ์ฌ์ฉํ๋ฉด ํจ์ฌ ํจ์จ์ ์ ๋๋ค.
// backend/src/workers/worker-pool.ts
import { Worker } from 'worker_threads';
import path from 'path';
export class WorkerPool {
private workers: Worker[] = [];
private availableWorkers: Worker[] = [];
private taskQueue: Array<{
data: any;
resolve: (value: any) => void;
reject: (error: any) => void;
}> = [];
constructor(private workerScript: string, private poolSize: number) {
this.initialize();
}
private initialize() {
for (let i = 0; i < this.poolSize; i++) {
const worker = new Worker(this.workerScript);
this.workers.push(worker);
this.availableWorkers.push(worker);
worker.on('message', (result) => {
this.availableWorkers.push(worker);
this.processQueue();
});
worker.on('error', (error) => {
console.error('Worker error:', error);
this.availableWorkers.push(worker);
this.processQueue();
});
}
}
public execute(data: any): Promise<any> {
return new Promise((resolve, reject) => {
this.taskQueue.push({ data, resolve, reject });
this.processQueue();
});
}
private processQueue() {
if (this.taskQueue.length === 0 || this.availableWorkers.length === 0) {
return;
}
const task = this.taskQueue.shift()!;
const worker = this.availableWorkers.shift()!;
worker.once('message', task.resolve);
worker.once('error', task.reject);
worker.postMessage(task.data);
}
public terminate() {
this.workers.forEach(worker => worker.terminate());
}
}
์ฌ์ฉ ์์
// backend/src/services/imageService.ts
import { WorkerPool } from '../workers/worker-pool.js';
import path from 'path';
const imagePool = new WorkerPool(
path.join(__dirname, '../workers/image-worker.js'),
4 // 4๊ฐ์ ์์ปค๋ฅผ ํ์ ์ ์ง
);
export async function resizeImage(imagePath: string) {
return imagePool.execute({
imagePath,
targetWidth: 800,
targetHeight: 600
});
}
๊ตฌํ ๊ฒ์ฆ ์ฒดํฌ๋ฆฌ์คํธ
๋ณธ ํ๋ก์ ํธ์ ๋ฉํฐ ํ๋ก์ธ์ค/์ค๋ ๋ ๊ตฌํ์ด ํ๋ก๋์ ๋ ๋ฒจ์ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๋์ง ๊ฒ์ฆํ๊ธฐ ์ํ ์ฒดํฌ๋ฆฌ์คํธ์ ๋๋ค.
โ Cluster ๋ชจ๋ (๋ฉํฐ ํ๋ก์ธ์ค)
1. USE_CLUSTER ํ๋๊ทธ๋ก ๋ฉํฐ์ฝ์ด ํ์ฅ ์ ์ด ๊ฐ๋ฅํ๊ฐ?
โ ๊ตฌํ ์๋ฃ - webserver/server.js:30-36
const USE_CLUSTER = process.env.USE_CLUSTER === 'true' || process.argv.includes('--cluster');
const NUM_WORKERS = process.env.NUM_WORKERS ? parseInt(process.env.NUM_WORKERS) : os.cpus().length;
console.log(`๐ง Cluster ๋ชจ๋: ${USE_CLUSTER ? 'ํ์ฑํ' : '๋นํ์ฑํ'}`);
๊ฒ์ฆ ๋ฐฉ๋ฒ:
# Cluster ๋นํ์ฑํ (๋จ์ผ ํ๋ก์ธ์ค)
npm start
# Cluster ํ์ฑํ (๋ฉํฐ ํ๋ก์ธ์ค)
USE_CLUSTER=true npm start
2. ์์ปค ์๋ฅผ OS CPU ์ฝ์ด ์๋ฅผ ์ฌ์ฉํ์ฌ ์๋์ผ๋ก ์ฑ ์ ํ ์ ์๋๊ฐ?
โ
๊ตฌํ ์๋ฃ - os.cpus().length ์ฌ์ฉ
const NUM_WORKERS = process.env.NUM_WORKERS
? parseInt(process.env.NUM_WORKERS) // ๋ช
์์ ์ง์
: os.cpus().length; // ์๋ ๊ฐ์ง
๊ฒ์ฆ ๋ฐฉ๋ฒ:
# CPU ์ฝ์ด ์๋งํผ ์๋ ์์ฑ
USE_CLUSTER=true npm start
# ์์ปค ์ ๋ช
์์ ์ง์
NUM_WORKERS=4 USE_CLUSTER=true npm start
์คํ ์์:
๐ Cluster ๋ชจ๋ ์์: 8๊ฐ ์์ปค ํ๋ก์ธ์ค ์์ฑ
๐ CPU ์ฝ์ด ์: 8
๐ง ์์ปค ํ๋ก์ธ์ค ์์ฑ๋จ: PID 12345
๐ง ์์ปค ํ๋ก์ธ์ค ์์ฑ๋จ: PID 12346
...
3. ์์ปค๊ฐ ์ฃฝ์์ ๋ Auto Restart ๊ฐ๋ฅํ๊ฐ?
โ ๊ตฌํ ์๋ฃ - webserver/server.js:491-501
cluster.on('exit', (worker, code, signal) => {
console.log(
`๐ ์์ปค ํ๋ก์ธ์ค ์ข
๋ฃ: PID ${worker.process.pid} (์ฝ๋: ${code}, ์๊ทธ๋: ${signal})`
);
// ์๊ธฐ์น ์์ ์ข
๋ฃ์ธ ๊ฒฝ์ฐ์๋ง ์ฌ์์
if (!worker.exitedAfterDisconnect) {
console.log('์์ปค ํ๋ก์ธ์ค ์ฌ์์ ์ค...');
const newWorker = cluster.fork();
console.log(`๐ง ์ ์์ปค ํ๋ก์ธ์ค ์์ฑ๋จ: PID ${newWorker.process.pid}`);
}
});
๊ฒ์ฆ ๋ฐฉ๋ฒ:
# 1. Cluster ๋ชจ๋๋ก ์๋ฒ ์คํ
USE_CLUSTER=true npm start
# 2. ์์ปค ํ๋ก์ธ์ค ๊ฐ์ ์ข
๋ฃ
kill -9 <์์ปค_PID>
# 3. ๋ก๊ทธ ํ์ธ - ์๋์ผ๋ก ์ ์์ปค๊ฐ ์์ฑ๋์ด์ผ ํจ
ํต์ฌ ํฌ์ธํธ:
worker.exitedAfterDisconnect: ์ ์ ์ข ๋ฃ(graceful shutdown)์ ์์ธ ์ข ๋ฃ ๊ตฌ๋ถ- ์์ธ ์ข ๋ฃ ์์๋ง ์๋ ์ฌ์์ → ๋ฌดํ ์ฌ์์ ๋ฃจํ ๋ฐฉ์ง
4. Graceful Shutdown ๊ฐ๋ฅํ ๊ตฌ์กฐ์ธ๊ฐ?
โ ๊ตฌํ ์๋ฃ - webserver/server.js:504-516
process.on('SIGINT', () => {
console.log('\n๐ ๋ง์คํฐ ํ๋ก์ธ์ค ์ข
๋ฃ ์ ํธ ์์ ');
console.log('๋ชจ๋ ์์ปค ํ๋ก์ธ์ค์ ์ข
๋ฃ ์ ํธ ์ ์ก...');
for (const id in cluster.workers) {
cluster.workers[id].kill();
}
setTimeout(() => {
console.log('๋ง์คํฐ ํ๋ก์ธ์ค ์ข
๋ฃ');
process.exit(0);
}, 2000); // 2์ด ํ์์์
});
๊ฒ์ฆ ๋ฐฉ๋ฒ:
# ์๋ฒ ์คํ ์ค Ctrl+C ๋๋ SIGINT ์ ์ก
USE_CLUSTER=true npm start
# Ctrl+C ์
๋ ฅ
# ๋๋
kill -SIGINT <๋ง์คํฐ_PID>
Graceful Shutdown ํ๋ก์ธ์ค:
- ๋ง์คํฐ๊ฐ SIGINT ์๊ทธ๋ ์์
- ๋ชจ๋ ์์ปค์๊ฒ ์ข ๋ฃ ์ ํธ ์ ์ก
- ์์ปค๋ค์ด ์งํ ์ค์ธ ์์ฒญ ์๋ฃ ๋๊ธฐ
- 2์ด ํ์์์ ํ ๋ง์คํฐ ํ๋ก์ธ์ค ์ข ๋ฃ
๊ฐ์ ์ ์:
// ๋ ์ ๊ตํ Graceful Shutdown
process.on('SIGINT', async () => {
console.log('\n๐ ๋ง์คํฐ ํ๋ก์ธ์ค ์ข
๋ฃ ์ ํธ ์์ ');
// ์๋ก์ด ์ฐ๊ฒฐ ์ฐจ๋จ
server.close(() => {
console.log('์๋ก์ด ์ฐ๊ฒฐ ์ฐจ๋จ ์๋ฃ');
});
// ์์ปค์๊ฒ graceful shutdown ์๊ทธ๋ ์ ์ก
for (const id in cluster.workers) {
cluster.workers[id].send({ cmd: 'shutdown' });
cluster.workers[id].disconnect(); // IPC ์ฑ๋ ์ข
๋ฃ
}
// ๋ชจ๋ ์์ปค ์ข
๋ฃ ๋๊ธฐ (์ต๋ 10์ด)
const timeout = setTimeout(() => {
console.log('โ ๏ธ ํ์์์: ๊ฐ์ ์ข
๋ฃํฉ๋๋ค');
process.exit(1);
}, 10000);
// ๋ชจ๋ ์์ปค ์ข
๋ฃ ์๋ฃ ์
cluster.on('disconnect', () => {
if (Object.keys(cluster.workers).length === 0) {
clearTimeout(timeout);
console.log('โ
๋ชจ๋ ์์ปค ์ ์ ์ข
๋ฃ ์๋ฃ');
process.exit(0);
}
});
});
Worker Threads (๋ฉํฐ ์ค๋ ๋)
5. USE_WORKER_THREADS ํ๋๊ทธ๋ก CPU ์ง์ฝ์ ์์ ๋ถ๋ฆฌ ๊ฐ๋ฅํ๊ฐ?
โ ๊ตฌํ ์๋ฃ - webserver/server.js:22-28
// webserver/server.js
const USE_WORKER_THREADS =
process.env.USE_WORKER_THREADS === 'true' ||
process.argv.includes('--worker-threads');
console.log(`๐งต Worker Threads ๋ชจ๋: ${USE_WORKER_THREADS ? 'ํ์ฑํ' : '๋นํ์ฑํ'}`);
์ค์ ์ ์ฉ ์ฝ๋ - webserver/server.js:175-191:
if (USE_WORKER_THREADS) {
// Worker Threads๋ฅผ ์ฌ์ฉํ ํ์ผ ์ฒ๋ฆฌ
const workerResult = await workerManager.processFile(
filePath,
requestedPath
);
contentType = workerResult.contentType;
responseBody = workerResult.fileContent;
} else {
// ๊ธฐ์กด ๋ฉ์ธ ์ค๋ ๋ ํ์ผ ์ฒ๋ฆฌ
contentType = getMimeType(filePath);
if (isTextMimeType(contentType)) {
responseBody = await fs.readFile(filePath, 'utf8');
} else {
responseBody = await fs.readFile(filePath);
}
}
๊ฒ์ฆ ๋ฐฉ๋ฒ:
# Worker Threads ๋นํ์ฑํ (๊ธฐ๋ณธ๊ฐ)
npm start
# Worker Threads ํ์ฑํ
USE_WORKER_THREADS=true npm start
# ๋๋
node server.js --worker-threads
# Cluster + Worker Threads ๋์ ํ์ฑํ (์ต๊ณ ์ฑ๋ฅ)
USE_CLUSTER=true USE_WORKER_THREADS=true npm start
์คํ ์์:
๐งต Worker Threads ๋ชจ๋: ํ์ฑํ
๐งต WorkerManager ์ด๊ธฐํ: ์ต๋ ์์ปค ์ 7
โ
Worker-0 ์์ฑ ์๋ฃ
โ
Worker-1 ์์ฑ ์๋ฃ
...
6. ์์ปค ํ์ด ํจ์จ์ ์ผ๋ก ์ฌ์ฉ/๊ด๋ฆฌ๋๊ณ ์๋๊ฐ? (ํ์, ๋ฐธ๋ฐ์ฑ, Recovery)
โ ๊ตฌํ ์๋ฃ - webserver/workers/workerManager.js
ํ๋ก์ ํธ์ ์๋ฒฝํ WorkerManager ํด๋์ค๊ฐ ๊ตฌํ๋์ด ์์ต๋๋ค!
์ฃผ์ ๊ธฐ๋ฅ:
1. ํ์ (Queueing) - FIFO ๋ฐฉ์
async executeTask(type, data) {
return new Promise((resolve, reject) => {
const taskId = ++this.taskIdCounter;
const task = { taskId, type, data, resolve, reject, createdAt: Date.now() };
// ์ฌ์ฉ ๊ฐ๋ฅํ ์์ปค๊ฐ ์์ผ๋ฉด ์ฆ์ ์คํ
const availableWorker = this.getAvailableWorker();
if (availableWorker) {
this.assignTaskToWorker(task, availableWorker);
} else {
// ๋ชจ๋ ์์ปค๊ฐ ๋ฐ์๋ฉด ํ์ ์ถ๊ฐ
this.taskQueue.push(task);
console.log(`โณ Task-${taskId} ํ์ ๋๊ธฐ ์ค (ํ ํฌ๊ธฐ: ${this.taskQueue.length})`);
}
});
}
2. ๋ฐธ๋ฐ์ฑ (Balancing) - ์ต์ ๋ถํ ์์ปค ์ ํ
getAvailableWorker() {
for (const [workerId, workerInfo] of this.workers) {
if (!workerInfo.busy) {
return { workerId, workerInfo };
}
}
return null;
}
3. Recovery - ์๋ ์์ปค ์ฌ์์ฑ
restartWorker(workerId) {
const workerInfo = this.workers.get(workerId);
if (workerInfo) {
try {
workerInfo.worker.terminate();
} catch (error) {
console.error(`์์ปค ์ข
๋ฃ ์ค ์๋ฌ:`, error);
}
this.workers.delete(workerId);
}
// ์ ์์ปค ์์ฑ (1์ด ํ)
setTimeout(() => {
if (!this.isShuttingDown) {
this.createWorker(workerId);
}
}, 1000);
}
์๋ฌ ์ฒ๋ฆฌ:
// ์์ปค ์๋ฌ ์ ์๋ ์ฌ์์
worker.on('error', (error) => {
console.error(`โ Worker-${workerId} ์๋ฌ:`, error);
this.restartWorker(workerId);
});
// ๋น์ ์ ์ข
๋ฃ ์ ์๋ ์ฌ์์
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`๐ Worker-${workerId} ๋น์ ์ ์ข
๋ฃ (์ฝ๋: ${code})`);
if (!this.isShuttingDown) {
this.restartWorker(workerId);
}
}
});
๊ฒ์ฆ ๋ฐฉ๋ฒ:
# Worker Threads ํ์ฑํํ๊ณ ๋ถํ ํ
์คํธ
USE_WORKER_THREADS=true npm start
# ๋ค๋ฅธ ํฐ๋ฏธ๋์์ ๋ถํ ์์ฑ
ab -n 1000 -c 50 http://localhost:8080/
์ค์๊ฐ ํต๊ณ ํ์ธ:
# Worker Pool ์ํ API ํธ์ถ
curl http://localhost:8080/api/worker-stats
# ์ถ๋ ฅ ์์:
{
"success": true,
"data": {
"totalWorkers": 7,
"busyWorkers": 3,
"queueSize": 5,
"activeTasks": 3,
"workers": [
{ "id": 0, "busy": true, "taskCount": 42, "uptime": 60000 },
{ "id": 1, "busy": false, "taskCount": 38, "uptime": 60000 },
...
]
}
}
7. ์์ปค์ ์๋ช ์ฃผ๊ธฐ๊ฐ ์ ๊ตํ๊ฒ ๊ด๋ฆฌ๋๊ณ ์๋๊ฐ?
โ ๊ตฌํ ์๋ฃ - 5๋จ๊ณ ์๋ช ์ฃผ๊ธฐ ์๋ฒฝ ๊ด๋ฆฌ
1. ์์ปค ์์ฑ (Initialization) โ
- webserver/workers/workerManager.js:35-39
- CPU ์ฝ์ด ์์ ๋ง์ถฐ ์๋ ์ด๊ธฐํ:
Math.max(2, os.cpus().length - 1) - ์์ปค ๋ฐ์ดํฐ์ ํจ๊ป ์์ฑ:
new Worker(script, { workerData: { workerId } })
initializeWorkers() {
for (let i = 0; i < this.maxWorkers; i++) {
this.createWorker(i);
}
}
2. ์์ปค ์คํ (Execution) โ
- webserver/workers/workerManager.js:178-196
- ๋ฉ์์ง ๊ธฐ๋ฐ ํต์ (
postMessage/on('message')) - ์์ ์๋ฃ ํ ์๋์ผ๋ก ํ๋ก ๋ณต๊ท
assignTaskToWorker(task, { workerId, workerInfo }) {
workerInfo.busy = true;
this.activeTasks.set(taskId, task);
workerInfo.worker.postMessage({ taskId, type, data });
console.log(`๐ Task-${taskId} -> Worker-${workerId} ํ ๋น`);
}
handleWorkerMessage(workerId, message) {
workerInfo.busy = false; // ์์ปค๋ฅผ ์ฌ์ฉ ๊ฐ๋ฅ ์ํ๋ก ๋ณ๊ฒฝ
workerInfo.taskCount++;
this.processNextTask(); // ๋ค์ ๋๊ธฐ ์์
์ฒ๋ฆฌ
}
3. ์์ปค ์๋ฌ ์ฒ๋ฆฌ (Error Handling) โ
- webserver/workers/workerManager.js:55-59
error์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฐ ์๋ ์ฌ์์
worker.on('error', (error) => {
console.error(`โ Worker-${workerId} ์๋ฌ:`, error);
this.restartWorker(workerId);
});
4. ์์ปค ์ข ๋ฃ (Termination) โ
- webserver/workers/workerManager.js:62-69
- ์ ์ ์ข ๋ฃ(code=0) vs ๋น์ ์ ์ข ๋ฃ ๊ตฌ๋ถ
exit์ด๋ฒคํธ๋ก ๊ฐ์ง ๋ฐ ์๋ ์ฌ์์ฑ
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`๐ Worker-${workerId} ๋น์ ์ ์ข
๋ฃ (์ฝ๋: ${code})`);
if (!this.isShuttingDown) {
this.restartWorker(workerId);
}
}
});
5. ์์ปค ํ ์ข ๋ฃ (Pool Termination) โ
- webserver/workers/workerManager.js:266-306
- Graceful Shutdown ์๋ฒฝ ๊ตฌํ
- ํ์ฑ ์์ ์๋ฃ ๋๊ธฐ (์ต๋ 5์ด)
- ๋ชจ๋ ์์ปค ์ ๋ฆฌ
async shutdown() {
this.isShuttingDown = true;
// 1. ๋๊ธฐ ์ค์ธ ์์
์ทจ์
for (const task of this.taskQueue) {
task.reject(new Error('Worker pool is shutting down'));
}
// 2. ํ์ฑ ์์
์๋ฃ ๋๊ธฐ (์ต๋ 5์ด)
while (this.activeTasks.size > 0) {
await new Promise(resolve => setTimeout(resolve, 100));
}
// 3. ๋ชจ๋ ์์ปค ์ข
๋ฃ
await Promise.allSettled(
Array.from(this.workers.values()).map(w => w.worker.terminate())
);
console.log('โ
WorkerManager ์ข
๋ฃ ์๋ฃ');
}
์๋ช ์ฃผ๊ธฐ ๋ค์ด์ด๊ทธ๋จ:
[์ด๊ธฐํ] โโโโโโโโโโโโโโโโโโโโโโโ
↓ โ
[๋๊ธฐ ์ค] ←โโโโโโโโโ โ
↓ โ โ
[์์
ํ ๋น] โ โ
↓ โ โ
[์์
์คํ] โ โ
↓ โ โ
[๊ฒฐ๊ณผ ๋ฐํ] โโโโโโโโโ โ
↓ (์๋ฌ ๋ฐ์) โ
[์์ปค ์ฌ์์ฑ] โโโโโโโโโโโโโโโโโโโ
↓ (shutdown)
[Graceful ์ข
๋ฃ]
์๊ทธ๋ ํธ๋ค๋ง:
// SIGINT/SIGTERM ์๊ทธ๋ ์ฒ๋ฆฌ
process.on('SIGINT', async () => {
await workerManager.shutdown();
process.exit(0);
});
process.on('SIGTERM', async () => {
await workerManager.shutdown();
process.exit(0);
});
์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋
ํ๋ก๋์ ํ๊ฒฝ์์ Cluster์ Worker Threads์ ์ฑ๋ฅ์ ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋งํ๊ธฐ ์ํ API ๊ตฌํ:
// webserver/server.js - ์๋ฒ ์ ๋ณด API
if (url === '/api/server-info') {
return createJsonResponse(200, {
success: true,
data: {
mode: USE_CLUSTER ? 'cluster' : 'single',
pid: process.pid,
cpuCores: os.cpus().length,
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
workers: USE_CLUSTER ? Object.keys(cluster.workers || {}).length : 1,
},
timestamp: new Date().toISOString(),
});
}
// backend/src/routes/workerRoutes.ts - Worker Pool ํต๊ณ API
export function createWorkerStatsRoute(pool: WorkerPool): RouteHandler {
return {
async handle(request: HttpRequest): Promise<HttpResponse> {
const stats = pool.getStats();
return {
statusCode: 200,
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: stats,
timestamp: new Date().toISOString(),
}),
};
},
};
}
๋ชจ๋ํฐ๋ง ์์:
# ์๋ฒ ์ ๋ณด ํ์ธ
curl http://localhost/api/server-info
# Worker Pool ํต๊ณ ํ์ธ
curl http://localhost/api/worker-stats
๋ฉํฐ ํ๋ก์ธ์ค vs ๋ฉํฐ ์ค๋ ๋: ํต์ฌ ์ฐจ์ด์
๊ฐ๋ ๋น๊ต
| ๊ตฌ๋ถ | Cluster (๋ฉํฐ ํ๋ก์ธ์ค) | Worker Threads (๋ฉํฐ ์ค๋ ๋) |
|---|---|---|
| ๊ฒฉ๋ฆฌ ์์ค | ์์ ํ ๋ ๋ฆฝ๋ ๋ฉ๋ชจ๋ฆฌ ๊ณต๊ฐ | ๊ฐ์ ํ๋ก์ธ์ค ๋ด ๋ฉ๋ชจ๋ฆฌ ๊ณต์ |
| ํต์ ๋ฐฉ์ | IPC (Inter-Process Communication) | ๋ฉ์์ง ํจ์ฑ (postMessage) |
| ์์ฑ ๋น์ฉ | ๋์ (ํ๋ก์ธ์ค ์ ์ฒด ๋ณต์ ) | ๋ฎ์ (์ค๋ ๋๋ง ์์ฑ) |
| ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ | ๊ฐ ํ๋ก์ธ์ค๋ง๋ค ๋ ๋ฆฝ์ ๋ฉ๋ชจ๋ฆฌ | ๊ณต์ ๋ฉ๋ชจ๋ฆฌ ์์ญ ํ์ฉ |
| ์ฅ์ ๊ฒฉ๋ฆฌ | ํ ํ๋ก์ธ์ค ์ฃฝ์ด๋ ๋ค๋ฅธ ํ๋ก์ธ์ค ์ํฅ ์์ | ํ ์ค๋ ๋ ์๋ฌ๊ฐ ํ๋ก์ธ์ค ์ ์ฒด์ ์ํฅ ๊ฐ๋ฅ |
| ์ ํฉํ ์์ | I/O ๋ฐ์ด๋, HTTP ์์ฒญ ์ฒ๋ฆฌ | CPU ๋ฐ์ด๋, ๊ณ์ฐ ์ง์ฝ์ ์์ |
์ค์ ํ๋ก์ ํธ ์ ์ฉ ์์
// webserver/server.js
// 1๏ธโฃ Cluster: ์ฌ๋ฌ ํ๋ก์ธ์ค๋ก HTTP ์์ฒญ ๋ถ์ฐ
if (USE_CLUSTER && cluster.isPrimary) {
// 8์ฝ์ด CPU๋ผ๋ฉด 8๊ฐ์ ๋
๋ฆฝ๋ ํ๋ก์ธ์ค ์์ฑ
for (let i = 0; i < os.cpus().length; i++) {
cluster.fork(); // ๊ฐ ํ๋ก์ธ์ค๋ ๋
๋ฆฝ๋ ๋ฉ๋ชจ๋ฆฌ ๊ณต๊ฐ ๋ณด์
}
}
// 2๏ธโฃ Worker Threads: ํ์ผ ์ฒ๋ฆฌ๋ฅผ ๋ณ๋ ์ค๋ ๋๋ก ๋ถ๋ฆฌ
if (USE_WORKER_THREADS) {
const workerResult = await workerManager.processFile(filePath);
// ๋ฉ์ธ ์ค๋ ๋๋ ์ฐจ๋จ๋์ง ์๊ณ ๋ค๋ฅธ ์์ฒญ ์ฒ๋ฆฌ ๊ฐ๋ฅ
}
๋ฉํฐ ์ค๋ ๋๋ฅผ ๋์ ํ๋ฉด ํญ์ ์ฑ๋ฅ์ด ๋ ์ข์์ง๋๊ฐ?
โ ๊ทธ๋ ์ง ์์ต๋๋ค!
๋ฉํฐ ์ค๋ ๋๋ ํน์ ์ํฉ์์๋ง ์ฑ๋ฅ ํฅ์์ ๊ฐ์ ธ์ต๋๋ค. ์คํ๋ ค ์๋ชป ์ฌ์ฉํ๋ฉด ์ฑ๋ฅ์ด ์ ํ๋ ์ ์์ต๋๋ค.
์ฑ๋ฅ์ด ํฅ์๋๋ ๊ฒฝ์ฐ โ
1. CPU ์ง์ฝ์ ์์
// โ
์ข์ ์: ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง (CPU ์์
)
await workerManager.processImage({
imagePath: '/uploads/large-image.jpg',
resize: { width: 1920, height: 1080 },
compress: true
});
// ๋ฉ์ธ ์ค๋ ๋๋ ์ฐจ๋จ๋์ง ์๊ณ ๋ค๋ฅธ HTTP ์์ฒญ ์ฒ๋ฆฌ ๊ฐ๋ฅ
์ฑ๋ฅ ๋น๊ต:
๋จ์ผ ์ค๋ ๋: ์ด๋ฏธ์ง ์ฒ๋ฆฌ ์๊ฐ 500ms → ๋ฉ์ธ ์ค๋ ๋ ์ฐจ๋จ → ๋ค๋ฅธ ์์ฒญ ๋๊ธฐ
๋ฉํฐ ์ค๋ ๋: ์ด๋ฏธ์ง ์ฒ๋ฆฌ 500ms → ๋ฉ์ธ ์ค๋ ๋ ๊ณ์ ๋์ → ๋ค๋ฅธ ์์ฒญ ์ฆ์ ์ฒ๋ฆฌ
2. ๋์ฉ๋ ๋ฐ์ดํฐ ๋ณํ
// โ
์ข์ ์: CSV ํ์ผ ํ์ฑ (CPU ์์
)
await workerManager.processData({
inputData: largeCSVFile,
operation: 'parse-and-transform'
});
3. ์ํธํ/ํด์ฑ ์์
// โ
์ข์ ์: ๋น๋ฐ๋ฒํธ ํด์ฑ (CPU ์์
)
await workerManager.executeTask('hash', {
password: userPassword,
algorithm: 'bcrypt',
rounds: 10
});
์ฑ๋ฅ์ด ์ ํ๋๋ ๊ฒฝ์ฐ โ
1. I/O ๋ฐ์ด๋ ์์
// โ ๋์ ์: ํ์ผ ์ฝ๊ธฐ (I/O ์์
)
// Node.js๋ ์ด๋ฏธ ๋น๋๊ธฐ I/O๋ก ์ต์ ํ๋์ด ์์
await workerManager.processFile('/path/to/file.txt');
// โ
๋ ๋์ ๋ฐฉ๋ฒ: ๊ทธ๋ฅ fs/promises ์ฌ์ฉ
await fs.readFile('/path/to/file.txt', 'utf8');
์ด์ : Node.js์ ๋น๋๊ธฐ I/O๋ libuv์ ์ค๋ ๋ ํ์ ์ด๋ฏธ ํ์ฉํ๊ณ ์์ต๋๋ค. Worker Threads๋ฅผ ์ถ๊ฐ๋ก ์ฌ์ฉํ๋ฉด ์ค๋ฒํค๋๋ง ์ฆ๊ฐํฉ๋๋ค.
2. ์์ ์์ ๋ค
// โ ๋์ ์: ๊ฐ๋จํ JSON ํ์ฑ
await workerManager.executeTask('parse', {
data: smallJsonString
});
// โ
๋ ๋์ ๋ฐฉ๋ฒ: ๋ฉ์ธ ์ค๋ ๋์์ ์ง์ ์ฒ๋ฆฌ
const parsed = JSON.parse(smallJsonString);
์ฑ๋ฅ ๋น๊ต:
์์
์๊ฐ: 1ms
์์ปค ์์ฑ/ํต์ ์ค๋ฒํค๋: 5-10ms
→ ์ด ์๊ฐ: 6-11ms (์คํ๋ ค 10๋ฐฐ ๋๋ฆผ!)
3. ๋น๋ฒํ ์์ ๋ฉ์์ง ์ ์ก
// โ ๋์ ์: ๋ฃจํ์์ ๋งค๋ฒ ์์ปค ํธ์ถ
for (let i = 0; i < 10000; i++) {
await workerManager.executeTask('increment', { value: i });
}
// ํต์ ์ค๋ฒํค๋ 10000๋ฐฐ ๋ฐ์!
// โ
๋ ๋์ ๋ฐฉ๋ฒ: ๋ฐฐ์น ์ฒ๋ฆฌ
await workerManager.executeTask('batch-increment', {
values: Array.from({ length: 10000 }, (_, i) => i)
});
์ธ์ ๋ฉํฐ ์ค๋ ๋๋ฅผ ์ฌ์ฉํด์ผ ํ๋๊ฐ?
๊ฒฐ์ ๊ธฐ์ค ์ฒดํฌ๋ฆฌ์คํธ
โ
CPU ์ฌ์ฉ๋ฅ ์ด 100%์ ๊ฐ๊น์ด๊ฐ?
โ
์์
์๊ฐ์ด 10ms ์ด์์ธ๊ฐ?
โ
์์
์ด ๋ฉ์ธ ์ด๋ฒคํธ ๋ฃจํ๋ฅผ ์ฐจ๋จํ๋๊ฐ?
โ
๋์ ์์ฒญ ์ฒ๋ฆฌ๊ฐ ์ค์ํ๊ฐ?
์ ์ง๋ฌธ์ ๋ชจ๋ YES๋ผ๋ฉด → Worker Threads ๊ณ ๋ ค
ํ๋๋ผ๋ NO๋ผ๋ฉด → ๊ธฐ์กด ๋น๋๊ธฐ I/O ์ฌ์ฉ
์ค์ ํ๋จ ์์
| ์์ | CPU ์ง์ฝ? | ์๊ฐ | ๋ฉํฐ ์ค๋ ๋ ํ์? |
|---|---|---|---|
| ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง | โ YES | 200-500ms | โ ํ์ํจ |
| ๋น๋์ค ์ธ์ฝ๋ฉ | โ YES | 5-30์ด | โ ํ์ํจ |
| ํ์ผ ์ฝ๊ธฐ | โ NO (I/O) | 5-50ms | โ ๋ถํ์ |
| JSON ํ์ฑ (์์) | โ ๏ธ ์ฝ๊ฐ | 1ms | โ ๋ถํ์ |
| JSON ํ์ฑ (๋์ฉ๋) | โ YES | 100ms+ | โ ํ์ํจ |
| DB ์ฟผ๋ฆฌ | โ NO (I/O) | 10-100ms | โ ๋ถํ์ |
| ๋ณต์กํ ์ ๊ท์ | โ YES | 50ms+ | โ ๊ณ ๋ ค |
์ฑ๋ฅ ์ธก์ ์ค์ต
1. ๋ฒค์น๋งํฌ ์ฝ๋
// benchmark.js
import { performance } from 'perf_hooks';
// ๋ฉ์ธ ์ค๋ ๋์์ ์คํ
async function benchmarkMainThread() {
const start = performance.now();
// CPU ์ง์ฝ์ ์์
์๋ฎฌ๋ ์ด์
let result = 0;
for (let i = 0; i < 10000000; i++) {
result += Math.sqrt(i);
}
const end = performance.now();
console.log(`๋ฉ์ธ ์ค๋ ๋: ${(end - start).toFixed(2)}ms`);
return result;
}
// Worker Thread์์ ์คํ
async function benchmarkWorkerThread() {
const start = performance.now();
const result = await workerManager.executeTask('heavy-compute', {
iterations: 10000000
});
const end = performance.now();
console.log(`์์ปค ์ค๋ ๋: ${(end - start).toFixed(2)}ms`);
return result;
}
// ๋์ ์์ฒญ ์๋๋ฆฌ์ค
async function benchmarkConcurrent() {
const start = performance.now();
// 10๊ฐ์ ๋์ ์์ฒญ
await Promise.all([
benchmarkMainThread(),
benchmarkMainThread(),
// ... ๋ค๋ฅธ ์์ฒญ๋ค
]);
const end = performance.now();
console.log(`๋์ ์ฒ๋ฆฌ (๋ฉ์ธ): ${(end - start).toFixed(2)}ms`);
}
2. ์์ ๊ฒฐ๊ณผ
# ๋จ์ผ ์์
๋น๊ต
๋ฉ์ธ ์ค๋ ๋: 245ms
์์ปค ์ค๋ ๋: 250ms (ํต์ ์ค๋ฒํค๋ +5ms)
→ ๊ฑฐ์ ๋์ผ
# ๋์ ์์
๋น๊ต (10๊ฐ)
๋ฉ์ธ ์ค๋ ๋ ์์ฐจ: 2450ms (10 × 245ms)
์์ปค ์ค๋ ๋ ๋ณ๋ ฌ: 300ms (์์ปค 8๊ฐ ์ฌ์ฉ)
→ 8๋ฐฐ ๋น ๋ฆ! ๐
์ค๋ฒํค๋ ์ดํดํ๊ธฐ
Worker Thread ์์ฑ ๋น์ฉ
const { Worker } = require('worker_threads');
console.time('์์ปค ์์ฑ');
const worker = new Worker('./worker.js');
console.timeEnd('์์ปค ์์ฑ');
// ์ถ๋ ฅ: ์์ปค ์์ฑ: 5.2ms
console.time('์์ปค ์คํ');
worker.postMessage({ task: 'simple' });
worker.on('message', () => {
console.timeEnd('์์ปค ์คํ');
// ์ถ๋ ฅ: ์์ปค ์คํ: 2.1ms
});
์ค๋ฒํค๋ ์์ฝ:
- ์์ปค ์์ฑ: ~5-10ms
- ๋ฉ์์ง ์ ์ก: ~1-2ms
- ๋ฐ์ดํฐ ์ง๋ ฌํ/์ญ์ง๋ ฌํ: ํฌ๊ธฐ์ ๋น๋ก
๊ฒฐ๋ก : ์์ ์๊ฐ์ด 10ms ์ดํ๋ผ๋ฉด ์ค๋ฒํค๋๊ฐ ์ด๋๋ณด๋ค ํด ์ ์์ต๋๋ค.
์ต์ข ๊ถ์ฅ์ฌํญ
ํ๋ก์ ํธ๋ณ ๊ฐ์ด๋
1. ์๊ท๋ชจ API ์๋ฒ (์ 1๋ง ์์ฒญ ๋ฏธ๋ง)
๊ถ์ฅ: Cluster๋ง ์ฌ์ฉ (Worker Threads ๋ถํ์)
์ด์ : ๋๋ถ๋ถ์ ์์
์ด I/O ๋ฐ์ด๋
2. ์ค๊ท๋ชจ ์น ์๋น์ค (์ 100๋ง ์์ฒญ)
๊ถ์ฅ: Cluster + ์ ํ์ Worker Threads
์ด์ : ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๋ฑ ํน์ CPU ์์
๋ง ๋ถ๋ฆฌ
3. ๋๊ท๋ชจ ๋ฐ์ดํฐ ์ฒ๋ฆฌ (์ค์๊ฐ ๋ถ์)
๊ถ์ฅ: Cluster + Worker Pool
์ด์ : CPU ์ง์ฝ์ ์์
์ด ์ง์์ ์ผ๋ก ๋ฐ์
์น ์ฑ๋ฅ ๋ถํ ํ ์คํธ ๋๊ตฌ : Apache Bench
์ด ๋, ์ฑ๋ฅ ๊ฐ์ ํ์ ์ ์ ์ฑ๋ฅ ์ฐจ์ด๋ฅผ ํ์ธํด๋ณด๊ธฐ ์ํด์ ์น ์ฑ๋ฅ ๋ถํ๋ฅผ ํ ์คํธ ํ๊ธฐ ์ํด Apache Bench ๋ฅผ ํ์ฉํ์ต๋๋ค.
Apache Bench (ab)
- ์ํ์น HTTP ์๋ฒ์ ํจ๊ป ์ ๊ณต๋๋ ๊ฐ๋จํ ๋ฒค์น๋งํฌ ๋๊ตฌ
- ๊ธฐ๋ณธ ๊ธฐ๋ฅ: ๋์ ์ ์ ์, ์์ฒญ ์๋ฅผ ์ง์ ํด์ ์๋ฒ์ ๋ถํ ๊ฑธ๊ธฐ
- ์:
- -n 1000 → ์ด 1000๋ฒ ์์ฒญ
- -c 100 → ๋์์ 100๊ฐ ์์ฒญ
- ab -n 1000 -c 100 http://localhost/
Part 4: ์ฑ๋ฅ ๋น๊ต ๋ฐ ๋ชจ๋ํฐ๋ง
๋ถํ ํ ์คํธ ์คํฌ๋ฆฝํธ
# Apache Bench๋ก ์ฑ๋ฅ ์ธก์
# ๋จ์ผ ํ๋ก์ธ์ค
ab -n 10000 -c 100 http://localhost:3000/api/posts
# Cluster ๋ชจ๋ (4 workers)
ab -n 10000 -c 100 http://localhost:3000/api/posts
์ค์ ์ฑ๋ฅ ์ฐจ์ด (์์)
| ๊ตฌ์ฑ | Requests/sec | ํ๊ท ์๋ต์๊ฐ |
|---|---|---|
| ๋จ์ผ ํ๋ก์ธ์ค | 1,234 req/s | 81ms |
| Cluster (4 workers) | 4,567 req/s | 22ms |
| + Worker Pool | 5,892 req/s | 17ms |
Best Practices
1. Cluster ์ค์
// ํ๊ฒฝ์ ๋ฐ๋ผ ์์ปค ์ ์กฐ์
const NUM_WORKERS = process.env.NODE_ENV === 'production'
? os.cpus().length
: 2; // ๊ฐ๋ฐ ํ๊ฒฝ์์๋ 2๊ฐ๋ก ์ ํ
2. Graceful Shutdown
// Master ํ๋ก์ธ์ค
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully...');
Object.values(cluster.workers || {}).forEach((worker) => {
worker?.kill('SIGTERM');
});
setTimeout(() => {
process.exit(0);
}, 10000); // 10์ด ํ์์์
});
3. Worker Thread ์๋ฌ ํธ๋ค๋ง
worker.on('error', (error) => {
logger.error('Worker thread error:', error);
// ์๋ฌ ๋ฐ์ ์ ์ฌ์๋ ๋ก์ง
retryTask(task);
});
ํ ์คํธ ์ํ ๊ฒฐ๊ณผ
(base) kimseoyeon@gimseoyeons-MacBook-Air-2 webserver % npm run test:performance
> codestargram-webserver@1.0.0 test:performance
> node test/performanceTest.js
๐ ์น์๋ฒ ์ฑ๋ฅ ํ
์คํธ ์์
๐ก ๋์ ์๋ฒ: http://127.0.0.1:80
============================================================
๐ ๊ธฐ๋ณธ ํ์ด์ง ๋ก๋ ์ฑ๋ฅ ํ
์คํธ
๐ ์คํ ์ค: ab -n 1000 -c 10 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: HTML ํ์ด์ง ๋ก๋ (index.html)
๐ JavaScript ํ์ผ ๋ก๋ ์ฑ๋ฅ ํ
์คํธ
๐ ์คํ ์ค: ab -n 500 -c 10 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/bundle.js
๐ ํ
์คํธ: JavaScript ํ์ผ ๋ก๋ (bundle.js)
๐ ํ๋น์ฝ ๋ก๋ ์ฑ๋ฅ ํ
์คํธ
๐ ์คํ ์ค: ab -n 500 -c 10 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/favicon.ico
๐ ํ
์คํธ: ํ๋น์ฝ ๋ก๋ (favicon.ico)
๐ ๋์ ์ฐ๊ฒฐ ์ ์คํธ๋ ์ค ํ
์คํธ
๐ฅ ๋์ ์ฐ๊ฒฐ ์: 1
๐ ์คํ ์ค: ab -n 200 -c 1 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: ๋์ ์ฐ๊ฒฐ 1๊ฐ
๐ฅ ๋์ ์ฐ๊ฒฐ ์: 5
๐ ์คํ ์ค: ab -n 200 -c 5 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: ๋์ ์ฐ๊ฒฐ 5๊ฐ
๐ฅ ๋์ ์ฐ๊ฒฐ ์: 10
๐ ์คํ ์ค: ab -n 200 -c 10 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: ๋์ ์ฐ๊ฒฐ 10๊ฐ
๐ฅ ๋์ ์ฐ๊ฒฐ ์: 20
๐ ์คํ ์ค: ab -n 200 -c 20 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: ๋์ ์ฐ๊ฒฐ 20๊ฐ
๐ฅ ๋์ ์ฐ๊ฒฐ ์: 50
๐ ์คํ ์ค: ab -n 200 -c 50 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: ๋์ ์ฐ๊ฒฐ 50๊ฐ
๐ Keep-Alive ์ฑ๋ฅ ๋น๊ต ํ
์คํธ
๐ ์คํ ์ค: ab -n 500 -c 10 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: Keep-Alive ๋นํ์ฑํ
๐ ์คํ ์ค: ab -n 500 -c 10 -s 60 -g gnuplot.dat -e results.csv -k http://127.0.0.1:80/
๐ ํ
์คํธ: Keep-Alive ํ์ฑํ
๐ ์ง์ ๋ถํ ํ
์คํธ (20์ด)
๐ ์คํ ์ค: ab -t 20 -c 10 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: 20์ด๊ฐ ์ง์ ๋ถํ ํ
์คํธ
============================================================
๐ ์ฑ๋ฅ ํ
์คํธ ๊ฒฐ๊ณผ ์์ฝ
============================================================
๐ฏ HTML ํ์ด์ง ๋ก๋ (index.html)
โ
์๋ฃ๋ ์์ฒญ: 1000
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 7008.74 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 1.43 ms
๐ ์ ์ก ์๋: 3114.24 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.143 ์ด
๐ฏ JavaScript ํ์ผ ๋ก๋ (bundle.js)
โ
์๋ฃ๋ ์์ฒญ: 500
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 4468.08 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 2.24 ms
๐ ์ ์ก ์๋: 163857.07 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.112 ์ด
๐ฏ ํ๋น์ฝ ๋ก๋ (favicon.ico)
โ
์๋ฃ๋ ์์ฒญ: 500
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 5574.26 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 1.79 ms
๐ ์ ์ก ์๋: 947.19 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.09 ์ด
๐ฏ ๋์ ์ฐ๊ฒฐ 1๊ฐ
โ
์๋ฃ๋ ์์ฒญ: 200
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 1048.29 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 0.95 ms
๐ ์ ์ก ์๋: 465.79 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.191 ์ด
๐ฏ ๋์ ์ฐ๊ฒฐ 5๊ฐ
โ
์๋ฃ๋ ์์ฒญ: 200
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 6928.81 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 0.72 ms
๐ ์ ์ก ์๋: 3078.72 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.029 ์ด
๐ฏ ๋์ ์ฐ๊ฒฐ 10๊ฐ
โ
์๋ฃ๋ ์์ฒญ: 200
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 8215.24 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 1.22 ms
๐ ์ ์ก ์๋: 3650.33 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.024 ์ด
๐ฏ ๋์ ์ฐ๊ฒฐ 20๊ฐ
โ
์๋ฃ๋ ์์ฒญ: 200
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 9794.80 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 2.04 ms
๐ ์ ์ก ์๋: 4352.18 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.02 ์ด
๐ฏ ๋์ ์ฐ๊ฒฐ 50๊ฐ
โ
์๋ฃ๋ ์์ฒญ: 200
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 8990.38 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 5.56 ms
๐ ์ ์ก ์๋: 3994.75 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.022 ์ด
๐ฏ Keep-Alive ๋นํ์ฑํ
โ
์๋ฃ๋ ์์ฒญ: 500
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 8943.58 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 1.12 ms
๐ ์ ์ก ์๋: 3973.96 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.056 ์ด
๐ฏ Keep-Alive ํ์ฑํ
โ
์๋ฃ๋ ์์ฒญ: 500
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 7413.89 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 1.35 ms
๐ ์ ์ก ์๋: 3294.26 KB/sec
โฐ ์ด ์์ ์๊ฐ: 0.067 ์ด
๐ฏ 20์ด๊ฐ ์ง์ ๋ถํ ํ
์คํธ
โ
์๋ฃ๋ ์์ฒญ: 12342
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 559.31 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 17.88 ms
๐ ์ ์ก ์๋: 248.52 KB/sec
โฐ ์ด ์์ ์๊ฐ: 22.067 ์ด
๐ ์ฑ๋ฅ ํ
์คํธ ์๋ฃ!
Cluster ๋ชจ๋ ๊ธฐ๋ฐ ๋ฉํฐ ์ค๋ ๋ ์๋ฒ ๊ตฌํ
(base) kimseoyeon@gimseoyeons-MacBook-Air-2 webserver % node test/clusterTest.js
๐ ํด๋ฌ์คํฐ ๋ชจ๋ ์ฑ๋ฅ ๋น๊ต ํ
์คํธ ์์
๐ก ๋์ ์๋ฒ: ClusterPerformanceTester
============================================================
๐ ๋จ์ผ vs ๋ฉํฐ ํ๋ก์ธ์ค ์ฑ๋ฅ ๋น๊ต ํ
์คํธ
๐น ๋จ์ผ ํ๋ก์ธ์ค ๋ชจ๋ ํ
์คํธ
๐ ์คํ ์ค: ab -n 2000 -c 50 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: ๋จ์ผ ํ๋ก์ธ์ค ๋ชจ๋
โณ ํด๋ฌ์คํฐ ๋ชจ๋ ์ ํ์ ์ํด ์๋ฒ ์ฌ์์ ํ์
๐ก ๋ค์ ๋ช
๋ น์ด๋ก ํด๋ฌ์คํฐ ๋ชจ๋ ์๋ฒ๋ฅผ ์์ํ์ธ์:
USE_CLUSTER=true node webserver/server.js
๋๋
node webserver/server.js --cluster
โธ๏ธ ํด๋ฌ์คํฐ ๋ชจ๋ ์๋ฒ ์์ ํ ์๋ฌด ํค๋ ๋๋ฅด์ธ์...
๐น ํด๋ฌ์คํฐ ๋ชจ๋ ํ
์คํธ
๐ ์คํ ์ค: ab -n 2000 -c 50 -s 60 -g gnuplot.dat -e results.csv http://127.0.0.1:80/
๐ ํ
์คํธ: ํด๋ฌ์คํฐ ๋ชจ๋
============================================================
๐ ํด๋ฌ์คํฐ ์ฑ๋ฅ ๋น๊ต ๋ถ์
============================================================
๐น ๋จ์ผ ํ๋ก์ธ์ค:
โก RPS: 6191.91 req/sec
โฑ๏ธ ์๋ต์๊ฐ: 8.07 ms
๐ ์ ์ก์๋: 2751.29 KB/sec
๐น ํด๋ฌ์คํฐ ๋ชจ๋:
โก RPS: 5421.27 req/sec
โฑ๏ธ ์๋ต์๊ฐ: 9.22 ms
๐ ์ ์ก์๋: 2408.87 KB/sec
๐ ์ฑ๋ฅ ๊ฐ์ ๋:
โก RPS ํฅ์: -12.45%
โฑ๏ธ ์๋ต์๊ฐ ๊ฐ์ : -14.22%
๐ ์ ์ก์๋ ํฅ์: -12.45%
โ ๏ธ ํด๋ฌ์คํฐ ๋ชจ๋์์ ์์๋ณด๋ค ์ฑ๋ฅ ํฅ์์ด ์ ์ต๋๋ค.
๋์ ๋์์ฑ ๋ถํ์์ ๋ ํฐ ์ฐจ์ด๋ฅผ ๋ณด์ผ ์ ์์ต๋๋ค.
============================================================
๐ ์ฑ๋ฅ ํ
์คํธ ๊ฒฐ๊ณผ ์์ฝ
============================================================
๐ฏ ๋จ์ผ ํ๋ก์ธ์ค ๋ชจ๋
โ
์๋ฃ๋ ์์ฒญ: 2000
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 6191.91 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 8.07 ms
๐ ์ ์ก ์๋: 2751.29 KB/sec
๐ฏ ํด๋ฌ์คํฐ ๋ชจ๋
โ
์๋ฃ๋ ์์ฒญ: 2000
โ ์คํจํ ์์ฒญ: 0
โก ์ด๋น ์์ฒญ ์: 5421.27 req/sec
โฑ๏ธ ํ๊ท ์๋ต ์๊ฐ: 9.22 ms
๐ ์ ์ก ์๋: 2408.87 KB/sec
๐ ์ฑ๋ฅ ํ
์คํธ ์๋ฃ!
๐พ ํด๋ฌ์คํฐ ๋น๊ต ๊ฒฐ๊ณผ ์ ์ฅ๋จ: cluster-performance-test-2025-09-17T13-26-31-597Z.json
(base) kimseoyeon@gimseoyeons-MacBook-Air-2 webserver %
๋ง๋ฌด๋ฆฌ
Node.js์ Cluster์ Worker Threads๋ ๊ฐ๊ฐ ๋ค๋ฅธ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
- Cluster: ์ฌ๋ฌ ํ๋ก์ธ์ค๋ก ์์ฒญ์ ๋ถ์ฐ (์ํ ํ์ฅ)
- Worker Threads: ๋ฌด๊ฑฐ์ด ์์ ์ ๋ณ๋ ์ค๋ ๋๋ก ๋ถ๋ฆฌ (๋ธ๋กํน ๋ฐฉ์ง)
์ด ๋์ ์กฐํฉํ๋ฉด ๋ฉํฐ์ฝ์ด๋ฅผ ์ต๋ํ ํ์ฉํ๋ฉด์๋ ๋ฉ์ธ ์ด๋ฒคํธ ๋ฃจํ๋ฅผ ๋ง์ง ์๋ ์ง์ ํ ๊ณ ์ฑ๋ฅ ์๋ฒ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ PM2๋ Kubernetes ๊ฐ์ ์ค์ผ์คํธ๋ ์ด์ ๋๊ตฌ์ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋์ฑ ๊ฐ๋ ฅํ ์์คํ ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค.
๋ ์ฝ์ด๋ณด๊ธฐ
'๐ WEB' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ์น ์ฌ์ดํธ ์ต์ ํ ๋ฐฉ๋ฒ (0) | 2026.01.19 |
|---|---|
| HTTP Multipart/form-data ์ง์ ํ์ ๋ง๋ค๋ฉฐ ์๋ฆฌ ์ดํดํ๊ธฐ (0) | 2025.10.01 |
| HTTP ํจํท ๋ถ์ํ๊ธฐ (0) | 2025.09.18 |
| HTTP ํจํท ๊ตฌ์กฐ, ์์ฒญ ํค๋/๋ฐ๋ (0) | 2025.09.17 |
| Web Server ์ WAS(Web Application Server)๋ ? (0) | 2025.09.15 |