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 마이크로서비스 아키텍처의 주요 구성 요소:

  1. 서비스 디스커버리

    • 서비스 등록
    • 서비스 검색
    • 헬스 체크
  2. API 게이트웨이

    • 라우팅
    • 로드 밸런싱
    • 인증/인가
  3. 서비스 간 통신

    • 메시지 큐
    • 이벤트 기반 통신
    • 동기/비동기 통신
  4. 장애 처리

    • 서킷 브레이커
    • 재시도 메커니즘
    • 폴백 전략
  5. 데이터 관리

    • 분산 트랜잭션
    • 데이터 일관성
    • 보상 트랜잭션

results matching ""

    No results matching ""