프로그래밍/C++

디자인 패턴: 싱글톤 패턴과 팩토리 패턴

shimdh 2025. 2. 4. 09:55
728x90

싱글톤 패턴

개념

싱글톤 패턴은 특정 클래스의 인스턴스를 오직 하나만 생성하고, 그 인스턴스에 대한 전역적인 접근을 제공하는 패턴입니다. 이는 주로 애플리케이션 전역에서 공유해야 하는 리소스(예: 설정, 로그 기록기)를 관리할 때 유용합니다.

필요성

  1. 전역 상태 관리: 여러 객체가 동일한 데이터나 리소스를 사용할 때 일관성을 유지합니다.
  2. 인스턴스 제어: 불필요한 인스턴스 생성을 방지하여 메모리 사용을 최적화합니다.
  3. 간편한 접근성: 어디서든 동일한 인스턴스를 참조할 수 있어 코드 관리가 용이해집니다.

구현 방법

싱글톤 패턴의 핵심은 다음과 같습니다:

실제 사례

싱글톤 패턴은 다양한 애플리케이션에서 다음과 같은 사례로 활용됩니다:

  1. 로깅 시스템: 애플리케이션 전반에서 로그를 기록하기 위해 단일 로깅 객체를 사용합니다.

  2. 설정 관리: 애플리케이션의 설정 데이터를 중앙 집중식으로 관리하고 참조합니다.

  3. 데이터베이스 연결 풀: 데이터베이스 연결 객체를 싱글톤으로 유지하여 성능을 최적화합니다.

  4. 프린터 스풀러: 프린터 작업을 효율적으로 관리하기 위해 단일 스풀러 객체를 활용합니다.

  5. 생성자를 private으로 설정하여 외부에서 객체를 직접 생성하지 못하도록 합니다.

  6. 정적 메서드를 통해 유일한 인스턴스를 반환합니다.

  7. 멀티스레드 환경에서 안전성을 보장하기 위해 동기화 메커니즘을 사용합니다.

C++ 예제 코드

#include <iostream>
#include <mutex>

class Singleton {
private:
    static Singleton* instance; // 정적 인스턴스 포인터
    static std::mutex mtx;      // 스레드 안전성을 위한 뮤텍스

    Singleton() {} // private 생성자

public:
    static Singleton* getInstance() {
        if (!instance) { // 인스턴스가 없을 경우
            std::lock_guard<std::mutex> lock(mtx); // 뮤텍스로 보호
            if (!instance) { // 다시 확인 (double-check locking)
                instance = new Singleton();
            }
        }
        return instance;
    }

    void showMessage() {
        std::cout << "싱글톤 클래스 메시지!" << std::endl;
    }
};

// 정적 변수 초기화
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

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

추가 예제: 로깅 시스템에 싱글톤 적용

#include <iostream>
#include <fstream>
#include <mutex>

class Logger {
private:
    static Logger* instance;
    static std::mutex mtx;
    std::ofstream logFile;

    Logger() {
        logFile.open("app.log", std::ios::app);
    }

public:
    static Logger* getInstance() {
        if (!instance) {
            std::lock_guard<std::mutex> lock(mtx);
            if (!instance) {
                instance = new Logger();
            }
        }
        return instance;
    }

    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(mtx);
        logFile << message << std::endl;
    }

    ~Logger() {
        if (logFile.is_open()) {
            logFile.close();
        }
    }
};

Logger* Logger::instance = nullptr;
std::mutex Logger::mtx;

int main() {
    Logger* logger = Logger::getInstance();
    logger->log("Application started");
    logger->log("Logging a new message");

    return 0;
}

이 예제에서는 싱글톤 패턴을 사용하여 로깅 시스템을 구현하였습니다. 애플리케이션 전역에서 동일한 로거 객체를 사용하여 로그 메시지를 기록할 수 있습니다.

장점과 단점

장점

  • 리소스 절약: 단일 인스턴스를 재사용하므로 메모리와 CPU 리소스를 절약할 수 있습니다.
  • 전역 접근성: 모든 코드에서 쉽게 접근 가능하여 편리합니다.
  • 일관성 보장: 여러 객체가 동일한 데이터를 사용해야 할 때 유용합니다.

단점

  • 테스트 어려움: 싱글톤은 전역 상태를 가지므로, 유닛 테스트에서 독립성을 유지하기 어렵습니다.
  • 남용 가능성: 필요 이상으로 사용하면 의존성이 높아져 코드가 복잡해질 수 있습니다.
  • 멀티스레드 문제: 잘못된 구현은 멀티스레드 환경에서 문제를 일으킬 수 있습니다.

활용 사례

1. 로깅 시스템

애플리케이션 전체에서 로그를 기록할 때, 단일 로그 객체를 사용하면 로그 파일에 기록하는 작업이 일관되게 유지됩니다.

2. 설정 관리

애플리케이션의 설정 데이터를 중앙에서 관리하고, 여러 모듈에서 이를 참조할 때 유용합니다.

3. 데이터베이스 연결

데이터베이스 연결 객체를 싱글톤으로 생성하여, 중복 연결을 방지하고 연결 관리의 효율성을 높일 수 있습니다.


팩토리 패턴

개념

팩토리 패턴은 객체 생성의 과정을 캡슐화하여 클라이언트가 객체를 직접 생성하지 않고, 인터페이스를 통해 필요한 객체를 생성하도록 하는 패턴입니다.

