본문 바로가기
C++

[C++] 디자인 패턴: 싱글톤, 팩토리, 데코레이터 구현하기 🏗️

by 다다면체 2025. 1. 8.
728x90
반응형

안녕하세요, C++ 프로그래밍을 사랑하는 여러분! 오늘은 코드의 효율성과 유지 보수성을 극대화하는 비밀, 바로 디자인 패턴의 세계로 여러분을 안내하려 합니다. 그중에서도 실전에서 정말 유용하게 쓰이는 세 가지 패턴, **싱글톤(Singleton), 팩토리(Factory), 데코레이터(Decorator)**를 C++ 코드로 샅샅이 파헤쳐 보겠습니다!  😊


1. 싱글톤 패턴으로 단일 인스턴스 보장하기 🛡️

싱글톤 패턴은 특정 클래스의 인스턴스를 단 하나만 생성하도록 보장하는 디자인 패턴입니다. 마치 국가의 대통령처럼, 프로그램 전체에서 유일하게 존재하는 객체를 관리할 때 유용하죠. 전역적인 접근이 가능하기 때문에, 설정 관리자나 로깅 시스템처럼 여러 곳에서 공유해야 하는 리소스를 효율적으로 관리할 수 있습니다.

 

핵심:

  • private 생성자로 외부에서의 무분별한 인스턴스 생성을 막습니다. ⛔
  • 정적 메서드(getInstance())를 통해 유일한 인스턴스에 접근합니다.
  • 멀티스레드 환경에서의 안전한 사용을 위해 뮤텍스(mutex)를 활용합니다.

예제: 싱글톤 패턴 구현

#include <iostream>
#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mutex;

    Singleton() { std::cout << "Singleton 생성!" << std::endl; } // 생성 시 메시지 추가

    ~Singleton() { std::cout << "Singleton 소멸!" << std::endl; } // 소멸자 추가

public:
    Singleton(const Singleton&) = delete;            // 복사 생성 방지
    Singleton& operator=(const Singleton&) = delete; // 대입 연산 방지

    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mutex); // 스레드 안전 보장
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }

    void showMessage() {
        std::cout << "싱글톤 인스턴스입니다! " << std::endl;
    }
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

int main() {
    Singleton* singleton = Singleton::getInstance();
    singleton->showMessage();

    Singleton* anotherSingleton = Singleton::getInstance(); // 같은 인스턴스 반환
    if (singleton == anotherSingleton) {
        std::cout << "두 싱글톤은 같은 인스턴스입니다! " << std::endl;
    }

    return 0;
}

주요 포인트

  1. private 생성자로 외부에서 직접 인스턴스를 생성하지 못하도록 합니다.
  2. 멀티스레드 환경에서 안전하게 동작하도록 mutex를 사용합니다.
  3. 전역적으로 접근 가능한 단일 객체를 제공합니다.

추가 설명: 싱글톤 패턴은 전역 변수와 유사한 면이 있어 남용될 경우 코드의 결합도를 높일 수 있으므로 주의해야 합니다. 꼭 필요한 상황에서만 신중하게 사용하는 것이 중요합니다.


반응형

2. 팩토리 패턴으로 객체 생성 로직 캡슐화하기 🏭

팩토리 패턴은 객체 생성 로직을 캡슐화하여, 클라이언트 코드가 어떤 구체적인 클래스를 생성해야 하는지 알 필요 없도록 해줍니다. 마치 공장에서 물건을 찍어내듯이, 필요한 객체를 요청하면 팩토리가 알아서 만들어주는 것이죠! 이를 통해 코드의 유연성과 확장성이 크게 향상됩니다.

핵심:

  • 객체 생성을 담당하는 팩토리 클래스를 정의합니다.
  • 클라이언트는 팩토리의 메서드를 통해 객체를 생성합니다. ➡️
  • 새로운 제품 유형이 추가되더라도 클라이언트 코드를 수정할 필요가 없습니다. ➕

예제: 팩토리 패턴 구현

#include <iostream>
#include <memory>
#include <stdexcept>

class Product {
public:
    virtual void use() = 0;
    virtual ~Product() {}
};

class ConcreteProductA : public Product {
public:
    void use() override {
        std::cout << "제품 A 사용 중! " << std::endl;
    }
};

class ConcreteProductB : public Product {
public:
    void use() override {
        std::cout << "제품 B 사용 중! " << std::endl;
    }
};

class Factory {
public:
    static std::unique_ptr<Product> createProduct(const std::string& type) {
        if (type == "A") {
            return std::make_unique<ConcreteProductA>();
        } else if (type == "B") {
            return std::make_unique<ConcreteProductB>();
        } else {
            throw std::invalid_argument("알 수 없는 제품 유형: " + type); // 예외 처리 추가
        }
    }
};

