본문 바로가기

study/design pattern

[디자인패턴][구조패턴] 플라이웨이트 Flyweight - C++

728x90

[모던 C++ 디자인 패턴] 책을 바탕으로 공부하는 내용을 정리한 내용이다.


Flyweight pattern

플라이웨이트 패턴은 많은 수의 가벼운 임시 객체를 스마트 참조로 사용하는 것을 말하며 그러한 객체들을 플라이웨이트라고 부른다. 종종 토큰, 쿠키라고 부르기도 한다. 플라이웨이트 패턴은 많은 수의 매우 비슷한 객체들이 사용되어야 할 때 메모리 사용량을 절감하는 방법으로서 자주 사용된다.


사용자 이름

대규모 멀티 플레이가 지원되는 온라인 게임을 생각해보자. 중복 이름이 허용된다면, 흔한 이름이 생성될 때마다 중복되는 공간을 낭비하게 된다. 대신에 이름을 한 번만 저장하고 같은 이름을 가진 사용자들은 그 이름을 포인터로 갖게 할 수도 있다. 이것은 적지 않은 메모리 절감이다.

더 나아가 이름을 "성"과 "이름"으로 구분하는 것도 고려할만 하다. 그러면 두 개의 포인터를 사용함으로써 하나의 긴 이름보다 중복을 보다 많이 관리할 수 있다.

다만, 여기서처럼 포인터로 이름들을 구분하는 것은 생각보다 포인터의 주소 크기도 작지 않게 될 수 있다. 그래서 이제는 인덱스를 사용할 것을 고려해야 한다. 인덱스로 사용될 타입을 typedef 후 사용하면 User를 다음과 같이 정의할 수 있다.

typedef uint32_t key; //< 인덱스로 사용할 타입

struct User {
	User(const std::string &firstName, const std::string &lastName) :
		m_firstName(add(firstName)), m_lastName(add(lastName)) {}

protected:
	static key add(const std::string &s) {
		std::map<std::string, key>::iterator it = m_nameKeys.find(s);
		if ( it != m_nameKeys.end() ) {
			return it->second;
		}

		m_nameKeys.insert(std::make_pair(s, ++seed));
		m_keyNames.insert(std::make_pair(seed, s));

		return seed;
	}

protected:
	key m_firstName, m_lastName;
	static std::map<key, std::string> m_keyNames;
	static std::map<std::string, key> m_nameKeys;
	static key seed;
};

 

위의 코드에서 add()는 키/값 쌍을 m_names 데이터 구조에 추가하며, 인덱스를 리턴하게 된다. 실제 예시에서는 m_names에 boost::bimap(양방향 map)을 사용했다.

add()는 가져오거나 추가하기 메커니즘의 표준적인 구현이다.

 

추가적으로 User 클래스에 실제 성과 이름을 제공하기 위한 적절한 get/set 멤버 함수를 만들어야 한다. 

 

실제 메모리가 얼마나 절감되었는지 정량적인 분석은 생략한다.


Boost.Flyweight

사실 플라이웨이트를 직접 만들지 않아도 Boost에서 제공하고 있다. boost::flyweight 타입을 이용하면 공간을 절약하기 위한 플라이웨이트를 쉽게 생성할 수 있다. User 클래스의 이름 멤버 변수에 boost::flyweight를 이용하면 아래와 같이 너무 간단한 플라이웨이트를 적용한 클래스가 탄생한다.

struct User2 {
	User2(const std::string &firstName, const std::string &lastName) :
		m_firstName(firstName), m_lastName(lastName) {}

	boost::flyweight<std::string> m_firstName, m_lastName;
};

문자열 범위

std::string::substring()을 호출하면 새로운 문자열이 생성되어 리턴될까? 리턴받은 부분 문자열을 수정하고 싶다면 수정할 수 있다. 그러면 원본 문자열에 영향이 있을까? 프로그래밍 언어마다 다르겠지만 부분 문자열을 리턴할 때 문자열 자체가 아니라 명시적으로 그 범위를 리턴하는 경우가 있다. 이는 리턴 받은 문자열을 수정할 수 있게 하는 것과 더불어 메모리 사용량을 정량하기 위한 플라이웨이트 패턴을 구현한 것이다.

동등한 기능으로 C++에서는 string_view를 통해 문자열 범위를 제공한다. array에 대해서도 어떤 복제도 피할 수 있도록 여러 확장 타입이 제공된다.

아래에서는 문자열 범위 기능을 구현해보도록 하자. 알파벳 텍스트의 특정 범위를 대문자로 바꾸려고 한다. 단, 원본 텍스트에는 변경을 가하지 않고, 스트림 출력 연산자로 텍스트를 내보낼 때만 그렇게 하고 싶다고 하자.


섣부른 접근 방법

매우 단순하고 비효율적인 방법 중 하나는 모든 문자와 대칭되는 원본 텍스트와 동일한 크기의 이진 배열을 만들고 대문자로 만들 문자를 표시하는 것이다.

동작은 잘 하게 쉽게 만들 수 있지만 멋진 방법은 아니다. 여기에 플라이웨이트를 적용하도록 하자.


플라이웨이트의 구현

먼저, 상위 클래스와 플라이웨이트 클래스를 정의한다. 플라이웨이트 클래스는 상위 클래스 안에서 중첩 클래스로서 정의한다.

class BetterFormattedText {
public:
	struct TextRange {
		TextRange(const int start, const int end) : m_start(start), m_end(end) {}
		bool covers(int position) const {
			return m_start <= position && position <= m_end;
		}

		int m_start;
		int m_end;
		bool m_capitalize;
	};

	TextRange &getRange(int start, int end) {
		m_formatting.emplace_back(TextRange(start, end));
		return *m_formatting.rbegin();
	}

private:
	std::string m_plainText;
	std::vector<TextRange> m_formatting;
};

 

getRange()에서는 아래의 3가지 작업이 일어난다.

  1. 새로운 TextRange를 생성
  2. 생성된 TextRange가 vector로 이동
  3. 마지막 항목에 대한 참조 리턴

위 구현에서는 중복 범위는 검사하지 않는다. 여기서는 생략하지만 있어야 할 기능이다.

 

이제 BetterFormattedText를 위한 operator << 를 구현할 수 있다.

	friend std::ostream &operator<<(std::ostream &os, const BetterFormattedText &obj) {
		std::string s;
		for ( size_t i = 0; i < obj.m_plainText.length(); ++i ) {
			char c = obj.m_plainText[i];
			for ( std::vector<TextRange>::const_iterator it = obj.m_formatting.begin(); it != obj.m_formatting.end(); ++it ) {
				const TextRange &rng = *it;
				if ( rng.covers(i) && rng.m_capitalize ) {
					c = toupper(c);
				}
				s += c;
			}
		}
		return os << s;
	}

요약

플라이웨이트 패턴은 기본적으로 공간 절약을 위한 테크닉이다. 실제 구현되는 형태는 매우 다양할 수 있다.

아무튼 C++에서도 편하게 사용하려면 boost::flyweight를 이용하자.

728x90