추상 팩토리 패턴을 알아볼 시간인데요, 팩토리 메소드 패턴을 설명할 때 사용했던 피자 프랜차이즈 사업의 사례를 계속 이용해 보겠습니다.

사업이 나날이 번창하면서 사장은 이윤을 극대화시키기 위해 피자의 원재료를 본점에서 직접 공급하기로 결정했습니다. 그동안 피자의 원재료는 각 지점마다 따로 구입해서 사용했지만 원재료를 본점에서 공급하면 보다 안정적인 가격과 품질을 보장할 수 있을 것 입니다. 문제는 각 지점마다 지역 소비자의 기호에 맞는 피자를 만들어야 하기 때문에 같은 레드 소스라도 LA 지점 레드 소스와 뉴욕 지점 레드 소스가 다르다는 점 입니다. 어떻게 하면 이 문제를 해결할 수 있을까요?

먼저 제품군(Product Familiy)에 대한 개념부터 알아보겠습니다.
제품(Product)이 여러 개가 있고, 각 제품들은 또 다시 여러 종류로 나뉠 때 같은 종류의 제품들을 모아놓은 것을 제품군이라고 합니다.
피자를 예로 들면, 피자는 도우, 소스, 토핑, 치즈 같은 제품으로 이루어져 있고 LA 풍 피자와 뉴욕 풍 피자 같은 제품군으로 나뉘어집니다. 다시말하면 LA 풍 제품군은 LA 풍 도우, LA 풍 소스 등의 제품으로 이루어져있고, 뉴욕 풍 제품군은 뉴욕 풍 도우, 뉴욕 풍 소스 등의 제품으로 이루어져있겠죠.
그러면 각각의 제품을 추상화하고 제품의 생성 과정을 앞서 배웠던 팩토리 메소드 패턴을 이용해서 캡슐화 해봅시다.
(이 글에서 사용되는 코드는 모두 의사 코드 입니다.)
class Dough
{
public:
	virtual ~Dough() = 0;
};

Dough::~Dough() {}

class Sauce
{
public:
	virtual ~Sauce() = 0;
};

Sauce::~Sauce() {}

class Topping
{
public:
	virtual ~Topping() = 0;
};

Topping::~Topping() {}

class Cheese
{
public:
	virtual ~Cheese() = 0;
};

Cheese::~Cheese() {}

class ThinDough: public Dough {};
class ThickDough: public Dough {};

class TomatoSauce: public Sauce {};
class MarinaraSauce: public Sauce {};

class VeggieTopping: public Topping {};
class ClamsTopping: public Topping {};

class MozzarellaCheese: public Cheese {};
class ReggianoCheese: public Cheese {};

class PizzaIngredientFactory
{
public:
	virtual Dough* CreateDough() = 0;
	virtual Sauce* CreateSauce() = 0;
	virtual Topping* CreateToppings() = 0;
	virtual Cheese* CreateCheese() = 0;
};

class LaPizzaIngredientFactory: public PizzaIngredientFactory
{
public:
	// LA 풍 피자(제품군)에 맞는 원재료(제품)를 생성 합니다.
	virtual Dough* CreateDough() { return new ThinDough; }
	virtual Sauce* CreateSauce() { return new TomatoSauce; }
	virtual Topping* CreateToppings() { return new VeggieTopping; }
	virtual Cheese* CreateCheese() { return new MozzarellaCheese; }
};

class NyPizzaIngredientFactory: public PizzaIngredientFactory
{
public:
	// 뉴욕 풍 피자(제품군)에 맞는 원재료(제품)를 생성 합니다.
	virtual Dough* CreateDough() { return new ThickDough; }
	virtual Sauce* CreateSauce() { return new MarinaraSauce; }
	virtual Topping* CreateToppings() { return new ClamsTopping; }
	virtual Cheese* CreateCheese() { return new ReggianoCheese; }
};
원재료 공장을 만들었으니 기존의 피자 가게에 공급만 하면 되겠네요.
class PizzaStore
{
public:
	Pizza* Order(const std::string& pizzaName)
	{
		Pizza* pizza = CreatePizza(pizzaName);

		pizza->Prepare();
		pizza->Bake();
		pizza->Cut();
		pizza->Box();

		return pizza;
	}

	PizzaStore(PizzaIngredientFactory* pizzaIngredientFactory)
		:	m_PizzaIngredientFactory(pizzaIngredientFactory) {}

protected:
	PizzaIngredientFactory* m_PizzaIngredientFactory;

private:
	virtual Pizza* CreatePizza(const std::string& pizzaName) = 0;
};
이제 각 지점에서는 피자를 만들 때 원하는 원재료를 얻을 수 있습니다.
class LaPizzaStore: public PizzaStore
{
private:
	Pizza* CreatePizza(const std::string& pizzaName)
	{
		Pizza* pizza = NULL;

		if (pizzaName == "LA 치즈 피자")
		{
			pizza = new LaCheesePizza(m_PizzaIngredientFactory);
		}
		else if (pizzaName == "LA 페퍼로니 피자")
		{
			pizza = new LaPepperoniPizza(m_PizzaIngredientFactory);
		}
		else if (pizzaName == "LA 야채 피자")
		{
			pizza = new LaVeggiePizza(m_PizzaIngredientFactory);
		}

		return pizza;
	}
};
도우에다 소스를 바르고 토핑과 치즈를 얹는 일련의 작업들은 피자 객체의 Prepare() 메소드에서 이루어집니다.
자, 그럼 뉴욕 풍 치즈 피자가 어떻게 만들어지는 지 한 번 볼까요?
class NyCheesePizza: public Pizza
{
public:
	virtual void Prepare()
	{
		Dough* dough = m_PizzaIngredientFactory->CreateDough();
		Sauce* sauce = m_PizzaIngredientFactory->CreateSauce();
		Topping* topping = m_PizzaIngredientFactory->CreateToppings();
		Cheese* cheese = m_PizzaIngredientFactory->CreateCheese();

		// 준비된 재료로 피자를 굽기 좋게 준비
		// ...
		// ...

		std::cout << "NY 치즈 피자 준비" << std::endl;
	}
	virtual void Bake() { std::cout << "NY 치즈 피자 굽기" << std::endl; }
	virtual void Cut() { std::cout << "NY 치즈 피자 자르기" << std::endl; }
	virtual void Box() { std::cout << "NY 치즈 피자 포장" << std::endl; }

	NyCheesePizza(PizzaIngredientFactory* pizzaIngredientFactory)
		:	Pizza(pizzaIngredientFactory) {}
};

