소프트웨어 디자인, 설계

디자인 패턴


디자인 패턴은 소프트웨어 개발에서 자주 발생하는 문제를 해결하기 위한 일반적인 해결책을 말합니다. 특정 언어에 종속되지 않으며, 코드 재사용성과 유지보수성을 향상시키는 데 도움이 됩니다.

디자인 패턴은 크게 GOF (Gang of Four) 패턴과 POSA (Pattern-Oriented Software Architecture) 패턴으로 나눌 수 있습니다.

1. GOF (Gang of Four) 디자인 패턴

객체지향 프로그래밍(OOP)에 초점을 맞추어 클래스 간의 관계와 상호작용을 정형화한 패턴입니다.

GOF 패턴의 분류

  1. 행위(Behavioral) : 객체 간의 상호작용 및 책임 분배와 관련된 패턴으로, 객체 간의 커뮤니케이션 방식을 최적화함.

1.1 생성 패턴

객체의 생성 로직을 효율적으로 관리하여, 불필요한 객체 생성을 방지하고 코드의 유연성을 높이는 패턴입니다.

  • 싱글톤 (Singleton) 하나의 인스턴스만 생성하고 이를 공유함.
  • 팩토리 메서드 (Factory Method) 객체 생성을 하위 클래스에서 결정하도록 위임함.
  • 추상 팩토리 (Abstract Factory) 관련 객체들을 일관된 방식으로 생성함.
  • 빌더 (Builder) 복잡한 객체 생성을 단계별로 수행함.
  • 프로토타입 (Prototype) 기존 객체를 복사하여 새로운 객체를 생성함.

글로 읽으면 이해가 잘 안되는데요. 소스코드로 예시 들게요. OOP, FP 두개다 예시 들겠습니다. 싱글톤만 예시를 듭니다.

싱글톤 패턴 예시

//OOP
class Singleton {
    constructor() {
        if (!Singleton.instance) {
            Singleton.instance = this;
        }
        return Singleton.instance;
    }

    sayHello() {
        console.log("Hello from Singleton!");
    }
}

const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
instance1.sayHello(); // "Hello from Singleton!"
//FP
//클래스를 사용하지 않고 클로저(Closure)를 활용하여 하나의 인스턴스만 유지합니다.

const createSingleton = (() => {
    let instance = null;
    return () => {
        if (!instance) {
            instance = { message: "Hello from Singleton!" };
        }
        return instance;
    };
})();

const instance1 = createSingleton();
const instance2 = createSingleton();

console.log(instance1 === instance2); // true
console.log(instance1.message); // "Hello from Singleton!"

1.2 구조 패턴

클래스와 객체의 구조를 다루며, 효율적인 클래스 설계 및 객체 조합 방법을 제공합니다.

  • 어댑터 (Adapter) : 서로 다른 인터페이스를 가진 클래스를 호환되도록 연결함.
  • 브리지 (Bridge) : 기능과 구현을 분리하여 독립적으로 확장 가능하도록 함.
  • 컴포지트 (Composite) : 객체를 트리 구조로 구성하여 계층적인 구조를 표현함.
  • 데코레이터 (Decorator) : 객체의 기능을 동적으로 확장할 수 있도록 함.
  • 퍼사드 (Facade) : 복잡한 시스템을 단순한 인터페이스로 제공함.
  • 플라이웨이트 (Flyweight) : 공유 가능한 객체를 사용하여 메모리 사용을 줄임.
  • 프록시 (Proxy) : 접근을 제어하는 대리 객체를 제공함.

어댑터 예시


// 기존 시스템 (Old System)
class OldSystem {
    oldMethod() {
        console.log("Old system method");
    }
}

// 새로운 시스템 (New System)
class NewSystem {
    newMethod() {
        console.log("New system method");
    }
}

// 어댑터
class Adapter {
    constructor(oldSystem) {
        this.oldSystem = oldSystem;
    }

    newMethod() {
        this.oldSystem.oldMethod(); // 기존 코드 재사용
    }
}

