[모던 C++로 다시 쓰는 GoF 디자인 패턴 총정리 #8] 플라이웨이트(Flyweight) 패턴: 메모리 절약과 공유 객체 관리의 현대적 구현

지난 글에서는 퍼사드(Facade) 패턴을 모던 C++ 관점에서 재해석하며, 모듈(Modules), Concepts, std::expected, std::format, std::variant 등을 활용해 복잡한 서브시스템을 단순화하는 전략을 살펴보았습니다. 이번 글에서는 또 하나의 구조적 패턴인 플라이웨이트(Flyweight) 패턴에 주목합니다.

etc-image-0

플라이웨이트 패턴은 다수의 유사한 객체를 효율적으로 다루기 위해, 공유 가능한 상태를 메모리 절약적으로 관리하는 방법을 제시합니다. 전통적으로는 "풀(Pool)을 두고 재활용"하거나 "공유된 객체를 전역 맵으로 관리"하는 방식으로 구현했으나, 모던 C++에서는 std::unordered_map, std::shared_ptr, std::string_view, Concepts, std::expected, 심지어는 Ranges와 std::variant 등을 활용해 더 안전하고 유연한 공유 전략을 세울 수 있습니다.

패턴 소개: 플라이웨이트(Flyweight)

의도:

  • 객체가 많을 때, 공통된 속성을 공유해 메모리 사용량을 줄이고, 성능을 개선.
  • 자주 반복되는 동일한 상태(예: 문서 내 글자, GUI 아이콘, 문자열)들을 "플라이웨이트"로 관리하여 중복 생성 방지.
  • 예: 텍스트 편집기에서 문자를 표시할 때, 각 문자 객체를 매번 생성하지 않고, 공유 가능한 폰트나 스타일 정보를 flyweight 객체로 관리.

전통적 구현 문제점:

  • 전역 pool이나 static 맵으로 flyweight 관리, 쓰레드 안전성이나 라이프사이클 관리 복잡.
  • 문자열 같은 자원 공유 시 사본 관리 어려움.
  • 타입 안전성과 문서화 미비.

기존 C++ 스타일 구현 (C++11/14/17)

예를 들어, 그래픽 아이콘 표시를 가정해봅시다. 많은 아이콘이 동일한 이미지를 공유하되, 위치나 상태만 다를 수 있습니다.

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

struct IconData {
    std::string imagePath;
    // 실제 이미지 데이터 가정
};

struct IconFlyweight {
    std::shared_ptr<IconData> data;
    IconFlyweight(std::shared_ptr<IconData> d) : data(std::move(d)) {}
    void render(int x, int y) const {
        std::cout << "Rendering " << data->imagePath << " at (" << x << "," << y << ")\n";
    }
};

struct IconFactory {
    std::unordered_map<std::string, std::weak_ptr<IconData>> cache;

    IconFlyweight getIcon(const std::string& path) {
        if (auto it = cache.find(path); it != cache.end()) {
            if (auto sp = it->second.lock()) {
                return IconFlyweight(sp);
            }
        }
        auto data = std::make_shared<IconData>(IconData{path});
        cache[path] = data;
        return IconFlyweight(data);
    }
};

int main() {
    IconFactory factory;
    auto icon1 = factory.getIcon("icon.png");
    auto icon2 = factory.getIcon("icon.png");
    icon1.render(10,10);
    icon2.render(20,20);
}

고찰:

  • IconFactory가 std::unordered_map으로 아이콘 데이터 공유.
  • std::weak_ptr로 캐시 관리, std::shared_ptr로 참조 카운팅.
  • 단순히 메모리 공유를 통해 flyweight 구현.

모던 C++20 이상의 개선: Concepts, std::expected, std::string_view, Ranges

1. Concepts로 Flyweight Factory 요구사항 정의

Flyweight Factory는 "키(key)로부터 공유 가능한 객체를 반환"해야 한다는 공통 요구사항을 Concepts로 정의할 수 있습니다.

template<typename F, typename Key>
concept FlyweightFactoryConcept = requires(F& f, const Key& k) {
    { f.getFlyweight(k) } -> std::convertible_to<std::shared_ptr<IconData>>;
};

고찰:

  • 상속 없이도 Flyweight Factory 조건을 명시, 컴파일 타임 제약 체크.

2. std::string_view로 키 관리 최적화

키가 문자열일 경우, std::string_view를 사용하면 키 조회 시 불필요한 복사 최소화.

#include <string_view>

struct ModernIconFactory {
    std::unordered_map<std::string, std::weak_ptr<IconData>> cache;

    std::shared_ptr<IconData> getFlyweight(std::string_view path) {
        auto it = cache.find(std::string(path));
        if(it != cache.end()) {
            if(auto sp = it->second.lock()) {
                return sp;
            }
        }
        auto data = std::make_shared<IconData>(IconData{std::string(path)});
        cache[std::string(path)] = data;
        return data;
    }
};

static_assert(FlyweightFactoryConcept<ModernIconFactory, std::string_view>);

고찰:

  • std::string_view로 성능 미세 최적화, 필요 시 std::format으로 디버그 정보 출력 가능.

3. std::expected로 로딩 실패 처리

만약 이미지 파일 로딩 과정에서 실패할 수 있다면 std::expected로 처리할 수 있습니다.

struct SafeIconFactory {
    std::unordered_map<std::string, std::weak_ptr<IconData>> cache;

    std::expected<std::shared_ptr<IconData>,std::string> getFlyweight(std::string_view path) {
        if (path.empty()) {
            return std::unexpected("Empty path");
        }

        std::string key(path);
        if (auto it = cache.find(key); it != cache.end()) {
            if (auto sp = it->second.lock()) {
                return sp;
            }
        }

        // 가상의 로딩 로직 (실패 가능)
        bool loadSuccess = true;
        if(!loadSuccess) {
            return std::unexpected(std::string("Failed to load: ") + key);
        }

        auto data = std::make_shared<IconData>(IconData{key});
        cache[key] = data;
        return data;
    }
};

고찰:

  • 로딩 실패 시 std::expected로 에러 반환, 클라이언트는 명시적으로 검사 가능.

4. Ranges와 파이프라인으로 Flyweight 조합

Flyweight 패턴은 종종 다수의 키에 대해 flyweight를 순차적으로 얻는 상황 발생. 이 때 Ranges를 통해 파이프라인화 가능.

#include <vector>
#include <ranges>
#include <algorithm>

SafeIconFactory safeFactory;
std::vector<std::string_view> icons = {"icon.png", "icon2.png", ""}; // 빈 string은 실패 예
auto results = icons | std::views::transform([&](auto sv) { return safeFactory.getFlyweight(sv); });

for (auto& res : results) {
    if(!res) {
        std::cerr << "Error: " << res.error() << "\n";
    } else {
        std::cout << "Loaded: " << (*res)->imagePath << "\n";
    }
}

고찰:

  • Ranges로 flyweight 획득 과정 파이프라인화.
  • 에러 처리와 성공 처리 로직을 람다에서 직관적으로 표현 가능.

5. std::format을 통한 디버깅 개선

Flyweight 캐시 상태를 std::format으로 정리해 출력할 수 있습니다.

#include <format>

void printCacheState(const SafeIconFactory& f) {
    std::cout << std::format("Cache size: {}\n", f.cache.size());
}

고찰:

  • 유지보수성과 디버깅 편의성 향상.

비교 및 분석

  • 전통적 구현(C++11 전후):
    • 전역이나 static 맵으로 flyweight 관리, 단순히 메모리 공유에 초점.
    • 에러 처리 시 단순 bool, nullptr 등 사용.
    • 타입 안전성, 문서화, 동적 상황 처리 제한.
  • 모던 C++(C++20 이상):
    • Concepts로 Flyweight Factory 제약 명시.
    • std::string_view로 성능 최적화, std::expected로 명확한 에러 처리.
    • std::format으로 디버그 출력 정돈.
    • Ranges를 통해 flyweight 획득 프로세스를 파이프라인화, 다양한 상황에 맞게 동적으로 처리 가능.
    • 상속 사용 최소화, 템플릿 및 표준 라이브러리 기능 적극 활용해 코드 간결화와 유지보수성 향상.

결국 모던 C++에서는 플라이웨이트 패턴도 상속과 전역 변수에 의존하지 않고, Concepts와 표준 라이브러리 기능을 통해 더 타입 안전하고 유지보수하기 쉬운 구조로 재구성할 수 있습니다. 에러 처리나 동적 상황 처리 면에서도 유연성이 증가합니다.

마무리

플라이웨이트 패턴은 많은 수의 유사 객체를 효율적으로 관리하기 위한 패턴이며, 모던 C++에서는 단순히 공유 포인터와 맵을 넘어, Concepts, std::expected, Ranges, std::string_view 등의 기능으로 더 정교하고 타입 안전하며 유지보수성이 높은 구현이 가능합니다. 이렇게 하면 대규모 시스템에서 자원 공유와 메모리 최적화가 필요할 때 더 깨끗하고 직관적인 코드를 작성할 수 있습니다.

다음 글에서는 구조적 패턴 중 남은 패턴인 프록시(Proxy) 패턴을 다루거나, 행동 패턴(Behavioral Patterns)으로 넘어가며 Strategy, Observer, State 등의 패턴을 모던 C++로 재해석하는 과정을 살펴보겠습니다.

반응형