필요성

  1. 복잡한 객체 생성 로직 캡슐화: 객체 생성 과정이 복잡하거나 조건에 따라 달라질 때 유용합니다.
  2. 결합도 감소: 클라이언트 코드가 구체적인 클래스 대신 인터페이스나 추상 클래스와 상호작용하도록 하여 유지보수성을 높입니다.
  3. 확장성 제공: 새로운 객체 유형을 쉽게 추가할 수 있습니다.

구현 방법

팩토리 패턴은 다음과 같은 요소로 구성됩니다:

  1. 제품(Product): 생성될 객체의 인터페이스를 정의합니다. 예를 들어, GUI 애플리케이션에서 GUIComponent라는 인터페이스가 버튼이나 텍스트박스 같은 다양한 컴포넌트를 정의할 수 있습니다.

  2. 구체적 제품(Concrete Product): 인터페이스를 구현하는 클래스들입니다. 위의 예제에서는 ButtonTextBox가 각각 구체적 제품의 역할을 합니다. Button 클래스는 버튼을 렌더링하는 구체적인 동작을 정의합니다.

  3. 팩토리(Factory): 제품 생성을 캡슐화하는 클래스입니다. GUIFactory 클래스는 컴포넌트 타입에 따라 적절한 객체(Button 또는 TextBox)를 생성하고 반환하는 역할을 합니다.

  • 제품(Product): 생성될 객체의 인터페이스를 정의합니다.
  • 구체적 제품(Concrete Product): 인터페이스를 구현하는 클래스들입니다.
  • 팩토리(Factory): 제품 생성을 캡슐화하는 클래스입니다.

C++ 예제 코드

#include <iostream>
#include <memory>

// 제품 인터페이스
class Product {
public:
    virtual void use() = 0; // 순수 가상 함수
    virtual ~Product() = default;
};

// 구체적 제품 A
class ConcreteProductA : public Product {
public:
    void use() override {
        std::cout << "Using Product A" << std::endl;
    }
};

// 구체적 제품 B
class ConcreteProductB : public Product {
public:
    void use() override {
        std::cout << "Using Product B" << std::endl;
    }
};

// 팩토리 클래스
class Factory {
public:
    enum class ProductType { A, B };

    static std::unique_ptr<Product> createProduct(ProductType type) {
        switch (type) {
            case ProductType::A:
                return std::make_unique<ConcreteProductA>();
            case ProductType::B:
                return std::make_unique<ConcreteProductB>();
            default:
                return nullptr;
        }
    }
};

int main() {
    // 팩토리를 통해 제품 생성
    auto productA = Factory::createProduct(Factory::ProductType::A);
    productA->use();

    auto productB = Factory::createProduct(Factory::ProductType::B);
    productB->use();

    return 0;
}

추가 예제: GUI 컴포넌트 생성

#include <iostream>
#include <memory>
#include <string>

// GUI 컴포넌트 인터페이스
class GUIComponent {
public:
    virtual void render() = 0;
    virtual ~GUIComponent() = default;
};

// 버튼 클래스
class Button : public GUIComponent {
public:
    void render() override {
        std::cout << "Rendering a Button" << std::endl;
    }
};

// 텍스트박스 클래스
class TextBox : public GUIComponent {
public:
    void render() override {
        std::cout << "Rendering a TextBox" << std::endl;
    }
};

// GUI 팩토리
class GUIFactory {
public:
    enum class ComponentType { BUTTON, TEXTBOX };

    static std::unique_ptr<GUIComponent> createComponent(ComponentType type) {
        switch (type) {
            case ComponentType::BUTTON:
                return std::make_unique<Button>();
            case ComponentType::TEXTBOX:
                return std::make_unique<TextBox>();
            default:
                return nullptr;
        }
    }
};

int main() {
    auto button = GUIFactory::createComponent(GUIFactory::ComponentType::BUTTON);
    button->render();

    auto textBox = GUIFactory::createComponent(GUIFactory::ComponentType::TEXTBOX);
    textBox->render();

    return 0;
}

이 예제에서는 GUI 컴포넌트 생성의 과정을 팩토리 패턴으로 캡슐화하였습니다. 클라이언트는 버튼이나 텍스트박스의 구체적인 구현을 알 필요 없이 팩토리를 통해 객체를 생성합니다.


결론

싱글톤 패턴과 팩토리 패턴은 각각 특정한 문제를 해결하기 위한 강력한 도구입니다.

  • 싱글톤 패턴은 전역 상태를 관리하고 리소스를 효율적으로 공유할 때 유용합니다.
  • 팩토리 패턴은 객체 생성의 복잡성을 줄이고 코드의 결합도를 낮추는 데 도움을 줍니다.

이 두 패턴을 올바르게 이해하고 적절한 상황에서 활용한다면 더 나은 소프트웨어 설계를 구현할 수 있습니다. 특히, 설계 초기 단계에서 이 패턴들을 고려하면 코드의 유지보수성과 확장성을 크게 향상시킬 수 있습니다. 그러나 초기 설계 시에는 지나치게 복잡한 패턴 적용을 피하고, 실제 요구사항과 상황에 적합한 패턴을 선택해야 합니다. 필요 이상의 복잡성은 코드 관리에 부담을 줄 수 있으므로, 간단한 해결책이 가능하다면 그것을 우선적으로 고려하는 것이 중요합니다. 개발자라면 이러한 패턴을 숙지하고 프로젝트에 알맞게 활용해 보세요!

728x90