icon
7장 : 자바스크립트 심화 II

디자인 패턴 in 자바스크립트


우리는 7장 "자바스크립트 심화 II"를 통해 비동기 프로그래밍, 이터레이터/제너레이터, 모듈 시스템, 메모리 관리 등 자바스크립트의 깊은 원리들을 학습했습니다. 이제 여러분은 단순한 문법을 넘어 언어의 강력한 특성과 동작 방식을 이해하게 되었습니다. 하지만 이러한 지식만으로는 항상 '좋은 코드'를 작성할 수 있는 것은 아닙니다. 좋은 코드는 단순히 동작하는 것을 넘어, 확장성, 유지보수성, 가독성, 그리고 재사용성이 뛰어납니다.

이러한 '좋은 코드'를 작성하기 위한 일종의 설계 지침 또는 문제 해결 전략이 바로 디자인 패턴(Design Pattern) 입니다. 디자인 패턴은 특정 유형의 문제에 대해 검증되고 반복적으로 사용될 수 있는 일반적인 해결책을 의미합니다. 이는 건축가가 건물 설계 시 여러 가지 설계 도면과 방법을 참고하는 것과 유사합니다.

자바스크립트는 매우 유연하고 동적인 언어이기 때문에, 다른 객체 지향 언어에서 유래한 디자인 패턴들을 자바스크립트의 특성에 맞게 적용하거나, 자바스크립트 고유의 패턴들을 활용하는 것이 중요합니다. 이번 장에서는 자바스크립트 개발에서 특히 유용하게 활용되는 몇 가지 핵심 디자인 패턴들을 살펴보고, 실제 코드 예시를 통해 어떻게 적용되는지 알아보겠습니다.


디자인 패턴이란 무엇인가?

디자인 패턴은 "GoF(Gang of Four)"라고 불리는 에리히 감마, 리처드 헬름, 랄프 존슨, 존 블리시데스 네 명의 소프트웨어 공학자들이 1994년 저술한 책 <Design Patterns: Elements of Reusable Object-Oriented Software>에서 시작되었습니다. 이 책에서는 객체 지향 프로그래밍에서 반복적으로 발생하는 설계 문제를 해결하기 위한 23가지 패턴을 소개했습니다.

디자인 패턴을 학습하는 이유는 다음과 같습니다.

  • 공통 언어: 개발자들 간에 특정 패턴을 지칭함으로써 코드를 더 쉽게 이해하고 소통할 수 있습니다.
  • 문제 해결 능력 향상: 특정 문제에 직면했을 때, 이미 검증된 해결책을 적용하여 시간과 노력을 절약할 수 있습니다.
  • 유지보수성 및 확장성: 패턴을 적용하여 작성된 코드는 일반적으로 구조가 명확하고 변경에 유연하게 대응할 수 있습니다.
  • 코드 품질 향상: 재사용 가능한 구성 요소를 설계하여 코드의 응집도를 높이고 결합도를 낮춥니다.

디자인 패턴은 크게 세 가지 카테고리로 분류됩니다.

생성 패턴 (Creational Patterns): 객체 생성 메커니즘을 추상화하여, 객체 생성 방식을 유연하게 제어합니다.

구조 패턴 (Structural Patterns): 클래스나 객체를 더 큰 구조로 조합하여 새로운 구조를 만듭니다.

행위 패턴 (Behavioral Patterns): 객체 간의 책임 분배 및 통신 방법을 정의하여 객체 간의 상호작용을 관리합니다.

자바스크립트의 유연성 때문에 모든 GoF 패턴이 그대로 적용되지는 않지만, 그 원칙들은 여전히 유효합니다. 이제 자바스크립트에서 자주 사용되는 몇 가지 패턴을 살펴보겠습니다.


자바스크립트에서 자주 사용되는 디자인 패턴

싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴은 특정 클래스의 인스턴스를 오직 하나만 생성하고, 어디서든 그 인스턴스에 접근할 수 있도록 하는 패턴입니다. 주로 애플리케이션 전반에 걸쳐 공유되어야 하는 유일한 리소스(예: 데이터베이스 연결, 설정 관리자, 로거)에 사용됩니다.

자바스크립트에서의 구현 클로저와 IIFE(즉시 실행 함수 표현)를 활용하여 외부에서 인스턴스를 여러 번 생성하는 것을 막고, 최초 한 번만 생성되도록 합니다.

const Singleton = (function() {
    let instance; // 클로저에 의해 은닉된 인스턴스

    function init() {
        // 싱글톤 객체의 실제 생성 로직
        const privateRandomNumber = Math.random(); // 단 한 번만 생성
        console.log(`새로운 인스턴스 생성됨: ${privateRandomNumber}`);

        return {
            getPublicNumber: function() {
                return privateRandomNumber;
            },
            publicMethod: function() {
                console.log("싱글톤 공개 메서드 호출");
            }
        };
    }

    return {
        getInstance: function() {
            if (!instance) { // 인스턴스가 없으면 새로 생성
                instance = init();
            }
            return instance; // 이미 존재하면 기존 인스턴스 반환
        }
    };
})();

