JavaScript 디자인 패턴 | 싱글톤, 팩토리, 옵저버 패턴
이 글의 핵심
JavaScript 디자인 패턴에 대한 실전 가이드입니다. 싱글톤, 팩토리, 옵저버 패턴 등을 예제와 함께 상세히 설명합니다.
들어가며
디자인 패턴이란?
디자인 패턴(Design Pattern)은 반복되는 설계 문제에 대해 이름 붙여 둔 검증된 해결 템플릿입니다. 팀 안에서 “팩토리 쓰자”처럼 짧게 의사소통할 때도 도움이 됩니다.
패턴을 알아 두면 좋은 이유:
- ✅ 검증된 해결책: 이미 검증된 방법
- ✅ 코드 품질: 유지보수 쉬움
- ✅ 의사소통: 개발자 간 공통 언어
- ✅ 재사용성: 다양한 상황에 적용
1. 싱글톤 패턴 (Singleton)
개념
하나의 인스턴스만 생성하고 전역에서 접근 가능하게 합니다.
구현
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect() {
if (!this.connection) {
this.connection = "DB 연결됨";
console.log(this.connection);
}
return this.connection;
}
}
// 사용
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true (같은 인스턴스)
db1.connect(); // DB 연결됨
db2.connect(); // (이미 연결됨)
모던 방식 (ES6 Module)
// database.js
class Database {
constructor() {
this.connection = null;
}
connect() {
if (!this.connection) {
this.connection = "DB 연결됨";
}
return this.connection;
}
}
export default new Database();
// main.js
import db from './database.js';
db.connect();
2. 팩토리 패턴 (Factory)
개념
객체 생성 로직을 캡슐화하여 유연하게 객체를 생성합니다.
구현
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
getPermissions() {
return [];
}
}
class Admin extends User {
getPermissions() {
return ['read', 'write', 'delete'];
}
}
class Guest extends User {
getPermissions() {
return ['read'];
}
}
class Member extends User {
getPermissions() {
return ['read', 'write'];
}
}
// Factory
class UserFactory {
static createUser(name, role) {
switch (role) {
case 'admin':
return new Admin(name, role);
case 'member':
return new Member(name, role);
case 'guest':
return new Guest(name, role);
default:
throw new Error(`알 수 없는 역할: ${role}`);
}
}
}
// 사용
const admin = UserFactory.createUser("홍길동", "admin");
console.log(admin.getPermissions()); // ['read', 'write', 'delete']
const guest = UserFactory.createUser("손님", "guest");
console.log(guest.getPermissions()); // ['read']
3. 모듈 패턴 (Module)
개념
캡슐화를 통해 private 변수와 public 메서드를 구분합니다.
구현 (IIFE)
const Counter = (function() {
// Private 변수
let count = 0;
// Private 함수
function log() {
console.log(`현재 카운트: ${count}`);
}
// Public API
return {
increment() {
count++;
log();
},
decrement() {
count--;
log();
},
getCount() {
return count;
}
};
})();
// 사용
Counter.increment(); // 현재 카운트: 1
Counter.increment(); // 현재 카운트: 2
console.log(Counter.getCount()); // 2
console.log(Counter.count); // undefined (private)
모던 방식 (ES6 Class)
class Counter {
#count = 0; // Private field
increment() {
this.#count++;
this.#log();
}
decrement() {
this.#count--;
this.#log();
}
getCount() {
return this.#count;
}
#log() {
console.log(`현재 카운트: ${this.#count}`);
}
}
const counter = new Counter();
counter.increment(); // 현재 카운트: 1
console.log(counter.getCount()); // 1
console.log(counter.#count); // SyntaxError (private)
4. 옵저버 패턴 (Observer)
개념
객체의 상태 변화를 구독자들에게 알립니다.
구현
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name}이(가) 알림 받음:`, data);
}
}
// 사용
const subject = new Subject();
const observer1 = new Observer("관찰자1");
const observer2 = new Observer("관찰자2");
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("새 데이터!");
// 관찰자1이(가) 알림 받음: 새 데이터!
// 관찰자2이(가) 알림 받음: 새 데이터!
subject.unsubscribe(observer1);
subject.notify("또 다른 데이터");
// 관찰자2이(가) 알림 받음: 또 다른 데이터
실전 예제: 이벤트 시스템
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
off(event, listener) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(l => l !== listener);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(listener => listener(data));
}
once(event, listener) {
const wrapper = (data) => {
listener(data);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
// 사용
const emitter = new EventEmitter();
function onUserLogin(user) {
console.log(`${user.name} 로그인`);
}
emitter.on('login', onUserLogin);
emitter.on('login', (user) => {
console.log(`환영합니다, ${user.name}님!`);
});
emitter.emit('login', { name: '홍길동' });
// 홍길동 로그인
// 환영합니다, 홍길동님!
// once: 한 번만 실행
emitter.once('logout', (user) => {
console.log(`${user.name} 로그아웃`);
});
emitter.emit('logout', { name: '홍길동' }); // 홍길동 로그아웃
emitter.emit('logout', { name: '홍길동' }); // (실행 안 됨)
5. 프록시 패턴 (Proxy)
개념
객체에 대한 접근을 제어하거나 추가 기능을 제공합니다.
구현 (ES6 Proxy)
const user = {
name: "홍길동",
age: 25,
email: "[email protected]"
};
const handler = {
get(target, prop) {
console.log(`${prop} 읽기`);
return target[prop];
},
set(target, prop, value) {
console.log(`${prop}을(를) ${value}로 설정`);
// 유효성 검사
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError("나이는 숫자여야 합니다");
}
target[prop] = value;
return true;
}
};
const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name); // name 읽기 -> 홍길동
proxyUser.age = 26; // age을(를) 26로 설정
// proxyUser.age = "26"; // TypeError
실전 예제: 캐싱 프록시
function createCachedFunction(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("캐시에서 반환");
return cache.get(key);
}
console.log("계산 중...");
const result = target.apply(thisArg, args);
cache.set(key, result);
return result;
}
});
}
// 사용
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const cachedFib = createCachedFunction(fibonacci);
console.log(cachedFib(10)); // 계산 중... -> 55
console.log(cachedFib(10)); // 캐시에서 반환 -> 55
6. 전략 패턴 (Strategy)
개념
알고리즘을 캡슐화하여 런타임에 선택할 수 있게 합니다.
구현
// 전략들
class CreditCardStrategy {
pay(amount) {
console.log(`신용카드로 ${amount}원 결제`);
}
}
class PayPalStrategy {
pay(amount) {
console.log(`PayPal로 ${amount}원 결제`);
}
}
class CryptoStrategy {
pay(amount) {
console.log(`암호화폐로 ${amount}원 결제`);
}
}
// Context
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executePayment(amount) {
this.strategy.pay(amount);
}
}
// 사용
const payment = new PaymentContext(new CreditCardStrategy());
payment.executePayment(10000); // 신용카드로 10000원 결제
payment.setStrategy(new PayPalStrategy());
payment.executePayment(20000); // PayPal로 20000원 결제
7. 실전 예제: 상태 관리
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.notify();
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notify() {
this.listeners.forEach(listener => listener(this.state));
}
}
// 사용
const store = new Store({ count: 0, user: null });
const unsubscribe = store.subscribe((state) => {
console.log("상태 변경:", state);
});
store.setState({ count: 1 });
// 상태 변경: { count: 1, user: null }
store.setState({ user: { name: "홍길동" } });
// 상태 변경: { count: 1, user: { name: "홍길동" } }
unsubscribe(); // 구독 해제
정리
핵심 요약
- 싱글톤: 하나의 인스턴스
- 팩토리: 객체 생성 캡슐화
- 모듈: private/public 구분
- 옵저버: 상태 변화 알림
- 프록시: 접근 제어
- 전략: 알고리즘 교체
다음 단계
- TypeScript 시작하기
- React 디자인 패턴
- Node.js 디자인 패턴
관련 글
- JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문
- JavaScript 변수와 데이터 타입 | let, const, var 완벽 정리