int main()
{
	NyPizzaIngredientFactory nyPizzaIngredientFactory;
	
	NyPizzaStore nyPizzaStore(&nyPizzaIngredientFactory);

	Pizza* pizza = nyPizzaStore.Order("NY 치즈 피자");

	return 0;
}
추상 팩토리 패턴이 가지는 장점은 팩토리 메소드 패턴과 동일합니다.
객체의 생성 과정을 캡슐화해서 클라이언트(코드 사용자 혹은 접근 영역의 코드 그 자체)가 객체의 구성이나 표현 방식에 의존적이지 않도록 만들어 줍니다. 다시말하면 객체의 생성 방식이나 종류가 바뀌더라도 클라이언트는 그 것에 대해 신경 쓸 필요가 없다는 뜻이죠. '수정이 예상되는 부분의 국지화' 팩토리 메소드 편에서 말씀드렸었죠?

추상 팩토리 패턴이 가지는 또 하나의 장점은 바로 제품과 제품군을 다룰 때 굉장히 편리하다는 점 입니다.
일단 제품군들이 실수로 섞여서 사용될 일이 자연스럽게 방지 되고, 각 제품군 간의 교체도 쉽습니다.
제품과 제품군이 팩토리 메소드 패턴과 추상 팩토리 패턴을 구분 짓는 핵심어라고 생각하시면 되겠네요.

참고/인용: Head First Design Patterns, GoF 디자인 패턴! 이렇게 활용한다
크리에이티브 커먼즈 라이선스
Creative Commons License
top

Trackback Address :: http://www.i-stew.com/trackback/70 관련글 쓰기

  1. Favicon of http://persian8.com BlogIcon 박수영 2011/02/08 09:57 댓글주소 | 수정/삭제 | 댓글

    좋은글 감사합니다 ^^

  2. Favicon of http://www.logodesignconsultant.com/logo-design-services.html BlogIcon logo design service 2012/04/26 16:15 댓글주소 | 수정/삭제 | 댓글

    I want to say that your post is fantastic. You have written it well. Keep on posting. I will come again to read new posts. Thanks.

Write a comment


사용자 삽입 이미지


팩토리 메소드는 객체 생성을 대행해주는 메소드 말합니다.

그냥 선언하거나 new 하면 되는 것을 그딴 걸 대체 왜 대행하냐 싶지만 때로는 객체 생성 과정이 복잡한 애들도 종종 있습니다. 어떤 때는 그 과정이 너무 복잡해서 신경쓰기도 싫어질 때가 있는데, 바로 이 때문에 대행해주는 애가 필요한 거죠. OOP 기초 공부할 때 골백번도 더 들은 얘기지만 들을 때마다 뜬구름 잡는 소리 같았던 정보 은닉(Infomation Hiding)이란 것이 바로 이 건가 봅니다.

구현 상세를 객체 내부에 숨기는 것.
누구로부터? 클라이언트(코드 사용자 혹은 접근 영역의 코드 그 자체)로부터.
왜? 클라이언트는 그런 거 신경쓰지 않는 것이 좋으니까.

객체 내부의 수정에 대해 일일이 신경쓰지 않아도 된다는 말은 그 만큼 수정이 국지화 되었다는 뜻이고 수정이 국지화 되었다는 것은 수정에 대한 비용이 작아졌다는 뜻이죠. 이 것이 객체 지향 설계의 가장 핵심적인 접근 방법이자 모든 디자인 패턴의 목적입니다.

그렇다면 팩토리 메소드 패턴의 경우에는 어떻게 그 목적을 이루게 되는 것인가가 오늘 알아볼 내용인데 일단 사례부터 살펴보겠습니다.
역시 Head First Design Patterns 에 나와있는 예제를 인용하겠습니다.

여러분은 지금 막 개업한 LA 피자 가게의 총괄 관리자 입니다. 사장은 여러가지 피자를 팔려고 하고있는데, 일단 피자 가게의 규모가 그리 크지 않으므로 치즈, 페퍼로니, 야채 이렇게 세가지 메뉴만 판매할 겁니다. 모든 피자는 준비(소스와 토핑 얹기), 굽기, 자르기, 포장 이라는 동일한 작업 과정을 거치게 됩니다. 그럼 추상화를 해볼까요?
(이 글에서 사용되는 코드는 모두 의사 코드 입니다.)
#include <iostream>
#include <string>

class Pizza
{
public:
	virtual void Prepare() = 0;
	virtual void Bake() = 0;
	virtual void Cut() = 0;
	virtual void Box() = 0;
};

class CheesePizza: public Pizza
{
public:
	virtual void Prepare() { std::cout << "치즈 피자 준비" << std::endl; }
	virtual void Bake() { std::cout << "치즈 피자 굽기" << std::endl; }
	virtual void Cut() { std::cout << "치즈 피자 자르기" << std::endl; }
	virtual void Box() { std::cout << "치즈 피자 포장" << std::endl; }
};

class PepperoniPizza: public Pizza
{
public:
	virtual void Prepare() { std::cout << "페퍼로니 피자 준비" << std::endl; }
	virtual void Bake() { std::cout << "페퍼로니 피자 굽기" << std::endl; }
	virtual void Cut() { std::cout << "페퍼로니 피자 자르기" << std::endl; }
	virtual void Box() { std::cout << "페퍼로니 피자 포장" << std::endl; }
};

class VeggiePizza: public Pizza
{
public:
	virtual void Prepare() { std::cout << "야채 피자 준비" << std::endl; }
	virtual void Bake() { std::cout << "야채 피자 굽기" << std::endl; }
	virtual void Cut() { std::cout << "야채 피자 자르기" << std::endl; }
	virtual void Box() { std::cout << "야채 피자 포장" << std::endl; }
};

class PizzaStore
{
public:
	Pizza* Order(const std::string& pizzaName)
	{
		Pizza* pizza = NULL;

		if (pizzaName == "치즈 피자")
		{
			pizza = new CheesePizza;
		}
		else if (pizzaName == "페퍼로니 피자")
		{
			pizza = new PepperoniPizza;
		}
		else if (pizzaName == "야채 피자")
		{
			pizza = new VeggiePizza;
		}

		pizza->Prepare();
		pizza->Bake();
		pizza->Cut();
		pizza->Box();

		return pizza;
	}
};

