hmk run dev

알아두면 쓸데 있는 GOF 디자인 패턴 본문

cs

알아두면 쓸데 있는 GOF 디자인 패턴

hmk run dev 2024. 3. 28. 22:50

디자인 패턴은 프로그램을 개발하는 과정에서 빈번하게 발생하는
디자인 설계문제를 정리해서 상황에 따라 간편하게 적용할 있게 정리한 것입니다

 

선배 개발자 분들의 시행착오 끝에 그중 가장 효과적이라고 알려진 패턴으로

 활용할 수만 있다면 적지 않은 시간과 노력, 시행착오를 줄일  있습니다.

 

간략하게 미리 알아두고 풀고 싶은 문제가 있을 때 적재적소에 적용한다면 좋겠습니다!

 


디자인 패턴 용도에 따라 나누기

 

가장 대중적으로 널리 알려진 나누는 기준은 생성, 구조, 행동으로 나눈 기준입니다.

 

 

 

 


 

생성 패턴(Creational Pattern) 

객체 인스턴스를 생성하는 패턴으로, 객체를 생성하는 방법과 시점을 추상화하고, 코드의 유연성, 재사용성 유지보수성을 향상시킵니다.

 

 

싱글턴 패턴(Singleton Pattern)

 

클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 패턴입니다.
패턴을 사용하면 해당 클래스의 인스턴스에 전역적으로 접근할 있는 지점을 제공하여, 어디서든 해당 인스턴스에 접근할 있게 됩니다.

 

 

싱글톤 패턴은 주로 리소스 공유, 로깅, 캐시, 설정 관리 등과 같이 시스템 전반에 걸쳐 하나의 인스턴스만 필요한 경우에 사용됩니다.
그러나 싱글톤 패턴은 전역 상태를 만들어내는 것이므로 남발하면 코드의 유지보수성과 테스트 용이성에 부정적인 영향을 있습니다.

 

 

사용예시

 

우리는 종종 하나의 객체를 프로그램 내에서 공유하고 싶을 때가 있습니다.
예를 들어, 데이터베이스 연결, 로그 라이터 등과 같은 리소스를 효율적으로 활용하고자 때가 그러한 경우입니다.

그러나 매번 새로운 객체를 생성하는 것은 비효율적일 뿐만 아니라 예기치 못한 문제를 초래할 있습니다.

 

싱글턴 패턴은 클래스의 인스턴스가 하나만 생성되도록 보장하고, 인스턴스에 대한 전역적인 접근점을 제공합니다.

 

 

 

 

추상 팩토리 패턴(Abstract Factory Pattern)

서로 관련된 객체들의 집합을 생성하기 위한 인터페이스를 제공합니다.
패턴은 클라이언트 코드가 구상 클래스를 직접 생성하는 것이 아니라, 팩토리 객체를 통해 객체를 생성하고 관리하도록 합니다.

 

 

사용예시

 

간단한 모바일 전략 게임을 개발 중입니다.  게임에서는 유닛(병사, 기병, 궁수 ) 생성해야 합니다.

 유닛은 공격력, 방어력 등의 속성을 가지고 있으며, 다른 유닛들과 전투 혹은 방어를 할  있어야 합니다.

이러한 상황에서 추상 팩토리 패턴을 활용할  있습니다.

 

코드

더보기
// 추상 유닛 클래스
abstract class Unit {
    abstract attack(): void;
    abstract defend(): void;
}

// 병사 클래스
class Infantry extends Unit {
    attack(): void {
        console.log("Infantry attacks with sword.");
    }

    defend(): void {
        console.log("Infantry defends with shield.");
    }
}

// 궁수 클래스
class Archer extends Unit {
    attack(): void {
        console.log("Archer attacks with bow and arrow.");
    }

    defend(): void {
        console.log("Archer defends with evasion.");
    }
}

// 추상 팩토리 인터페이스
interface ArmyFactory {
    createUnit(): Unit;
}

