Node.js 인터뷰 질문 83

질문: Node.js에서 미들웨어 패턴이란 무엇이며, Express.js와 같은 프레임워크에서 어떻게 구현되고 활용되는지 설명해주세요.

답변:

미들웨어 패턴은 Node.js 웹 애플리케이션에서 HTTP 요청과 응답 사이에 기능을 추가하는 방식으로, 요청 처리 파이프라인을 구성하는 핵심 개념입니다. 특히 Express.js와 같은 웹 프레임워크에서 광범위하게 사용됩니다.

1. 미들웨어의 기본 개념

미들웨어는 요청 객체(req), 응답 객체(res), 그리고 다음 미들웨어 함수를 호출하는 next 함수에 접근할 수 있는 함수입니다.

// 기본 미들웨어 형태
function middleware(req, res, next) {
  // 요청 또는 응답 객체 조작
  req.customProperty = "value";

  // 다음 미들웨어로 제어 전달
  next();
}

2. Express.js에서의 미들웨어

2.1 기본 미들웨어 사용법

const express = require("express");
const app = express();

// 애플리케이션 레벨 미들웨어
app.use((req, res, next) => {
  console.log("요청 시간:", Date.now());
  next();
});

// 특정 경로에 적용되는 미들웨어
app.use("/api", (req, res, next) => {
  console.log("API 요청 발생");
  next();
});

// 라우트 핸들러 (종점 미들웨어)
app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(3000);

2.2 오류 처리 미들웨어

// 일반 미들웨어
app.use((req, res, next) => {
  // 오류 발생 시
  if (!req.headers.authorization) {
    // next에 인자를 전달하면 오류 처리 미들웨어로 건너뜀
    return next(new Error("인증 필요"));
  }
  next();
});

// 오류 처리 미들웨어 (4개의 인자를 가짐)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send("서버 오류 발생!");
});

3. 미들웨어 유형과 사용 사례

3.1 내장 미들웨어

Express.js는 몇 가지 내장 미들웨어를 제공합니다:

// JSON 요청 바디 파싱
app.use(express.json());

// URL 인코딩된 바디 파싱
app.use(express.urlencoded({ extended: true }));

// 정적 파일 제공
app.use(express.static("public"));

3.2 써드파티 미들웨어

const morgan = require("morgan");
const cors = require("cors");
const helmet = require("helmet");

// 요청 로깅
app.use(morgan("dev"));

// CORS 활성화
app.use(cors());

// 보안 헤더 설정
app.use(helmet());

3.3 커스텀 미들웨어 예시

인증 미들웨어:

const jwt = require("jsonwebtoken");

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];

  if (!token) {
    return res.status(401).json({ message: "인증 토큰이 필요합니다" });
  }

  try {
    const decoded = jwt.verify(token, "your-secret-key");
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ message: "유효하지 않은 토큰입니다" });
  }
}

// 특정 라우트에 미들웨어 적용
app.get("/protected", authMiddleware, (req, res) => {
  res.json({ message: "보호된 데이터", user: req.user });
});

요청 검증 미들웨어:

function validateUserInput(req, res, next) {
  const { username, email } = req.body;

  if (!username || !email) {
    return res.status(400).json({
      message: "사용자 이름과 이메일이 필요합니다",
    });
  }

  if (typeof email !== "string" || !email.includes("@")) {
    return res.status(400).json({
      message: "유효한 이메일 주소가 필요합니다",
    });
  }

  next();
}

app.post("/users", validateUserInput, (req, res) => {
  // 사용자 생성 로직
  res.status(201).json({ success: true });
});

4. 미들웨어 체인과 실행 순서

app.use((req, res, next) => {
  console.log("첫 번째 미들웨어");
  next();
});

app.use((req, res, next) => {
  console.log("두 번째 미들웨어");
  next();
});

app.get("/", (req, res, next) => {
  console.log("라우트 핸들러");
  res.send("Hello");
});

// 출력 순서:
// 첫 번째 미들웨어
// 두 번째 미들웨어
// 라우트 핸들러

미들웨어는 정의된 순서대로 실행됩니다. 이 순서는 애플리케이션의 작동 방식에 중요한 영향을 미칩니다.

5. 라우터 레벨 미들웨어

Express.js에서는 라우터 인스턴스를 사용하여 미들웨어를 그룹화할 수 있습니다:

const express = require("express");
const app = express();
const userRouter = express.Router();

// 사용자 라우터 미들웨어
userRouter.use((req, res, next) => {
  console.log("사용자 라우터 미들웨어");
  next();
});

// 사용자 라우트
userRouter.get("/", (req, res) => {
  res.json({ users: [] });
});

userRouter.post("/", (req, res) => {
  res.status(201).json({ success: true });
});

// 메인 앱에 라우터 마운트
app.use("/users", userRouter);

6. 비동기 미들웨어 처리

Express.js에서 비동기 미들웨어를 다루는 방법:

// 비동기 미들웨어 (Promise 반환)
app.use(async (req, res, next) => {
  try {
    const result = await someAsyncOperation();
    req.result = result;
    next();
  } catch (error) {
    next(error); // 오류를 다음 오류 처리 미들웨어로 전달
  }
});

// Express 4.x에서는 비동기 오류를 자동으로 캐치하지 않음
// Express 5.x에서는 개선됨

7. 미들웨어 패턴의 자체 구현

Express.js 스타일의 미들웨어 패턴을 직접 구현한 예:

function createApp() {
  const middlewares = [];

  const app = function (req, res) {
    let index = 0;

    function next(err) {
      // 오류 발생 시 오류 처리 미들웨어로 이동
      if (err) {
        const errorHandler = middlewares.find(
          (middleware) => middleware.length === 4
        );

        if (errorHandler) {
          return errorHandler(err, req, res, next);
        } else {
          res.statusCode = 500;
          return res.end("Internal Server Error");
        }
      }

      // 다음 미들웨어 가져오기
      const middleware = middlewares[index++];

      if (!middleware) {
        return res.end("Not Found");
      }

      try {
        // 일반 미들웨어 실행
        if (middleware.length < 4) {
          middleware(req, res, next);
        } else {
          // 오류 처리 미들웨어는 오류가 없으면 건너뜀
          next();
        }
      } catch (err) {
        next(err);
      }
    }

    // 미들웨어 체인 시작
    next();
  };

  // 미들웨어 등록 메서드
  app.use = function (middleware) {
    middlewares.push(middleware);
    return app;
  };

  return app;
}

// 사용 예시
const app = createApp();

app.use((req, res, next) => {
  console.log("미들웨어 1");
  next();
});

app.use((req, res, next) => {
  console.log("미들웨어 2");
  res.end("Hello World");
});

// 서버 생성
const http = require("http");
http.createServer(app).listen(3000);

8. 미들웨어 설계 모범 사례

8.1 단일 책임 원칙 적용

// 좋은 사례: 각 미들웨어는 한 가지 작업만 수행
// 로깅 미들웨어
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

// 인증 미들웨어
app.use((req, res, next) => {
  authenticateUser(req, res, next);
});

// 나쁜 사례: 하나의 미들웨어가 여러 작업 수행
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  authenticateUser(req);
  validateInput(req);
  // 너무 많은 책임
  next();
});

8.2 미들웨어 구성

// 미들웨어 팩토리 패턴
function rateLimit(options) {
  const { windowMs = 60000, max = 100 } = options;
  const requests = new Map();

  return (req, res, next) => {
    const ip = req.ip;
    const now = Date.now();

    // 기존 기록 정리
    const windowStart = now - windowMs;
    requests.forEach((timestamp, key) => {
      if (timestamp < windowStart) {
        requests.delete(key);
      }
    });

    // 현재 IP의 요청 확인
    const requestTimestamps = requests.get(ip) || [];
    const recentRequests = requestTimestamps.filter(
      (timestamp) => timestamp > windowStart
    );

    // 요청 제한 확인
    if (recentRequests.length >= max) {
      return res.status(429).json({
        message: "너무 많은 요청을 보냈습니다. 잠시 후 다시 시도하세요.",
      });
    }

    // 현재 요청 기록
    requests.set(ip, [...recentRequests, now]);
    next();
  };
}

// 다양한 설정으로 여러 곳에서 사용
app.use("/api", rateLimit({ windowMs: 60000, max: 100 }));
app.use("/login", rateLimit({ windowMs: 60000, max: 5 }));

9. 실제 애플리케이션에서의 미들웨어 활용

9.1 API 로깅 및 분석

const morgan = require("morgan");
const fs = require("fs");
const path = require("path");

// 로그 파일 설정
const accessLogStream = fs.createWriteStream(
  path.join(__dirname, "access.log"),
  { flags: "a" }
);

// 커스텀 토큰 정의
morgan.token("body", (req) => JSON.stringify(req.body));

// 개발 환경 로깅
if (process.env.NODE_ENV === "development") {
  app.use(morgan("dev"));
}

// 프로덕션 환경 로깅
if (process.env.NODE_ENV === "production") {
  app.use(
    morgan(
      ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :body',
      { stream: accessLogStream }
    )
  );
}

9.2 사용자 인증 및 권한 부여

// JWT 기반 인증 미들웨어
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({ message: "인증 필요" });
  }

  const token = authHeader.split(" ")[1];

  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) {
      return res.status(403).json({ message: "유효하지 않은 토큰" });
    }

    req.user = decoded;
    next();
  });
}

// 권한 확인 미들웨어
function checkRole(role) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ message: "인증 필요" });
    }

    if (req.user.role !== role) {
      return res.status(403).json({ message: "권한 없음" });
    }

    next();
  };
}

// 미들웨어 적용
app.get("/users", authenticate, (req, res) => {
  // 모든 인증된 사용자가 접근 가능
  res.json({ users: [] });
});

app.delete("/users/:id", authenticate, checkRole("admin"), (req, res) => {
  // 관리자만 접근 가능
  res.json({ success: true });
});

요약

미들웨어 패턴은 Node.js 웹 애플리케이션에서 코드 구성과 재사용성을 향상시키는 강력한 개념입니다. 미들웨어의 주요 특징과 이점은 다음과 같습니다:

  1. 파이프라인 구성: 요청-응답 주기를 작은 기능 단위로 분할
  2. 코드 재사용: 여러 라우트에서 공통 기능 활용
  3. 관심사 분리: 인증, 로깅, 데이터 파싱 등의 기능을 분리
  4. 유연성: 미들웨어를 추가/제거/재정렬하여 애플리케이션 동작 변경 가능
  5. 확장성: 써드파티 미들웨어 활용 가능

Express.js와 같은 웹 프레임워크에서는 미들웨어 패턴이 핵심 아키텍처이며, 이를 통해 모듈화된 애플리케이션을 구축할 수 있습니다. 이 개념은 Koa.js, Fastify, NestJS 등 다른 Node.js 웹 프레임워크에서도 변형된 형태로 널리 사용됩니다.

results matching ""

    No results matching ""