18. C++ 디자인 패턴

18. C++ 디자인 패턴

학습 목표

  • GoF 디자인 패턴의 핵심 패턴 이해
  • 모던 C++를 활용한 패턴 구현
  • CRTP, PIMPL 등 C++ 특화 이디엄 습득
  • 패턴 선택 기준과 적용 사례 학습

목차

  1. 디자인 패턴 개요
  2. 생성 패턴
  3. 구조 패턴
  4. 행동 패턴
  5. C++ 특화 이디엄
  6. 연습 문제

1. 디자인 패턴 개요

1.1 디자인 패턴이란?

디자인 패턴은 소프트웨어 설계에서 자주 발생하는 문제에 대한 재사용 가능한 해결책입니다.

┌─────────────────────────────────────────────────────────────┐
                    GoF 디자인 패턴 분류                       
├─────────────────────────────────────────────────────────────┤
                                                             
  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         
    생성 패턴       구조 패턴       행동 패턴            
   Creational     Structural     Behavioral           
  ├─────────────┤  ├─────────────┤  ├─────────────┤         
   Singleton      Adapter        Observer             
   Factory        Decorator      Strategy             
   Builder        Facade         Command              
   Prototype      Composite      State                
   Abstract       Proxy          Iterator             
     Factory      Bridge         Template             
                  Flyweight        Method             
  └─────────────┘  └─────────────┘  └─────────────┘         
                                                             
└─────────────────────────────────────────────────────────────┘

1.2 SOLID 원칙

// SOLID 원칙 - 좋은 객체지향 설계의 기초

// S - Single Responsibility Principle (단일 책임 원칙)
// 클래스는 하나의 책임만 가져야 함

// 나쁜 예: 여러 책임이 혼재
class BadUserManager {
public:
    void createUser(const std::string& name) { /* ... */ }
    void saveToDatabase() { /* ... */ }     // DB 책임
    void sendEmail() { /* ... */ }          // 이메일 책임
    void generateReport() { /* ... */ }     // 리포트 책임
};

// 좋은 예: 책임 분리
class User {
public:
    User(const std::string& name) : name_(name) {}
    std::string getName() const { return name_; }
private:
    std::string name_;
};

class UserRepository {
public:
    void save(const User& user) { /* DB 저장 */ }
};

class EmailService {
public:
    void sendWelcome(const User& user) { /* 이메일 발송 */ }
};

// O - Open/Closed Principle (개방-폐쇄 원칙)
// 확장에는 열려있고, 수정에는 닫혀있어야 함

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;  // 새 도형 추가 시 기존 코드 수정 불필요
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override { return width * height; }
private:
    double width, height;
};

class Circle : public Shape {
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
private:
    double radius;
};

// L - Liskov Substitution Principle (리스코프 치환 원칙)
// 자식 클래스는 부모 클래스를 대체할 수 있어야 함

// I - Interface Segregation Principle (인터페이스 분리 원칙)
// 클라이언트는 사용하지 않는 인터페이스에 의존하면 안 됨

// 나쁜 예: 거대한 인터페이스
class IBadWorker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;    // 로봇은 먹지 않음
    virtual void sleep() = 0;  // 로봇은 자지 않음
};

// 좋은 예: 분리된 인터페이스
class IWorkable {
public:
    virtual void work() = 0;
};

class IFeedable {
public:
    virtual void eat() = 0;
};

// D - Dependency Inversion Principle (의존성 역전 원칙)
// 고수준 모듈이 저수준 모듈에 의존하면 안 됨

class ILogger {
public:
    virtual ~ILogger() = default;
    virtual void log(const std::string& message) = 0;
};

class FileLogger : public ILogger {
public:
    void log(const std::string& message) override {
        // 파일에 로그 기록
    }
};

class Application {
public:
    // 구체 클래스가 아닌 인터페이스에 의존
    Application(std::shared_ptr<ILogger> logger) : logger_(logger) {}

    void run() {
        logger_->log("Application started");
    }
private:
    std::shared_ptr<ILogger> logger_;
};

2. 생성 패턴

2.1 Singleton (싱글톤)

인스턴스가 하나만 존재하도록 보장합니다.

#include <mutex>
#include <memory>

// 기본 싱글톤 (C++11 이상, 스레드 안전)
class Singleton {
public:
    // 복사/이동 방지
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

    // Meyers' Singleton - C++11부터 스레드 안전 보장
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }

    void doSomething() {
        std::cout << "Singleton doing something\n";
    }

private:
    Singleton() {
        std::cout << "Singleton created\n";
    }
    ~Singleton() {
        std::cout << "Singleton destroyed\n";
    }
};

// 템플릿 싱글톤
template<typename T>
class SingletonBase {
public:
    SingletonBase(const SingletonBase&) = delete;
    SingletonBase& operator=(const SingletonBase&) = delete;

    static T& getInstance() {
        static T instance;
        return instance;
    }

protected:
    SingletonBase() = default;
    ~SingletonBase() = default;
};

// 사용
class Logger : public SingletonBase<Logger> {
    friend class SingletonBase<Logger>;
public:
    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "[LOG] " << msg << "\n";
    }
private:
    Logger() = default;
    std::mutex mutex_;
};

// 사용 예
int main() {
    Singleton::getInstance().doSomething();
    Logger::getInstance().log("Hello, Singleton!");
    return 0;
}

2.2 Factory Method (팩토리 메서드)

객체 생성을 서브클래스에 위임합니다.

#include <memory>
#include <string>
#include <unordered_map>
#include <functional>

// 제품 인터페이스
class Document {
public:
    virtual ~Document() = default;
    virtual void open() = 0;
    virtual void save() = 0;
    virtual std::string getType() const = 0;
};

// 구체 제품들
class PDFDocument : public Document {
public:
    void open() override {
        std::cout << "Opening PDF document\n";
    }
    void save() override {
        std::cout << "Saving PDF document\n";
    }
    std::string getType() const override { return "PDF"; }
};

