Node.js 인터뷰 질문 47

질문: Node.js의 EventEmitter 클래스에 대해 설명하고, 이를 활용한 이벤트 기반 프로그래밍의 예시를 들어주세요.

답변:

Node.js의 EventEmitter는 이벤트 기반 프로그래밍의 핵심 클래스로, Node.js의 많은 내장 모듈이 이 클래스를 상속받아 이벤트 기반 아키텍처를 구현합니다. EventEmitter를 사용하면 이벤트를 발생(emit)시키고 이벤트 리스너(listener)를 등록하여 비동기적으로 작업을 처리할 수 있습니다.

EventEmitter의 기본 개념

EventEmitter는 이벤트를 발생시키고 이벤트에 반응하는 콜백 함수(리스너)를 등록하는 메커니즘을 제공합니다. 이는 발행-구독(Publish-Subscribe) 패턴의 구현체라고 볼 수 있습니다.

주요 메서드:

  • emitter.on(eventName, listener): 이벤트 리스너 등록
  • emitter.emit(eventName[, ...args]): 이벤트 발생
  • emitter.once(eventName, listener): 한 번만 실행되는 이벤트 리스너 등록
  • emitter.removeListener(eventName, listener): 특정 이벤트 리스너 제거
  • emitter.removeAllListeners([eventName]): 모든 리스너 제거
  • emitter.listeners(eventName): 특정 이벤트의 리스너 배열 반환
  • emitter.listenerCount(eventName): 특정 이벤트의 리스너 개수 반환

기본 사용법

const EventEmitter = require("events");

// EventEmitter 인스턴스 생성
const myEmitter = new EventEmitter();

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

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

한 번만 실행되는 이벤트 리스너

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

// 한 번만 실행되는 이벤트 리스너 등록
myEmitter.once("oneTimeEvent", () => {
  console.log("이 리스너는 한 번만 호출됩니다.");
});

// 이벤트 두 번 발생
myEmitter.emit("oneTimeEvent"); // 출력: 이 리스너는 한 번만 호출됩니다.
myEmitter.emit("oneTimeEvent"); // 아무것도 출력되지 않음

이벤트 리스너 제거

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

function listener1() {
  console.log("리스너 1 실행");
}

function listener2() {
  console.log("리스너 2 실행");
}

// 이벤트 리스너 등록
myEmitter.on("event", listener1);
myEmitter.on("event", listener2);

// 이벤트 발생
myEmitter.emit("event");
// 출력:
// 리스너 1 실행
// 리스너 2 실행

// 특정 리스너 제거
myEmitter.removeListener("event", listener1);
// 또는 myEmitter.off('event', listener1); (Node.js 10.0.0 이상)

// 이벤트 다시 발생
myEmitter.emit("event");
// 출력: 리스너 2 실행

에러 이벤트 처리

EventEmitter에서 'error' 이벤트는 특별하게 처리됩니다. 'error' 이벤트에 대한 리스너가 없으면, Node.js는 오류를 던지고 프로세스를 종료할 수 있습니다.

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

// 'error' 이벤트 리스너 등록
myEmitter.on("error", (err) => {
  console.error("에러 발생:", err.message);
});

// 에러 이벤트 발생
myEmitter.emit("error", new Error("에러가 발생했습니다!"));
// 출력: 에러 발생: 에러가 발생했습니다!

클래스 상속을 통한 EventEmitter 사용

일반적으로 EventEmitter는 클래스 상속을 통해 사용됩니다. 이를 통해 자신만의 이벤트를 발생시키는 클래스를 만들 수 있습니다.

const EventEmitter = require("events");

// EventEmitter를 상속받는 클래스 생성
class MyStream extends EventEmitter {
  constructor() {
    super();
  }

  write(data) {
    this.emit("data", data);
  }
}

// 인스턴스 생성
const stream = new MyStream();

// 'data' 이벤트 리스너 등록
stream.on("data", (data) => {
  console.log(`데이터 수신: ${data}`);
});

