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에서 마이크로서비스 아키텍처를 구현할 때 고려해야 할 주요 사항:

  1. 서비스 분리

    • 각 서비스는 독립적으로 개발, 배포, 확장 가능
    • 명확한 책임과 경계 정의
  2. 통신 패턴

    • REST API를 통한 동기식 통신
    • 메시지 큐를 통한 비동기식 통신
    • 서비스 디스커버리와 로드 밸런싱
  3. 데이터 관리

    • 데이터베이스 분리
    • 데이터 일관성 유지
    • 분산 트랜잭션 처리
  4. 장애 처리

    • 서킷 브레이커 패턴
    • 폴백 메커니즘
    • 재시도 전략
  5. 모니터링과 로깅

    • 중앙 집중식 로깅
    • 분산 추적
    • 성능 메트릭 수집
  6. 배포와 확장

    • 컨테이너화
    • 자동화된 배포
    • 수평적 확장

results matching ""

    No results matching ""