const singletonA = Singleton.getInstance();
const singletonB = Singleton.getInstance();

console.log(singletonA.getPublicNumber()); // 동일한 숫자 출력
console.log(singletonB.getPublicNumber()); // 동일한 숫자 출력

console.log(singletonA === singletonB); // 결과: true (동일한 인스턴스)

singletonA.publicMethod();

주의: 싱글톤 패턴은 남용하면 전역 상태를 만들고 결합도를 높여 테스트하기 어려워질 수 있으므로 신중하게 사용해야 합니다. ES Modules 자체도 모듈 스코프 내에서 싱글톤처럼 동작하는 경향이 있습니다.

팩토리 패턴 (Factory Pattern)

팩토리 패턴은 객체를 생성하는 로직을 별도의 '팩토리(Factory)' 객체에 캡슐화하는 패턴입니다. 클라이언트는 특정 객체를 직접 생성하는 대신 팩토리 메서드를 호출하여 객체를 얻습니다. 이는 생성할 객체의 종류가 많거나, 객체 생성 로직이 복잡할 때 유용합니다.

// 팩토리 함수
function createCharacter(type, name) {
    if (type === 'Warrior') {
        return {
            name: name,
            attack: function() {
                console.log(`${this.name}이(가) 검으로 공격합니다!`);
            }
        };
    } else if (type === 'Mage') {
        return {
            name: name,
            attack: function() {
                console.log(`${this.name}이(가) 마법으로 공격합니다!`);
            },
            castSpell: function() {
                console.log(`${this.name}이(가) 마법을 시전합니다!`);
            }
        };
    } else {
        throw new Error("알 수 없는 캐릭터 타입입니다.");
    }
}

const warrior = createCharacter('Warrior', '아서');
const mage = createCharacter('Mage', '멀린');

warrior.attack(); // 결과: 아서이(가) 검으로 공격합니다!
mage.attack();    // 결과: 멀린이(가) 마법으로 공격합니다!
mage.castSpell(); // 결과: 멀린이(가) 마법을 시전합니다!

팩토리 패턴은 객체 생성에 대한 유연성을 제공하고, 클라이언트 코드와 생성 로직을 분리하여 느슨한 결합을 유지하게 합니다.

옵저버 패턴 (Observer Pattern)

옵저버 패턴은 객체 간의 일대다(one-to-many) 의존성을 정의하는 패턴입니다. 하나의 '주제(Subject)' 객체가 변경될 때마다, 그 주제를 '구독(subscribe)'하고 있는 모든 '관찰자(Observer)' 객체들에게 자동으로 알림이 전송됩니다. 이 패턴은 이벤트 시스템이나 비동기 통신에서 매우 흔하게 사용됩니다. 발행-구독 패턴은 옵저버 패턴의 변형으로, 중간에 메시지 브로커(이벤트 버스)가 있어 발행자와 구독자 간의 직접적인 의존성을 제거합니다.

// 간단한 이벤트 발행자 (Subject) 구현
class EventPublisher {
    constructor() {
        this.observers = []; // 구독자 목록
    }

    // 구독자 추가
    subscribe(observer) {
        this.observers.push(observer);
        console.log(`새로운 구독자 추가됨: ${observer.name}`);
    }

    // 구독자 제거
    unsubscribe(observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
        console.log(`구독자 제거됨: ${observer.name}`);
    }

    // 모든 구독자에게 알림
    notify(data) {
        console.log("이벤트 발생! 모든 구독자에게 알림...");
        this.observers.forEach(observer => observer.update(data));
    }
}

// 구독자 (Observer) 구현
class EmailSender {
    constructor(name) {
        this.name = name;
    }
    update(data) {
        console.log(`${this.name}: 이메일 전송 - ${data.message}`);
    }
}

class Logger {
    constructor(name) {
        this.name = name;
    }
    update(data) {
        console.log(`${this.name}: 로그 기록 - ${data.message} (발생 시간: ${new Date().toLocaleTimeString()})`);
    }
}

const newsPublisher = new EventPublisher();

const emailService = new EmailSender("이메일 서비스");
const loggerService = new Logger("로깅 서비스");
const analyticsService = new Logger("분석 서비스"); // Logger를 재사용

newsPublisher.subscribe(emailService);
newsPublisher.subscribe(loggerService);
newsPublisher.subscribe(analyticsService);

newsPublisher.notify({ message: "새로운 기사가 발행되었습니다!" });

newsPublisher.unsubscribe(loggerService); // 로깅 서비스 구독 해제