// 데이터 쓰기 및 이벤트 발생
stream.write("Hello, world!");
// 출력: 데이터 수신: Hello, world!

비동기 이벤트 처리

이벤트는 비동기 작업을 처리하는 데 매우 유용합니다. 다음은 파일 읽기 작업이 완료되면 이벤트를 발생시키는 예제입니다.

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

class FileReader extends EventEmitter {
  readFile(file) {
    fs.readFile(file, "utf8", (err, data) => {
      if (err) {
        this.emit("error", err);
        return;
      }
      this.emit("fileRead", data);
    });
  }
}

const reader = new FileReader();

reader.on("fileRead", (data) => {
  console.log("파일 내용:", data);
});

reader.on("error", (err) => {
  console.error("파일 읽기 오류:", err.message);
});

// 파일 읽기 시작
reader.readFile("example.txt");

이벤트 리스너 실행 순서

이벤트 리스너는 등록된 순서대로 실행됩니다. 특정 리스너를 다른 리스너보다 먼저 실행하려면 prependListener 메서드를 사용할 수 있습니다.

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

// 첫 번째 리스너 등록
myEmitter.on("event", () => {
  console.log("리스너 1 실행");
});

// 두 번째 리스너 등록
myEmitter.on("event", () => {
  console.log("리스너 2 실행");
});

// 세 번째 리스너를 리스너 목록의 앞부분에 추가
myEmitter.prependListener("event", () => {
  console.log("리스너 3 실행 (앞부분에 추가)");
});

// 이벤트 발생
myEmitter.emit("event");
// 출력:
// 리스너 3 실행 (앞부분에 추가)
// 리스너 1 실행
// 리스너 2 실행

Node.js 내장 모듈의 EventEmitter 사용 예시

Node.js의 많은 내장 모듈이 EventEmitter를 상속합니다. 대표적인 예로 http.Server, fs.ReadStream, net.Socket 등이 있습니다.

1. HTTP 서버 예제

const http = require("http");

// http.Server는 EventEmitter를 상속받습니다
const server = http.createServer();

// 'request' 이벤트 리스너 등록
server.on("request", (req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello World\n");
});

// 'listening' 이벤트 리스너 등록
server.on("listening", () => {
  console.log("서버가 포트 3000에서 실행 중입니다.");
});

// 서버 시작
server.listen(3000);

2. 파일 스트림 예제

const fs = require("fs");

// fs.ReadStream은 EventEmitter를 상속받습니다
const readStream = fs.createReadStream("example.txt");

readStream.on("open", (fd) => {
  console.log(`파일이 열렸습니다. 파일 디스크립터: ${fd}`);
});

readStream.on("data", (chunk) => {
  console.log(`${chunk.length} 바이트 데이터 수신`);
});

readStream.on("end", () => {
  console.log("파일 읽기 완료");
});

readStream.on("error", (err) => {
  console.error("파일 읽기 오류:", err);
});

실제 사용 사례

1. 작업 진행 상황 보고

작업이 진행됨에 따라 진행 상황을 보고하는 이벤트를 발생시키는 예제입니다.

const EventEmitter = require("events");

class TaskRunner extends EventEmitter {
  constructor(tasks) {
    super();
    this.tasks = tasks;
  }

  run() {
    this.emit("start", this.tasks.length);

    let completed = 0;

    // 각 작업을 비동기적으로 실행
    this.tasks.forEach((task, index) => {
      // 작업 시뮬레이션 (실제로는 데이터베이스 쿼리, API 호출 등)
      setTimeout(() => {
        const result = task();
        completed++;

        // 작업 완료 이벤트 발생
        this.emit("task_complete", {
          taskIndex: index,
          taskResult: result,
          completed: completed,
          total: this.tasks.length,
        });

        // 모든 작업이 완료되면 'end' 이벤트 발생
        if (completed === this.tasks.length) {
          this.emit("end");
        }
      }, Math.random() * 1000); // 랜덤 지연 시간
    });
  }
}