class WordDocument : public Document {
public:
    void open() override {
        std::cout << "Opening Word document\n";
    }
    void save() override {
        std::cout << "Saving Word document\n";
    }
    std::string getType() const override { return "Word"; }
};

class ExcelDocument : public Document {
public:
    void open() override {
        std::cout << "Opening Excel spreadsheet\n";
    }
    void save() override {
        std::cout << "Saving Excel spreadsheet\n";
    }
    std::string getType() const override { return "Excel"; }
};

// 팩토리 클래스
class DocumentFactory {
public:
    using Creator = std::function<std::unique_ptr<Document>()>;

    // 생성자 등록
    static void registerType(const std::string& type, Creator creator) {
        getRegistry()[type] = std::move(creator);
    }

    // 객체 생성
    static std::unique_ptr<Document> create(const std::string& type) {
        auto it = getRegistry().find(type);
        if (it != getRegistry().end()) {
            return it->second();
        }
        throw std::runtime_error("Unknown document type: " + type);
    }

private:
    static std::unordered_map<std::string, Creator>& getRegistry() {
        static std::unordered_map<std::string, Creator> registry;
        return registry;
    }
};

// 자동 등록 헬퍼
template<typename T>
struct DocumentRegistrar {
    DocumentRegistrar(const std::string& type) {
        DocumentFactory::registerType(type, []() {
            return std::make_unique<T>();
        });
    }
};

// 등록
static DocumentRegistrar<PDFDocument> pdfReg("pdf");
static DocumentRegistrar<WordDocument> wordReg("word");
static DocumentRegistrar<ExcelDocument> excelReg("excel");

// 사용 예
int main() {
    auto doc1 = DocumentFactory::create("pdf");
    doc1->open();

    auto doc2 = DocumentFactory::create("word");
    doc2->open();

    return 0;
}

2.3 Builder (빌더)

복잡한 객체를 단계별로 생성합니다.

#include <string>
#include <vector>
#include <optional>
#include <iostream>

// 복잡한 객체
class Computer {
public:
    void showSpecs() const {
        std::cout << "=== Computer Specs ===\n";
        std::cout << "CPU: " << cpu << "\n";
        std::cout << "RAM: " << ram << "GB\n";
        std::cout << "Storage: " << storage << "GB " << storageType << "\n";
        std::cout << "GPU: " << gpu.value_or("Integrated") << "\n";
        std::cout << "OS: " << os.value_or("None") << "\n";
    }

    // Builder가 직접 멤버에 접근하도록 friend 선언
    friend class ComputerBuilder;

private:
    std::string cpu;
    int ram;
    int storage;
    std::string storageType;
    std::optional<std::string> gpu;
    std::optional<std::string> os;
};

// Fluent Builder
class ComputerBuilder {
public:
    ComputerBuilder& setCPU(const std::string& cpu) {
        computer_.cpu = cpu;
        return *this;
    }

    ComputerBuilder& setRAM(int gb) {
        computer_.ram = gb;
        return *this;
    }

    ComputerBuilder& setStorage(int gb, const std::string& type = "SSD") {
        computer_.storage = gb;
        computer_.storageType = type;
        return *this;
    }

    ComputerBuilder& setGPU(const std::string& gpu) {
        computer_.gpu = gpu;
        return *this;
    }

    ComputerBuilder& setOS(const std::string& os) {
        computer_.os = os;
        return *this;
    }

    Computer build() {
        // 유효성 검사
        if (computer_.cpu.empty()) {
            throw std::runtime_error("CPU is required");
        }
        if (computer_.ram <= 0) {
            throw std::runtime_error("RAM must be positive");
        }
        return std::move(computer_);
    }

private:
    Computer computer_;
};

// Director (선택적) - 미리 정의된 구성
class ComputerDirector {
public:
    static Computer buildGamingPC() {
        return ComputerBuilder()
            .setCPU("Intel i9-13900K")
            .setRAM(64)
            .setStorage(2000, "NVMe SSD")
            .setGPU("NVIDIA RTX 4090")
            .setOS("Windows 11")
            .build();
    }

    static Computer buildOfficePC() {
        return ComputerBuilder()
            .setCPU("Intel i5-13400")
            .setRAM(16)
            .setStorage(512, "SSD")
            .setOS("Windows 11")
            .build();
    }

    static Computer buildDeveloperWorkstation() {
        return ComputerBuilder()
            .setCPU("AMD Ryzen 9 7950X")
            .setRAM(128)
            .setStorage(4000, "NVMe SSD")
            .setGPU("NVIDIA RTX 4080")
            .setOS("Ubuntu 22.04")
            .build();
    }
};

// 사용 예
int main() {
    // 직접 빌드
    auto custom = ComputerBuilder()
        .setCPU("AMD Ryzen 7 7800X3D")
        .setRAM(32)
        .setStorage(1000)
        .setGPU("AMD RX 7900 XTX")
        .build();
    custom.showSpecs();

    std::cout << "\n";

    // Director 사용
    auto gaming = ComputerDirector::buildGamingPC();
    gaming.showSpecs();

    return 0;
}

3. 구조 패턴

3.1 Adapter (어댑터)

호환되지 않는 인터페이스를 연결합니다.

#include <memory>
#include <iostream>

// 기존 인터페이스 (Target)
class MediaPlayer {
public:
    virtual ~MediaPlayer() = default;
    virtual void play(const std::string& filename) = 0;
};

// 적응해야 할 클래스 (Adaptee)
class AdvancedMediaPlayer {
public:
    virtual ~AdvancedMediaPlayer() = default;
    virtual void playVLC(const std::string& filename) = 0;
    virtual void playMP4(const std::string& filename) = 0;
};

class VLCPlayer : public AdvancedMediaPlayer {
public:
    void playVLC(const std::string& filename) override {
        std::cout << "Playing VLC file: " << filename << "\n";
    }
    void playMP4(const std::string& filename) override {
        // 지원하지 않음
    }
};

