Node.js 인터뷰 질문 74

질문: Node.js를 사용한 마이크로서비스 아키텍처를 어떻게 구현하고, 서비스 간 통신을 어떻게 처리하나요?

답변:

마이크로서비스 아키텍처는 하나의 큰 애플리케이션을 작고 독립적인 서비스로 분리하여 개발하는 방식입니다. Node.js는 경량성과 비동기 특성으로 인해 마이크로서비스 구현에 매우 적합합니다. 이제 Node.js를 사용한 마이크로서비스 구현 방법과 서비스 간 통신 전략에 대해 알아보겠습니다.

1. 마이크로서비스 아키텍처 기본 구조

마이크로서비스 아키텍처는 다음과 같은 구성 요소를 포함합니다:

  1. 독립 서비스: 각각 고유한 비즈니스 기능을 담당
  2. API 게이트웨이: 클라이언트 요청을 적절한 서비스로 라우팅
  3. 서비스 디스커버리: 서비스 위치를 동적으로 찾는 메커니즘
  4. 서비스 간 통신: 동기/비동기 통신 방식
  5. 데이터 관리: 각 서비스별 독립적인 데이터베이스

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를 사용한 마이크로서비스 아키텍처 구현의 핵심 요소:

  1. 서비스 분리: 기능별 독립적인 서비스로 분리하여 개발 및 배포
  2. API 게이트웨이: 클라이언트 요청을 적절한 서비스로 라우팅
  3. 통신 방식:
    • 동기식 통신: HTTP/REST를 통한 직접 호출
    • 비동기식 통신: 메시지 큐를 통한 이벤트 기반 통신
  4. 서비스 디스커버리: Consul, Etcd 등을 사용한 동적 서비스 검색
  5. 데이터 관리: 각 서비스별 독립 데이터베이스 또는 데이터 파티셔닝
  6. 로깅 및 모니터링: 중앙 집중식 로깅 및 모니터링 시스템
  7. 컨테이너화 및 오케스트레이션: Docker, Kubernetes를 활용한 배포 및 관리

마이크로서비스 아키텍처는 확장성과 유연성을 제공하지만, 복잡성도 증가시킵니다. 따라서 프로젝트의 규모와 요구사항에 따라 적절히 도입하는 것이 중요합니다. 작은 애플리케이션은 모놀리식으로 시작하고, 필요에 따라 점진적으로 마이크로서비스로 전환하는 전략이 효과적일 수 있습니다.

results matching ""

    No results matching ""