Node.js 인터뷰 질문 82

질문: Node.js의 이벤트 이미터(Event Emitters)에 대해 설명하고, 실제 애플리케이션에서 어떻게 활용할 수 있는지 예제와 함께 설명해주세요.

답변:

Node.js의 이벤트 이미터(Event Emitters)는 Node.js의 핵심 아키텍처 패턴으로, 옵저버 패턴을 구현한 것입니다. 이벤트 기반 비동기 프로그래밍을 가능하게 하며, Node.js 자체와 많은 내장 모듈이 이벤트 이미터 기반으로 작동합니다.

1. 이벤트 이미터의 기본 개념

이벤트 이미터는 이벤트를 발생(emit)시키고, 해당 이벤트에 대한 리스너(listener)를 등록하여 이벤트가 발생했을 때 특정 콜백 함수가 실행되도록 하는 메커니즘입니다.

const EventEmitter = require("events");

// 이벤트 이미터 인스턴스 생성
const myEmitter = new EventEmitter();

// 이벤트 리스너 등록
myEmitter.on("event", function (a, b) {
  console.log("이벤트 발생!", a, b);
});

// 이벤트 발생
myEmitter.emit("event", "a", "b");
// 출력: 이벤트 발생! a b

2. 이벤트 이미터의 주요 메서드

const EventEmitter = require("events");
const emitter = new EventEmitter();

// 이벤트 리스너 등록
emitter.on("event", () => console.log("이벤트 발생"));

// 한 번만 실행되는 이벤트 리스너 등록
emitter.once("oneTimeEvent", () => console.log("한 번만 실행됨"));

// 리스너 최대 개수 설정 (기본값: 10)
emitter.setMaxListeners(15);

// 특정 이벤트의 모든 리스너 제거
emitter.removeAllListeners("event");

// 특정 리스너 제거
const listener = () => console.log("리스너");
emitter.on("event", listener);
emitter.removeListener("event", listener); // 또는 emitter.off('event', listener)

// 현재 등록된 이벤트 목록 조회
console.log(emitter.eventNames());

// 특정 이벤트의 리스너 개수 확인
console.log(emitter.listenerCount("event"));

// 특정 이벤트의 리스너 배열 가져오기
console.log(emitter.listeners("event"));

3. 커스텀 이벤트 이미터 구현

const EventEmitter = require("events");

// 커스텀 이벤트 이미터 클래스 정의
class MyApp extends EventEmitter {
  constructor() {
    super();
    this.init();
  }

  init() {
    // 초기화 작업
    this.on("error", this.handleError);
  }

  process(data) {
    // 유효성 검사
    if (!data) {
      this.emit("error", new Error("데이터가 없습니다"));
      return;
    }

    // 데이터 처리
    console.log("데이터 처리:", data);

    // 처리 완료 이벤트 발생
    this.emit("processed", data);
  }

  handleError(err) {
    console.error("오류 발생:", err.message);
  }
}

// 사용 예시
const app = new MyApp();

// 처리 완료 이벤트 리스너 등록
app.on("processed", (data) => {
  console.log("처리 완료:", data);
});

// 데이터 처리
app.process("테스트 데이터");
app.process(); // 오류 발생

4. 비동기 작업 관리에 이벤트 이미터 활용

const EventEmitter = require("events");
const fs = require("fs");

class FileProcessor extends EventEmitter {
  constructor(directory) {
    super();
    this.directory = directory;
    this.processedFiles = 0;
  }

  processFiles(fileList) {
    this.totalFiles = fileList.length;
    this.emit("start", { total: this.totalFiles });

    fileList.forEach((file, index) => {
      const filePath = `${this.directory}/${file}`;

      // 비동기 파일 읽기
      fs.readFile(filePath, "utf8", (err, content) => {
        if (err) {
          this.emit("error", { file, error: err });
          return;
        }

        // 파일 처리 로직
        const processedContent = content.toUpperCase();

        // 비동기 파일 쓰기
        fs.writeFile(`${filePath}.processed`, processedContent, (err) => {
          if (err) {
            this.emit("error", { file, error: err });
            return;
          }

          this.processedFiles++;

          // 진행 상황 이벤트 발생
          const progress = Math.round(
            (this.processedFiles / this.totalFiles) * 100
          );
          this.emit("progress", {
            file,
            processed: this.processedFiles,
            total: this.totalFiles,
            percent: progress,
          });

          // 모든 파일 처리 완료 확인
          if (this.processedFiles === this.totalFiles) {
            this.emit("complete", {
              processed: this.processedFiles,
              total: this.totalFiles,
            });
          }
        });
      });
    });
  }
}

// 사용 예시
const processor = new FileProcessor("./data");

