소프트웨어 설계의 핵심: 디자인 패턴
디자인 패턴은 소프트웨어 개발 중 반복적으로 발생하는 문제들에 대한 재사용 가능한 해결책입니다. 자바스크립트 개발에서 디자인 패턴의 핵심은 '변하는 것과 변하지 않는 것을 분리'하여 변화에 유연하게 대응하는 것에 있습니다.
전통적인 SOLID 원칙 중 자바스크립트 디자인 패턴에서 특히 강조되는 두 가지는 다음과 같습니다.
- 단일 책임 원칙 (SRP): 클래스나 모듈은 하나의 기능만을 담당해야 합니다.
- 개방-폐쇄 원칙 (OCP): 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다.
1. 팩토리 패턴 (Factory Pattern)
객체 생성 로직을 별도의 클래스나 함수로 캡슐화하여 인스턴스화 과정을 추상화하는 패턴입니다. 복잡한 초기화 과정이나 조건에 따른 객체 생성이 필요할 때 유용합니다.
// 간단한 팩토리 패턴 예시
class Vehicle {
constructor(name, type, specs) {
this.name = name;
this.type = type;
this.specs = specs;
}
}
class VehicleFactory {
createVehicle(name, type) {
let specs;
switch(type) {
case 'Car':
specs = ['4 Wheels', 'AC', 'Automatic'];
break;
case 'Bike':
specs = ['2 Wheels', 'Manual'];
break;
default:
specs = [];
}
return new Vehicle(name, type, specs);
}
}
const factory = new VehicleFactory();
const myCar = factory.createVehicle('Tesla Model 3', 'Car');
추상 팩토리 패턴은 연관된 객체들의 군을 생성하기 위한 인터페이스를 제공하며, 구체적인 클래스를 명시하지 않고도 제품군을 교체할 수 있게 합니다.
2. 싱글톤 패턴 (Singleton Pattern)
애플리케이션 전체에서 클래스의 인스턴스를 단 하나만 생성하고, 어디서든 동일한 인스턴스에 접근할 수 있도록 보장합니다. 전역 상태 관리(Vuex, Pinia)나 설정 객체에 주로 사용됩니다.
class AppConfig {
constructor() {
if (!AppConfig.instance) {
this.settings = { theme: 'dark', lang: 'ko' };
AppConfig.instance = this;
}
return AppConfig.instance;
}
get(key) {
return this.settings[key];
}
}
const config1 = new AppConfig();
const config2 = new AppConfig();
console.log(config1 === config2); // true
3. 프록시 패턴 (Proxy Pattern)
한 객체가 다른 객체에 접근하는 것을 제어하기 위해 대리자(Proxy)를 두는 패턴입니다. 보안, 캐싱, 지연 로딩(Lazy Loading) 등에 활용됩니다. ES6의 Proxy 객체는 이를 언어 차원에서 지원합니다.
// 계산 결과를 캐싱하는 프록시 예시
const heavyComputation = (n) => {
console.log('복잡한 계산 중...');
return n * 2;
};
const memoProxy = new Proxy(heavyComputation, {
cache: new Map(),
apply(target, thisArg, args) {
const key = args[0];
if (this.cache.has(key)) {
return this.cache.get(key);
}
const result = target(...args);
this.cache.set(key, result);
return result;
}
});
4. 관찰자 패턴 (Observer Pattern) 및 발행-구독 패턴 (Pub-Sub)
객체의 상태 변화를 관찰하는 관찰자 목록을 유지하고, 상태 변화 시마다 메서드 호출 등을 통해 알림을 보내는 패턴입니다.
관찰자 패턴 (Observer)
발행자가 구독자의 존재를 직접 알고 관리합니다. 관계가 비교적 밀접합니다.
발행-구독 패턴 (Pub-Sub)
발행자와 구독자 사이에 '이벤트 채널' 또는 '브로커'가 존재하여 서로의 존재를 몰라도 통신이 가능합니다. 완전한 디커플링을 지향합니다.
// 간단한 EventEmitter (Pub-Sub) 구현
class MyEventEmitter {
constructor() {
this.events = {};
}
on(topic, listener) {
if (!this.events[topic]) this.events[topic] = [];
this.events[topic].push(listener);
}
emit(topic, ...data) {
if (!this.events[topic]) return;
this.events[topic].forEach(fn => fn(...data));
}
}
const bus = new MyEventEmitter();
bus.on('orderCreated', (id) => console.log(`주문 완료: ${id}`));
bus.emit('orderCreated', 'ORD-123');
5. 전략 패턴 (Strategy Pattern)
동일한 목적을 가진 알고리즘들을 개별 클래스나 함수로 캡슐화하여, 런타임에 필요에 따라 교체하여 사용할 수 있게 합니다.
const discountStrategies = {
VIP: (price) => price * 0.8,
GOLD: (price) => price * 0.9,
NORMAL: (price) => price
};
function calculatePrice(type, price) {
return discountStrategies[type](price);
}
6. 장식자 패턴 (Decorator Pattern)
객체의 결합을 통해 기능을 동적으로 확장합니다. 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어 유연성이 높습니다. 자바스크립트에서는 고차 함수(HOC)나 클래스 데코레이터 제안을 통해 구현됩니다.
Vue.js와 디자인 패턴
Vue 프레임워크 내부와 에코시스템에는 다양한 디자인 패턴이 녹아 있습니다.
- 관찰자 패턴: Vue의 핵심인 반응형 시스템(Reactivity)은 데이터(Subject)와 이를 사용하는 컴포넌트/이펙트(Observer) 간의 관계를 관리합니다.
- 단일체 패턴: Vuex나 Pinia의 Store는 애플리케이션 전체에서 단일 상태 트리를 유지합니다.
- 어댑터 패턴: Axios와 같은 라이브러리는 브라우저의
XMLHttpRequest와 Node.js의http모듈 간의 차이를 어댑터를 통해 해결합니다. - 조합 패턴 (Composite Pattern): 트리 구조의 컴포넌트 계층 구조는 부분과 전체를 동일하게 취급할 수 있게 합니다.