int main()
{
	PizzaStore pizzaStore;

	Pizza* pizza = pizzaStore.Order("야채 피자");

	return 0;
}
사업이 번창해서 분점이 생겼습니다. 뉴욕에 지점을 차려야 하는데 문제는 LA 사람들과 뉴욕 사람들의 입맛이 조금 달라서 같은 치츠 피자라도 LA풍과 뉴욕풍으로 나눠서 만들어야 됩니다. 일단 생각나는 대로 해볼까요.
class PizzaStore
{
public:
	Pizza* Order(const std::string& pizzaName)
	{
		Pizza* pizza = NULL;

		if (pizzaName == "LA 치즈 피자")
		{
			pizza = new LaCheesePizza;
		}
		else if (pizzaName == "LA 페퍼로니 피자")
		{
			pizza = new LaPepperoniPizza;
		}
		else if (pizzaName == "LA 야채 피자")
		{
			pizza = new LaVeggiePizza;
		}
		else if (pizzaName == "NY 치즈 피자")
		{
			pizza = new NyCheesePizza;
		}
		else if (pizzaName == "NY 페퍼로니 피자")
		{
			pizza = new NyPepperoniPizza;
		}
		else if (pizzaName == "NY 야채 피자")
		{
			pizza = new NyVeggiePizza;
		}

		pizza->Prepare();
		pizza->Bake();
		pizza->Cut();
		pizza->Box();

		return pizza;
	}
};
뉴욕 지점에서 대체 왜 자신들이 LA풍 피자 만드는 법을 알아야하느냐고 항의가 들어왔지만 뭐 그리 큰 문제는 아니니 그냥 넘어갔습니다.
바로 다음 날 뉴욕 지점에서 자기네들이 개발한 '뉴욕풍 조개 피자'라는 신메뉴를 추가해주기를 원했습니다. 뭐 까짓거 추가해주지 하고 추가를 해줬는데 뭐가 잘못됐는지 피자가 엉망진창이 되서 나왔습니다. 문제는 뉴욕만 그런 것이 아니라 LA 지점 피자도 엉망진창이 되었습니다.
밤새도록 똥줄 타며 수정을 했더니 다음 날 사장이 와서 말했습니다. "시카고에 새로운 분점을 차릴 거야."

미칠 노릇이지만 짤리지 않으려면 PizzaStore 설계를 개선해야 합니다.
PizzaStorer 가 가지고 있었던 첫번째 문제는 쓸데없이 다른 지점의 피자를 만드는 법까지 알아야 된다는 점이었고 두번째는 수정에 대해 모든 지점이 영향을 받는다는 점이었습니다.
class PizzaStore
{
public:
	Pizza* Order(const std::string& pizzaName)
	{
		Pizza* pizza = CreatePizza(pizzaName);

		pizza->Prepare();
		pizza->Bake();
		pizza->Cut();
		pizza->Box();

		return pizza;
	}

private:
	virtual Pizza* CreatePizza(const std::string& pizzaName) = 0;
};

class LaPizzaStore: public PizzaStore
{
private:
	virtual Pizza* CreatePizza(const std::string& pizzaName)
	{
		Pizza* pizza = NULL;

		if (pizzaName == "LA 치즈 피자")
		{
			pizza = new LaCheesePizza;
		}
		else if (pizzaName == "LA 페퍼로니 피자")
		{
			pizza = new LaPepperoniPizza;
		}
		else if (pizzaName == "LA 야채 피자")
		{
			pizza = new LaVeggiePizza;
		}

		return pizza;
	}
};

class NyPizzaStore: public PizzaStore
{
private:
	virtual Pizza* CreatePizza(const std::string& pizzaName)
	{
		Pizza* pizza = NULL;

		if (pizzaName == "NY 치즈 피자")
		{
			pizza = new NyCheesePizza;
		}
		else if (pizzaName == "NY 페퍼로니 피자")
		{
			pizza = new NyPepperoniPizza;
		}
		else if (pizzaName == "NY 야채 피자")
		{
			pizza = new NyVeggiePizza;
		}

		return pizza;
	}
};

int main()
{
	LaPizzaStore pizzaStore;

	Pizza* pizza = pizzaStore.Order("LA 야채 피자");

	return 0;
}
팩토리 메소드 패턴을 이용한 피자 프랜차이즈 시스템이 완성되었네요. 피자 만드는 방법은 각 지점이 알아서 결정하기로 되어있기 때문에 자기 지점이 아닌 메뉴까지 알아야 되는 불편이 없어졌고, 한 지점에서 메뉴를 추가하다 실수를 한다고 해도 다른 지점에는 영향이 없도록 되어있습니다. 마찬가지로 시카고든 시애틀이든 새로운 지점을 차릴 때도 기존 다른 지점들은 신경 쓸 일이 전혀 없죠. 피자 만들기의 기본 틀(비가상 메소드)은 기본 클래스에서 제공하고 파생 클래스에서는 각 단계만 구현하기 때문에 일관된 공정으로 피자를 만들 수 있어 품질 관리에도 유리합니다.

기본 틀 제공, 각 단계만 구현. 어디서 많이 들어본 얘기 아닌가요? 맞습니다. 앞서 배웠던 템플릿 메소드 패턴이 품질 관리에 이용되었네요.
이렇듯 디자인 패턴을 이용하게 될 때는 여러가지 디자인 패턴을 혼합하는 형태로 적용하는 경우가 많이 있습니다. 그렇기 때문에 더더욱 정확한 이해와 응용력이 중요하죠.

다음 글에서는 또 다른 팩토리 패턴인 추상 팩토리 패턴에 대해서 알아 보겠습니다.

참고/인용: Head First Design Patterns, GoF 디자인 패턴! 이렇게 활용한다
크리에이티브 커먼즈 라이선스
Creative Commons License
top

Trackback Address :: http://www.i-stew.com/trackback/69 관련글 쓰기

Write a comment


//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 이름: RCSP.hpp
// 내용: 참조 카운팅 기반 스마트 포인터
//		 불완전한 형식 지원을 위해 특성 타입과 삭제자를 이용한다.
// 작성: planar210
// 갱신: 09-12-03
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#pragma once

// 특성
template< typename T >
struct RCSPTraits
{
	typedef T& Reference;
};

template<>
struct RCSPTraits< void >
{
	typedef void Reference;
};

// 삭제자
template< typename T >
struct TypedDeleter
{
	inline static void Delete(void* typelessObj)
	{
		T* typedObj = static_cast(typelessObj);
		CheckedDelete(typedObj);
	}

	inline static void CheckedDelete(T* p)
	{
		typedef char TypeMustBeComplete[sizeof(T) ? 1 : -1];
		static_cast< void >( sizeof(TypeMustBeComplete) );

		delete p;
	}
};

// RCSP
template< typename T >
class RCSP
{
public:
	// 기능: 자원을 해제하거나 재할당한다.
	// 입력: srcPtr - 객체에 할당할 자원의 원시 포인터
	inline void Reset(T* srcPtr = 0);

	// 기능: 할당된 자원의 원시 포인터를 가져온다.
	// 출력: 할당된 자원의 원시 포인터
	inline T* Get();

