디자인 패턴은 반복적으로 발생하는 설계 문제에 대한 검증된 해결책입니다. GoF의 23가지 패턴 중 JavaScript 실무에서 자주 쓰이는 핵심 패턴들을 실제 코드로 살펴봅니다. 패턴 자체보다 어떤 문제를 해결하는지에 집중하세요.
1. 싱글턴 (Singleton)
클래스의 인스턴스를 하나만 생성하고 전역 접근점을 제공합니다. DB 연결, 설정, 로거에 적합합니다.
// ES 모듈 자체가 싱글턴 (가장 간단한 방법)
// db.js
let connection = null;
export function getDB() {
if (!connection) {
connection = createDatabaseConnection();
}
return connection;
}
// 클래스 기반 싱글턴
class ConfigManager {
static #instance = null;
#config = {};
static getInstance() {
if (!ConfigManager.#instance) {
ConfigManager.#instance = new ConfigManager();
}
return ConfigManager.#instance;
}
set(key, value) { this.#config[key] = value; }
get(key) { return this.#config[key]; }
}
const config = ConfigManager.getInstance();
config.set('apiUrl', 'https://api.devlog.kr');
2. 옵저버 (Observer)
객체의 상태 변화를 여러 곳에서 구독(subscribe)하는 패턴. 이벤트 시스템, 상태 관리에 광범위하게 사용됩니다.
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(callback);
// 구독 해제 함수 반환
return () => this.off(event, callback);
}
off(event, callback) {
this.#listeners.get(event)?.delete(callback);
}
emit(event, data) {
this.#listeners.get(event)?.forEach(cb => cb(data));
}
}
// 사용 예시
const store = new EventEmitter();
const unsubscribe = store.on('userLogin', (user) => {
console.log(`${user.name} 로그인`);
updateUI(user);
});
store.emit('userLogin', { name: '홍길동', role: 'admin' });
// 필요 없을 때 구독 해제
unsubscribe();
3. 팩토리 (Factory)
객체 생성 로직을 캡슐화합니다. 생성할 객체 타입이 런타임에 결정되거나, 복잡한 초기화 로직이 있을 때 유용합니다.
// 알림 타입에 따라 다른 알림 객체 생성
class Notification {
constructor(message) { this.message = message; }
send() { throw new Error('구현 필요'); }
}
class EmailNotification extends Notification {
send() { console.log(`📧 이메일: ${this.message}`); }
}
class SMSNotification extends Notification {
send() { console.log(`📱 SMS: ${this.message}`); }
}
class PushNotification extends Notification {
send() { console.log(`🔔 푸시: ${this.message}`); }
}
// 팩토리 함수
function createNotification(type, message) {
const types = {
email: EmailNotification,
sms: SMSNotification,
push: PushNotification,
};
const NotificationClass = types[type];
if (!NotificationClass) throw new Error(`알 수 없는 알림 타입: ${type}`);
return new NotificationClass(message);
}
// 사용
const notification = createNotification('email', '새 댓글이 달렸습니다');
notification.send();
4. 전략 (Strategy)
알고리즘을 캡슐화하고 교체 가능하게 만듭니다. 정렬 방식, 결제 방법, 인증 방식 같은 교체 가능한 로직에 적합합니다.
// 결제 전략
const paymentStrategies = {
card: async ({ amount, cardNumber }) => {
// 카드 결제 API 호출
return { success: true, method: 'card', amount };
},
kakao: async ({ amount, userId }) => {
// 카카오페이 API 호출
return { success: true, method: 'kakao', amount };
},
transfer: async ({ amount, accountNumber }) => {
// 계좌이체 처리
return { success: true, method: 'transfer', amount };
}
};
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
async pay(amount, details) {
return this.strategy({ amount, ...details });
}
}
const processor = new PaymentProcessor(paymentStrategies.card);
await processor.pay(10000, { cardNumber: '1234-5678-9012-3456' });
// 런타임에 전략 교체
processor.setStrategy(paymentStrategies.kakao);
await processor.pay(5000, { userId: 'user123' });
5. 데코레이터 (Decorator)
기존 함수/객체를 수정하지 않고 기능을 추가합니다. 로깅, 캐싱, 검증 등 횡단 관심사(cross-cutting concerns)에 이상적입니다.
// 함수 데코레이터 — 로깅
function withLogging(fn) {
return async function (...args) {
console.log(`[${fn.name}] 호출`, args);
const start = Date.now();
try {
const result = await fn(...args);
console.log(`[${fn.name}] 완료 (${Date.now() - start}ms)`, result);
return result;
} catch (err) {
console.error(`[${fn.name}] 오류`, err);
throw err;
}
};
}
// 캐싱 데코레이터
function withCache(fn, ttl = 60000) {
const cache = new Map();
return async function (...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.time < ttl) {
return cached.value;
}
const value = await fn(...args);
cache.set(key, { value, time: Date.now() });
return value;
};
}
// 조합 사용
const fetchUser = withLogging(withCache(
async (id) => fetch(`/api/users/${id}`).then(r => r.json()),
300000 // 5분 캐시
));
핵심 정리
디자인 패턴은 도구이지 목표가 아닙니다. 싱글턴은 공유 자원 관리, 옵저버는 이벤트 기반 통신, 팩토리는 유연한 객체 생성, 전략은 교체 가능한 알고리즘, 데코레이터는 기능 합성에 각각 강합니다. 패턴을 억지로 끼워 맞추기보다, 실제 문제가 생겼을 때 적합한 패턴을 자연스럽게 적용하는 것이 좋습니다.
디자인 패턴은 도구이지 목표가 아닙니다. 싱글턴은 공유 자원 관리, 옵저버는 이벤트 기반 통신, 팩토리는 유연한 객체 생성, 전략은 교체 가능한 알고리즘, 데코레이터는 기능 합성에 각각 강합니다. 패턴을 억지로 끼워 맞추기보다, 실제 문제가 생겼을 때 적합한 패턴을 자연스럽게 적용하는 것이 좋습니다.