class MP4Player : public AdvancedMediaPlayer {
public:
    void playVLC(const std::string& filename) override {
        // 지원하지 않음
    }
    void playMP4(const std::string& filename) override {
        std::cout << "Playing MP4 file: " << filename << "\n";
    }
};

// 어댑터 (Object Adapter)
class MediaAdapter : public MediaPlayer {
public:
    MediaAdapter(const std::string& audioType) {
        if (audioType == "vlc") {
            player_ = std::make_unique<VLCPlayer>();
        } else if (audioType == "mp4") {
            player_ = std::make_unique<MP4Player>();
        }
    }

    void play(const std::string& filename) override {
        std::string ext = getExtension(filename);
        if (ext == "vlc") {
            player_->playVLC(filename);
        } else if (ext == "mp4") {
            player_->playMP4(filename);
        }
    }

private:
    std::string getExtension(const std::string& filename) {
        size_t pos = filename.rfind('.');
        if (pos != std::string::npos) {
            return filename.substr(pos + 1);
        }
        return "";
    }

    std::unique_ptr<AdvancedMediaPlayer> player_;
};

// 통합 플레이어
class AudioPlayer : public MediaPlayer {
public:
    void play(const std::string& filename) override {
        std::string ext = getExtension(filename);

        if (ext == "mp3") {
            std::cout << "Playing MP3 file: " << filename << "\n";
        } else if (ext == "vlc" || ext == "mp4") {
            MediaAdapter adapter(ext);
            adapter.play(filename);
        } else {
            std::cout << "Unsupported format: " << ext << "\n";
        }
    }

private:
    std::string getExtension(const std::string& filename) {
        size_t pos = filename.rfind('.');
        if (pos != std::string::npos) {
            return filename.substr(pos + 1);
        }
        return "";
    }
};

// 함수 어댑터 (람다 사용)
template<typename OldFunc, typename NewFunc>
class FunctionAdapter {
public:
    FunctionAdapter(OldFunc old, NewFunc adapter)
        : oldFunc_(old), adapter_(adapter) {}

    template<typename... Args>
    auto operator()(Args&&... args) {
        return adapter_(oldFunc_, std::forward<Args>(args)...);
    }

private:
    OldFunc oldFunc_;
    NewFunc adapter_;
};

// 사용 예
int main() {
    AudioPlayer player;

    player.play("song.mp3");
    player.play("movie.vlc");
    player.play("video.mp4");
    player.play("image.png");

    return 0;
}

3.2 Decorator (데코레이터)

객체에 동적으로 기능을 추가합니다.

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

// 컴포넌트 인터페이스
class Coffee {
public:
    virtual ~Coffee() = default;
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
};

// 기본 구현
class Espresso : public Coffee {
public:
    std::string getDescription() const override {
        return "Espresso";
    }
    double getCost() const override {
        return 2.00;
    }
};

class Americano : public Coffee {
public:
    std::string getDescription() const override {
        return "Americano";
    }
    double getCost() const override {
        return 2.50;
    }
};

// 데코레이터 기본 클래스
class CoffeeDecorator : public Coffee {
public:
    explicit CoffeeDecorator(std::unique_ptr<Coffee> coffee)
        : coffee_(std::move(coffee)) {}

    std::string getDescription() const override {
        return coffee_->getDescription();
    }

    double getCost() const override {
        return coffee_->getCost();
    }

protected:
    std::unique_ptr<Coffee> coffee_;
};

// 구체 데코레이터들
class Milk : public CoffeeDecorator {
public:
    explicit Milk(std::unique_ptr<Coffee> coffee)
        : CoffeeDecorator(std::move(coffee)) {}

    std::string getDescription() const override {
        return coffee_->getDescription() + ", Milk";
    }

    double getCost() const override {
        return coffee_->getCost() + 0.50;
    }
};

class Whip : public CoffeeDecorator {
public:
    explicit Whip(std::unique_ptr<Coffee> coffee)
        : CoffeeDecorator(std::move(coffee)) {}

    std::string getDescription() const override {
        return coffee_->getDescription() + ", Whip";
    }

    double getCost() const override {
        return coffee_->getCost() + 0.70;
    }
};

class Mocha : public CoffeeDecorator {
public:
    explicit Mocha(std::unique_ptr<Coffee> coffee)
        : CoffeeDecorator(std::move(coffee)) {}

    std::string getDescription() const override {
        return coffee_->getDescription() + ", Mocha";
    }

    double getCost() const override {
        return coffee_->getCost() + 0.80;
    }
};

// 헬퍼 함수 (팩토리 스타일)
template<typename Decorator, typename... Args>
std::unique_ptr<Coffee> addDecorator(std::unique_ptr<Coffee> coffee, Args&&... args) {
    return std::make_unique<Decorator>(std::move(coffee), std::forward<Args>(args)...);
}

// 사용 예
int main() {
    // 기본 에스프레소
    std::unique_ptr<Coffee> coffee1 = std::make_unique<Espresso>();
    std::cout << coffee1->getDescription() << " $" << coffee1->getCost() << "\n";

    // 모카 라떼
    std::unique_ptr<Coffee> coffee2 = std::make_unique<Espresso>();
    coffee2 = std::make_unique<Milk>(std::move(coffee2));
    coffee2 = std::make_unique<Mocha>(std::move(coffee2));
    std::cout << coffee2->getDescription() << " $" << coffee2->getCost() << "\n";

    // 풀옵션
    std::unique_ptr<Coffee> coffee3 = std::make_unique<Americano>();
    coffee3 = std::make_unique<Milk>(std::move(coffee3));
    coffee3 = std::make_unique<Mocha>(std::move(coffee3));
    coffee3 = std::make_unique<Whip>(std::move(coffee3));
    std::cout << coffee3->getDescription() << " $" << coffee3->getCost() << "\n";

    return 0;
}

3.3 Facade (퍼사드)