	inline RCSP& operator=(const RCSP& obj);
	inline T& operator*() const;
	inline T* operator->() const;

	explicit RCSP(T* srcPtr = 0);
	RCSP(const RCSP& obj);
	~RCSP();

private:
	inline void Dispose();

private:
	typedef typename RCSPTraits::Reference Reference;
	typedef void (*TypelessDeleter)(void*);

private:
	T* m_SrcPtr;				// 객체가 가지고 있는 포인터
	size_t* m_ReferCnt;			// 참조 카운팅
	TypelessDeleter m_Deleter;	// 삭제자
};

//─────────────────────────────────────────────────────
template< typename T >
RCSP::RCSP(T* srcPtr = 0)
:	m_SrcPtr(srcPtr),
	m_ReferCnt( new size_t(0) ),
	m_Deleter(&TypedDeleter::Delete)
{
	++*m_ReferCnt;
}

//─────────────────────────────────────────────────────
template< typename T >
RCSP::RCSP(const RCSP& obj)
:	m_SrcPtr(obj.m_SrcPtr),
	m_ReferCnt(obj.m_ReferCnt),
	m_Deleter(obj.m_Deleter)
{
	++*m_ReferCnt;
}

//─────────────────────────────────────────────────────
template< typename T >
RCSP::~RCSP()
{
	Dispose();
}

//─────────────────────────────────────────────────────
template< typename T >
RCSP& RCSP::operator=(const RCSP& obj)
{
	if (this != &obj)
	{
		Dispose();

		m_SrcPtr = obj.m_SrcPtr;
		m_ReferCnt = obj.m_ReferCnt;
		m_Deleter = obj.m_Deleter;

		++*m_ReferCnt;
	}

	return *this;
}

//─────────────────────────────────────────────────────
template< typename T >
T& RCSP::operator*() const
{
	return *m_SrcPtr;
}

//─────────────────────────────────────────────────────
template< typename T >
T* RCSP::operator->() const
{
	return m_SrcPtr;
}

//─────────────────────────────────────────────────────
template< typename T >
void RCSP::Reset(T* srcPtr)
{
	if (m_SrcPtr != srcPtr)
	{
		Dispose();
		
		m_ReferCnt = new size_t(1);
		m_SrcPtr = srcPtr;
		m_Deleter = &TypedDeleter::Delete;
	}
}

//─────────────────────────────────────────────────────
template< typename T >
T* RCSP::Get()
{
	return m_SrcPtr;
}

//─────────────────────────────────────────────────────
template< typename T >
void RCSP::Dispose()
{
	if (--*m_ReferCnt == 0)
	{
		delete m_ReferCnt;
		m_ReferCnt = 0;

		m_Deleter(m_SrcPtr);
		m_SrcPtr = 0;
	}
}
TR1 을 공부할 때 해봤던 소스를 수정해봤습니다.

사용할 일은 없겠지만...
크리에이티브 커먼즈 라이선스
Creative Commons License
top

Trackback Address :: http://www.i-stew.com/trackback/47 관련글 쓰기

Write a comment



템플릿 메소드(Template Method) 패턴은 C++ 의 template 과는 아무 상관이 없습니다. 단지 어떤 '틀'을 포함하고 있는 메소드라는 것을 의미하기 위해 붙여진 이름이죠.

일단 템플릿 메소트 패턴은 굉장히 직관적으로 파악하기 쉬운 패턴입니다. 아주 조그마한 개인 프로젝트라도 끝까지 완료한 경험이 있으시다면 열의 아홉은 분명 템플릿 메소드 패턴을 알게 모르게 활용 하셨을 겁니다. 구현이 어려워서 그렇지 개념은 절대 어렵지 않습니다. 뭐 대부분은 디자인 패턴이 그렇지만...좌우지간 Head First Design Patterns 책은 제가 자주 보는 디자인 패턴 책인데 디자인 패턴을 알기 쉽게 설명한 책이거든요, 이 책에 있는 내용을 인용해서 템플릿 메소드 패턴을 알아보도록 하겠습니다.

자, 그럼 차 전문점에서 일한다 치고, 커피와 홍차를 만드는 방법을 한번 추상화 해 봅시다.

- 커피 만드는 방법
1. 물을 끓인다.
2. 커피를 우려낸다.
3. 컵에 따른다.
4. 설탕과 우유를 첨가한다.

- 홍차 만드는 방법
1. 물을 끓인다.
2. 차를 우려낸다.
3. 컵에 따른다.
4. 레몬을 첨가한다.

- OOP 과정
1. 두가지 모두 '음료'라는 공통점이 있네요. 언뜻봐도 두 놈을 음료(Beverage)를 의미하는 클래스로 추상화하면 좋을 것 같습니다.
2. Beverage 라는 기본 클래스를 만들고, 이 클래스를 커피와 홍차를 의미하는 Coffee 클래스와 Tea 클래스로 파생시키면 되겠네요.
3. 이게 먼저네 저게 먼저네 혼란스러울 여지가 있기 때문에 기본 클래스(Beverage)에서 음료를 만드는 각 단계를 PrepareRecipe 라는
   메소드에 묶어서 비가상 함수로 상속
하면 좋을 것 같습니다.
4. 음 그런데 문제는 음료를 만드는 두번째, 네번째 단계가 커피하고 홍차하고 좀 다르네요.
    다형성을 이용하면 어떻게 해결할 수 있을 것도 같습니다. 기본 클래스에서는 각각의 단계(틀!)만 제공하고 파생 클래스에서 각 단계의
   구현을 맡는
식으로 구현하면 될 것 같은데요.

지금까지의 내용을 C++ 로 구현해보겠습니다.
(이 글에서 사용되는 코드는 모두 의사 코드 입니다.)
#include < iostream >

using std::cout;
using std::cin;
using std::endl;

class Beverage
{
public:
	// 템플릿(틀) 메소드.
	// 알고리즘의 각 단계가 파생 클래스에서 수정되는 것을 방지하기 위해 비가상으로 선언되었다.
	void PrepareRecipe()
	{
		BoilWater();
		Brew();
		PourInCup();
		if ( CustomerWantsCondiments() ) // 후킹!
		{
			AddCondiments();
		}
	}

private:
	// 파생 클래스에서는 모든 가상 메소드를 정의 해야만 한다.
	virtual void Brew() = 0;
	virtual void AddCondiments() = 0;

	// 후크.
	// 기본 클래스에 있는 템플릿 메소드의 알고리즘 단계에 파생 클래스가 여러가지 방식으로 개입할 수 있게 된다.
	virtual const bool CustomerWantsCondiments() { return true; } // 기본값: 첨가물 추가

