본문으로 건너뛰기
Previous
Next
JavaScript Design Patterns | Singleton· Factory

JavaScript Design Patterns | Singleton· Factory

JavaScript Design Patterns | Singleton· Factory

이 글의 핵심

JavaScript design patterns: Singleton, Factory, Observer patterns. Learn principles, code implementation, and practical applications with real-world examples.

Introduction

What are Design Patterns?

Design Patterns are proven solution templates with names for recurring design problems. They help teams communicate efficiently with phrases like “let’s use a factory.” Why Learn Patterns:

  • Proven Solutions: Already validated approaches
  • Code Quality: Easier maintenance
  • Communication: Common language among developers
  • Reusability: Applicable to various situations

1. Singleton Pattern

Concept

Creates only one instance and makes it globally accessible.

Implementation

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and perform branching with conditionals. Understand the role of each part as you examine the code.

class Database {
    constructor() {
        if (Database.instance) {
            return Database.instance;
        }
        
        this.connection = null;
        Database.instance = this;
    }
    
    connect() {
        if (!this.connection) {
            this.connection = "DB Connected";
            console.log(this.connection);
        }
        return this.connection;
    }
}
// Usage
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2);  // true (same instance)
db1.connect();  // DB Connected
db2.connect();  // (already connected)

Modern Approach (ES6 Module)

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and perform branching with conditionals. Understand the role of each part as you examine the code.

// database.js
class Database {
    constructor() {
        this.connection = null;
    }
    
    connect() {
        if (!this.connection) {
            this.connection = "DB Connected";
        }
        return this.connection;
    }
}
export default new Database();
// main.js
import db from './database.js';
db.connect();

2. Factory Pattern

Concept

Encapsulates object creation logic for flexible object instantiation.

Implementation

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, handle errors for stability, and perform branching with conditionals. Understand the role of each part as you examine the code.

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(`Unknown role: ${role}`);
        }
    }
}
// Usage
const admin = UserFactory.createUser("John", "admin");
console.log(admin.getPermissions());  // ['read', 'write', 'delete']
const guest = UserFactory.createUser("Guest", "guest");
console.log(guest.getPermissions());  // ['read']

3. Module Pattern

Concept

Uses encapsulation to distinguish between private variables and public methods.

Implementation (IIFE)

The following is a detailed implementation code using JavaScript. Understand the role of each part as you examine the code.

const Counter = (function() {
    // Private variable
    let count = 0;
    
    // Private function
    function log() {
        console.log(`Current count: ${count}`);
    }
    
    // Public API
    return {
        increment() {
            count++;
            log();
        },
        decrement() {
            count--;
            log();
        },
        getCount() {
            return count;
        }
    };
})();
// Usage
Counter.increment();  // Current count: 1
Counter.increment();  // Current count: 2
console.log(Counter.getCount());  // 2
console.log(Counter.count);  // undefined (private)

Modern Approach (ES6 Class)

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality. Understand the role of each part as you examine the code.

class Counter {
    #count = 0;  // Private field
    
    increment() {
        this.#count++;
        this.#log();
    }
    
    decrement() {
        this.#count--;
        this.#log();
    }
    
    getCount() {
        return this.#count;
    }
    
    #log() {
        console.log(`Current count: ${this.#count}`);
    }
}
const counter = new Counter();
counter.increment();  // Current count: 1
console.log(counter.getCount());  // 1
// console.log(counter.#count);  // SyntaxError (private)

4. Observer Pattern

Concept

Notifies subscribers of object state changes.