// 이벤트 리스너 등록
processor.on("start", (info) => {
  console.log(`파일 처리 시작: 총 ${info.total}개 파일`);
});

processor.on("progress", (info) => {
  console.log(`진행 상황: ${info.percent}% (${info.processed}/${info.total})`);
});

processor.on("error", (info) => {
  console.error(`오류 발생 (${info.file}):`, info.error);
});

processor.on("complete", (info) => {
  console.log(`모든 파일 처리 완료: ${info.processed}개 파일`);
});

// 파일 처리 시작
processor.processFiles(["file1.txt", "file2.txt", "file3.txt"]);

5. 실제 애플리케이션 사례

5.1 HTTP 서버 모니터링

const http = require("http");
const EventEmitter = require("events");

class ServerMonitor extends EventEmitter {
  constructor() {
    super();
    this.requests = 0;
    this.errors = 0;
    this.startTime = Date.now();
  }

  trackRequest(req, res) {
    const requestId = ++this.requests;
    const startTime = Date.now();
    const method = req.method;
    const url = req.url;

    // 요청 시작 이벤트
    this.emit("requestStart", { requestId, method, url });

    // 응답 완료 시 이벤트 발생
    res.on("finish", () => {
      const duration = Date.now() - startTime;
      const statusCode = res.statusCode;

      if (statusCode >= 400) {
        this.errors++;
        this.emit("requestError", {
          requestId,
          method,
          url,
          statusCode,
          duration,
        });
      }

      this.emit("requestComplete", {
        requestId,
        method,
        url,
        statusCode,
        duration,
      });

      // 통계 수집
      this.emit("stats", this.getStats());
    });

    return requestId;
  }

  getStats() {
    const uptime = (Date.now() - this.startTime) / 1000;
    return {
      uptime,
      requests: this.requests,
      errors: this.errors,
      rps: this.requests / uptime,
      errorRate: (this.errors / this.requests) * 100,
    };
  }
}

// 서버 설정
const monitor = new ServerMonitor();
const server = http.createServer((req, res) => {
  const requestId = monitor.trackRequest(req, res);

  // 요청 처리 로직
  if (req.url === "/") {
    res.statusCode = 200;
    res.end("Hello World");
  } else if (req.url === "/error") {
    res.statusCode = 500;
    res.end("Server Error");
  } else {
    res.statusCode = 404;
    res.end("Not Found");
  }
});

// 이벤트 리스너 등록
monitor.on("requestStart", (data) => {
  console.log(`요청 시작 [${data.requestId}]: ${data.method} ${data.url}`);
});

monitor.on("requestError", (data) => {
  console.error(
    `요청 오류 [${data.requestId}]: ${data.statusCode} - ${data.method} ${data.url} (${data.duration}ms)`
  );
});

monitor.on("stats", (stats) => {
  // 주기적으로 통계 로깅 (실제로는 특정 간격으로 필터링)
  if (stats.requests % 100 === 0) {
    console.log("서버 통계:", stats);
  }
});

server.listen(3000, () => {
  console.log("서버가 포트 3000에서 실행 중입니다");
});

5.2 데이터베이스 연결 관리

const EventEmitter = require("events");
const mysql = require("mysql2/promise");

class Database extends EventEmitter {
  constructor(config) {
    super();
    this.config = config;
    this.pool = null;
    this.isConnected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
  }

  async connect() {
    try {
      this.pool = mysql.createPool(this.config);

      // 연결 테스트
      const connection = await this.pool.getConnection();
      connection.release();

      this.isConnected = true;
      this.reconnectAttempts = 0;
      this.emit("connect", { time: new Date() });

      console.log("데이터베이스에 연결되었습니다");
      return true;
    } catch (error) {
      this.isConnected = false;
      this.emit("error", {
        error,
        time: new Date(),
        message: "데이터베이스 연결 오류",
      });

      // 재연결 시도
      await this.reconnect();
      return false;
    }
  }

  async reconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      this.emit("reconnectFailed", {
        attempts: this.reconnectAttempts,
        time: new Date(),
      });