	void BoilWater() { cout << _T("물 끓이는 중") << endl; }
	void PourInCup() { cout << _T("컵에 따르는 중") << endl; }
};

class Coffee: public Beverage
{
private:
	virtual void Brew() { BrewCoffeGrinds(); }
	virtual void AddCondiments() { AddSugarAndMilk(); }
	virtual const bool CustomerWantsCondiments()
	{
		cout << _T("설탕과 우유를 넣으시겠습니까? : 1. 네, 2. 아니오") << endl;
		
		int answer(0);
		cin >> answer;
		
		bool res(0);
		if (answer == 1) res = true;
		else if (answer == 2) res = false;

		return res;
	}

	void BrewCoffeGrinds() { cout << _T("필터를 통해서 커피를 우려내는 중") << endl; }
	void AddSugarAndMilk() { cout << _T("설탕과 우유를 추가하는 중") << endl; }
};

class Tea: public Beverage
{
private:
	virtual void Brew() { SteepTeaBag(); }
	virtual void AddCondiments() { AddLemon(); }

	void SteepTeaBag() { cout << _T("티백에서 차를 우려내는 중") << endl; }
	void AddLemon() { cout << _T("레몬을 추가하는 중") << endl; }
};
위의 소스코드 처럼 템플릿 메소드 패턴의 전형은 기본 클래스의 메소드에서 알고리즘의 골격을 정의하고, 알고리즘의 여러 단계 중 일부(음료 레서피의 두번째, 네번째)는 파생 클래스에서 구현하는 식으로 이루어집니다. 템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 하위 클래스에서 특정 단계를 재정의 할 수 있는 거죠.
뭔소리하는 지 알긴 알겠는데 그렇게 하면 대체 뭐가 좋아지는 거냐라고 하신 다면 위의 소스코드를 다시한번 보시기 바랍니다.
템플릿 메소드는 알고리즘을 재사용하고 인터페이스를 단일화 해야 하는 거의 모든 부분에서 사용될 수 있습니다.

템플릿 메소드 패턴과 밀접하게 관련되어 있는 비가상 인터페이스(Non Virtual Interface: NVI)란 것에 대해서도 한번 알아보겠습니다.
비가상 인터페이스는 private 가상 함수를 감싸는 publice 비가상 함수(비가상 인터페이스)를 만들어 인터페이스를 단일화하는 것을 의미하는데요, 템플릿 메소드 패턴으로 구현합니다.
위에 설명드린 템플릿 메소드 패턴이 내세우는 효용에 정확히 일치하는 개념이기에 뭐 더 이상 설명의 여지가 없네요.

NVI 에 대해서 조금 더 설명 드리자면, 위에 소스에 나와 있는 CustomerWantsCondiments() 라는 함수의 역할을 보시기 바랍니다. 보통 저런 역할을 하는 함수를 Hook 라고 부르는데, 주석에 써있는 대로 알고리즘의 각 단계에 여러가지 방식으로 개입할 수 있는 유연성을 가져다 줍니다.
class AAA
{
public:
	void NVI()
	{
		// Hook
		f();
		// Hook
	}

private:
	virtual void f() {}
};
위 처럼 전형적인 NVI 에서 Hook 가 할 수 있는 것은 여러가지입니다. 일단 첫번째로 아무것도 안 할 수도 있고, 뮤택스를 잠그거나, 로그 정보를 만든다거나, 클래스의 불변속성을 검증하는 등 좌우지간 f()의 실행 전/후에 해야하는 모든 일을 할 수 있습니다. 음료수 만들기에서는 첨가물을 넣기 전에 고객한테 넣을 지 안 넣을지를 물어보는 역할이네요.
NVI 단어 자체에 집중할 것이 아니라 템플릿 메소드 패턴의 개념과 상관지어 이해하시면 좋으실 듯 합니다.

여담인데요, 공부하다 보면 NVI, RTTI 처럼 개념보다는 말이 더 어려워 난감한 경우가 자주 있는데 그냥 대충 넘기시지 마시고 한번 정도는 단어의 의미를 파악해 기억해 보려고 해보세요. 초급서야 상관 없지만 중급서만 되도 기본적인 개념에 대한 세부사항은 언급을 하지 않는 책이 많거든요.
그런 것들을 죄다 언급하다보면 책만 엄청 두꺼워지고 책의 성격은 흐릿해지니 어쩔 수 없는 것이겠죠. 때문에 일부러 수고를 감수하면서까지 이름을 붙이고 하는 것이겠구요. 사실 이 부분에 있어서는 디자인 패턴도 마찬가지 입니다. '전역으로 접근을 할 수 있고 말야, 초기화 순서를 결정할 수도 있고, 또 객체가 유일한 객체임을 보장할 수 있도록 설계했어~' 라고 말하는 것 보다 그냥 간단하게 '싱글턴으로 구현했어~' 하고 말하는 게 무지 편하다는 거죠. 세부 구현을 전혀 몰라도 무슨 말을 하는 지 단박에 알아 들을 수 있거든요.
이러한 점 역시 디자인 패턴의 중요한 효용으로 볼 수 있습니다.

참고/인용: Head First Design Patterns, GoF 디자인 패턴! 이렇게 활용한다, Effective C++
크리에이티브 커먼즈 라이선스
Creative Commons License
top

Trackback Address :: http://www.i-stew.com/trackback/43 관련글 쓰기

Write a comment


1년 전 프로그래밍을 막 공부하기 시작할 때 부터 타입 변환에 대한 왠지 모를 거부감이 잔뜩 있었습니다.
설명할 수는 없지만 왠지 찝찝하고 불쾌한 기분까지 들었었죠. 그렇기 때문에 Effective C++ 에서 '캐스팅은 절약, 또 절약! 잊지 말자' 라는 항목을 보았을 때 진심으로 옳타쿠나 싶었습니다.

아마추어 영역에서는 특히 더 그런 것 같습니다. 대부분이 설계에 있어서 차선책이라는 의미보다는 단지 나쁜 습관일 뿐인 경우를 정말 많이 봤거든요. Symphony of Heroes 를 만들 때도 타입 변환에 대해 굉장히 관대한 친구가 한 명 있었는데, 각자의 코딩 스타일을 존중해 줬지만(존중할 수 밖에 없었지만이 더 정확한 표현) 이 부분에 있어서는 타협의 여지가 없었습니다.

어쨌거나 타입 변환은 제 개인적인 호불호를 떠나서 남용하게 되면 정말 안 좋습니다.
그럼 가볍게 일단 타입 변환 연산자들에는 어떤 것들이 있는지 부터 살펴 볼까요.

