C++ Composite 패턴

객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 동일하게 다룹니다. 파일 시스템처럼 파일(리프)과 디렉터리(복합)를 단일 인터페이스로 처리할 수 있습니다.

Gist
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <numeric>
#include <algorithm>
#include <iomanip>

// ──────────────────────────────────────────────
// 컴포넌트 인터페이스: 파일과 디렉터리가 공유
// ──────────────────────────────────────────────
class FileSystemNode {
public:
    virtual ~FileSystemNode() = default;

    virtual std::string name() const = 0;
    virtual std::size_t size() const = 0;         // 바이트 단위 크기
    virtual void print(int depth = 0) const = 0;  // 트리 출력

    // 복합 노드 전용 연산 — 기본 구현은 예외 발생
    virtual void add(std::unique_ptr<FileSystemNode> node) {
        throw std::logic_error("리프 노드에는 자식을 추가할 수 없습니다: " + name());
    }
    virtual void remove(const std::string& nodeName) {
        throw std::logic_error("리프 노드에서는 자식을 삭제할 수 없습니다: " + name());
    }
    virtual bool isDirectory() const { return false; }

protected:
    // 트리 출력 시 들여쓰기 생성 헬퍼
    static std::string indent(int depth) {
        return std::string(depth * 2, ' ');
    }

    // 파일 크기를 읽기 좋은 단위로 변환
    static std::string humanSize(std::size_t bytes) {
        if (bytes < 1024)       return std::to_string(bytes) + " B";
        if (bytes < 1024*1024)  return std::to_string(bytes / 1024) + " KB";
        return std::to_string(bytes / (1024*1024)) + " MB";
    }
};

// ──────────────────────────────────────────────
// 리프(Leaf): 파일 — 자식 없음
// ──────────────────────────────────────────────
class File : public FileSystemNode {
public:
    File(std::string name, std::size_t sizeBytes)
        : name_(std::move(name)), size_(sizeBytes) {}

    std::string name() const override { return name_; }
    std::size_t size() const override { return size_; }
    bool isDirectory() const override { return false; }

    void print(int depth = 0) const override {
        std::cout << indent(depth) << "📄 " << name_
                  << " (" << humanSize(size_) << ")\n";
    }

private:
    std::string name_;
    std::size_t size_;
};

// ──────────────────────────────────────────────
// 복합(Composite): 디렉터리 — 자식 노드 보유
// ──────────────────────────────────────────────
class Directory : public FileSystemNode {
public:
    explicit Directory(std::string name) : name_(std::move(name)) {}

    std::string name() const override { return name_; }
    bool isDirectory() const override { return true; }

    // 하위 모든 파일 크기의 합산 (재귀)
    std::size_t size() const override {
        return std::accumulate(
            children_.begin(), children_.end(), std::size_t{0},
            [](std::size_t sum, const auto& child) {
                return sum + child->size();
            });
    }

    // 자식 노드 추가
    void add(std::unique_ptr<FileSystemNode> node) override {
        children_.push_back(std::move(node));
    }

    // 이름으로 자식 노드 삭제
    void remove(const std::string& nodeName) override {
        auto it = std::remove_if(children_.begin(), children_.end(),
            [&](const auto& child) { return child->name() == nodeName; });
        if (it == children_.end()) {
            throw std::runtime_error("노드를 찾을 수 없음: " + nodeName);
        }
        children_.erase(it, children_.end());
    }

    // 트리 구조 재귀 출력
    void print(int depth = 0) const override {
        std::cout << indent(depth) << "📁 " << name_
                  << "/ (" << humanSize(size()) << ")\n";
        for (const auto& child : children_) {
            child->print(depth + 1); // 자식에게 위임
        }
    }

    // 이름으로 직접 자식 검색 (1단계)
    FileSystemNode* find(const std::string& nodeName) const {
        for (const auto& child : children_) {
            if (child->name() == nodeName) return child.get();
        }
        return nullptr;
    }

    std::size_t childCount() const { return children_.size(); }

private:
    std::string name_;
    std::vector<std::unique_ptr<FileSystemNode>> children_;
};

// ──────────────────────────────────────────────
// 파일 시스템 통계 출력 (클라이언트 코드)
// 리프/복합 구분 없이 동일 인터페이스 사용
// ──────────────────────────────────────────────
void printStats(const FileSystemNode& node) {
    std::cout << "  경로: " << node.name()
              << " | 타입: " << (node.isDirectory() ? "디렉터리" : "파일")
              << " | 크기: " << FileSystemNode::humanSize(node.size()) << "\n";
}

// ──────────────────────────────────────────────
// 사용 예시
// ──────────────────────────────────────────────
int main() {
    std::cout << "=== 파일 시스템 (Composite 패턴) ===\n\n";

    // 루트 디렉터리 구성
    auto root = std::make_unique<Directory>("project");

    // src/ 디렉터리
    auto src = std::make_unique<Directory>("src");
    src->add(std::make_unique<File>("main.cpp",       3'200));
    src->add(std::make_unique<File>("renderer.cpp",  12'800));
    src->add(std::make_unique<File>("renderer.h",     1'400));

    auto utils = std::make_unique<Directory>("utils");
    utils->add(std::make_unique<File>("logger.cpp",   2'560));
    utils->add(std::make_unique<File>("logger.h",       800));
    src->add(std::move(utils)); // utils를 src 하위에 추가

    // assets/ 디렉터리
    auto assets = std::make_unique<Directory>("assets");
    assets->add(std::make_unique<File>("logo.png",    512'000));
    assets->add(std::make_unique<File>("bg.jpg",    2'048'000));

    // docs/ 디렉터리
    auto docs = std::make_unique<Directory>("docs");
    docs->add(std::make_unique<File>("README.md",     4'096));
    docs->add(std::make_unique<File>("API.md",        8'192));

    // 루트에 모두 추가
    root->add(std::move(src));
    root->add(std::move(assets));
    root->add(std::move(docs));
    root->add(std::make_unique<File>("CMakeLists.txt", 1'024));

    // 트리 전체 출력 (파일/디렉터리 구분 없이 동일 메서드 호출)
    std::cout << "[전체 파일 트리]\n";
    root->print();

    // 개별 노드 통계 (리프/복합 동일 취급)
    std::cout << "\n[노드별 크기 통계]\n";
    printStats(*root);

    // 파일 삭제 후 크기 변화 확인
    std::cout << "\n[bg.jpg 삭제 후]\n";
    auto* assetsPtr = dynamic_cast<Directory*>(root->find("assets"));
    if (assetsPtr) {
        assetsPtr->remove("bg.jpg");
    }
    root->print();

    return 0;
}