      console.error(
        `최대 재연결 시도 횟수(${this.maxReconnectAttempts})를 초과했습니다`
      );
      return false;
    }

    this.reconnectAttempts++;

    this.emit("reconnect", {
      attempt: this.reconnectAttempts,
      max: this.maxReconnectAttempts,
      time: new Date(),
    });

    console.log(
      `데이터베이스 재연결 시도 중... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`
    );

    // 지수 백오프를 사용한 재시도 지연
    const delay = Math.min(
      1000 * Math.pow(2, this.reconnectAttempts - 1),
      30000
    );
    await new Promise((resolve) => setTimeout(resolve, delay));

    return this.connect();
  }

  async query(sql, params = []) {
    if (!this.isConnected) {
      await this.connect();
    }

    try {
      const [results] = await this.pool.query(sql, params);

      this.emit("query", {
        sql,
        params,
        time: new Date(),
        rowCount: results.length || results.affectedRows,
      });

      return results;
    } catch (error) {
      this.emit("queryError", {
        sql,
        params,
        error,
        time: new Date(),
      });

      // 연결 오류인 경우 재연결 시도
      if (
        error.code === "PROTOCOL_CONNECTION_LOST" ||
        error.code === "ECONNRESET"
      ) {
        this.isConnected = false;
        await this.reconnect();
      }

      throw error;
    }
  }

  async end() {
    if (this.pool) {
      await this.pool.end();
      this.isConnected = false;
      this.emit("disconnect", { time: new Date() });
      console.log("데이터베이스 연결이 종료되었습니다");
    }
  }
}

// 사용 예시
async function main() {
  const db = new Database({
    host: "localhost",
    user: "user",
    password: "password",
    database: "mydatabase",
  });

  // 이벤트 리스너 등록
  db.on("connect", (info) => {
    console.log(`데이터베이스 연결됨: ${info.time}`);
  });

  db.on("error", (info) => {
    console.error(`데이터베이스 오류: ${info.message}`, info.error);
  });

  db.on("query", (info) => {
    console.log(`쿼리 실행: ${info.sql} (${info.rowCount}개 결과)`);
  });

  // 데이터베이스 작업
  try {
    await db.connect();

    const users = await db.query("SELECT * FROM users WHERE active = ?", [
      true,
    ]);
    console.log(`${users.length}명의 활성 사용자가 있습니다`);

    await db.end();
  } catch (error) {
    console.error("데이터베이스 작업 오류:", error);
  }
}

main();

6. 이벤트 이미터의 고급 패턴

6.1 이벤트 공유 및 전파

const EventEmitter = require("events");

// 전역 이벤트 버스 생성
const eventBus = new EventEmitter();

// 여러 모듈에서 이벤트 공유
class UserService extends EventEmitter {
  constructor(eventBus) {
    super();
    this.eventBus = eventBus;

    // 로컬 이벤트 리스너
    this.on("userAction", (data) => {
      console.log("UserService에서 처리:", data);

      // 전역 이벤트 버스로 이벤트 전파
      this.eventBus.emit("globalUserAction", {
        ...data,
        service: "UserService",
        time: new Date(),
      });
    });
  }

  createUser(userData) {
    // 사용자 생성 로직
    const user = { id: Date.now(), ...userData };

    // 로컬 이벤트 발생
    this.emit("userAction", {
      action: "create",
      user,
    });

    return user;
  }
}

// 다른 모듈에서 이벤트 구독
class NotificationService {
  constructor(eventBus) {
    this.eventBus = eventBus;

    // 전역 이벤트 구독
    this.eventBus.on("globalUserAction", (data) => {
      if (data.action === "create") {
        this.sendWelcomeMessage(data.user);
      }
    });
  }

  sendWelcomeMessage(user) {
    console.log(`${user.name}님, 환영합니다! 가입을 축하합니다.`);
  }
}

// 사용 예시
const userService = new UserService(eventBus);
const notificationService = new NotificationService(eventBus);

// 사용자 생성 시 환영 메시지가 자동으로 전송됨
userService.createUser({ name: "홍길동", email: "hong@example.com" });

6.2 이벤트 기반 상태 관리

const EventEmitter = require("events");

class Store extends EventEmitter {
  constructor(initialState = {}) {
    super();
    this._state = initialState;
  }

  get state() {
    return { ...this._state }; // 불변성을 위한 복사본 반환
  }

  setState(partialState) {
    // 이전 상태 저장
    const previousState = { ...this._state };

    // 상태 업데이트
    this._state = {
      ...this._state,
      ...partialState,
    };

    // 변경된 속성 확인
    const changedProps = Object.keys(partialState).filter(
      (key) => previousState[key] !== this._state[key]
    );

    if (changedProps.length > 0) {
      // 변경 이벤트 발생
      this.emit("stateChange", {
        previousState,
        currentState: this.state,
        changedProps,
      });

      // 개별 속성 변경 이벤트
      changedProps.forEach((prop) => {
        this.emit(`change:${prop}`, {
          property: prop,
          previousValue: previousState[prop],
          currentValue: this._state[prop],
        });
      });
    }
  }

  // 특정 상태 변경 구독
  onStateChange(callback) {
    this.on("stateChange", callback);
    return () => this.off("stateChange", callback); // 구독 취소 함수 반환
  }