C 스타일의 타입 변환 연산자
(int)1000; // (변환 타입)표현식 방식
int(1000); // 변환 타입(표현식) 방식
단순, 무식, 과격. 이거저거 젤 것 없이 '그냥' 변환 합니다.

C++ 스타일의 타입 변환 연산자
int i = 1000;
static_cast< const int >(i);
암시적 타입 변환을 명시적으로 진행할 때 사용됩니다.
다운캐스팅(Downcasting 기본 클래스->파생 클래스 변환)에도 사용할 수 있습니다. 가장 흔하게 쓰이는 타입 변환입니다.
class Base
{
public:
	virtual ~Base() {}

	virtual void F1() {}
};

class Derived: public Base
{
public:
	virtual void F2() {}
};

Base* ptr(new Derived);

ptr->F1();
try
{
	dynamic_cast< Derived* >(ptr)->F2();
}
catch (std::bad_cast&)
{
	// 예외 처리
}
'안전한' 다운캐스팅(Safe Downcasting)을 할 때 사용합니다.
주어진 객체가 특정 타입의 상속 계통에 속한 타입인지 아닌지를 판단합니다.
주어진 객체가 합당한(안전한) 타입의 상속 계통에 속해 있지 않다면 0(NULL)을 반환하고 bad_cast 예외를 발생 시킵니다.
실행 비용이 높은 연산자 입니다.
const int CONST_INT = 1000;
const_cast< int >(CONST_INT);
상수성(const)이나 휘발성(volatile)을 없애는 용도로 사용 됩니다.
char* str = "C++";
reinterpret_cast< int >(str);
하부 수준(Low Level)의 타입 변환을 위해 만들어진 연산자로서, 적용 결과는 구현 환경마다 다릅니다. 즉, 이식성이 없습니다.

그럼 기본 타입간의 안전한(예측 가능한) 타입 변환에 대해서 잠깐 살펴 보겠습니다.

두 타입의 부호 비트가 같을 때,
1. 메모리 크기가 작은 정수형에서 메모리 크기가 같거나 더 큰 정수형으로의 변환.
- 값의 왜곡 없음. 예) short -> int
2. 메모리 크기가 작은 부동소수점형에서 메모리 크기가 같거나 더 큰 부동소수점형으로의 변환.
- 역시 값의 왜곡 없음. 예) float -> double
3. 메모리 크기가 작은 정수형에서 메모리 크기가 같거나 더 큰 부동소수점형으로의 변환.
- 값의 왜곡은 없으나 데이터의 구조가 달라짐. 미묘함. 예) int -> float
4. 메모리 크기가 작은 부동소수점형에서 메모리 크기가 같거나 더 큰 정수형으로의 변환.
- 소수부가 소실됨. 역시 데이터의 구조 달라짐. 미묘함. 예) float -> long

위의 네 가지를 제외하고는 모조리 잠재적인 문제점을 가지고 있습니다. 그냥 변환 불가라고 생각하셔도 될 듯 하네요.
이런 기본 중의 기본을 여기서 왜 이야기 하나 싶으신 분들이 있으실까 싶어서 말씀드리는데 저 것 안 지키는 인간 정말 많습니다.
심지어는 정수형/부동소수점형 승급 연산과 관련한 기본 타입의 암시적 타입 변환에 대해 이야기할 때 난 대체 니가 무슨 소리하는 지 모르겠다는 표정을 짓는 4년차 실무자도 봤습니다.

어쨌거나 기본 타입간의 변환은 그렇다치고, 사용자 타입(클래스)간의 타입 변환에서는 어떤 점을 주의해야 할까요.
일단 위에서 언급한 C 스타일 타입 변환의 두번째 방식을 다시 보시면 어디서 뭔가 많이 봤던 모양임을 느끼실 겁니다.
int 형 임시 객체를 생성할 때도 저 것과 똑같은 모양이죠. 다시말하면 객체의 명시적 타입 변환 모습이 '1개의 인자를 전달하는 생성자의 모습'과 똑같습니다.
즉, AAA a = 1000 이라는 표현식이 허용된다면 그 표현식 안에서 AAA a = AAA(1000) 이라는 암시적 타입 변환이 이루어졌다는 것을 의미하는 것이죠. 딱보면 한 눈에 잘 들어오지도 않고 더군다나 저런 식의 암시적 변환이 자주 이루어지는 곳은 함수에 인자를 전달할 때 입니다. 코드를 아주 꼼꼼하게 보는 사람이면 상관없겠지만 조금 느긋한 사람이라면 언제 어디서 암시적 변환이 이루어지는지 예측하기 힘들죠. explicit 키워드를 사용하면 이런 암시적 타입 변환을 막을 수 있습니다.
class AAA
{
	public:
		explicit AAA(const int i = 1000) : m_i(i) {}

	private:
		int m_i;
}
이렇게 하면 소스 코드 곳곳에서 int 형 정수가 AAA 타입으로 은밀하게 변환되는 것을 막을 수 있습니다.

다음으로 주의할 점은 특정 타입의 주소값(포인터)을 임의의 타입으로 변환하는 것 입니다.

단적으로 이야기해서 특정 주소값이 딱 몇(정수)이 될 것이다라고 단정 짓는 생각 자체가 C++ 에서는 위험한 생각입니다.
제가 이전에 썼던 싱글턴 패턴 글을 보시면 싱글턴 클래스가 다중상속 했을 때 객체의 머릿 포인터(최초 바이트의 주소값)를 찾기 위해 타입 변환을 하는 것을 보실 수가 있는데, 바로 이런 것을 말하는 겁니다. 일단 복잡하고 머리 아프고 혹시라도 살짝 실수라도 한다치면 어떤 식으로 문제가 발생할지 예상할 수도 없습니다. 게다가 객체의 메모리 배치구조는 구현 환경마다 다 다르다고 하니 이식성도 없습니다. C++ 타입 변환 연산자 중에는 reinterpret_cast 연산자가 이러한 역할을 합니다.
좀 다른 이야기지만 전 프로그래밍 시작하고 포인터 연산이나 함수 포인터를 사용하는 코드를 작성해 본 적이 한 번도 없습니다. 아마 C 에 대한 이해와 경험이 그리 깊지 않은 상태에서 C++로 이동했기 때문에 그렇지 않나 싶은데, 앞으로도 계속 그럴 것 같습니다. C++ 에서는 반복자(Iterator)와 함수 객체만으로도 충분한 것 같아요.

그 다음 주의할 점은 dynamic_cast 의 실행 비용입니다.