// 사용 예시
const tasks = [
  () => "작업 1 결과",
  () => "작업 2 결과",
  () => "작업 3 결과",
  () => "작업 4 결과",
  () => "작업 5 결과",
];

const runner = new TaskRunner(tasks);

runner.on("start", (totalTasks) => {
  console.log(`총 ${totalTasks}개 작업 시작`);
});

runner.on("task_complete", (info) => {
  const percent = Math.round((info.completed / info.total) * 100);
  console.log(`작업 ${info.taskIndex + 1} 완료: ${info.taskResult}`);
  console.log(`진행률: ${percent}% (${info.completed}/${info.total})`);
});

runner.on("end", () => {
  console.log("모든 작업 완료!");
});

// 작업 실행
runner.run();

2. 사용자 정의 스트림 구현

EventEmitter를 사용하여 사용자 정의 스트림을 구현하는 예제입니다.

const EventEmitter = require("events");

// 간단한 데이터 변환 스트림 구현
class StringTransformer extends EventEmitter {
  constructor() {
    super();
    this.buffer = "";
  }

  // 데이터 입력 처리
  write(data) {
    // 데이터 버퍼링
    this.buffer += data;

    // 라인 단위로 처리
    const lines = this.buffer.split("\n");
    this.buffer = lines.pop(); // 마지막 불완전한 라인 저장

    lines.forEach((line) => {
      // 데이터 변환 및 'data' 이벤트 발생
      const transformed = line.toUpperCase();
      this.emit("data", transformed);
    });

    return true;
  }

  // 스트림 종료
  end(data) {
    if (data) {
      this.write(data);
    }

    // 마지막 버퍼 처리
    if (this.buffer) {
      const transformed = this.buffer.toUpperCase();
      this.emit("data", transformed);
      this.buffer = "";
    }

    this.emit("end");
  }
}

// 사용 예시
const transformer = new StringTransformer();

transformer.on("data", (chunk) => {
  console.log("변환된 데이터:", chunk);
});

transformer.on("end", () => {
  console.log("변환 완료");
});

// 데이터 처리
transformer.write("hello\nworld\nnode");
transformer.end(".js is awesome!");

// 출력:
// 변환된 데이터: HELLO
// 변환된 데이터: WORLD
// 변환된 데이터: NODE.JS IS AWESOME!
// 변환 완료

3. 실시간 알림 시스템

이벤트 기반 아키텍처를 사용하여 실시간 알림 시스템을 구현하는 예제입니다.

const EventEmitter = require("events");

// 알림 관리자 클래스
class NotificationManager extends EventEmitter {
  constructor() {
    super();
    this.users = new Map(); // 사용자별 구독 정보 저장
  }

  // 사용자 구독 등록
  subscribe(userId, channels) {
    if (!this.users.has(userId)) {
      this.users.set(userId, new Set());
    }

    channels.forEach((channel) => {
      this.users.get(userId).add(channel);
    });

    this.emit("user_subscribed", { userId, channels });
  }

  // 사용자 구독 취소
  unsubscribe(userId, channels) {
    if (!this.users.has(userId)) return;

    channels.forEach((channel) => {
      this.users.get(userId).delete(channel);
    });

    this.emit("user_unsubscribed", { userId, channels });
  }

  // 특정 채널에 알림 발송
  notify(channel, message) {
    // 해당 채널을 구독한 모든 사용자 찾기
    const notifiedUsers = [];

    this.users.forEach((subscribedChannels, userId) => {
      if (subscribedChannels.has(channel)) {
        // 실제로는 여기서 사용자에게 알림 전송
        // (푸시 알림, 이메일, 웹소켓 메시지 등)
        notifiedUsers.push(userId);
      }
    });

    this.emit("notification_sent", {
      channel,
      message,
      userCount: notifiedUsers.length,
      users: notifiedUsers,
    });

    return notifiedUsers;
  }
}