  // 특정 속성 변경 구독
  onPropertyChange(property, callback) {
    const eventName = `change:${property}`;
    this.on(eventName, callback);
    return () => this.off(eventName, callback); // 구독 취소 함수 반환
  }
}

// 사용 예시
const userStore = new Store({
  name: "",
  email: "",
  isLoggedIn: false,
});

// 상태 변경 이벤트 구독
userStore.onStateChange(({ currentState, changedProps }) => {
  console.log("상태가 변경되었습니다:", changedProps);
  console.log("현재 상태:", currentState);
});

// 특정 속성 변경 구독
const unsubscribe = userStore.onPropertyChange(
  "isLoggedIn",
  ({ currentValue }) => {
    if (currentValue) {
      console.log("사용자가 로그인했습니다");
    } else {
      console.log("사용자가 로그아웃했습니다");
    }
  }
);

// 상태 변경
userStore.setState({ name: "홍길동", email: "hong@example.com" });
userStore.setState({ isLoggedIn: true });

// 구독 취소
unsubscribe();

// 구독 취소 후에는 이벤트가 처리되지 않음
userStore.setState({ isLoggedIn: false });

7. 이벤트 이미터 사용 시 주의사항

7.1 메모리 누수 방지

const EventEmitter = require("events");
const emitter = new EventEmitter();

// 잘못된 사용 예시 (메모리 누수 가능성)
function createHandler() {
  const resource = new Array(10000).fill("데이터");

  return function handler() {
    console.log("리소스 크기:", resource.length);
  };
}

// 이벤트 핸들러 등록 (이벤트 구독 취소 없음)
emitter.on("data", createHandler());

// 올바른 사용 예시
function setupHandler(emitter) {
  const resource = new Array(10000).fill("데이터");

  function handler() {
    console.log("리소스 크기:", resource.length);
  }

  emitter.on("data", handler);

  // 정리 함수 반환
  return function cleanup() {
    emitter.off("data", handler);
    resource.length = 0; // 리소스 정리
  };
}

const cleanup = setupHandler(emitter);

// 사용 완료 후 정리
cleanup();

7.2 오류 처리

const EventEmitter = require("events");
const emitter = new EventEmitter();

// 오류 이벤트 핸들러를 등록하지 않으면 프로세스가 종료될 수 있음
emitter.on("error", (err) => {
  console.error("오류 이벤트 발생:", err.message);
  // 적절한 오류 처리
});

// 이벤트 핸들러에서 발생한 예외 처리
emitter.on("data", (data) => {
  try {
    // 오류 발생 가능성이 있는 코드
    const result = JSON.parse(data);
    console.log("처리 결과:", result);
  } catch (err) {
    // 오류를 이벤트로 전파
    emitter.emit("error", new Error(`데이터 처리 오류: ${err.message}`));
  }
});

// 잘못된 JSON 데이터 전송
emitter.emit("data", "{잘못된 JSON}");

7.3 이벤트 리스너 제한 관리

const EventEmitter = require("events");

// 기본적으로 EventEmitter는 이벤트당 최대 10개의 리스너 허용
const emitter = new EventEmitter();

// 경고 활성화
emitter.setMaxListeners(2);

// 3개의 리스너 등록
emitter.on("event", () => console.log("리스너 1"));
emitter.on("event", () => console.log("리스너 2"));
emitter.on("event", () => console.log("리스너 3")); // 경고 발생

// 최대 리스너 수 늘리기
emitter.setMaxListeners(15);

// 주의: 리스너 제한 증가는 메모리 누수의 징후일 수 있음
// 많은 수의 리스너가 필요한 경우 아키텍처 재검토 고려

요약

Node.js의 이벤트 이미터는 비동기, 이벤트 기반 프로그래밍의 핵심 메커니즘으로 다음과 같은 이점을 제공합니다:

  1. 느슨한 결합: 컴포넌트 간의 의존성 감소로 모듈화 및 유지보수 용이
  2. 비동기 작업 관리: 비동기 작업의 진행 상황과 완료를 처리하는 일관된 방법 제공
  3. 확장성: 이벤트 구독자를 쉽게 추가하거나 제거 가능
  4. 코드 구성: 관심사 분리를 통한 코드 구성 개선

실제 애플리케이션에서는 네트워크 서버, 데이터베이스 연결 관리, 파일 처리, 사용자 인터페이스 이벤트 등 다양한 상황에서 이벤트 이미터 패턴을 활용할 수 있습니다.

이벤트 이미터를 효과적으로 사용하면 유지보수가 용이하고 확장성이 뛰어난 Node.js 애플리케이션을 구축할 수 있습니다. 다만, 메모리 누수와 오류 처리에 주의하여 설계해야 합니다.

results matching ""

    No results matching ""