Node.js 인터뷰 질문 90

질문: Node.js에서 자주 사용되는 디자인 패턴들과 그 구현 방법에 대해 설명해주세요.

답변:

Node.js에서는 다양한 디자인 패턴을 활용하여 코드의 재사용성, 유지보수성, 확장성을 향상시킬 수 있습니다. 주요 디자인 패턴들과 그 구현 방법을 살펴보겠습니다.

1. 싱글톤 패턴 (Singleton Pattern)

전역적으로 하나의 인스턴스만 유지하는 패턴입니다.

// database.js
class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }

    this.connected = false;
    Database.instance = this;
  }

  connect() {
    if (this.connected) {
      return Promise.resolve(this);
    }

    return new Promise((resolve, reject) => {
      // 데이터베이스 연결 로직
      setTimeout(() => {
        this.connected = true;
        resolve(this);
      }, 1000);
    });
  }

  query(sql) {
    if (!this.connected) {
      throw new Error("데이터베이스에 연결되어 있지 않습니다");
    }
    // 쿼리 실행 로직
  }
}

// 싱글톤 인스턴스 내보내기
module.exports = new Database();

// 사용 예시
const db = require("./database");

async function main() {
  await db.connect();
  const result = await db.query("SELECT * FROM users");
}

2. 팩토리 패턴 (Factory Pattern)

객체 생성 로직을 캡슐화하는 패턴입니다.

// payment-processor.js
class PaymentProcessor {
  constructor(type) {
    this.type = type;
  }

  process(amount) {
    throw new Error("process 메서드를 구현해야 합니다");
  }
}

class CreditCardProcessor extends PaymentProcessor {
  process(amount) {
    console.log(`신용카드로 ${amount}원 결제 처리`);
  }
}

class PayPalProcessor extends PaymentProcessor {
  process(amount) {
    console.log(`PayPal로 ${amount}원 결제 처리`);
  }
}

class BankTransferProcessor extends PaymentProcessor {
  process(amount) {
    console.log(`계좌이체로 ${amount}원 결제 처리`);
  }
}

// 팩토리 클래스
class PaymentProcessorFactory {
  static createProcessor(type) {
    switch (type) {
      case "credit-card":
        return new CreditCardProcessor();
      case "paypal":
        return new PayPalProcessor();
      case "bank-transfer":
        return new BankTransferProcessor();
      default:
        throw new Error("지원하지 않는 결제 방식입니다");
    }
  }
}

// 사용 예시
const processor = PaymentProcessorFactory.createProcessor("credit-card");
processor.process(50000);

3. 옵저버 패턴 (Observer Pattern)

이벤트 기반 프로그래밍을 위한 패턴입니다.

// event-system.js
const EventEmitter = require("events");

class OrderSystem extends EventEmitter {
  constructor() {
    super();
    this.orders = new Map();
  }

  createOrder(orderId, items) {
    this.orders.set(orderId, {
      id: orderId,
      items,
      status: "pending",
    });

    this.emit("orderCreated", this.orders.get(orderId));
  }

  updateOrderStatus(orderId, status) {
    const order = this.orders.get(orderId);
    if (!order) {
      throw new Error("주문을 찾을 수 없습니다");
    }

    order.status = status;
    this.emit("orderStatusUpdated", order);
  }
}

// 옵저버들
class EmailNotifier {
  onOrderCreated(order) {
    console.log(`주문 생성 이메일 발송: 주문번호 ${order.id}`);
  }

  onOrderStatusUpdated(order) {
    console.log(
      `주문 상태 변경 이메일 발송: 주문번호 ${order.id}, 상태 ${order.status}`
    );
  }
}

class InventoryManager {
  onOrderCreated(order) {
    console.log(`재고 확인 중: ${order.items.length}개 상품`);
  }
}

// 사용 예시
const orderSystem = new OrderSystem();
const emailNotifier = new EmailNotifier();
const inventoryManager = new InventoryManager();

orderSystem.on("orderCreated", (order) => emailNotifier.onOrderCreated(order));
orderSystem.on("orderCreated", (order) =>
  inventoryManager.onOrderCreated(order)
);
orderSystem.on("orderStatusUpdated", (order) =>
  emailNotifier.onOrderStatusUpdated(order)
);

orderSystem.createOrder("ORDER-123", [
  { id: "ITEM-1", quantity: 2 },
  { id: "ITEM-2", quantity: 1 },
]);

orderSystem.updateOrderStatus("ORDER-123", "processing");

4. 전략 패턴 (Strategy Pattern)

알고리즘을 캡슐화하고 런타임에 교체 가능하게 하는 패턴입니다.

// validation-strategy.js
class ValidationStrategy {
  validate(data) {
    throw new Error("validate 메서드를 구현해야 합니다");
  }
}

class UserValidationStrategy extends ValidationStrategy {
  validate(data) {
    if (!data.email || !data.password) {
      throw new Error("이메일과 비밀번호는 필수입니다");
    }

    if (data.password.length < 8) {
      throw new Error("비밀번호는 8자 이상이어야 합니다");
    }

    return true;
  }
}

class ProductValidationStrategy extends ValidationStrategy {
  validate(data) {
    if (!data.name || !data.price) {
      throw new Error("상품명과 가격은 필수입니다");
    }

    if (data.price <= 0) {
      throw new Error("가격은 0보다 커야 합니다");
    }

    return true;
  }
}

// 컨텍스트 클래스
class Validator {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  validate(data) {
    return this.strategy.validate(data);
  }
}

// 사용 예시
const validator = new Validator(new UserValidationStrategy());