// 병사를 생성하는 구체적인 팩토리 클래스
class InfantryFactory implements ArmyFactory {
    createUnit(): Unit {
        return new Infantry();
    }
}

// 궁수를 생성하는 구체적인 팩토리 클래스
class ArcherFactory implements ArmyFactory {
    createUnit(): Unit {
        return new Archer();
    }
}

// 게임 클라이언트
class GameClient {
    constructor(private factory: ArmyFactory) {}

    prepareArmy(): void {
        const unit = this.factory.createUnit();
        unit.attack();
        unit.defend();
    }
}

// 테스트
const infantryFactory = new InfantryFactory();
const archerFactory = new ArcherFactory();

const gameClient = new GameClient(infantryFactory);
gameClient.prepareArmy();

const gameClient2 = new GameClient(archerFactory);
gameClient2.prepareArmy();

 

 

팩토리 메서드 패턴(Abstract Factory Pattern)

패턴은 객체를 생성하기 위한 인터페이스를 정의하고, 객체의 생성을 서브 클래스에 위임하여 객체 생성 방법을 캡슐화합니다.

 

 

사용예시

 

메시지의 타입에 따라 각기 다른 클래스 인스턴스를 반환하는 PacketFactory 

 

더보기

 

export class PacketFactory {
  static createProcessor<T, G>(type: MessageType): PacketProcessor<T, G> {
    let processor;

    switch (type) {
      case 'authentication':
        processor = new AuthenticationProcessor();
        break;
      case 'exchangeRate':
        processor = new ExchangeRateProcessor();
        break;
      case 'keywordAnalysis':
        processor = new NaverKeywordTrendProcessor();
        break;
      case 'getHTMLStringFromUrl':
        processor = new ScrapHtmlProcessor();
        break;
      case 'getNaverImageSearch':
        processor = new NaverImageSearchProcessor();
        break;
      case 'getAccessToken':
        processor = new AccessTokenProcessor();
        break;
      case 'sendProduct':
        processor = new ProductProcessor();
        break;
      case 'taobaoImageSearch':
        processor = new TaobaoImageSearchProcessor();
        break;
      case 'taobaoProductSearch':
        processor = new TaobaoProductSearchProcessor();
        break;
      case 'getMainKeyword':
        processor = new MainKeywordProcessor();
        break;
      case 'getBlobs':
        processor = new BlobConvertProcessor();
        break;
      default:
        throw new Error(`no process for ${type}`);
    }

    return processor;
  }
}

 

 

  • 추상 팩토리 패턴: 관련된 객체들의 집합을 생성하기 위한 인터페이스를 제공하고, 해당 인터페이스의 구현을 통해 객체 생성을 위임합니다. 여러 종류의 관련 객체를 생성할  사용됩니다.
  • 팩토리 메서드 패턴: 객체를 생성하는 인터페이스를 정의하고, 이를 서브 클래스에서 구현하여 객체 생성 방법을 변경할 수 있도록 합니다. 즉, 객체 생성을 처리하는 로직을 Creator 클래스와 Concrete Creator 클래스로 분리합니다.


 

행동 패턴(Behavioral Pattern) 

객체 간의 상호작용과 책임 분배에 중점을 둡니다

 

 

템플릿 메소드 패턴(Abstract Factory Pattern)

알고리즘의 뼈대를 정의하고 알고리즘의 일부를 서브클래스로 디자인 패턴 확장하여
알고리즘의 일부를 재정의할 있도록 하는 패턴입니다.

 

 

템플릿 메소드 패턴은 코드의 중복을 줄이고 유연성을 향상시킵니다향상합니다.
공통으로 사용되는 알고리즘의 구조를 곳에 유지하면서,

알고리즘의 일부를 서브클래스에서 오버라이드하여 다양한 구현을 제공할 있습니다.

 

 

사용예시

 