복잡한 서브시스템에 단순한 인터페이스를 제공합니다.

#include <iostream>
#include <memory>

// 복잡한 서브시스템들
class CPU {
public:
    void freeze() { std::cout << "CPU: Freezing...\n"; }
    void jump(long position) {
        std::cout << "CPU: Jumping to " << position << "\n";
    }
    void execute() { std::cout << "CPU: Executing...\n"; }
};

class Memory {
public:
    void load(long position, const std::string& data) {
        std::cout << "Memory: Loading '" << data
                  << "' at position " << position << "\n";
    }
};

class HardDrive {
public:
    std::string read(long lba, int size) {
        std::cout << "HardDrive: Reading " << size
                  << " bytes from sector " << lba << "\n";
        return "boot_data";
    }
};

class Display {
public:
    void turnOn() { std::cout << "Display: Turning on\n"; }
    void showLogo() { std::cout << "Display: Showing boot logo\n"; }
};

// 퍼사드 - 간단한 인터페이스 제공
class ComputerFacade {
public:
    ComputerFacade()
        : cpu_(std::make_unique<CPU>())
        , memory_(std::make_unique<Memory>())
        , hardDrive_(std::make_unique<HardDrive>())
        , display_(std::make_unique<Display>()) {}

    void start() {
        std::cout << "=== Computer Starting ===\n";
        display_->turnOn();
        display_->showLogo();
        cpu_->freeze();
        memory_->load(BOOT_ADDRESS, hardDrive_->read(BOOT_SECTOR, SECTOR_SIZE));
        cpu_->jump(BOOT_ADDRESS);
        cpu_->execute();
        std::cout << "=== Computer Ready ===\n";
    }

    void shutdown() {
        std::cout << "=== Computer Shutting Down ===\n";
        // 종료 로직...
    }

private:
    static constexpr long BOOT_ADDRESS = 0x00000;
    static constexpr long BOOT_SECTOR = 0;
    static constexpr int SECTOR_SIZE = 512;

    std::unique_ptr<CPU> cpu_;
    std::unique_ptr<Memory> memory_;
    std::unique_ptr<HardDrive> hardDrive_;
    std::unique_ptr<Display> display_;
};

// 사용 예
int main() {
    ComputerFacade computer;
    computer.start();
    // ... 사용 ...
    computer.shutdown();
    return 0;
}

4. 행동 패턴

4.1 Observer (옵저버)

객체 상태 변화를 다른 객체에 통지합니다.

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <functional>
#include <string>

// 모던 C++ 옵저버 (함수 기반)
template<typename... Args>
class Signal {
public:
    using Slot = std::function<void(Args...)>;
    using SlotId = size_t;

    SlotId connect(Slot slot) {
        slots_.push_back({nextId_, std::move(slot)});
        return nextId_++;
    }

    void disconnect(SlotId id) {
        slots_.erase(
            std::remove_if(slots_.begin(), slots_.end(),
                [id](const auto& p) { return p.first == id; }),
            slots_.end()
        );
    }

    void emit(Args... args) {
        for (auto& [id, slot] : slots_) {
            slot(args...);
        }
    }

    void operator()(Args... args) {
        emit(args...);
    }

private:
    std::vector<std::pair<SlotId, Slot>> slots_;
    SlotId nextId_ = 0;
};

// 사용 예: 주식 가격 모니터링
class Stock {
public:
    Stock(const std::string& symbol, double price)
        : symbol_(symbol), price_(price) {}

    const std::string& getSymbol() const { return symbol_; }
    double getPrice() const { return price_; }

    void setPrice(double price) {
        double oldPrice = price_;
        price_ = price;
        priceChanged.emit(symbol_, oldPrice, price_);
    }

    Signal<std::string, double, double> priceChanged;

private:
    std::string symbol_;
    double price_;
};

// 전통적인 옵저버 패턴 (인터페이스 기반)
class IObserver {
public:
    virtual ~IObserver() = default;
    virtual void update(const std::string& message) = 0;
};

class ISubject {
public:
    virtual ~ISubject() = default;
    virtual void attach(std::shared_ptr<IObserver> observer) = 0;
    virtual void detach(std::shared_ptr<IObserver> observer) = 0;
    virtual void notify() = 0;
};

class NewsAgency : public ISubject {
public:
    void attach(std::shared_ptr<IObserver> observer) override {
        observers_.push_back(observer);
    }

    void detach(std::shared_ptr<IObserver> observer) override {
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [&observer](const std::weak_ptr<IObserver>& wp) {
                    auto sp = wp.lock();
                    return !sp || sp == observer;
                }),
            observers_.end()
        );
    }

    void notify() override {
        for (auto it = observers_.begin(); it != observers_.end();) {
            if (auto observer = it->lock()) {
                observer->update(news_);
                ++it;
            } else {
                it = observers_.erase(it);  // 만료된 옵저버 제거
            }
        }
    }

    void setNews(const std::string& news) {
        news_ = news;
        notify();
    }

private:
    std::vector<std::weak_ptr<IObserver>> observers_;
    std::string news_;
};

class NewsChannel : public IObserver {
public:
    NewsChannel(const std::string& name) : name_(name) {}

    void update(const std::string& message) override {
        std::cout << name_ << " received: " << message << "\n";
    }

private:
    std::string name_;
};

// 사용 예
int main() {
    // 모던 Signal/Slot 방식
    Stock apple("AAPL", 150.0);

    auto id1 = apple.priceChanged.connect(
        [](const std::string& symbol, double oldPrice, double newPrice) {
            std::cout << symbol << ": $" << oldPrice << " -> $" << newPrice;
            if (newPrice > oldPrice) {
                std::cout << " (+)\n";
            } else {
                std::cout << " (-)\n";
            }
        }
    );

    apple.setPrice(155.0);
    apple.setPrice(148.0);

    std::cout << "\n--- Traditional Observer ---\n";

    // 전통적인 방식
    auto agency = std::make_shared<NewsAgency>();
    auto cnn = std::make_shared<NewsChannel>("CNN");
    auto bbc = std::make_shared<NewsChannel>("BBC");

    agency->attach(cnn);
    agency->attach(bbc);

    agency->setNews("Breaking: Major tech announcement!");

    return 0;
}

