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

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

이 글의 핵심

JavaScript 디자인 패턴: 싱글톤, 팩토리, 옵저버 패턴. 싱글톤 패턴 (Singleton)·팩토리 패턴 (Factory).

들어가며

디자인 패턴이란?

디자인 패턴(Design Pattern)은 반복되는 설계 문제에 대해 이름 붙여 둔 검증된 해결 템플릿입니다. 팀 안에서 “팩토리 쓰자”처럼 짧게 의사소통할 때도 도움이 됩니다. 패턴을 알아 두면 좋은 이유:

  • 검증된 해결책: 이미 검증된 방법
  • 코드 품질: 유지보수 쉬움
  • 의사소통: 개발자 간 공통 언어
  • 재사용성: 다양한 상황에 적용

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

1. 싱글톤 패턴 (Singleton)

개념

하나의 인스턴스만 생성하고 전역에서 접근 가능하게 합니다.

구현

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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)

개념

객체 생성 로직을 캡슐화하여 유연하게 객체를 생성합니다.

구현

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

개념

객체의 상태 변화를 구독자들에게 알립니다.

구현

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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이(가) 알림 받음: 또 다른 데이터

실전 예제: 이벤트 시스템

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

다음은 javascript를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

const user = {
    name: "홍길동",
    age: 25,
    email: "hong@test.com"
};
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

실전 예제: 캐싱 프록시

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

개념

알고리즘을 캡슐화하여 런타임에 선택할 수 있게 합니다.

구현

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 전략들
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. 실전 예제: 상태 관리

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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. 전략: 알고리즘 교체

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3