디자인 패턴 (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. 성능과 가독성에 미치는 영향
디자인 패턴이 코드에 어떤 영향을 주는지 살펴보자:
- 성능: 패턴에 따라 객체 생성이나 호출이 늘어날 수 있지만, 구조화로 인해 최적화 가능성이 커진다.
- 가독성: 로직이 분리되고 역할이 명확해져서 복잡도가 줄어든다.
패턴을 적절히 선택하면 코드가 단순해지고 유지보수가 쉬워진다는 점이 핵심이다.
마무리
디자인 패턴은 자바스크립트에서 코드를 체계적으로 관리하는 강력한 도구다. 싱글톤, 팩토리, 옵저버 등 다양한 패턴을 상황에 맞춰 활용하면 복잡한 로직도 깔끔하게 정리할 수 있다.
'코딩 공부 > 자바스크립트' 카테고리의 다른 글
99. 자바스크립트 리액티브 프로그래밍 (Reactive Programming) (0) | 2025.04.02 |
---|---|
98. 자바스크립트 웹어셈블리 입문 (WebAssembly Introduction) (0) | 2025.04.02 |
97. 자바스크립트 함수형 프로그래밍 기초 (Functional Programming Basics) (0) | 2025.04.02 |
96. 자바스크립트 문서화 (Documentation Practices) (0) | 2025.04.01 |
95. 자바스크립트 코드 리뷰 (Code Review Practices) (4) | 2025.04.01 |