try {
  validator.validate({
    email: "user@example.com",
    password: "short",
  });
} catch (error) {
  console.error(error.message); // "비밀번호는 8자 이상이어야 합니다"
}

validator.setStrategy(new ProductValidationStrategy());

try {
  validator.validate({
    name: "Product 1",
    price: -100,
  });
} catch (error) {
  console.error(error.message); // "가격은 0보다 커야 합니다"
}

5. 미들웨어 패턴 (Middleware Pattern)

요청 처리 파이프라인을 구성하는 패턴입니다.

// middleware-chain.js
class MiddlewareChain {
  constructor() {
    this.middlewares = [];
  }

  use(middleware) {
    this.middlewares.push(middleware);
    return this;
  }

  execute(data) {
    let index = 0;

    const next = (err) => {
      if (err) {
        return Promise.reject(err);
      }

      const middleware = this.middlewares[index++];
      if (!middleware) {
        return Promise.resolve();
      }

      try {
        return Promise.resolve(middleware(data, next));
      } catch (err) {
        return Promise.reject(err);
      }
    };

    return next();
  }
}

// 사용 예시
const chain = new MiddlewareChain();

// 로깅 미들웨어
chain.use(async (data, next) => {
  console.log("요청 데이터:", data);
  await next();
  console.log("응답 완료");
});

// 인증 미들웨어
chain.use(async (data, next) => {
  if (!data.token) {
    throw new Error("인증이 필요합니다");
  }
  data.user = { id: 1, name: "John" };
  await next();
});

// 비즈니스 로직 미들웨어
chain.use(async (data, next) => {
  console.log("사용자 처리:", data.user);
  await next();
});

// 실행
chain
  .execute({ token: "valid-token" })
  .then(() => console.log("처리 완료"))
  .catch((err) => console.error("오류:", err.message));

6. 데코레이터 패턴 (Decorator Pattern)

객체에 동적으로 기능을 추가하는 패턴입니다.

// service-decorator.js
class UserService {
  async getUser(id) {
    // 데이터베이스에서 사용자 조회
    return { id, name: "John Doe" };
  }
}

// 캐싱 데코레이터
class CachingDecorator {
  constructor(service) {
    this.service = service;
    this.cache = new Map();
  }

  async getUser(id) {
    if (this.cache.has(id)) {
      console.log("캐시에서 사용자 조회");
      return this.cache.get(id);
    }

    const user = await this.service.getUser(id);
    this.cache.set(id, user);
    return user;
  }
}

// 로깅 데코레이터
class LoggingDecorator {
  constructor(service) {
    this.service = service;
  }

  async getUser(id) {
    console.log(`사용자 조회 시작: ${id}`);
    const startTime = Date.now();

    try {
      const result = await this.service.getUser(id);
      console.log(`사용자 조회 완료: ${Date.now() - startTime}ms`);
      return result;
    } catch (error) {
      console.error(`사용자 조회 실패: ${error.message}`);
      throw error;
    }
  }
}

// 사용 예시
const userService = new UserService();
const cachedService = new CachingDecorator(userService);
const loggingService = new LoggingDecorator(cachedService);

async function main() {
  // 첫 번째 호출: 캐시 미스
  await loggingService.getUser(1);

  // 두 번째 호출: 캐시 히트
  await loggingService.getUser(1);
}

7. 프록시 패턴 (Proxy Pattern)

객체에 대한 접근을 제어하는 패턴입니다.

// api-proxy.js
class API {
  async request(url) {
    // 실제 API 요청 로직
    return fetch(url).then((res) => res.json());
  }
}

// 프록시 클래스
class APIProxy {
  constructor(api) {
    this.api = api;
    this.cache = new Map();
  }

  async request(url) {
    // 캐시 확인
    if (this.cache.has(url)) {
      return this.cache.get(url);
    }

    // 속도 제한 확인
    if (this.isRateLimited(url)) {
      throw new Error("너무 많은 요청을 보냈습니다");
    }

    // 실제 API 호출
    const response = await this.api.request(url);

    // 캐시 저장
    this.cache.set(url, response);

    // 속도 제한 기록
    this.recordRequest(url);

    return response;
  }

  isRateLimited(url) {
    // 속도 제한 로직 구현
    return false;
  }

  recordRequest(url) {
    // 요청 기록 로직 구현
  }
}

// 사용 예시
const api = new API();
const proxy = new APIProxy(api);

async function fetchData() {
  try {
    const data = await proxy.request("https://api.example.com/data");
    console.log("데이터:", data);
  } catch (error) {
    console.error("오류:", error.message);
  }
}

요약

Node.js에서 자주 사용되는 디자인 패턴들:

  1. 싱글톤 패턴

    • 전역 상태 관리
    • 리소스 공유
    • 데이터베이스 연결
  2. 팩토리 패턴

    • 객체 생성 로직 캡슐화
    • 유연한 인스턴스 생성
    • 의존성 관리
  3. 옵저버 패턴

    • 이벤트 기반 프로그래밍
    • 느슨한 결합
    • 실시간 업데이트
  4. 전략 패턴

    • 알고리즘 교체 가능
    • 유연한 동작 변경
    • 코드 재사용
  5. 미들웨어 패턴

    • 요청 처리 파이프라인
    • 모듈식 기능 추가
    • 관심사 분리
  6. 데코레이터 패턴

    • 동적 기능 추가
    • 기존 코드 수정 없이 확장
    • 책임 분리
  7. 프록시 패턴

    • 접근 제어
    • 캐싱
    • 로깅

results matching ""

    No results matching ""