Node.js 인터뷰 질문 86
질문: Node.js에서 마이크로서비스 아키텍처를 구현하는 방법과 주요 고려사항에 대해 설명해주세요.
답변:
마이크로서비스 아키텍처는 하나의 큰 애플리케이션을 작고 독립적인 서비스들로 분리하여 구현하는 방식입니다. Node.js는 가볍고 확장성이 좋아 마이크로서비스 구현에 적합합니다.
1. 기본 마이크로서비스 구조
// 사용자 서비스 (user-service.js)
const express = require("express");
const app = express();
app.use(express.json());
const users = new Map();
app.get("/users/:id", (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.status(404).json({ error: "사용자를 찾을 수 없습니다" });
}
res.json(user);
});
app.post("/users", (req, res) => {
const id = Date.now().toString();
users.set(id, { id, ...req.body });
res.status(201).json({ id });
});
app.listen(3001, () => {
console.log("사용자 서비스가 3001 포트에서 실행 중입니다");
});
// 주문 서비스 (order-service.js)
const express = require("express");
const axios = require("axios");
const app = express();
app.use(express.json());
const orders = new Map();
app.post("/orders", async (req, res) => {
try {
// 사용자 서비스에서 사용자 정보 조회
const userResponse = await axios.get(
`http://localhost:3001/users/${req.body.userId}`
);
const user = userResponse.data;
const orderId = Date.now().toString();
const order = {
id: orderId,
userId: user.id,
items: req.body.items,
status: "pending",
};
orders.set(orderId, order);
res.status(201).json(order);
} catch (error) {
res.status(500).json({ error: "주문 생성 실패" });
}
});
app.listen(3002, () => {
console.log("주문 서비스가 3002 포트에서 실행 중입니다");
});
2. 서비스 디스커버리와 로드 밸런싱
// 서비스 레지스트리 구현
const express = require("express");
const app = express();
const services = new Map();
app.post("/register", (req, res) => {
const { serviceName, host, port } = req.body;
if (!services.has(serviceName)) {
services.set(serviceName, []);
}
services.get(serviceName).push({ host, port });
res.status(200).json({ message: "서비스 등록 완료" });
});
app.get("/discover/:serviceName", (req, res) => {
const serviceInstances = services.get(req.params.serviceName);
if (!serviceInstances) {
return res.status(404).json({ error: "서비스를 찾을 수 없습니다" });
}
// 간단한 라운드 로빈 로드 밸런싱
const instance =
serviceInstances[Math.floor(Math.random() * serviceInstances.length)];
res.json(instance);
});
app.listen(3000, () => {
console.log("서비스 레지스트리가 3000 포트에서 실행 중입니다");
});
3. API 게이트웨이 구현
const express = require("express");
const httpProxy = require("http-proxy");
const app = express();
const proxy = httpProxy.createProxyServer();
// 서비스 라우팅 설정
const serviceRoutes = {
"/users": "http://localhost:3001",
"/orders": "http://localhost:3002",
};
// 프록시 미들웨어
Object.entries(serviceRoutes).forEach(([path, target]) => {
app.use(path, (req, res) => {
proxy.web(req, res, { target }, (err) => {
res.status(500).json({ error: "서비스 호출 실패" });
});
});
});
// 인증 미들웨어
app.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: "인증이 필요합니다" });
}
// 토큰 검증 로직
next();
});
app.listen(8000, () => {
console.log("API 게이트웨이가 8000 포트에서 실행 중입니다");
});
4. 서비스 간 통신
4.1 동기식 통신 (REST)
const axios = require("axios");
class UserService {
async getUser(userId) {
try {
const response = await axios.get(
`http://user-service:3001/users/${userId}`
);
return response.data;
} catch (error) {
throw new Error("사용자 정보 조회 실패");
}
}
}
// 서킷 브레이커 패턴 구현
class CircuitBreaker {
constructor(request, options = {}) {
this.request = request;
this.state = "CLOSED";
this.failureCount = 0;
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 60000;
}
async execute(...args) {
if (this.state === "OPEN") {
throw new Error("Circuit breaker is OPEN");
}
try {
const response = await this.request(...args);
this.failureCount = 0;
return response;
} catch (error) {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "OPEN";
setTimeout(() => {
this.state = "HALF-OPEN";
}, this.resetTimeout);
}
throw error;
}
}
}
4.2 비동기식 통신 (메시지 큐)
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 publish(queue, message) {
await this.channel.assertQueue(queue);
this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)));
}
async subscribe(queue, callback) {
await this.channel.assertQueue(queue);
this.channel.consume(queue, (msg) => {
const content = JSON.parse(msg.content.toString());
callback(content);
this.channel.ack(msg);
});
}
}
// 사용 예시
const broker = new MessageBroker();
await broker.connect();
// 주문 생성 이벤트 발행
await broker.publish("order_created", { orderId: "123", userId: "456" });
// 이메일 서비스에서 구독
broker.subscribe("order_created", async (order) => {
await sendOrderConfirmationEmail(order);
});
5. 데이터 관리
5.1 데이터베이스 분리
// 사용자 서비스의 데이터베이스 연결
const mongoose = require("mongoose");
mongoose.connect("mongodb://user-db:27017/users", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const userSchema = new mongoose.Schema({
name: String,
email: String,
// 사용자 관련 필드만 포함
});
// 주문 서비스의 데이터베이스 연결
mongoose.connect("mongodb://order-db:27017/orders", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const orderSchema = new mongoose.Schema({
userId: String,
items: Array,
// 주문 관련 필드만 포함
});
5.2 데이터 일관성 관리
// 분산 트랜잭션 구현 (Saga 패턴)
class OrderSaga {
async createOrder(orderData) {
try {
// 1. 주문 생성
const order = await this.orderService.create(orderData);
// 2. 재고 확인 및 차감
await this.inventoryService.reserve(order.items);
// 3. 결제 처리
await this.paymentService.process(order.totalAmount);
// 4. 주문 확정
await this.orderService.confirm(order.id);
return order;
} catch (error) {
// 보상 트랜잭션 실행
await this.compensate(error);
throw error;
}
}
async compensate(error) {
// 실패한 단계에 따른 보상 트랜잭션 실행
if (error.step === "INVENTORY") {
await this.orderService.cancel(order.id);
}
if (error.step === "PAYMENT") {
await this.inventoryService.release(order.items);
await this.orderService.cancel(order.id);
}
}
}
6. 모니터링과 로깅
const winston = require("winston");
const { createLogger, format, transports } = winston;
// 중앙 집중식 로깅 구현
const logger = createLogger({
format: format.combine(format.timestamp(), format.json()),
defaultMeta: { service: "user-service" },
transports: [
new transports.File({ filename: "error.log", level: "error" }),
new transports.File({ filename: "combined.log" }),
],
});
// 프로메테우스 메트릭 설정
const prometheus = require("prom-client");
const collectDefaultMetrics = prometheus.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });
// 사용자 정의 메트릭
const httpRequestDurationMicroseconds = new prometheus.Histogram({
name: "http_request_duration_ms",
help: "Duration of HTTP requests in ms",
labelNames: ["method", "route", "code"],
buckets: [0.1, 5, 15, 50, 100, 500],
});
// 메트릭 엔드포인트
app.get("/metrics", async (req, res) => {
res.set("Content-Type", prometheus.register.contentType);
res.end(await prometheus.register.metrics());
});
7. 배포 및 확장
// Docker Compose 설정 예시
// docker-compose.yml
version: '3'
services:
user-service:
build: ./user-service
ports:
- "3001:3001"
environment:
- MONGODB_URI=mongodb://user-db:27017/users
depends_on:
- user-db
order-service:
build: ./order-service
ports:
- "3002:3002"
environment:
- MONGODB_URI=mongodb://order-db:27017/orders
depends_on:
- order-db
api-gateway:
build: ./api-gateway
ports:
- "8000:8000"
depends_on:
- user-service
- order-service
user-db:
image: mongo:latest
volumes:
- user-data:/data/db
order-db:
image: mongo:latest
volumes:
- order-data:/data/db
volumes:
user-data:
order-data:
요약
Node.js에서 마이크로서비스 아키텍처를 구현할 때 고려해야 할 주요 사항:
서비스 분리
- 각 서비스는 독립적으로 개발, 배포, 확장 가능
- 명확한 책임과 경계 정의
통신 패턴
- REST API를 통한 동기식 통신
- 메시지 큐를 통한 비동기식 통신
- 서비스 디스커버리와 로드 밸런싱
데이터 관리
- 데이터베이스 분리
- 데이터 일관성 유지
- 분산 트랜잭션 처리
장애 처리
- 서킷 브레이커 패턴
- 폴백 메커니즘
- 재시도 전략
모니터링과 로깅
- 중앙 집중식 로깅
- 분산 추적
- 성능 메트릭 수집
배포와 확장
- 컨테이너화
- 자동화된 배포
- 수평적 확장