JavaScript 디자인 패턴 | 싱글톤, 팩토리, 옵저버 패턴

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();  // 구독 해제

정리

핵심 요약

  1. 싱글톤: 하나의 인스턴스
  2. 팩토리: 객체 생성 캡슐화
  3. 모듈: private/public 구분
  4. 옵저버: 상태 변화 알림
  5. 프록시: 접근 제어
  6. 전략: 알고리즘 교체

다음 단계

  • TypeScript 시작하기
  • React 디자인 패턴
  • Node.js 디자인 패턴

관련 글

  • JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문
  • JavaScript 변수와 데이터 타입 | let, const, var 완벽 정리