// 사용 예시
const notifier = new NotificationManager();

// 이벤트 리스너 등록
notifier.on("user_subscribed", (data) => {
  console.log(
    `사용자 ${data.userId}${data.channels.join(", ")} 채널을 구독했습니다.`
  );
});

notifier.on("notification_sent", (data) => {
  console.log(`채널 ${data.channel}에 알림 발송: "${data.message}"`);
  console.log(
    `총 ${data.userCount}명의 사용자에게 전송됨: ${data.users.join(", ")}`
  );
});

// 사용자 구독 등록
notifier.subscribe("user1", ["news", "sports"]);
notifier.subscribe("user2", ["news", "tech"]);
notifier.subscribe("user3", ["sports", "entertainment"]);

// 알림 발송
notifier.notify("news", "주요 뉴스: Node.js 17 출시!");
notifier.notify("sports", "스포츠 소식: 올림픽 개막!");

EventEmitter 사용 시 주의사항

  1. 메모리 누수 방지: 이벤트 리스너를 너무 많이 등록하면 메모리 누수가 발생할 수 있습니다. removeListener를 사용하여 더 이상 필요하지 않은 리스너를 제거하세요.

  2. 리스너 제한: 기본적으로 하나의 이벤트에 최대 10개의 리스너를 등록할 수 있습니다. 더 많은 리스너가 필요한 경우 emitter.setMaxListeners(n)을 사용하여 제한을 높일 수 있습니다.

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

// 최대 리스너 수 확인
console.log(myEmitter.getMaxListeners()); // 기본값: 10

// 최대 리스너 수 변경
myEmitter.setMaxListeners(20);
console.log(myEmitter.getMaxListeners()); // 20
  1. 비동기 실행: 이벤트 리스너는 동기적으로 호출됩니다. 긴 작업이나 I/O 작업은 프로세스를 차단할 수 있으므로, setImmediate()process.nextTick()을 사용하여 비동기적으로 실행하는 것이 좋습니다.
const EventEmitter = require("events");
const myEmitter = new EventEmitter();

// 비동기 이벤트 리스너
myEmitter.on("event", (data) => {
  setImmediate(() => {
    // 시간이 오래 걸리는 작업
    console.log("비동기적으로 처리:", data);
  });
});

myEmitter.emit("event", "some data");
console.log("이벤트 발생 후 즉시 실행");

// 출력 순서:
// 1. 이벤트 발생 후 즉시 실행
// 2. 비동기적으로 처리: some data
  1. 에러 이벤트 처리: 항상 'error' 이벤트 리스너를 등록하여 예상치 못한 프로세스 종료를 방지하세요.

요약

Node.js의 EventEmitter는 이벤트 기반 프로그래밍을 위한 핵심 클래스로, 다음과 같은 특징을 가집니다:

  1. 이벤트 발행-구독 패턴: 이벤트를 발생시키고(emit) 이벤트 리스너를 등록하여 처리합니다.

  2. 비동기 작업 처리: 이벤트를 통해 비동기 작업을 효과적으로 관리할 수 있습니다.

  3. 내장 모듈과의 통합: Node.js의 많은 내장 모듈이 EventEmitter를 상속받아 이벤트 기반 인터페이스를 제공합니다.

  4. 유연한 이벤트 관리: 다양한 메서드를 통해 이벤트 리스너를 추가, 제거, 관리할 수 있습니다.

EventEmitter는 복잡한 비동기 워크플로우를 관리하거나, 상태 변화를 알리거나, 느슨하게 결합된 컴포넌트 간의 통신을 구현하는 데 매우 유용합니다. Node.js의 비동기 이벤트 기반 아키텍처를 효과적으로 활용하기 위해서는 EventEmitter의 사용법과 패턴을 잘 이해하는 것이 중요합니다.

results matching ""

    No results matching ""