4.2 Strategy (전략)

알고리즘을 캡슐화하여 교체 가능하게 만듭니다.

#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
#include <functional>

// 전략 인터페이스
class SortStrategy {
public:
    virtual ~SortStrategy() = default;
    virtual void sort(std::vector<int>& data) = 0;
    virtual std::string getName() const = 0;
};

// 구체 전략들
class BubbleSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        for (size_t i = 0; i < data.size(); ++i) {
            for (size_t j = 0; j < data.size() - i - 1; ++j) {
                if (data[j] > data[j + 1]) {
                    std::swap(data[j], data[j + 1]);
                }
            }
        }
    }
    std::string getName() const override { return "Bubble Sort"; }
};

class QuickSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        quickSort(data, 0, data.size() - 1);
    }
    std::string getName() const override { return "Quick Sort"; }

private:
    void quickSort(std::vector<int>& arr, int low, int high) {
        if (low < high) {
            int pi = partition(arr, low, high);
            quickSort(arr, low, pi - 1);
            quickSort(arr, pi + 1, high);
        }
    }

    int partition(std::vector<int>& arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1;
        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++;
                std::swap(arr[i], arr[j]);
            }
        }
        std::swap(arr[i + 1], arr[high]);
        return i + 1;
    }
};

class MergeSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        mergeSort(data, 0, data.size() - 1);
    }
    std::string getName() const override { return "Merge Sort"; }

private:
    void mergeSort(std::vector<int>& arr, int l, int r) {
        if (l < r) {
            int m = l + (r - l) / 2;
            mergeSort(arr, l, m);
            mergeSort(arr, m + 1, r);
            merge(arr, l, m, r);
        }
    }

    void merge(std::vector<int>& arr, int l, int m, int r) {
        std::vector<int> left(arr.begin() + l, arr.begin() + m + 1);
        std::vector<int> right(arr.begin() + m + 1, arr.begin() + r + 1);

        int i = 0, j = 0, k = l;
        while (i < left.size() && j < right.size()) {
            arr[k++] = (left[i] <= right[j]) ? left[i++] : right[j++];
        }
        while (i < left.size()) arr[k++] = left[i++];
        while (j < right.size()) arr[k++] = right[j++];
    }
};

// 컨텍스트
class Sorter {
public:
    void setStrategy(std::unique_ptr<SortStrategy> strategy) {
        strategy_ = std::move(strategy);
    }

    void sort(std::vector<int>& data) {
        if (strategy_) {
            std::cout << "Sorting with " << strategy_->getName() << "\n";
            strategy_->sort(data);
        }
    }

private:
    std::unique_ptr<SortStrategy> strategy_;
};

// 모던 C++ 방식: 함수 객체 사용
class ModernSorter {
public:
    using Strategy = std::function<void(std::vector<int>&)>;

    void setStrategy(Strategy strategy) {
        strategy_ = std::move(strategy);
    }

    void sort(std::vector<int>& data) {
        if (strategy_) {
            strategy_(data);
        }
    }

private:
    Strategy strategy_;
};

// 사용 예
int main() {
    std::vector<int> data = {64, 34, 25, 12, 22, 11, 90};

    // 전통적인 전략 패턴
    Sorter sorter;

    auto data1 = data;
    sorter.setStrategy(std::make_unique<BubbleSort>());
    sorter.sort(data1);

    auto data2 = data;
    sorter.setStrategy(std::make_unique<QuickSort>());
    sorter.sort(data2);

    // 모던 방식 - 람다 사용
    ModernSorter modernSorter;

    auto data3 = data;
    modernSorter.setStrategy([](std::vector<int>& v) {
        std::sort(v.begin(), v.end());  // STL 사용
    });
    modernSorter.sort(data3);

    // 결과 출력
    std::cout << "Sorted: ";
    for (int x : data3) std::cout << x << " ";
    std::cout << "\n";

    return 0;
}

4.3 Command (커맨드)

요청을 객체로 캡슐화합니다.

#include <iostream>
#include <memory>
#include <stack>
#include <vector>
#include <functional>

// 수신자 (Receiver)
class Document {
public:
    void write(const std::string& text) {
        content_ += text;
        std::cout << "Writing: '" << text << "'\n";
    }

    void erase(size_t count) {
        if (count <= content_.size()) {
            std::string erased = content_.substr(content_.size() - count);
            content_.erase(content_.size() - count);
            std::cout << "Erasing: '" << erased << "'\n";
        }
    }

    std::string getContent() const { return content_; }

    void setContent(const std::string& content) {
        content_ = content;
    }

private:
    std::string content_;
};

// 커맨드 인터페이스
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
};

// 구체 커맨드들
class WriteCommand : public Command {
public:
    WriteCommand(Document& doc, const std::string& text)
        : doc_(doc), text_(text) {}

    void execute() override {
        doc_.write(text_);
    }

    void undo() override {
        doc_.erase(text_.size());
    }

private:
    Document& doc_;
    std::string text_;
};

class EraseCommand : public Command {
public:
    EraseCommand(Document& doc, size_t count)
        : doc_(doc), count_(count) {}

    void execute() override {
        // 실행 전 상태 저장
        std::string content = doc_.getContent();
        if (count_ <= content.size()) {
            erasedText_ = content.substr(content.size() - count_);
        }
        doc_.erase(count_);
    }

    void undo() override {
        doc_.write(erasedText_);
    }

private:
    Document& doc_;
    size_t count_;
    std::string erasedText_;
};

