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에서 자주 사용되는 디자인 패턴들:
싱글톤 패턴
- 전역 상태 관리
- 리소스 공유
- 데이터베이스 연결
팩토리 패턴
- 객체 생성 로직 캡슐화
- 유연한 인스턴스 생성
- 의존성 관리
옵저버 패턴
- 이벤트 기반 프로그래밍
- 느슨한 결합
- 실시간 업데이트
전략 패턴
- 알고리즘 교체 가능
- 유연한 동작 변경
- 코드 재사용
미들웨어 패턴
- 요청 처리 파이프라인
- 모듈식 기능 추가
- 관심사 분리
데코레이터 패턴
- 동적 기능 추가
- 기존 코드 수정 없이 확장
- 책임 분리
프록시 패턴
- 접근 제어
- 캐싱
- 로깅