전자제품을 생산하는 공장을 운영한다고 가정해봅시다가정해 봅시다.
공장에서는 제품을 생산하기 위한 표준적인 절차 있을 것입니다.

이를 템플릿 메소드 패턴을 사용하여 구현할 있습니다.

 

코드

더보기
// 전자제품 클래스
abstract class ElectronicsProduct {
    // 제품 생산의 템플릿 메소드
    produce(): void {
        this.assembleComponents();
        this.testProduct();
        this.packageProduct();
    }

    // 제품을 조립하는 단계
    abstract assembleComponents(): void;

    // 제품을 테스트하는 단계
    abstract testProduct(): void;

    // 제품을 포장하는 단계
    abstract packageProduct(): void;
}

// 스마트폰 클래스
class Smartphone extends ElectronicsProduct {
    assembleComponents(): void {
        console.log("Assembling smartphone components.");
    }

    testProduct(): void {
        console.log("Testing smartphone functionality.");
    }

    packageProduct(): void {
        console.log("Packaging smartphone for shipment.");
    }
}

// 노트북 클래스
class Laptop extends ElectronicsProduct {
    assembleComponents(): void {
        console.log("Assembling laptop components.");
    }

    testProduct(): void {
        console.log("Testing laptop functionality.");
    }

    packageProduct(): void {
        console.log("Packaging laptop for shipment.");
    }
}

// 테스트
const smartphone = new Smartphone();
smartphone.produce();
console.log("---------------------");
const laptop = new Laptop();
laptop.produce();

 

 

상태 패턴(State Pattern)

객체의 내부 상태가 바뀔 객체의 행동을 변경할 있도록 하는 패턴입니다.
객체의 행동은 현재 상태에 따라 달라지며, 상태를 객체로 캡슐화하여 상태에 따른 행동을 별도로 구현합니다.
패턴은 객체가 다양한 상태를 가질 있고, 상태에 따라 다르게 동작해야 유용하게 사용됩니다.

 

사용예시

 

자판기를 상태 패턴으로 구현한다고 가정해봅시다.

자판기는 여러 가지 상태를 가질 있습니다.

예를 들어, "동전 투입 대기", "음료수 선택 대기", "음료수 배출 대기" 등의 상태가 있을 있습니다.

 

코드

더보기
// 상태 인터페이스
interface VendingMachineState {
    insertCoin(): void;
    selectItem(): void;
    dispenseItem(): void;
}

// 자판기 클래스
class VendingMachine {
    private state: VendingMachineState;

    constructor(state: VendingMachineState) {
        this.state = state;
    }

    // 상태 변경 메소드
    setState(state: VendingMachineState) {
        this.state = state;
    }

    // 자판기의 행동 메소드
    insertCoin() {
        this.state.insertCoin();
    }

    selectItem() {
        this.state.selectItem();
    }

    dispenseItem() {
        this.state.dispenseItem();
    }
}

// 각 상태에 따른 클래스 구현
class WaitForCoin implements VendingMachineState {
    insertCoin() {
        console.log("동전을 투입했습니다.");
        // 다음 상태로 변경
        vendingMachine.setState(new SelectItem());
    }

    selectItem() {
        console.log("동전을 먼저 투입해주세요.");
    }

    dispenseItem() {
        console.log("음료수를 선택해주세요.");
    }
}

class SelectItem implements VendingMachineState {
    insertCoin() {
        console.log("이미 동전이 투입되었습니다.");
    }

    selectItem() {
        console.log("음료수를 선택했습니다.");
        // 다음 상태로 변경
        vendingMachine.setState(new DispenseItem());
    }

    dispenseItem() {
        console.log("음료수를 먼저 선택해주세요.");
    }
}

class DispenseItem implements VendingMachineState {
    insertCoin() {
        console.log("이미 동전이 투입되었습니다.");
    }

    selectItem() {
        console.log("이미 음료수가 선택되었습니다.");
    }

    dispenseItem() {
        console.log("음료수를 배출합니다.");
    }
}