const oldSystem = new OldSystem();
const adapter = new Adapter(oldSystem);
adapter.newMethod(); // "Old system method"
const oldSystem = () => "Old system method";

const adapter = (fn) => () => fn(); 

const newSystem = adapter(oldSystem);
console.log(newSystem()); // "Old system method"

1.3 행동 패턴

객체 간의 상호작용을 다루며, 효율적인 커뮤니케이션과 책임 분배를 위한 패턴입니다.

  • 책임 연쇄 (Chain of Responsibility) : 요청을 처리할 객체를 체인 형태로 연결하여 순차적으로 처리함.
  • 커맨드 (Command) : 요청을 객체로 캡슐화하여 실행을 나중에 할 수 있도록 함.
  • 인터프리터 (Interpreter) : 언어의 문법을 정의하고 해석하는 구조를 제공함.
  • 반복자 (Iterator) : 컬렉션 요소에 접근하는 방법을 표준화함.
  • 중재자 (Mediator) : 객체 간의 직접적인 통신을 막고 중재자를 통해 상호작용하도록 함.
  • 메멘토 (Memento) : 객체의 상태를 저장하고 복원할 수 있도록 함.
  • 옵저버 (Observer) : 상태 변화가 있을 때 여러 객체에게 자동으로 알림을 보냄.
  • 상태 (State) : 객체의 상태에 따라 행동을 변경할 수 있도록 함.
  • 전략 (Strategy) : 알고리즘을 캡슐화하여 동적으로 변경할 수 있도록 함.
  • 템플릿 메서드 : (Template Method) 알고리즘의 구조를 정의하고, 세부 구현은 하위 클래스에서 처리하도록 함.
  • 방문자 (Visitor) : 객체 구조를 변경하지 않고 새로운 연산을 추가할 수 있도록 함.

옵저버 패턴 예시

class Subject {
    constructor() {
        this.observers = [];
    }

    addObserver(observer) {
        this.observers.push(observer);
    }

    notifyObservers(message) {
        this.observers.forEach(observer => observer.update(message));
    }
}

class Observer {
    constructor(name) {
        this.name = name;
    }

    update(message) {
        console.log(`${this.name} received: ${message}`);
    }
}

const chatRoom = new Subject();
const user1 = new Observer("Alice");
const user2 = new Observer("Bob");

chatRoom.addObserver(user1);
chatRoom.addObserver(user2);

chatRoom.notifyObservers("New message in chat!");
// Alice received: New message in chat!
// Bob received: New message in chat!
const createSubject = () => {
    let observers = [];
    return {
        subscribe: (observer) => (observers = [...observers, observer]),
        notify: (message) => observers.forEach((observer) => observer(message))
    };
};

const subject = createSubject();
const observer1 = (msg) => console.log(`Alice received: ${msg}`);
const observer2 = (msg) => console.log(`Bob received: ${msg}`);

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify("New message in chat!");

2. POSA 패턴

POSA 패턴은 아키텍처 수준의 패턴을 다루며, 크게 4가지 카테고리로 나뉩니다.

  1. 상호작용 (Distributed Systems) : 여러 컴포넌트가 통신하는 방식을 정의
  2. 적응성 (Adaptation & Extension) : 변경과 확장을 쉽게 할 수 있도록 설계하는 패턴
  3. 동시성 (Concurrency) : 병렬 실행을 다룰 수 있도록 설계하는 패턴

2.1 분할

  • 소프트웨어 시스템을 명확한 모듈로 나누는 패턴
  • 시스템을 여러 계층(layer) 으로 분리하여, 각 계층이 특정 역할을 수행하도록 설계.
  • 보통 Presentation → Business Logic → Data Access 형태로 나뉨.
class DataAccess {
    fetchData() {
        return "Fetching data from database";
    }
}

class BusinessLogic {
    constructor(dataAccess) {
        this.dataAccess = dataAccess;
    }
    processData() {
        return this.dataAccess.fetchData() + " → Processing data";
    }
}

class Presentation {
    constructor(businessLogic) {
        this.businessLogic = businessLogic;
    }
    render() {
        console.log(this.businessLogic.processData() + " → Rendering UI");
    }
}