// 인보커 (Invoker) - Undo/Redo 지원
class CommandManager {
public:
    void execute(std::unique_ptr<Command> cmd) {
        cmd->execute();
        undoStack_.push(std::move(cmd));
        // 새 명령 실행 시 redo 스택 클리어
        while (!redoStack_.empty()) redoStack_.pop();
    }

    void undo() {
        if (!undoStack_.empty()) {
            auto cmd = std::move(undoStack_.top());
            undoStack_.pop();
            cmd->undo();
            redoStack_.push(std::move(cmd));
        }
    }

    void redo() {
        if (!redoStack_.empty()) {
            auto cmd = std::move(redoStack_.top());
            redoStack_.pop();
            cmd->execute();
            undoStack_.push(std::move(cmd));
        }
    }

    bool canUndo() const { return !undoStack_.empty(); }
    bool canRedo() const { return !redoStack_.empty(); }

private:
    std::stack<std::unique_ptr<Command>> undoStack_;
    std::stack<std::unique_ptr<Command>> redoStack_;
};

// 매크로 커맨드 (복합 커맨드)
class MacroCommand : public Command {
public:
    void addCommand(std::unique_ptr<Command> cmd) {
        commands_.push_back(std::move(cmd));
    }

    void execute() override {
        for (auto& cmd : commands_) {
            cmd->execute();
        }
    }

    void undo() override {
        for (auto it = commands_.rbegin(); it != commands_.rend(); ++it) {
            (*it)->undo();
        }
    }

private:
    std::vector<std::unique_ptr<Command>> commands_;
};

// 사용 예
int main() {
    Document doc;
    CommandManager manager;

    // 명령 실행
    manager.execute(std::make_unique<WriteCommand>(doc, "Hello"));
    manager.execute(std::make_unique<WriteCommand>(doc, " World"));
    manager.execute(std::make_unique<WriteCommand>(doc, "!"));

    std::cout << "Content: " << doc.getContent() << "\n\n";

    // Undo
    std::cout << "--- Undo ---\n";
    manager.undo();
    std::cout << "Content: " << doc.getContent() << "\n\n";

    manager.undo();
    std::cout << "Content: " << doc.getContent() << "\n\n";

    // Redo
    std::cout << "--- Redo ---\n";
    manager.redo();
    std::cout << "Content: " << doc.getContent() << "\n\n";

    return 0;
}

4.4 Template Method (템플릿 메서드)

알고리즘의 골격을 정의하고 일부 단계를 서브클래스에서 구현합니다.

#include <iostream>
#include <string>
#include <fstream>

// 추상 클래스 - 템플릿 메서드 정의
class DataParser {
public:
    virtual ~DataParser() = default;

    // 템플릿 메서드 - 알고리즘 골격
    void parseFile(const std::string& filename) {
        std::cout << "=== Parsing " << filename << " ===\n";

        openFile(filename);
        extractData();
        parseData();
        analyzeData();
        closeFile();

        std::cout << "=== Done ===\n\n";
    }

protected:
    // 기본 구현
    virtual void openFile(const std::string& filename) {
        std::cout << "Opening file: " << filename << "\n";
    }

    virtual void closeFile() {
        std::cout << "Closing file\n";
    }

    // 순수 가상 함수 - 서브클래스에서 반드시 구현
    virtual void extractData() = 0;
    virtual void parseData() = 0;

    // Hook 메서드 - 선택적으로 오버라이드
    virtual void analyzeData() {
        // 기본적으로 아무것도 하지 않음
    }
};

// 구체 클래스들
class CSVParser : public DataParser {
protected:
    void extractData() override {
        std::cout << "Extracting CSV rows and columns\n";
    }

    void parseData() override {
        std::cout << "Parsing CSV: splitting by commas\n";
    }

    void analyzeData() override {
        std::cout << "CSV Analysis: counting rows and columns\n";
    }
};

class JSONParser : public DataParser {
protected:
    void extractData() override {
        std::cout << "Extracting JSON objects and arrays\n";
    }

    void parseData() override {
        std::cout << "Parsing JSON: building object tree\n";
    }

    void analyzeData() override {
        std::cout << "JSON Analysis: validating schema\n";
    }
};

class XMLParser : public DataParser {
protected:
    void extractData() override {
        std::cout << "Extracting XML elements and attributes\n";
    }

    void parseData() override {
        std::cout << "Parsing XML: building DOM tree\n";
    }
    // analyzeData() 사용하지 않음 (기본 구현 사용)
};

// 사용 예
int main() {
    CSVParser csvParser;
    csvParser.parseFile("data.csv");

    JSONParser jsonParser;
    jsonParser.parseFile("data.json");

    XMLParser xmlParser;
    xmlParser.parseFile("config.xml");

    return 0;
}

5. C++ 특화 이디엄

5.1 CRTP (Curiously Recurring Template Pattern)

정적 다형성을 구현하는 기법입니다.

#include <iostream>
#include <memory>
#include <vector>

// CRTP 기본 형태
template<typename Derived>
class Base {
public:
    void interface() {
        // 파생 클래스의 구현 호출 (정적 다형성)
        static_cast<Derived*>(this)->implementation();
    }

    // 기본 구현
    void implementation() {
        std::cout << "Base implementation\n";
    }
};

class Derived1 : public Base<Derived1> {
public:
    void implementation() {
        std::cout << "Derived1 implementation\n";
    }
};

class Derived2 : public Base<Derived2> {
public:
    void implementation() {
        std::cout << "Derived2 implementation\n";
    }
};

// CRTP 활용 1: 믹스인 (Mixin)
template<typename Derived>
class Counter {
public:
    static int getCount() { return count_; }

protected:
    Counter() { ++count_; }
    ~Counter() { --count_; }

private:
    static inline int count_ = 0;
};

class Widget : public Counter<Widget> {
public:
    Widget(int id) : id_(id) {}
private:
    int id_;
};

class Gadget : public Counter<Gadget> {
public:
    Gadget(const std::string& name) : name_(name) {}
private:
    std::string name_;
};