// 테스트
const vendingMachine = new VendingMachine(new WaitForCoin());
vendingMachine.insertCoin(); // 동전을 투입했습니다.
vendingMachine.selectItem(); // 음료수를 선택해주세요.
vendingMachine.dispenseItem(); // 음료수를 선택해주세요.

 

 

반복자 패턴(iterator Pattern)

객체들의 집합을 순차적으로 접근하고 조작하기 위한 디자인 패턴입니다.
패턴은 컬렉션의 내부 구조에 독립적으로 컬렉션의 요소를 열거할 있도록 합니다.
이는 컬렉션의 요소를 순차적으로 접근하는 일반적인 인터페이스를 정의하고, 인터페이스를 구현한 반복자 객체를 사용하여 컬렉션의 요소에 접근합니다.

 

 

 

옵서버 패턴(observer Pattern)

옵저버 패턴은 객체 간의 일대다 종속성을 정의하는 디자인 패턴입니다.
패턴은 주로 상태나 데이터의 변화를 감지하고 이에 대응하는 행동을 수행할 사용됩니다.

 

프런트엔드 상태관리 라이브러리인 redu 툴은 observer 패턴을 기반으로 구현되어 있습니다.

참고로 zustand는 React의 ContextApi를 이용해 상태관리 제공

 

사용예시

 

주제(Subject) 구독자(Observer) 나타내는 클래스를 구현하여 이벤트 발생 구독자들에게 알리는 방법

 

코드

  • Subject 인터페이스는 주제(Subject) 역할을 정의하고, 구독자 관리와 알림을 위한 메소드를 포함합니다.
  • Observer 인터페이스는 구독자(Observer) 역할을 정의하고, 주제로부터 업데이트를 받기 위한 update 메소드를 포함합니다.
  • ConcreteSubject 클래스는 실제 주제를 구현하며, 상태 변경 시 등록된 모든 구독자에게 알림을 보냅니다.
  • ConcreteObserver 클래스는 실제 구독자를 구현하며, 주제로부터 메시지를 받아 출력합니다.
더보기
// 주제(Subject) 인터페이스
interface Subject {
    attach(observer: Observer): void;
    detach(observer: Observer): void;
    notify(): void;
}

// 구독자(Observer) 인터페이스
interface Observer {
    update(message: any): void;
}

// 구체적인 주제 클래스
class ConcreteSubject implements Subject {
    private observers: Observer[] = [];
    private state: any;

    getState(): any {
        return this.state;
    }

    setState(state: any): void {
        this.state = state;
        this.notify();
    }

    attach(observer: Observer): void {
        this.observers.push(observer);
    }

    detach(observer: Observer): void {
        const index = this.observers.indexOf(observer);
        if (index !== -1) {
            this.observers.splice(index, 1);
        }
    }

    notify(): void {
        for (const observer of this.observers) {
            observer.update(this.state);
        }
    }
}

// 구체적인 구독자 클래스
class ConcreteObserver implements Observer {
    update(message: any): void {
        console.log("Received message:", message);
    }
}

// 테스트
const subject = new ConcreteSubject();
const observer1 = new ConcreteObserver();
const observer2 = new ConcreteObserver();

// 구독자들을 주제에 등록
subject.attach(observer1);
subject.attach(observer2);

// 상태 변경 시 모든 구독자들에게 알림
subject.setState("New state!");

 


 

구조 패턴(Structural Pattern) 

클래스나 객체들 사이의 관계를 강화하고 구성하는 방법을 제공하여 코드의 유지보수성, 재사용성, 확장성을 향상시킵니다.

 

 

데코레이터 패턴(Decorator Pattern)

객체에 동적으로 새로운 기능을 추가하는 패턴입니다. 패턴은 상속을 사용하지 않고도 객체의 기능을 확장할 있게 해줍니다.

서브클래스를 만들 때보다 유연하게 기능흘 확장할 수 있습니다.

 

 

