디자인 패턴 (Design Patterns in JavaScript)

디자인 패턴 (Design Patterns in JavaScript)

자바스크립트에서 코드를 구조화하고 유지보수성을 높이려면 디자인 패턴을 이해하는 게 중요하다. 이는 반복되는 문제를 해결하는 재사용 가능한 해법을 제공한다. 이번에는 자바스크립트에서 자주 쓰이는 디자인 패턴들을 기본부터 심화까지 코드와 함께 풀어보려고 한다.

디자인 패턴을 잘 활용하면 코드가 깔끔해지고, 복잡한 로직도 체계적으로 관리할 수 있다. 단계별로 살펴보자.

디자인 패턴이란 무엇인가

디자인 패턴은 소프트웨어 설계에서 자주 마주치는 문제를 해결하기 위한 템플릿 같은 개념이다. 자바스크립트에서는 객체 지향 특성과 함수형 특성을 모두 활용해서 다양한 패턴을 구현할 수 있다.

대표적으로 생성 패턴, 구조 패턴, 행동 패턴으로 나눌 수 있는데, 여기서는 자바스크립트에 맞춘 몇 가지 핵심 패턴을 다뤄보자.

1. 싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴은 클래스의 인스턴스를 하나만 생성하도록 보장한다. 전역 상태를 관리할 때 유용하다:

const Singleton = (function () {
    let instance;

    function createInstance() {
        return {
            name: "싱글톤",
            getName: function () {
                return this.name;
            }
        };
    }

    return {
        getInstance: function () {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
})();

const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true
console.log(s1.getName()); // "싱글톤"

즉시 실행 함수로 인스턴스를 단일화했다. 두 변수가 동일한 객체를 참조하는 걸 확인할 수 있다.

2. 팩토리 패턴 (Factory Pattern)

팩토리 패턴은 객체 생성을 캡슐화해서 유연하게 처리한다. 조건에 따라 다른 객체를 만들 때 유용하다:

function CarFactory() {
    this.createVehicle = function (type) {
        let vehicle;
        if (type === "sedan") {
            vehicle = { type: "Sedan", doors: 4 };
        } else if (type === "suv") {
            vehicle = { type: "SUV", doors: 5 };
        }
        return vehicle;
    };
}

const factory = new CarFactory();
const sedan = factory.createVehicle("sedan");
const suv = factory.createVehicle("suv");
console.log(sedan); // { type: "Sedan", doors: 4 }
console.log(suv); // { type: "SUV", doors: 5 }

생성 로직을 한 곳에 모아서 객체를 만들어냈다. 조건에 따라 다른 결과를 얻을 수 있다.

3. 모듈 패턴 (Module Pattern)

모듈 패턴은 비공개 변수와 공개 메서드를 분리해서 캡슐화를 구현한다:

const CounterModule = (function () {
    let count = 0; // 비공개 변수

    return {
        increment: function () {
            count += 1;
            return count;
        },
        getCount: function () {
            return count;
        }
    };
})();

console.log(CounterModule.increment()); // 1
console.log(CounterModule.increment()); // 2
console.log(CounterModule.getCount()); // 2
console.log(CounterModule.count); // undefined

count는 외부에서 접근할 수 없고, 공개된 메서드로만 조작할 수 있다.

4. 옵저버 패턴 (Observer Pattern)

옵저버 패턴은 객체 상태 변화를 감지하고 구독자들에게 알린다:

function Subject() {
    this.observers = [];

    this.subscribe = function (observer) {
        this.observers.push(observer);
    };

    this.notify = function (data) {
        this.observers.forEach(observer => observer(data));
    };
}

const subject = new Subject();
const observer1 = data => console.log("첫 번째: " + data);
const observer2 = data => console.log("두 번째: " + data);
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("알림!");
// "첫 번째: 알림!"
// "두 번째: 알림!"

구독자들이 이벤트를 받아서 반응하도록 설계했다.

5. 데코레이터 패턴 (Decorator Pattern)

데코레이터 패턴은 객체에 동적으로 기능을 추가한다:

function Coffee() {
    this.cost = 5;
}

function MilkDecorator(coffee) {
    const baseCost = coffee.cost;
    coffee.cost = baseCost + 2;
    return coffee;
}

function SugarDecorator(coffee) {
    const baseCost = coffee.cost;
    coffee.cost = baseCost + 1;
    return coffee;
}

let coffee = new Coffee();
coffee = MilkDecorator(coffee);
coffee = SugarDecorator(coffee);
console.log(coffee.cost); // 8

기본 객체에 추가 기능을 붙여서 확장했다.

6. 전략 패턴 (Strategy Pattern)

전략 패턴은 알고리즘을 런타임에 교체할 수 있게 한다:

function Shipping() {
    this.strategy = null;
    this.setStrategy = function (strategy) {
        this.strategy = strategy;
    };
    this.calculate = function (weight) {
        return this.strategy.calculate(weight);
    };
}

const FastShipping = {
    calculate: weight => weight * 10
};
const SlowShipping = {
    calculate: weight => weight * 5
};

const shipping = new Shipping();
shipping.setStrategy(FastShipping);
console.log(shipping.calculate(2)); // 20
shipping.setStrategy(SlowShipping);
console.log(shipping.calculate(2)); // 10

동적으로 전략을 바꿔서 계산 방식을 조정했다.

7. 프록시 패턴 (Proxy Pattern)

프록시 패턴은 객체에 대한 접근을 제어한다:

function NetworkRequest() {
    this.fetchData = function (url) {
        console.log("데이터 가져오기: " + url);
        return "데이터";
    };
}

function ProxyRequest(request) {
    this.cache = {};
    this.fetchData = function (url) {
        if (this.cache[url]) {
            console.log("캐시에서 가져오기: " + url);
            return this.cache[url];
        }
        const data = request.fetchData(url);
        this.cache[url] = data;
        return data;
    };
}

const request = new NetworkRequest();
const proxy = new ProxyRequest(request);
console.log(proxy.fetchData("url1")); // "데이터 가져오기: url1", "데이터"
console.log(proxy.fetchData("url1")); // "캐시에서 가져오기: url1", "데이터"

캐싱을 추가해서 요청을 최적화했다.

8. 컴포지트 패턴 (Composite Pattern)

컴포지트 패턴은 객체를 트리 구조로 관리한다:

class Component {
    constructor(name) {
        this.name = name;
    }
    display() {}
}

class Leaf extends Component {
    display() {
        console.log("Leaf: " + this.name);
    }
}

class Composite extends Component {
    constructor(name) {
        super(name);
        this.children = [];
    }
    add(component) {
        this.children.push(component);
    }
    display() {
        console.log("Composite: " + this.name);
        this.children.forEach(child => child.display());
    }
}

const root = new Composite("Root");
root.add(new Leaf("Leaf1"));
root.add(new Leaf("Leaf2"));
const sub = new Composite("Sub");
sub.add(new Leaf("Leaf3"));
root.add(sub);
root.display();
// "Composite: Root"
// "Leaf: Leaf1"
// "Leaf: Leaf2"
// "Composite: Sub"
// "Leaf: Leaf3"

계층 구조를 만들어서 일관되게 처리했다.

9. 상태 패턴 (State Pattern)

상태 패턴은 객체의 상태에 따라 동작을 변경한다:

class TrafficLight {
    constructor() {
        this.state = new RedState(this);
    }
    changeState(state) {
        this.state = state;
    }
    display() {
        this.state.display();
    }
}

class RedState {
    constructor(light) {
        this.light = light;
    }
    display() {
        console.log("빨간불");
        this.light.changeState(new GreenState(this.light));
    }
}

class GreenState {
    constructor(light) {
        this.light = light;
    }
    display() {
        console.log("초록불");
        this.light.changeState(new YellowState(this.light));
    }
}

class YellowState {
    constructor(light) {
        this.light = light;
    }
    display() {
        console.log("노란불");
        this.light.changeState(new RedState(this.light));
    }
}

const light = new TrafficLight();
light.display(); // "빨간불"
light.display(); // "초록불"
light.display(); // "노란불"

상태 전환을 객체로 분리해서 관리했다.

10. 명령 패턴 (Command Pattern)

명령 패턴은 요청을 객체로 캡슐화한다:

class Light {
    turnOn() { console.log("불 켜짐"); }
    turnOff() { console.log("불 꺼짐"); }
}

class Command {
    execute() {}
}

class TurnOnCommand extends Command {
    constructor(light) {
        super();
        this.light = light;
    }
    execute() {
        this.light.turnOn();
    }
}

class TurnOffCommand extends Command {
    constructor(light) {
        super();
        this.light = light;
    }
    execute() {
        this.light.turnOff();
    }
}

class Remote {
    setCommand(command) {
        this.command = command;
    }
    pressButton() {
        this.command.execute();
    }
}

const light = new Light();
const remote = new Remote();
remote.setCommand(new TurnOnCommand(light));
remote.pressButton(); // "불 켜짐"
remote.setCommand(new TurnOffCommand(light));
remote.pressButton(); // "불 꺼짐"

동작을 명령 객체로 분리해서 유연하게 호출했다.

11. 혼합 패턴 활용

여러 패턴을 조합해서 복잡한 로직을 처리할 수 있다:

const EventBus = (function () { // 싱글톤 + 모듈
    let instance;
    function createBus() {
        const events = {};
        return {
            on: function (event, callback) { // 옵저버
                if (!events[event]) events[event] = [];
                events[event].push(callback);
            },
            emit: function (event, data) {
                if (events[event]) {
                    events[event].forEach(cb => cb(data));
                }
            }
        };
    }
    return {
        getInstance: function () {
            if (!instance) instance = createBus();
            return instance;
        }
    };
})();

const bus = EventBus.getInstance();
bus.on("update", data => console.log("업데이트: " + data));
bus.emit("update", "새 데이터"); // "업데이트: 새 데이터"

싱글톤, 모듈, 옵저버를 결합해서 이벤트 시스템을 만들었다.

12. 성능과 가독성에 미치는 영향

디자인 패턴이 코드에 어떤 영향을 주는지 살펴보자:

- 성능: 패턴에 따라 객체 생성이나 호출이 늘어날 수 있지만, 구조화로 인해 최적화 가능성이 커진다.

- 가독성: 로직이 분리되고 역할이 명확해져서 복잡도가 줄어든다.

패턴을 적절히 선택하면 코드가 단순해지고 유지보수가 쉬워진다는 점이 핵심이다.


마무리

디자인 패턴은 자바스크립트에서 코드를 체계적으로 관리하는 강력한 도구다. 싱글톤, 팩토리, 옵저버 등 다양한 패턴을 상황에 맞춰 활용하면 복잡한 로직도 깔끔하게 정리할 수 있다.


+ Recent posts