위에도 적었다시피 dynamic_cast 는 보통 실행 시점에 타입을 평가(RTTI 뒤에 설명)하기 위해 사용되는데 실행 비용이 좀 크다고 하네요. 이 것에 대해서는 제가 사용해 본적도 없고 테스트 해본 적도 없는데 Effective C++ 의 저자 말에 의하면 '신경 쓰일 정도'의 실행 비용이라고 하네요.
실제 공동작업을 할 때는 dynamic_cast 가 필요해지는 경우가 꽤 자주 있습니다.
다른 사람이 만든 기반 클래스의 인터페이스(public 접근 함수)가 부족해서 파생 클래스로 타입 변환(다운캐스팅)을 해야 될 성 싶을 때가 많이 있는데, 그 때 마다 전 아예 기반 클래스의 인터페이스를 늘려버리거나 static_cast 로 타입 변환을 해버렸습니다. 만약 지금 다시 그런 경우가 생긴다면 역시 기반 클래스의 인터페이스를 늘리는 쪽으로 하지 않을까 합니다. Effective C++ 에서는 파생 클래스의 포인터를 아예 다른 곳에 저장해두고 쓰는 것을 한 가지 방법으로 제시하고 있는데 그렇게 썩 가슴에 와닿지는 않네요.

다음 마지막 주의점으로 와서, 정말 별 수가 없다 싶어 타입 변환을 하게 되실 때는 기왕이면 C++ 스타일 타입 변환을 이용하시라는 겁니다.
일단 의도가 명확하게 보여서 좋고, C 스타일 타입 변환이 워낙에 단무지 스타일이라 C 스타일은 실수의 여지도 큽니다.

그러면 타입 변환에 대한 이야기는 대충 마무리하고 이번에는 RTTI(Runtime Type Identification 실행 시간 타입 식별)에 대해서 이야기 해보겠습니다.

RTTI 는 말 그대로 실행 시간에 타입 정보를 검증하는 표준을 제공하는 것이 목적입니다.
C++ 의 RTTI 요소에는 dynamic_cast 연산자, typeid 연산자, type_info 구조체 세가지가 있습니다.
dynamic_cast 는 앞서 설명 드렸고, typeid 는 객체의 타입을 식별하는 연산자인데 sizeof 연산자와 사용 방법이 같습니다.
class AAA {}
AAA a;

typeid(a); // 혹은
typeid(AAA);
typeid 연산자는 type_info(헤더 파일: type_info) 구조체를 반환하고 type_info 에는 타입 정보와 타입간의 비교 연산을 할 수 있는 연산자가 정의되어 있습니다.
RTTI 에 대한 개념은 객체 지향에 대한 책을 읽다 보면 시도 때도 없이 나오는데, 정작 쓰임새는 개인적으로 그리 많이 체감하지 못하고 있습니다. 실행 시간 다형성(Runtime Polymorphism)이란 것이 실행 시간에 타입 정보를 검증할 필요가 없기 때문에 유용한 것 아닌가 하는 생각이 들기도 하는데 사실 이 부분에 대해서는 아직 많이 공부해야 될 것 같네요.

참고/인용: C++ 기초 플러스 5판, Effective C++, More Effective C++
크리에이티브 커먼즈 라이선스
Creative Commons License
top

Trackback Address :: http://www.i-stew.com/trackback/42 관련글 쓰기

Write a comment


예전에 atof 함수가 기대와는 다르게 float 이 아닌 double 을 반환해서 이상하게 생각했던 적이 있었습니다.
알아보니 ato~ 로 시작하는 문자열->숫자 변환 함수들이 내부적으로 strto~ 로 시작하는 함수들로 구현되어 있더라구요.
double atof(const char* str)
{
   return strtod(str, (char**)NULL);
}
위와 같은 방식으로 단순이 감싸고 있는 함수인데, 무슨 일인지 atod(?) 같은 함수는 없더라구요.
strtof 가 없는 것도 아니고 strtof -> atof, strtod -> atod 이런 식으로 만들면 직관적이고 보기도 깔끔하고 좋을텐데 좀 난잡한 생각이 드네요.
sscanf 를 써도 되지만 deprecated 경고와 문자집합 변환 매크로까지 생각하면 _stscanf_s 같은 식으로 엄청 추해집니다.

좌우지간 이런 저런 미학(?)적인 이유 때문에 C 계열 표준 라이브러리는 잘 사용 안 하게 되는데, 인터넷을 뒤지다가 이런 코드를 발견했습니다.
#include <sstream>

template< typename To, typename From >
const To StringCast(const From& from)
{
	std::stringstream strStream;
	strStream << from;

	To result;
	strStream >> result;

	if ( strStream.fail() && !strStream.eof() ) return 0;

	return result;
}

// 사용법
int main()
{
	int i = StringCast< int >("100");
	float f = StringCast< float >("100.5");
	std::string str = StringCast< std::string >(110);

	return 0;
}
타입 변환을 std::stringstream 타입의 연산자에게 맡기고 있네요.
크리에이티브 커먼즈 라이선스
Creative Commons License
top

Trackback Address :: http://www.i-stew.com/trackback/37 관련글 쓰기

Write a comment



객체 지향의 한 분야에는 디자인 패턴이라는 것이 있습니다.
말 그대로 패턴화된 설계 방식들을 집대성하여 체계화 시킨 분야 인데, 선지자들이 시행착오 끝에 얻어낸 일종의 객체 지향 설계 방법론 같은 것이죠.

처음 디자인 패턴을 공부하게 되면 그 기발한 아이디어에 매료되어 개발 환경이나 요구 사항을 디자인 패턴에 무리하게 끼워 맞추는 주객전도의 상황이 발생하기도 하는데요, 분명 디자인 패턴에 공식처럼 여겨지는 구현 방법 같은 것은 있지만 이 공식이란 것이 상황별로 천차만별입니다. 때문에 디자인 패턴을 공부할 때 중요한 것은 어떻게 보다는 왜? 임을 잊지 않으셔야 합니다.

좌우지간 디자인 패턴이란 건 대충 그런 거고, 이 글에서는 디자인 패턴의 챕터1에 해당하는 싱글턴 패턴을 알아 보겠습니다.

전역 객체가 나쁘다 어쩌다 하는 말은 한번씩 들어보셨을 겁니다. 사실 왜 나쁜지는 저도 안지 얼마 안됐는데요, 컴파일러 폴더를 뒤져 보시면 obj 라는 확장자를 지닌 파일들이 보이실 겁니다. 사람이 이해할 수 있는 C 나 C++ 같은 프로그래밍 언어에서 기계가 이해할 수 있는 기계어로 번역된 목적 파일들인데요. 이 파일들 하나 하나가 각각의 개별 번역 단위에 해당합니다. cpp 파일에 이런 저런 헤더 파일 붙이고 컴파일 딱 누르면 obj 파일이 만들어 지는 것이죠.