Implementation

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and process data with loops. Understand the role of each part as you examine the code.

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} received notification:`, data);
    }
}
// Usage
const subject = new Subject();
const observer1 = new Observer("Observer1");
const observer2 = new Observer("Observer2");
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("New data!");
// Observer1 received notification: New data!
// Observer2 received notification: New data!
subject.unsubscribe(observer1);
subject.notify("Another data");
// Observer2 received notification: Another data

Real-world Example: Event System

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, implement logic through functions, process data with loops, and perform branching with conditionals. Understand the role of each part as you examine the code.

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);
    }
}
// Usage
const emitter = new EventEmitter();
function onUserLogin(user) {
    console.log(`${user.name} logged in`);
}
emitter.on('login', onUserLogin);
emitter.on('login', (user) => {
    console.log(`Welcome, ${user.name}!`);
});
emitter.emit('login', { name: 'John' });
// John logged in
// Welcome, John!
// once: Execute only once
emitter.once('logout', (user) => {
    console.log(`${user.name} logged out`);
});
emitter.emit('logout', { name: 'John' });  // John logged out
emitter.emit('logout', { name: 'John' });  // (not executed)

5. Proxy Pattern

Concept

Controls access to objects or provides additional functionality.

Implementation (ES6 Proxy)

The following is a detailed implementation code using JavaScript. Handle errors for stability and perform branching with conditionals. Understand the role of each part as you examine the code.

const user = {
    name: "John",
    age: 25,
    email: "[email protected]"
};
const handler = {
    get(target, prop) {
        console.log(`Reading ${prop}`);
        return target[prop];
    },
    set(target, prop, value) {
        console.log(`Setting ${prop} to ${value}`);
        
        // Validation
        if (prop === 'age' && typeof value !== 'number') {
            throw new TypeError("Age must be a number");
        }
        
        target[prop] = value;
        return true;
    }
};
const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name);  // Reading name -> John
proxyUser.age = 26;  // Setting age to 26
// proxyUser.age = "26";  // TypeError

Real-world Example: Caching Proxy

The following is a detailed implementation code using JavaScript. Implement logic through functions and perform branching with conditionals. Understand the role of each part as you examine the code.

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("Returning from cache");
                return cache.get(key);
            }
            
            console.log("Computing...");
            const result = target.apply(thisArg, args);
            cache.set(key, result);
            return result;
        }
    });
}
// Usage
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}
const cachedFib = createCachedFunction(fibonacci);
console.log(cachedFib(10));  // Computing....-> 55
console.log(cachedFib(10));  // Returning from cache -> 55

6. Strategy Pattern

Concept

Encapsulates algorithms to allow runtime selection.

Implementation

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality. Understand the role of each part as you examine the code.

// Strategies
class CreditCardStrategy {
    pay(amount) {
        console.log(`Paying ${amount} with Credit Card`);
    }
}
class PayPalStrategy {
    pay(amount) {
        console.log(`Paying ${amount} with PayPal`);
    }
}
class CryptoStrategy {
    pay(amount) {
        console.log(`Paying ${amount} with Cryptocurrency`);
    }
}
// Context
class PaymentContext {
    constructor(strategy) {
        this.strategy = strategy;
    }
    
    setStrategy(strategy) {
        this.strategy = strategy;
    }
    
    executePayment(amount) {
        this.strategy.pay(amount);
    }
}
// Usage
const payment = new PaymentContext(new CreditCardStrategy());
payment.executePayment(10000);  // Paying 10000 with Credit Card
payment.setStrategy(new PayPalStrategy());
payment.executePayment(20000);  // Paying 20000 with PayPal

7. Real-world Example: State Management

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and process data with loops. Understand the role of each part as you examine the code.

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));
    }
}
// Usage
const store = new Store({ count: 0, user: null });
const unsubscribe = store.subscribe((state) => {
    console.log("State changed:", state);
});
store.setState({ count: 1 });
// State changed: { count: 1, user: null }
store.setState({ user: { name: "John" } });
// State changed: { count: 1, user: { name: "John" } }
unsubscribe();  // Unsubscribe

Summary

Key Takeaways

  1. Singleton: Single instance
  2. Factory: Encapsulate object creation
  3. Module: Separate private/public
  4. Observer: Notify state changes
  5. Proxy: Control access
  6. Strategy: Swap algorithms

Next Steps



자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. JavaScript design patterns: Singleton, Factory, Observer patterns. Learn principles, code implementation, and practical … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. JavaScript 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

JavaScript, Design Patterns, Singleton, Factory, Observer, Proxy 등으로 검색하시면 이 글이 도움이 됩니다.