사용예시

 

커피 주문 커피에 여러 가지 토핑을 추가하는 상황을 가정하겠습니다.

  • Coffee 인터페이스는 기본 커피의 동작을 정의합니다.
  • SimpleCoffee 클래스는 기본 커피를 구현합니다.
  • CoffeeDecorator 추상 클래스는 데코레이터의 기본 구조를 정의합니다.
  • MilkDecorator WhippedCreamDecorator 클래스는 각각 우유와 휘핑 크림을 추가하는 데코레이터를 구현합니다.

코드

더보기
// 커피 인터페이스
interface Coffee {
    cost(): number;
    getDescription(): string;
}

// 기본 커피 클래스
class SimpleCoffee implements Coffee {
    cost(): number {
        return 5;
    }

    getDescription(): string {
        return "Simple Coffee";
    }
}

// 데코레이터 클래스
abstract class CoffeeDecorator implements Coffee {
    protected decoratedCoffee: Coffee;

    constructor(coffee: Coffee) {
        this.decoratedCoffee = coffee;
    }

    cost(): number {
        return this.decoratedCoffee.cost();
    }

    getDescription(): string {
        return this.decoratedCoffee.getDescription();
    }
}

// 토핑 데코레이터 - 우유
class MilkDecorator extends CoffeeDecorator {
    constructor(coffee: Coffee) {
        super(coffee);
    }

    cost(): number {
        return super.cost() + 2;
    }

    getDescription(): string {
        return super.getDescription() + ", Milk";
    }
}

// 토핑 데코레이터 - 휘핑 크림
class WhippedCreamDecorator extends CoffeeDecorator {
    constructor(coffee: Coffee) {
        super(coffee);
    }

    cost(): number {
        return super.cost() + 3;
    }

    getDescription(): string {
        return super.getDescription() + ", Whipped Cream";
    }
}

// 테스트
let coffee: Coffee = new SimpleCoffee();
console.log(coffee.getDescription(), "costs", coffee.cost());

coffee = new MilkDecorator(coffee);
console.log(coffee.getDescription(), "costs", coffee.cost());

coffee = new WhippedCreamDecorator(coffee);
console.log(coffee.getDescription(), "costs", coffee.cost());

 

 

프록시 패턴(Proxy Pattern)

객체에 대한 접근을 제어하거나 대리자 역할을 하는 객체를 제공하여 추가적인 기능을 제공하거나 제어하는 패턴입니다.

 

사용예시

 

이미지 로딩을 지연시키는 프록시를 구현해보겠습니다.

 

코드

  • Image 인터페이스는 이미지의 기본 동작을 정의합니다.
  • RealImage 클래스는 실제 이미지를 로딩하고 표시합니다.
  • ImageProxy 클래스는 실제 이미지를 로딩하기 전까지는 실제 이미지를 생성하지 않습니다. 대신, 이미지가 요청될 때마다 실제 이미지를 로딩하여 표시합니다.
더보기
// 이미지 인터페이스
interface Image {
    display(): void;
}

// 실제 이미지 클래스
class RealImage implements Image {
    private filename: string;

    constructor(filename: string) {
        this.filename = filename;
        this.loadFromDisk();
    }

    private loadFromDisk(): void {
        console.log("Loading image:", this.filename);
    }

    display(): void {
        console.log("Displaying image:", this.filename);
    }
}

// 이미지 프록시 클래스
class ImageProxy implements Image {
    private filename: string;
    private realImage: RealImage | null;

    constructor(filename: string) {
        this.filename = filename;
        this.realImage = null;
    }

    display(): void {
        if (!this.realImage) {
            this.realImage = new RealImage(this.filename);
        }
        this.realImage.display();
    }
}

// 테스트
const image1: Image = new ImageProxy("image1.jpg");
const image2: Image = new ImageProxy("image2.jpg");