newsPublisher.notify({ message: "특별 할인 이벤트 시작!" });

DOM 이벤트(addEventListener)나 비동기 데이터 스트림(RxJS 같은 라이브러리)에서 이 패턴의 원리가 광범위하게 사용됩니다.

모듈 패턴 (Module Pattern)

자바스크립트의 모듈 패턴은 IIFE와 클로저를 활용하여 비공개(private) 상태와 공개(public) API를 분리하는 강력한 패턴입니다. ES Modules가 나오기 전에는 가장 널리 사용되던 모듈화 방식이었으며, 여전히 클로저의 활용을 보여주는 좋은 예시입니다. (사실상 3.2.1의 IIFE 예시와 동일합니다.)

const ShoppingCart = (function() {
    let items = []; // private 변수 (클로저에 의해 외부 접근 불가)

    function findItem(name) { // private 함수
        return items.find(item => item.name === name);
    }

    return { // public API
        addItem: function(name, price, quantity) {
            const existingItem = findItem(name);
            if (existingItem) {
                existingItem.quantity += quantity;
            } else {
                items.push({ name, price, quantity });
            }
            console.log(`${name} ${quantity}개 추가됨.`);
        },
        removeItem: function(name) {
            items = items.filter(item => item.name !== name);
            console.log(`${name} 제거됨.`);
        },
        getTotal: function() {
            return items.reduce((total, item) => total + (item.price * item.quantity), 0);
        },
        getCartItems: function() {
            return [...items]; // 배열 복사본을 반환하여 원본 배열 직접 수정 방지 (불변성)
        }
    };
})();

ShoppingCart.addItem("사과", 1000, 2);
ShoppingCart.addItem("바나나", 1500, 1);
ShoppingCart.addItem("사과", 1000, 3); // 사과 2 + 3 = 5개 됨

console.log("장바구니 총액:", ShoppingCart.getTotal()); // 결과: 8500
console.log("장바구니 품목:", ShoppingCart.getCartItems());

// console.log(ShoppingCart.items); // undefined (접근 불가)
// console.log(ShoppingCart.findItem); // undefined (접근 불가)

ES Modules가 대중화되면서 이 패턴의 필요성은 줄어들었지만, 클로저를 이용한 정보 은닉의 원리를 이해하는 데는 매우 중요한 패턴입니다.


디자인 패턴 적용의 중요성

디자인 패턴은 단순히 코드를 더 멋지게 만드는 것이 아니라, 소프트웨어 개발의 고질적인 문제들을 해결하고 장기적인 관점에서 코드의 건강을 유지하는 데 도움을 줍니다. 하지만 패턴을 맹목적으로 적용하는 것은 오히려 코드를 복잡하게 만들 수 있습니다. 중요한 것은 문제를 정확히 이해하고, 그 문제에 가장 적합한 패턴을 선택하는 지혜입니다.

  • 과도한 패턴 사용은 피하라: 작은 문제에 거창한 패턴을 적용하는 것은 '과도한 설계(Over-engineering)'가 될 수 있습니다.
  • 유연성과 단순성을 유지하라: 패턴은 유연성을 제공하지만, 때로는 단순한 해결책이 더 나을 때도 있습니다.
  • 패턴을 학습하되, 맹신하지 마라: 패턴은 가이드라인이지, 절대적인 규칙이 아닙니다. 자바스크립트의 특성을 고려하여 적절히 변형하거나 조합하는 능력이 필요합니다.

마무리하며

이번 장에서는 자바스크립트 개발에서 디자인 패턴(Design Pattern) 이 왜 중요하며, 어떻게 코드를 더 효율적이고 유지보수하기 쉽게 설계하는 데 도움을 주는지 학습했습니다.

여러분은 싱글톤 패턴(단 하나의 인스턴스 보장), 팩토리 패턴(객체 생성 로직 캡슐화), 옵저버 패턴(이벤트/알림 시스템 구현), 그리고 모듈 패턴(정보 은닉과 모듈화)과 같은 주요 패턴들을 실제 자바스크립트 코드 예시와 함께 살펴보았습니다. 각 패턴이 어떤 문제를 해결하고 어떤 장점을 제공하는지 이해했기를 바랍니다.

디자인 패턴은 소프트웨어 공학의 오랜 지혜가 담긴 보물 같은 지식입니다. 이들을 학습함으로써 여러분은 복잡한 설계 문제를 효과적으로 해결하고, 다른 개발자들과 더 원활하게 소통할 수 있는 능력을 갖추게 될 것입니다. 실제 프로젝트를 진행하면서 다양한 문제에 부딪히고, 그때마다 어떤 패턴을 적용할 수 있을지 고민해보는 과정을 통해 디자인 패턴에 대한 이해를 더욱 심화시켜 나가세요.