Node.js 인터뷰 질문 96
질문: Node.js를 사용한 마이크로서비스 아키텍처의 구현 방법과 주요 패턴에 대해 설명해주세요.
답변:
Node.js를 사용한 마이크로서비스 아키텍처의 구현 방법과 주요 패턴을 살펴보겠습니다.
1. 서비스 디스커버리 및 로드 밸런싱
// service-registry.js
const express = require("express");
const Redis = require("ioredis");
const redis = new Redis();
class ServiceRegistry {
constructor() {
this.services = new Map();
this.healthCheckInterval = 30000; // 30초
this.startHealthCheck();
}
// 서비스 등록
async registerService(name, host, port) {
const serviceId = `${name}-${host}-${port}`;
const serviceInfo = {
name,
host,
port,
id: serviceId,
timestamp: Date.now(),
};
await redis.hset("services", serviceId, JSON.stringify(serviceInfo));
console.log(`서비스 등록됨: ${serviceId}`);
return serviceId;
}
// 서비스 검색
async findService(name) {
const services = await redis.hgetall("services");
const serviceList = Object.values(services)
.map((s) => JSON.parse(s))
.filter((s) => s.name === name);
if (serviceList.length === 0) {
throw new Error(`서비스를 찾을 수 없음: ${name}`);
}
// 라운드 로빈 방식으로 서비스 선택
const service = serviceList[Math.floor(Math.random() * serviceList.length)];
return service;
}
// 서비스 제거
async unregisterService(serviceId) {
await redis.hdel("services", serviceId);
console.log(`서비스 제거됨: ${serviceId}`);
}
// 헬스 체크
async startHealthCheck() {
setInterval(async () => {
const services = await redis.hgetall("services");
for (const [serviceId, serviceInfo] of Object.entries(services)) {
const service = JSON.parse(serviceInfo);
const isAlive = await this.checkHealth(service);
if (!isAlive) {
await this.unregisterService(serviceId);
}
}
}, this.healthCheckInterval);
}
async checkHealth(service) {
try {
const response = await fetch(
`http://${service.host}:${service.port}/health`
);
return response.status === 200;
} catch (error) {
return false;
}
}
}
2. API 게이트웨이 구현
// api-gateway.js
const express = require("express");
const httpProxy = require("http-proxy");
const rateLimit = require("express-rate-limit");
class ApiGateway {
constructor(serviceRegistry) {
this.app = express();
this.proxy = httpProxy.createProxyServer();
this.serviceRegistry = serviceRegistry;
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
// 속도 제한
this.app.use(
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
})
);
// 요청 로깅
this.app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// 인증 미들웨어
this.app.use(this.authenticate);
}
setupRoutes() {
// 사용자 서비스 라우팅
this.app.use("/users", async (req, res) => {
try {
const service = await this.serviceRegistry.findService("user-service");
this.proxy.web(req, res, {
target: `http://${service.host}:${service.port}`,
});
} catch (error) {
res.status(500).json({ error: "서비스를 찾을 수 없습니다." });
}
});
// 주문 서비스 라우팅
this.app.use("/orders", async (req, res) => {
try {
const service = await this.serviceRegistry.findService("order-service");
this.proxy.web(req, res, {
target: `http://${service.host}:${service.port}`,
});
} catch (error) {
res.status(500).json({ error: "서비스를 찾을 수 없습니다." });
}
});
}
authenticate(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: "인증이 필요합니다." });
}
// 토큰 검증 로직
next();
}
start(port) {
this.app.listen(port, () => {
console.log(`API Gateway가 포트 ${port}에서 실행 중입니다.`);
});
}
}
3. 서비스 간 통신
// message-broker.js
const amqp = require("amqplib");
class MessageBroker {
constructor() {
this.connection = null;
this.channel = null;
}
async connect() {
this.connection = await amqp.connect("amqp://localhost");
this.channel = await this.connection.createChannel();
}
async publishMessage(queue, message) {
if (!this.channel) {
await this.connect();
}
await this.channel.assertQueue(queue);
this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)));
}
async consumeMessages(queue, callback) {
if (!this.channel) {
await this.connect();
}
await this.channel.assertQueue(queue);
this.channel.consume(queue, (message) => {
const content = JSON.parse(message.content.toString());
callback(content);
this.channel.ack(message);
});
}
}
// 사용 예시
const broker = new MessageBroker();
// 주문 서비스
broker.consumeMessages("order_created", async (order) => {
// 재고 확인
const inventory = await checkInventory(order.items);
if (inventory.available) {
await updateInventory(order.items);
await broker.publishMessage("order_confirmed", {
orderId: order.id,
status: "confirmed",
});
} else {
await broker.publishMessage("order_rejected", {
orderId: order.id,
reason: "out_of_stock",
});
}
});
4. 서킷 브레이커 패턴
// circuit-breaker.js
class CircuitBreaker {
constructor(request, options = {}) {
this.request = request;
this.state = "CLOSED";
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = null;
this.options = {
failureThreshold: options.failureThreshold || 5,
resetTimeout: options.resetTimeout || 60000,
monitorInterval: options.monitorInterval || 10000,
};
}
async exec(...args) {
if (this.state === "OPEN") {
if (Date.now() - this.lastFailureTime >= this.options.resetTimeout) {
this.state = "HALF_OPEN";
} else {
throw new Error("Circuit Breaker is OPEN");
}
}
try {
const result = await this.request(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === "HALF_OPEN") {
this.successCount++;
if (this.successCount >= 2) {
this.state = "CLOSED";
this.successCount = 0;
}
}
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = "OPEN";
}
}
// 상태 모니터링
startMonitoring() {
setInterval(() => {
console.log({
state: this.state,
failures: this.failureCount,
successes: this.successCount,
lastFailure: this.lastFailureTime,
});
}, this.options.monitorInterval);
}
}
// 사용 예시
const breaker = new CircuitBreaker(async (url) => {
const response = await fetch(url);
if (!response.ok) throw new Error("Service failed");
return response.json();
});
// API 호출
try {
const data = await breaker.exec("http://api.example.com/data");
console.log("데이터 수신:", data);
} catch (error) {
console.error("서비스 호출 실패:", error);
}
5. 분산 트랜잭션 관리
// transaction-manager.js
class TransactionManager {
constructor(services) {
this.services = services;
this.transactions = new Map();
}
async beginTransaction(transactionId) {
const transaction = {
id: transactionId,
status: "PENDING",
participants: [],
compensations: [],
};
this.transactions.set(transactionId, transaction);
return transaction;
}
async addParticipant(transactionId, service, operation, compensation) {
const transaction = this.transactions.get(transactionId);
if (!transaction) throw new Error("Transaction not found");
transaction.participants.push({
service,
operation,
});
transaction.compensations.push({
service,
operation: compensation,
});
}
async commit(transactionId) {
const transaction = this.transactions.get(transactionId);
if (!transaction) throw new Error("Transaction not found");
try {
// 모든 참가자의 작업 실행
for (const participant of transaction.participants) {
await participant.operation();
}
transaction.status = "COMMITTED";
} catch (error) {
// 실패 시 보상 트랜잭션 실행
await this.rollback(transactionId);
throw error;
}
}
async rollback(transactionId) {
const transaction = this.transactions.get(transactionId);
if (!transaction) throw new Error("Transaction not found");
// 보상 트랜잭션 실행
for (const compensation of transaction.compensations.reverse()) {
try {
await compensation.operation();
} catch (error) {
console.error("보상 트랜잭션 실패:", error);
}
}
transaction.status = "ROLLED_BACK";
}
}
// 사용 예시
const tm = new TransactionManager();
// 주문 처리 트랜잭션
async function processOrder(order) {
const txId = `order-${Date.now()}`;
await tm.beginTransaction(txId);
try {
// 결제 서비스
await tm.addParticipant(
txId,
"payment",
async () => await processPayment(order.payment),
async () => await refundPayment(order.payment)
);
// 재고 서비스
await tm.addParticipant(
txId,
"inventory",
async () => await updateInventory(order.items),
async () => await restoreInventory(order.items)
);
// 배송 서비스
await tm.addParticipant(
txId,
"shipping",
async () => await createShipment(order),
async () => await cancelShipment(order)
);
await tm.commit(txId);
} catch (error) {
console.error("주문 처리 실패:", error);
await tm.rollback(txId);
}
}
요약
Node.js 마이크로서비스 아키텍처의 주요 구성 요소:
서비스 디스커버리
- 서비스 등록
- 서비스 검색
- 헬스 체크
API 게이트웨이
- 라우팅
- 로드 밸런싱
- 인증/인가
서비스 간 통신
- 메시지 큐
- 이벤트 기반 통신
- 동기/비동기 통신
장애 처리
- 서킷 브레이커
- 재시도 메커니즘
- 폴백 전략
데이터 관리
- 분산 트랜잭션
- 데이터 일관성
- 보상 트랜잭션