// 이미지 1은 처음에 로딩이 발생
image1.display();
// 이미지 1은 두 번째 호출부터 로딩이 발생하지 않음
image1.display();
// 이미지 2는 처음에 로딩이 발생
image2.display();

 

어댑터 패턴(Proxy Pattern)

서로 다른 인터페이스를 가진 개의 클래스를 함께 작동할 있도록 변환하는 패턴입니다.

이는 기존의 코드를 수정하지 않고도 새로운 인터페이스에 맞춰 사용 있도록 도와줍니다.

 

 

사용예시

 

콘센트를 변환하여 다른 지역에서도 사용할 있는 장비를 구현해보겠습니다.

 

 

코드

더보기
// 한국 콘센트 인터페이스
interface KoreanPlug {
    supplyPower(): void;
}

// 한국 콘센트 구현 클래스
class KoreanSocket implements KoreanPlug {
    supplyPower(): void {
        console.log("Korean plug is supplying power.");
    }
}

// 미국 콘센트 인터페이스
interface USPlug {
    provideElectricity(): void;
}

// 미국 콘센트 구현 클래스
class USASocket implements USPlug {
    provideElectricity(): void {
        console.log("US plug is providing electricity.");
    }
}

// 어댑터 클래스
class KoreanToUSAdapter implements USPlug {
    private koreanPlug: KoreanPlug;

    constructor(koreanPlug: KoreanPlug) {
        this.koreanPlug = koreanPlug;
    }

    provideElectricity(): void {
        console.log("Adapter is converting Korean plug to US plug.");
        this.koreanPlug.supplyPower();
    }
}

// 테스트
const koreanPlug: KoreanPlug = new KoreanSocket();
const adapter: USPlug = new KoreanToUSAdapter(koreanPlug);

adapter.provideElectricity();

 

파사드 패턴(Proxy Pattern)

 

복잡한 서브시스템의 인터페이스를 단순화시켜 사용자에게 쉽게 접근할 있도록 하는 패턴입니다.

이를 통해 클라이언트는 복잡한 서브시스템을 직접 다루지 않고도 사용할 있습니다.

 

 

사용예시

 

컴퓨터를 부팅하는 과정을 파사드 패턴을 사용하여 구현해보겠습니다.

 

 

코드

  • CPU, Memory, Display 클래스는 각각 CPU, 메모리, 디스플레이의 동작을 구현합니다.
  • ComputerFacade 클래스는 이러한 서브시스템을 간단한 인터페이스로 묶어줍니다. startComputer 메서드를 통해 클라이언트는 컴퓨터를 부팅할 수 있습니다.
  • 클라이언트 코드에서는 ComputerFacade 인스턴스를 생성하고 startComputer 메서드를 호출하여 컴퓨터를 부팅합니다.
더보기
// CPU 클래스
class CPU {
    public bootUp(): void {
        console.log("CPU is booting up...");
    }
}

// 메모리 클래스
class Memory {
    public load(): void {
        console.log("Memory is loading...");
    }
}

// 디스플레이 클래스
class Display {
    public displayImage(): void {
        console.log("Displaying image...");
    }
}

// 파사드 클래스
class ComputerFacade {
    private cpu: CPU;
    private memory: Memory;
    private display: Display;

    constructor() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.display = new Display();
    }

    public startComputer(): void {
        console.log("Starting computer...");
        this.cpu.bootUp();
        this.memory.load();
        this.display.displayImage();
        console.log("Computer started.");
    }
}

// 클라이언트 코드
const computerFacade = new ComputerFacade();
computerFacade.startComputer();

Reference

https://m.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823

 

'cs' 카테고리의 다른 글

프로세스 스케줄링 알고리즘  (0) 2024.04.03
빠른 CPU를 위한 설계 기법  (0) 2024.03.02
소스 코드와 명령어  (0) 2024.03.02
0과 1로 문자를 표현하기  (0) 2024.03.02
0과 1  (0) 2024.03.02
Comments