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 웹 애플리케이션에서 코드 구성과 재사용성을 향상시키는 강력한 개념입니다. 미들웨어의 주요 특징과 이점은 다음과 같습니다:
- 파이프라인 구성: 요청-응답 주기를 작은 기능 단위로 분할
- 코드 재사용: 여러 라우트에서 공통 기능 활용
- 관심사 분리: 인증, 로깅, 데이터 파싱 등의 기능을 분리
- 유연성: 미들웨어를 추가/제거/재정렬하여 애플리케이션 동작 변경 가능
- 확장성: 써드파티 미들웨어 활용 가능
Express.js와 같은 웹 프레임워크에서는 미들웨어 패턴이 핵심 아키텍처이며, 이를 통해 모듈화된 애플리케이션을 구축할 수 있습니다. 이 개념은 Koa.js, Fastify, NestJS 등 다른 Node.js 웹 프레임워크에서도 변형된 형태로 널리 사용됩니다.