// CRTP 활용 2: 정적 인터페이스 (Static Polymorphism)
template<typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->areaImpl();
    }

    void draw() const {
        static_cast<const Derived*>(this)->drawImpl();
    }
};

class Circle : public Shape<Circle> {
public:
    Circle(double r) : radius_(r) {}

    double areaImpl() const {
        return 3.14159 * radius_ * radius_;
    }

    void drawImpl() const {
        std::cout << "Drawing circle with radius " << radius_ << "\n";
    }

private:
    double radius_;
};

class Rectangle : public Shape<Rectangle> {
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}

    double areaImpl() const {
        return width_ * height_;
    }

    void drawImpl() const {
        std::cout << "Drawing rectangle " << width_ << "x" << height_ << "\n";
    }

private:
    double width_, height_;
};

// 템플릿 함수로 정적 다형성 활용
template<typename T>
void printArea(const Shape<T>& shape) {
    std::cout << "Area: " << shape.area() << "\n";
}

// CRTP 활용 3: Fluent Interface / Method Chaining
template<typename Derived>
class Builder {
public:
    Derived& setName(const std::string& name) {
        name_ = name;
        return static_cast<Derived&>(*this);
    }

protected:
    std::string name_;
};

class PersonBuilder : public Builder<PersonBuilder> {
public:
    PersonBuilder& setAge(int age) {
        age_ = age;
        return *this;
    }

    PersonBuilder& setCity(const std::string& city) {
        city_ = city;
        return *this;
    }

    void build() {
        std::cout << "Person: " << name_ << ", " << age_ << ", " << city_ << "\n";
    }

private:
    int age_ = 0;
    std::string city_;
};

// 사용 예
int main() {
    // 기본 CRTP
    Derived1 d1;
    Derived2 d2;
    d1.interface();
    d2.interface();

    std::cout << "\n--- Counter Mixin ---\n";
    {
        Widget w1(1), w2(2), w3(3);
        Gadget g1("A"), g2("B");

        std::cout << "Widgets: " << Widget::getCount() << "\n";
        std::cout << "Gadgets: " << Gadget::getCount() << "\n";
    }
    std::cout << "After scope - Widgets: " << Widget::getCount() << "\n";

    std::cout << "\n--- Static Polymorphism ---\n";
    Circle circle(5.0);
    Rectangle rect(3.0, 4.0);

    printArea(circle);
    printArea(rect);

    std::cout << "\n--- Fluent Builder ---\n";
    PersonBuilder()
        .setName("John")
        .setAge(30)
        .setCity("Seoul")
        .build();

    return 0;
}

5.2 PIMPL (Pointer to Implementation)

구현을 헤더에서 숨기는 기법입니다.

// === widget.h ===
#ifndef WIDGET_H
#define WIDGET_H

#include <memory>
#include <string>

class Widget {
public:
    Widget();
    ~Widget();

    // 이동 연산
    Widget(Widget&& other) noexcept;
    Widget& operator=(Widget&& other) noexcept;

    // 복사 연산 (필요시)
    Widget(const Widget& other);
    Widget& operator=(const Widget& other);

    // 공개 인터페이스
    void setName(const std::string& name);
    std::string getName() const;
    void doSomething();

private:
    // 전방 선언만 - 구현 숨김
    class Impl;
    std::unique_ptr<Impl> pImpl_;
};

#endif

// === widget.cpp ===
// #include "widget.h"
#include <iostream>

// 구현 클래스 정의
class Widget::Impl {
public:
    std::string name;
    int internalData = 0;

    void internalProcess() {
        std::cout << "Internal processing for: " << name << "\n";
        internalData++;
    }
};

// Widget 구현
Widget::Widget() : pImpl_(std::make_unique<Impl>()) {}

Widget::~Widget() = default;

Widget::Widget(Widget&& other) noexcept = default;
Widget& Widget::operator=(Widget&& other) noexcept = default;

Widget::Widget(const Widget& other)
    : pImpl_(std::make_unique<Impl>(*other.pImpl_)) {}

Widget& Widget::operator=(const Widget& other) {
    if (this != &other) {
        pImpl_ = std::make_unique<Impl>(*other.pImpl_);
    }
    return *this;
}

void Widget::setName(const std::string& name) {
    pImpl_->name = name;
}

std::string Widget::getName() const {
    return pImpl_->name;
}

void Widget::doSomething() {
    pImpl_->internalProcess();
}

// === main.cpp ===
int main() {
    Widget w;
    w.setName("MyWidget");
    w.doSomething();
    std::cout << "Widget name: " << w.getName() << "\n";

    // 이동
    Widget w2 = std::move(w);
    w2.doSomething();

    return 0;
}

5.3 Type Erasure (타입 소거)

다양한 타입을 단일 인터페이스로 다룹니다.

#include <iostream>
#include <memory>
#include <vector>

// Type Erasure 패턴 구현
class Drawable {
public:
    template<typename T>
    Drawable(T obj) : pImpl_(std::make_shared<Model<T>>(std::move(obj))) {}

    void draw() const {
        pImpl_->draw();
    }

private:
    // 개념 (Concept) - 인터페이스
    struct Concept {
        virtual ~Concept() = default;
        virtual void draw() const = 0;
    };

    // 모델 (Model) - 구체 타입 래핑
    template<typename T>
    struct Model : Concept {
        Model(T obj) : object_(std::move(obj)) {}

        void draw() const override {
            object_.draw();  // T가 draw() 메서드를 가져야 함
        }

        T object_;
    };

    std::shared_ptr<const Concept> pImpl_;
};

// draw() 메서드를 가진 다양한 타입들
class Circle {
public:
    Circle(double r) : radius_(r) {}
    void draw() const {
        std::cout << "Circle with radius " << radius_ << "\n";
    }
private:
    double radius_;
};

class Square {
public:
    Square(double s) : side_(s) {}
    void draw() const {
        std::cout << "Square with side " << side_ << "\n";
    }
private:
    double side_;
};