int main() {
    try {
        auto productA = Factory::createProduct("A");
        auto productB = Factory::createProduct("B");
        auto productC = Factory::createProduct("C"); // 예외 발생!

        productA->use();
        productB->use();
        productC->use();

    } catch (const std::invalid_argument& e) {
        std::cerr << "오류: " << e.what() << std::endl;
    }
    return 0;
}

주요 포인트

  1. 팩토리 메서드를 통해 객체 생성 로직을 캡슐화합니다.
  2. 클라이언트는 구체적인 클래스에 의존하지 않고, 인터페이스를 통해 객체를 사용합니다.
  3. 새로운 제품 유형이 추가될 때, 팩토리 메서드만 수정하면 됩니다.

추가 설명: 팩토리 패턴은 객체 생성 로직을 한 곳에서 관리하기 때문에, 코드의 유지 보수성을 높여줍니다. 또한, 객체 생성 과정을 추상화하여 클라이언트 코드의 의존성을 줄여줍니다.


3. 데코레이터 패턴으로 동적 기능 추가하기 🎨

데코레이터 패턴은 기존 객체의 기능을 동적으로 확장할 수 있도록 해주는 패턴입니다. 마치 크리스마스 트리에 장식을 더하듯이, 기존 객체에 새로운 기능을 추가하면서도 원래 객체의 구조는 변경하지 않습니다. 상속을 사용하는 것보다 훨씬 유연하게 기능을 조합할 수 있다는 장점이 있습니다.

핵심:

  • 기본 기능을 제공하는 컴포넌트 인터페이스를 정의합니다.
  • 컴포넌트 인터페이스를 구현하는 구체적인 컴포넌트 클래스를 만듭니다.
  • 데코레이터 클래스는 컴포넌트 인터페이스를 구현하고, 다른 컴포넌트를 감싸는 형태로 기능을 추가합니다.

예제: 데코레이터 패턴 구현

#include <iostream>
#include <memory>

class Component {
public:
    virtual void operation() = 0;
    virtual ~Component() {}
};

class ConcreteComponent : public Component {
public:
    void operation() override {
        std::cout << "기본 기능 수행! ⚙️" << std::endl;
    }
};

class Decorator : public Component {
protected:
    std::unique_ptr<Component> component;

public:
    Decorator(std::unique_ptr<Component> comp) : component(std::move(comp)) {}
    void operation() override {
        component->operation(); // 기본 기능 수행
    }
    virtual ~Decorator() {}
};

class ConcreteDecoratorA : public Decorator {
public:
    ConcreteDecoratorA(std::unique_ptr<Component> comp) : Decorator(std::move(comp)) {}

    void operation() override {
        Decorator::operation();
        std::cout << "+ 추가 기능 A 수행! ✨" << std::endl;
    }
};

class ConcreteDecoratorB : public Decorator {
public:
    ConcreteDecoratorB(std::unique_ptr<Component> comp) : Decorator(std::move(comp)) {}

    void operation() override {
        Decorator::operation();
        std::cout << "+ 추가 기능 B 수행! " << std::endl;
    }
};

int main() {
    auto component = std::make_unique<ConcreteComponent>();
    auto decoratorA = std::make_unique<ConcreteDecoratorA>(std::move(component));
    auto decoratorB = std::make_unique<ConcreteDecoratorB>(std::move(decoratorA));

    decoratorB->operation(); // 모든 기능 수행

    return 0;
}

주요 포인트

  1. 기존 클래스(Component)를 상속한 데코레이터를 사용해 기능을 추가합니다.
  2. 데코레이터는 원래 객체의 동작을 유지하면서 추가적인 동작을 더합니다.
  3. 데코레이터를 중첩하여 다양한 조합의 기능을 구현할 수 있습니다.

추가 설명: 데코레이터 패턴을 사용하면 객체의 기능을 유연하게 조합할 수 있습니다. 예를 들어, 위 코드에서 ConcreteComponent에 ConcreteDecoratorA와 ConcreteDecoratorB를 각각 또는 함께 적용하여 다양한 기능을 구현할 수 있습니다.


4. 마무리 🎉

오늘 우리는 C++에서 유용하게 사용되는 세 가지 디자인 패턴, 싱글톤, 팩토리, 데코레이터를 자세히 살펴보았습니다. 이 패턴들을 적재적소에 활용하면 코드를 더욱 깔끔하고 효율적으로 작성할 수 있습니다. 이제 여러분의 C++ 프로젝트에 이 강력한 도구들을 적극 활용해 보세요! 궁금한 점이 있다면 언제든지 질문해주세요! 🚀

728x90
반응형