근데 문제는 두 개의 전역 객체가 각기 다른 번역 단위에 흩어져 있는 경우 이 전역 객체들의 초기화 순서를 알 수가 없다는 겁니다. 초기화 되지도 않은 객체를 참조할 위험성이 생기는 것이죠.
초기화의 중요성에 대한 이야기는 다음으로 미루고요, 여하튼 객체는 사용하기 전에 무조건 초기화해야 하는데 초기화가 됐는지 안 됐는지 알 길이 없고... 뭔가 전역 객체의 초기화 순서를 마음대로 할 수 있는 방법 같은 것이 없을까~ 할 때, 이 때 쓸 수 있는 것이 바로 싱글턴 패턴입니다.
#include <iostream>

template<typename T>
class Singleton
{
public:
	static T& Get() { static T inst; return inst; }

private:
	Singleton() {}
	~Singleton() {}
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
};

// 싱글턴 클래스
class Unique
{
	friend Singleton<Unique>;

public:
	void Hello() { std::cout << "Hello" << std::endl; }

private:
	Unique() {} // 허용되지 않는 생성을 방지
	~Unique() {}
	Unique(const Unique&); // 허용되지 않는 복사를 방지
	Unique& operator=(const Unique&); // 허용되지 않는 대입을 방지(자기대입)
};

// 사용법
int main()
{
	Singleton<Unique>::Get().Hello();

	return 0;
}
실제 Symphony of Heroes 에서 사용했던 싱글턴 패턴을 약간 수정한 것인데요, 싱글턴 패턴을 구현하는 하는 방법은 정말 여러가지가 있습니다. 그냥 다른 것 신경 쓰지 마시고 객체를 생성하는 방법과 생성자의 접근 권한만 보세요. 프로그램의 실행 흐름에 따라서 초기화 순서가 결정되는 건데요, 저런 식으로 하면 실행 흐름이 변수 선언 코드 부분에 다다르게 되면 그 때 초기화가 됩니다. 달리 말하면 싱글턴 패턴을 사용한다고 해도 실행 흐름을 직접 결정하는 작성자 스스로가 싱글턴 객체를 잘못 참조한다면 싱글턴 패턴도 소용 없다는 이야기죠.

싱글턴 패턴의 중요한 두번째 특징 중 하나는 바로 싱글턴 객체가 단 하나의 유일한 생성 객체(인스턴스)임을 보장한다는 것 입니다.
두번째로 꼽긴했지만 사실 싱글턴의 가장 핵심적인 특징이죠. 인스턴스가 하나 이상 생성되면 안되거나 하나 이상 생성할 필요가 없는 경우에 쓰입니다. 객체를 정적으로 선언하지 않고 동적할당을 하는 방법도 있는데, 정적으로 선언하는 것 보다는 사실 이 방법이 좀 더 일반적입니다. 이렇게 할 경우 객체의 생성주기를 보다 쉽게 제어할 수 있고 인스턴스의 제한 갯수도 하나던 둘이던 마음대로 조절할 수 있습니다.

정리를 해보자면 디자인 패턴에는 싱글턴 패턴이라는 것이 있는데 이 것을 이용하면 객체에 전역 접근이 제공되고 인스턴스의 갯수도 마음대로 제한할 수 있어서 유용하더라 되겠습니다.

마지막으로 한 가지 더 말씀드리면 어찌됐던 일단 싱글턴 객체는 전역 객체고, 전역 객체라는 건 항상 좋은 것은 아닙니다. 특히나 멀티스레드 프로그래밍 환경에서는 더더욱 그렇죠. 싱글턴을 사용하기에 앞서 멀티스레드에 대한 안정장치들이 완벽하게 마련되어 있는 지 꼭 고민해 보시기 바랍니다.

참고/인용: Effective C++, Head First Design Patterns, GoF 디자인 패턴! 이렇게 활용한다
---------------------------------------------------------------------------------------------------------------------------------
추가적으로 GPG 1권에 나와 있는 싱글턴 클래스 템플릿을 소개해 드리겠습니다.
#include <cassert>

template <typename T> class Singleton
{
    static T* ms_Singleton;

public:
    Singleton( void )
    {
        assert( !ms_Singleton );
        int offset = (int)(T*)1 - (int)(Singleton <T>*)(T*)1;
        ms_Singleton = (T*)((int)this + offset);
    }
   ~Singleton( void )
        {  assert( ms_Singleton );  ms_Singleton = 0;  }
    static T& GetSingleton( void )
        {  assert( ms_Singleton );  return ( *ms_Singleton );  }
    static T* GetSingletonPtr( void )
        {  return ( ms_Singleton );  }
};

template <typename T> T* Singleton <T>::ms_Singleton = 0;

// 사용법
class TextureMgr : public Singleton <TextureMgr>
{
public:
    Texture* GetTexture( const char* name );
};

#define g_TextureMgr TextureMgr::GetSingleton()

void SomeFunction( void )
{
    Texture* stone1 = TextureMgr::GetSingleton().GetTexture( "stone1" );
    Texture* wood6 = g_TextureMgr.GetTexture( "wood6" );
}
상속을 사용해서 간편하게 하위 클래스를 싱글턴으로 만들 수 있는 템플릿인데요, 다중 상속의 문제 때문에 생성자에 복잡한 포인터 연산이 들어가 있습니다. 전 이 방식은 복잡해서 friend 같은 것이나 외부 함수를 이용하는 것을 더 선호 합니다.

라이브러리화 하지 않고 클래스마다 구현하는 것도 나쁘지 않은 방법입니다. 경우에 따라 구현 방법이 달라져야 일이 워낙 많다보니 해당 클래스에 직접적으로 의도를 아주 정확하게 심는 거죠. Head First Design Patterns 에서도 디자인 패턴을 무리하게 라이브러리화 하는 것을 경계하라고 나와 있습니다. 과연 이 것이 막강한 일반화 프로그래밍을 지원하는 C++ 에도 해당되는 문제인지는 생각해봐야 겠네요.

참고/인용: Game Programming Gems
크리에이티브 커먼즈 라이선스
Creative Commons License
top

Trackback Address :: http://www.i-stew.com/trackback/24 관련글 쓰기

  1. Favicon of http://www.logodesignconsultant.com/logo-designer.html BlogIcon Logo designers 2012/04/18 20:07 댓글주소 | 수정/삭제 | 댓글

    It's my fortune to read your article. It is very informative and useful for all of us .Keep on posting such articles. Thanks.

Write a comment