class Text {
public:
    Text(std::string t) : text_(std::move(t)) {}
    void draw() const {
        std::cout << "Text: " << text_ << "\n";
    }
private:
    std::string text_;
};

// std::function도 Type Erasure의 예시
// std::any도 Type Erasure의 예시

// 사용 예
int main() {
    // 다양한 타입을 하나의 컨테이너에 저장
    std::vector<Drawable> shapes;

    shapes.emplace_back(Circle(5.0));
    shapes.emplace_back(Square(3.0));
    shapes.emplace_back(Text("Hello"));

    // 동일한 인터페이스로 다루기
    for (const auto& shape : shapes) {
        shape.draw();
    }

    return 0;
}

5.4 RAII (Resource Acquisition Is Initialization)

리소스 관리를 생성자/소멸자에 연결합니다.

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

// 파일 핸들 RAII 래퍼
class File {
public:
    explicit File(const std::string& filename, const std::string& mode = "r") {
        file_ = fopen(filename.c_str(), mode.c_str());
        if (!file_) {
            throw std::runtime_error("Failed to open file: " + filename);
        }
        std::cout << "File opened: " << filename << "\n";
    }

    ~File() {
        if (file_) {
            fclose(file_);
            std::cout << "File closed\n";
        }
    }

    // 복사 금지
    File(const File&) = delete;
    File& operator=(const File&) = delete;

    // 이동 허용
    File(File&& other) noexcept : file_(other.file_) {
        other.file_ = nullptr;
    }

    File& operator=(File&& other) noexcept {
        if (this != &other) {
            if (file_) fclose(file_);
            file_ = other.file_;
            other.file_ = nullptr;
        }
        return *this;
    }

    FILE* get() const { return file_; }

private:
    FILE* file_ = nullptr;
};

// 락 가드 (std::lock_guard 유사)
template<typename Mutex>
class LockGuard {
public:
    explicit LockGuard(Mutex& mutex) : mutex_(mutex) {
        mutex_.lock();
    }

    ~LockGuard() {
        mutex_.unlock();
    }

    LockGuard(const LockGuard&) = delete;
    LockGuard& operator=(const LockGuard&) = delete;

private:
    Mutex& mutex_;
};

// 스코프 가드 (범용 정리 작업)
template<typename Func>
class ScopeGuard {
public:
    explicit ScopeGuard(Func&& func)
        : func_(std::forward<Func>(func)), active_(true) {}

    ~ScopeGuard() {
        if (active_) {
            func_();
        }
    }

    void dismiss() { active_ = false; }

    ScopeGuard(const ScopeGuard&) = delete;
    ScopeGuard& operator=(const ScopeGuard&) = delete;

    ScopeGuard(ScopeGuard&& other) noexcept
        : func_(std::move(other.func_)), active_(other.active_) {
        other.active_ = false;
    }

private:
    Func func_;
    bool active_;
};

// 헬퍼 함수
template<typename Func>
ScopeGuard<Func> makeScopeGuard(Func&& func) {
    return ScopeGuard<Func>(std::forward<Func>(func));
}

// 사용 예
int main() {
    // RAII 파일
    try {
        File file("/tmp/test.txt", "w");
        fprintf(file.get(), "Hello RAII!\n");
        // 스코프 종료 시 자동으로 파일 닫힘
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
    }

    // 스코프 가드
    std::cout << "\n--- Scope Guard ---\n";
    {
        auto guard = makeScopeGuard([]() {
            std::cout << "Cleanup executed!\n";
        });

        std::cout << "Doing work...\n";
        // 스코프 종료 시 cleanup 실행
    }

    // dismiss 사용
    {
        auto guard = makeScopeGuard([]() {
            std::cout << "This won't run\n";
        });

        guard.dismiss();  // cleanup 취소
    }

    return 0;
}

6. 연습 문제

연습 1: 로그 시스템 (Singleton + Strategy)

// 요구사항:
// 1. 싱글톤 Logger 구현
// 2. 다양한 출력 전략 (Console, File, Network)
// 3. 로그 레벨 (DEBUG, INFO, WARNING, ERROR)

// 힌트:
class ILogStrategy {
public:
    virtual void write(const std::string& message) = 0;
};

class Logger {
public:
    static Logger& getInstance();
    void setStrategy(std::unique_ptr<ILogStrategy> strategy);
    void log(LogLevel level, const std::string& message);
};

연습 2: UI 컴포넌트 (Composite + Decorator)

// 요구사항:
// 1. UI 컴포넌트 계층 구조 (Window, Panel, Button)
// 2. 데코레이터로 테두리, 스크롤바 추가
// 3. 컴포지트로 컨테이너 구현

// 힌트:
class UIComponent {
public:
    virtual void render() = 0;
    virtual void add(std::shared_ptr<UIComponent> component) {}
};

연습 3: 문서 편집기 (Command + Memento)

// 요구사항:
// 1. 텍스트 삽입, 삭제, 서식 명령 구현
// 2. 무제한 Undo/Redo 지원
// 3. 스냅샷 저장/복원 (Memento)

// 힌트:
class Memento {
    friend class Document;
    std::string state_;
};

class Document {
public:
    Memento createMemento();
    void restore(const Memento& memento);
};

연습 4: 플러그인 시스템 (Factory + Type Erasure)

// 요구사항:
// 1. 런타임에 플러그인 등록/해제
// 2. 다양한 플러그인 타입 지원
// 3. 공통 인터페이스로 실행

// 힌트:
class Plugin {
public:
    template<typename T>
    Plugin(T plugin);
    void execute();
};

class PluginManager {
    std::unordered_map<std::string, Plugin> plugins_;
};

다음 단계

  • 15_C++17_기능 - 모던 C++ 기능 복습
  • 디자인 패턴 심화 서적 참고 (GoF, Head First)
  • 실제 프로젝트에 패턴 적용 연습

참고 자료


← 이전: C++20 심화 | 목차

to navigate between lessons