Node.js 인터뷰 질문 74
질문: Node.js를 사용한 마이크로서비스 아키텍처를 어떻게 구현하고, 서비스 간 통신을 어떻게 처리하나요?
답변:
마이크로서비스 아키텍처는 하나의 큰 애플리케이션을 작고 독립적인 서비스로 분리하여 개발하는 방식입니다. Node.js는 경량성과 비동기 특성으로 인해 마이크로서비스 구현에 매우 적합합니다. 이제 Node.js를 사용한 마이크로서비스 구현 방법과 서비스 간 통신 전략에 대해 알아보겠습니다.
1. 마이크로서비스 아키텍처 기본 구조
마이크로서비스 아키텍처는 다음과 같은 구성 요소를 포함합니다:
- 독립 서비스: 각각 고유한 비즈니스 기능을 담당
- API 게이트웨이: 클라이언트 요청을 적절한 서비스로 라우팅
- 서비스 디스커버리: 서비스 위치를 동적으로 찾는 메커니즘
- 서비스 간 통신: 동기/비동기 통신 방식
- 데이터 관리: 각 서비스별 독립적인 데이터베이스
2. Node.js 마이크로서비스 구현 예시
2.1 기본 마이크로서비스 예시 (사용자 서비스)
// user-service/index.js
const express = require("express");
const mongoose = require("mongoose");
const app = express();
// 미들웨어 설정
app.use(express.json());
// MongoDB 연결
mongoose.connect("mongodb://localhost:27017/user-service", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// 사용자 모델
const User = mongoose.model(
"User",
new mongoose.Schema({
username: String,
email: { type: String, unique: true },
password: String,
createdAt: { type: Date, default: Date.now },
})
);
// API 엔드포인트
app.get("/users", async (req, res) => {
try {
const users = await User.find({}, "-password");
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get("/users/:id", async (req, res) => {
try {
const user = await User.findById(req.params.id, "-password");
if (!user)
return res.status(404).json({ error: "사용자를 찾을 수 없습니다" });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post("/users", async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 서버 시작
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`사용자 서비스가 포트 ${PORT}에서 실행 중입니다`);
});
2.2 주문 서비스 예시
// order-service/index.js
const express = require("express");
const mongoose = require("mongoose");
const axios = require("axios");
const app = express();
// 미들웨어 설정
app.use(express.json());
// MongoDB 연결
mongoose.connect("mongodb://localhost:27017/order-service", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// 주문 모델
const Order = mongoose.model(
"Order",
new mongoose.Schema({
userId: String,
products: [{ productId: String, quantity: Number, price: Number }],
totalAmount: Number,
status: { type: String, default: "pending" },
createdAt: { type: Date, default: Date.now },
})
);
// API 엔드포인트
app.post("/orders", async (req, res) => {
try {
// 사용자 서비스에서 사용자 정보 가져오기 (서비스 간 통신)
const userResponse = await axios.get(
`http://localhost:3001/users/${req.body.userId}`
);
if (!userResponse.data) {
return res.status(404).json({ error: "사용자를 찾을 수 없습니다" });
}
// 제품 서비스에서 제품 정보 및 가격 가져오기
const productPromises = req.body.products.map((product) =>
axios.get(`http://localhost:3002/products/${product.productId}`)
);
const productResponses = await Promise.all(productPromises);
// 주문 총액 계산
let totalAmount = 0;
const products = req.body.products.map((item, index) => {
const product = productResponses[index].data;
const itemTotal = product.price * item.quantity;
totalAmount += itemTotal;
return {
productId: item.productId,
quantity: item.quantity,
price: product.price,
};
});
// 주문 생성
const order = new Order({
userId: req.body.userId,
products,
totalAmount,
});
await order.save();
res.status(201).json(order);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get("/orders/:id", async (req, res) => {
try {
const order = await Order.findById(req.params.id);
if (!order)
return res.status(404).json({ error: "주문을 찾을 수 없습니다" });
res.json(order);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 서버 시작
const PORT = process.env.PORT || 3003;
app.listen(PORT, () => {
console.log(`주문 서비스가 포트 ${PORT}에서 실행 중입니다`);
});
3. API 게이트웨이 구현
// api-gateway/index.js
const express = require("express");
const httpProxy = require("http-proxy");
const app = express();
const proxy = httpProxy.createProxyServer();
// 서비스 라우팅 설정
const SERVICES = {
USERS: "http://localhost:3001",
PRODUCTS: "http://localhost:3002",
ORDERS: "http://localhost:3003",
};
// 사용자 서비스 라우팅
app.use("/api/users", (req, res) => {
proxy.web(req, res, {
target: SERVICES.USERS,
pathRewrite: { "^/api/users": "/users" },
});
});
// 제품 서비스 라우팅
app.use("/api/products", (req, res) => {
proxy.web(req, res, {
target: SERVICES.PRODUCTS,
pathRewrite: { "^/api/products": "/products" },
});
});
// 주문 서비스 라우팅
app.use("/api/orders", (req, res) => {
proxy.web(req, res, {
target: SERVICES.ORDERS,
pathRewrite: { "^/api/orders": "/orders" },
});
});
// 프록시 오류 처리
proxy.on("error", (err, req, res) => {
res.status(500).json({ error: "서비스 연결 오류", details: err.message });
});
// 서버 시작
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API 게이트웨이가 포트 ${PORT}에서 실행 중입니다`);
});
4. 서비스 간 통신 방식
4.1 동기식 통신 (REST API)
위의 예시처럼 HTTP를 통한 직접 호출 방식입니다:
// HTTP를 통한 동기식 통신
async function getUserDetails(userId) {
try {
const response = await axios.get(
`http://user-service:3001/users/${userId}`
);
return response.data;
} catch (error) {
console.error("사용자 정보 조회 실패:", error);
throw error;
}
}
4.2 비동기식 통신 (메시지 큐)
RabbitMQ를 사용한 비동기 통신 예시:
// order-service/events.js
const amqp = require("amqplib");
async function connectQueue() {
try {
const connection = await amqp.connect("amqp://localhost");
const channel = await connection.createChannel();
// 주문 생성 이벤트를 위한 큐 설정
await channel.assertQueue("order_created", { durable: true });
return { connection, channel };
} catch (error) {
console.error("RabbitMQ 연결 실패:", error);
throw error;
}
}
// 주문 생성 이벤트 발행
async function publishOrderCreated(order) {
const { channel } = await connectQueue();
channel.sendToQueue("order_created", Buffer.from(JSON.stringify(order)), {
persistent: true,
});
console.log(`주문 생성 이벤트 발행: ${order._id}`);
}
module.exports = { publishOrderCreated };
// 주문 서비스에서 이벤트 발행
// order-service/index.js
const { publishOrderCreated } = require("./events");
app.post("/orders", async (req, res) => {
try {
// ... 주문 생성 로직 ...
await order.save();
// 주문 생성 이벤트 발행
await publishOrderCreated(order);
res.status(201).json(order);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// inventory-service/events.js
// 재고 서비스에서 이벤트 수신
const amqp = require("amqplib");
async function startConsumer() {
try {
const connection = await amqp.connect("amqp://localhost");
const channel = await connection.createChannel();
await channel.assertQueue("order_created", { durable: true });
console.log("주문 이벤트 대기 중...");
channel.consume("order_created", async (msg) => {
if (msg !== null) {
const order = JSON.parse(msg.content.toString());
console.log(`주문 수신: ${order._id}`);
// 재고 업데이트 로직
await updateInventory(order);
// 메시지 처리 완료 확인
channel.ack(msg);
}
});
} catch (error) {
console.error("메시지 소비자 시작 실패:", error);
}
}
async function updateInventory(order) {
// 재고 업데이트 로직
console.log("주문에 따른 재고 업데이트");
}
startConsumer();
5. 서비스 디스커버리 및 구성 관리
5.1 Consul을 사용한 서비스 디스커버리
// user-service/service-registry.js
const Consul = require("consul");
const consul = new Consul({
host: "localhost",
port: 8500,
});
function registerService(serviceName, serviceId, port) {
consul.agent.service.register(
{
name: serviceName,
id: serviceId,
port: port,
check: {
http: `http://localhost:${port}/health`,
interval: "10s",
timeout: "5s",
},
},
(err) => {
if (err) {
console.error("서비스 등록 실패:", err);
return;
}
console.log(`서비스 등록됨: ${serviceName}:${serviceId}`);
}
);
}
// 서비스 등록 해제 (프로세스 종료 시)
process.on("SIGINT", () => {
consul.agent.service.deregister(serviceId, (err) => {
console.log("서비스 등록 해제됨");
process.exit();
});
});
module.exports = { registerService };
// 사용자 서비스에서 서비스 등록
// user-service/index.js
const { registerService } = require("./service-registry");
const serviceId = `user-service-${Math.floor(Math.random() * 10000)}`;
// 헬스 체크 엔드포인트
app.get("/health", (req, res) => {
res.status(200).send("OK");
});
app.listen(PORT, () => {
console.log(`사용자 서비스가 포트 ${PORT}에서 실행 중입니다`);
registerService("user-service", serviceId, PORT);
});
5.2 API 게이트웨이에서 서비스 디스커버리 사용
// api-gateway/service-discovery.js
const Consul = require("consul");
const consul = new Consul({
host: "localhost",
port: 8500,
});
async function getServiceAddress(serviceName) {
return new Promise((resolve, reject) => {
consul.catalog.service.nodes(serviceName, (err, result) => {
if (err) {
return reject(err);
}
if (result.length === 0) {
return reject(new Error(`서비스를 찾을 수 없음: ${serviceName}`));
}
// 로드 밸런싱을 위해 무작위 인스턴스 선택
const service = result[Math.floor(Math.random() * result.length)];
resolve(`http://${service.ServiceAddress}:${service.ServicePort}`);
});
});
}
module.exports = { getServiceAddress };
// api-gateway/index.js
const { getServiceAddress } = require("./service-discovery");
// 동적 서비스 검색을 사용한 라우팅
app.use("/api/users", async (req, res) => {
try {
const target = await getServiceAddress("user-service");
proxy.web(req, res, { target, pathRewrite: { "^/api/users": "/users" } });
} catch (error) {
res
.status(500)
.json({ error: "서비스를 찾을 수 없습니다", details: error.message });
}
});
6. 로그 집계 및 모니터링
// 중앙 집중식 로깅 예시
const winston = require("winston");
const { ElasticsearchTransport } = require("winston-elasticsearch");
// Elasticsearch 전송 설정
const esTransportOpts = {
level: "info",
clientOpts: { node: "http://localhost:9200" },
indexPrefix: "microservices-logs",
};
// Winston 로거 생성
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: "user-service" },
transports: [
new winston.transports.Console(),
new ElasticsearchTransport(esTransportOpts),
],
});
// 로깅 사용 예시
app.get("/users/:id", async (req, res) => {
logger.info("사용자 정보 요청", { userId: req.params.id });
try {
const user = await User.findById(req.params.id);
if (!user) {
logger.warn("사용자를 찾을 수 없음", { userId: req.params.id });
return res.status(404).json({ error: "사용자를 찾을 수 없습니다" });
}
logger.info("사용자 정보 반환", { userId: req.params.id });
res.json(user);
} catch (error) {
logger.error("사용자 조회 오류", {
userId: req.params.id,
error: error.message,
stack: error.stack,
});
res.status(500).json({ error: error.message });
}
});
7. 마이크로서비스 배포 및 확장
// docker-compose.yml 예시
version: '3'
services:
api-gateway:
build: ./api-gateway
ports:
- "3000:3000"
environment:
- PORT=3000
depends_on:
- user-service
- product-service
- order-service
- rabbitmq
- consul
user-service:
build: ./user-service
environment:
- PORT=3001
- MONGODB_URI=mongodb://mongo-user:27017/user-service
- CONSUL_HOST=consul
depends_on:
- mongo-user
- consul
product-service:
build: ./product-service
environment:
- PORT=3002
- MONGODB_URI=mongodb://mongo-product:27017/product-service
- CONSUL_HOST=consul
depends_on:
- mongo-product
- consul
order-service:
build: ./order-service
environment:
- PORT=3003
- MONGODB_URI=mongodb://mongo-order:27017/order-service
- RABBITMQ_URL=amqp://rabbitmq
- CONSUL_HOST=consul
depends_on:
- mongo-order
- rabbitmq
- consul
mongo-user:
image: mongo
volumes:
- mongo-user-data:/data/db
mongo-product:
image: mongo
volumes:
- mongo-product-data:/data/db
mongo-order:
image: mongo
volumes:
- mongo-order-data:/data/db
rabbitmq:
image: rabbitmq:3-management
ports:
- "15672:15672"
consul:
image: consul
ports:
- "8500:8500"
volumes:
mongo-user-data:
mongo-product-data:
mongo-order-data:
요약
Node.js를 사용한 마이크로서비스 아키텍처 구현의 핵심 요소:
- 서비스 분리: 기능별 독립적인 서비스로 분리하여 개발 및 배포
- API 게이트웨이: 클라이언트 요청을 적절한 서비스로 라우팅
- 통신 방식:
- 동기식 통신: HTTP/REST를 통한 직접 호출
- 비동기식 통신: 메시지 큐를 통한 이벤트 기반 통신
- 서비스 디스커버리: Consul, Etcd 등을 사용한 동적 서비스 검색
- 데이터 관리: 각 서비스별 독립 데이터베이스 또는 데이터 파티셔닝
- 로깅 및 모니터링: 중앙 집중식 로깅 및 모니터링 시스템
- 컨테이너화 및 오케스트레이션: Docker, Kubernetes를 활용한 배포 및 관리
마이크로서비스 아키텍처는 확장성과 유연성을 제공하지만, 복잡성도 증가시킵니다. 따라서 프로젝트의 규모와 요구사항에 따라 적절히 도입하는 것이 중요합니다. 작은 애플리케이션은 모놀리식으로 시작하고, 필요에 따라 점진적으로 마이크로서비스로 전환하는 전략이 효과적일 수 있습니다.