// 계층 연결
const dataLayer = new DataAccess();
const businessLayer = new BusinessLogic(dataLayer);
const uiLayer = new Presentation(businessLayer);

uiLayer.render();
// Output: Fetching data from database → Processing data → Rendering UI
const fetchData = () => "Fetching data from database";

const processData = (fetchFn) => () => fetchFn() + " → Processing data";

const render = (processFn) => () => console.log(processFn() + " → Rendering UI");

// 계층 연결
const businessLayer = processData(fetchData);
const uiLayer = render(businessLayer);

uiLayer();
// Output: Fetching data from database → Processing data → Rendering UI

2.2 상호작용

  • 중앙의 브로커(중개자)가 여러 개의 클라이언트와 서비스를 연결하는 구조.
  • 주로 메시지 큐(Message Queue)와 이벤트 기반 시스템에서 사용됨.
class Broker {
    constructor() {
        this.services = {};
    }

    registerService(name, service) {
        this.services[name] = service;
    }

    requestService(name, data) {
        return this.services[name] ? this.services[name](data) : "Service not found";
    }
}

// 서비스 정의
const broker = new Broker();
broker.registerService("logging", (message) => `Logging: ${message}`);
broker.registerService("auth", (user) => `Authenticating: ${user}`);

// 서비스 호출
console.log(broker.requestService("logging", "User logged in"));
console.log(broker.requestService("auth", "Alice"));

//Broker 클래스가 모든 요청을 중개
const broker = (() => {
    const services = new Map();

    return {
        register: (name, service) => services.set(name, service),
        request: (name, data) => services.get(name)?.(data) || "Service not found",
    };
})();

// 서비스 등록
broker.register("logging", (message) => `Logging: ${message}`);
broker.register("auth", (user) => `Authenticating: ${user}`);

// 서비스 호출
console.log(broker.request("logging", "User logged in"));
console.log(broker.request("auth", "Alice"));

// 클로저(Closure)를 사용해 상태를 유지
// broker 객체가 직접 함수 등록 및 실행

2.3 적응성

  • 핵심 기능(코어)을 최소화하고, 나머지는 플러그인(확장 기능)으로 추가하는 방식.
  • 예시: VSCode의 확장 플러그인, Express.js 미들웨어

//OOP
class Microkernel {
    constructor() {
        this.plugins = [];
    }

    registerPlugin(plugin) {
        this.plugins.push(plugin);
    }

    execute() {
        this.plugins.forEach((plugin) => plugin());
    }
}

// 플러그인 추가
const kernel = new Microkernel();
kernel.registerPlugin(() => console.log("Plugin 1 executed"));
kernel.registerPlugin(() => console.log("Plugin 2 executed"));

kernel.execute();

//Microkernel 클래스가 핵심 기능을 제공

//FP
const createKernel = () => {
    let plugins = [];

    return {
        register: (plugin) => (plugins = [...plugins, plugin]),
        execute: () => plugins.forEach((plugin) => plugin()),
    };
};

// 플러그인 추가
const kernel = createKernel();
kernel.register(() => console.log("Plugin 1 executed"));
kernel.register(() => console.log("Plugin 2 executed"));

kernel.execute();

3. 결론: GOF, POSA 패턴의 차이점과 현업에서는 어떻게 사용될까?

GOF 패턴 : OOP 기반의 개발 (ex. Java, C#), 객체 간의 관계를 효율적으로 관리
POSA 패턴 : 대규모 시스템 설계 (ex. 마이크로서비스, 분산 시스템, 이벤트 기반 처리)

JavaScript/Node.js 기반 프로젝트에서는 다음 패턴이 많이 사용됨

  • GOF 패턴 → 싱글톤, 팩토리, 옵저버
  • POSA 패턴 → MVC, API Gateway, Event-Driven Architecture

적절한 패턴을 선택하면 유지보수성과 확장성이 뛰어난 소프트웨어를 만들 수 있다고 생각합니다.

Previous
디자인 원리