3 minute read

‘전문가를 위한 C++ - Marc Gregoire 지음, 남기혁 옮김’ 책을 참고하여 작성한 포스트입니다.


디자인 패턴(Design Pattern)

  • 다양한 문제를 해결하는 프로그램을 구성하는 표준화된 접근 방법론이다.

의존성 주입(dependency injection)

  • 의존성 역전 원칙(Dependency inversion Principle, DIP)을 지원하는 방법 중 하나다.
  • 의존성 관계를 역전시키는 데 인터페이스를 활용한다.
  • 모든 서비스에 대해 인터페이스를 만들고, 서비스가 필요한 컴포넌트에 그 서비스에 해당하는 인터페이스를 주입한다.
  • 그러면 서비스에 대한 목업을 쉽게 만들 수 있어 단위 테스트를 쉽게 할 수 있게 된다.

    구현 방법

  • 먼저 원하는 서비스 기능을 가진 인터페이스를 정의한다.
  • 이 인터페이스에 따라 클래스를 구현한다.
  • 이제 이 서비스를 원하는 클래스에 인스턴스를 주입한다.

예시

class Foo
{
public:
	Foo(ILogger& logger) : m_logger {logger} {}
	...
private:
	ILogger& m_logger;
};

Logger ConcreteLogger { "Log.out" };
Foo f { ConcreteLogger };

추상 팩토리 패턴

  • factory pattern 은 객체 생성 시 해당 객체의 생성자를 호출하는 것이 아니라 객체 생성을 담당하는 팩토리에 요청한다.
  • 예를 들어 해당 객체의 메서드를 호출해 객체 생성을 할 수 있다.
  • 이 경우 클래스 타입을 정확히 몰라도 클래스 계층에 맞게 객체를 생성할 수 있는 장점이 있다.
  • 그리고 객체 생성 과정을 추상화해서, 의존성 주입 패턴을 적용해 얼마든지 다른 팩토리로 교체 가능한 장점이 있다.

구현 예시

  • 자동차 공장 을 예로 보자
class ICar { ... };
class Ford : public ICar { };
class FordSedan : public Ford { ... };
class FordSuv : public Ford { ... };

class Toyota : public ICar {};
class ToyotaSedan : public Toyota { ... };
class ToyotaSuv : public Toyota { ... };

class IAbstractCarFactory { ... };
class FordFactory : public IAbstractCarFactory { ... };
class ToyotaFactory : public IAbstractCarFactory { ... };

void createSomeCars(IAbstractCarFactory& CarFactory)
{
	auto Sedan = CarFactory.MaekSedan();
	auto Suv = CarFactory.MakeSuv();
}

int main()
{
	FordFactory fordFactory;
	ToyotaFactory toyotaFactory;
	createSomeCars(fordFactory);
	createSomeCars(toyotaFactory);
}

팩토리 메서드 패턴

  • 생성할 객체 종류를 전적으로 구체적인 팩토리가 결정한다.
  • 단일 클래스로 구현해도 된다.

구현 예시

  • 위의 코드를 이어서 본다.
  • Ford사의 아무 차나 요청한다고 가정한다.
int main()
{
	FordFactory myFactory;
	auto myCar = myFactory.requestCar();
	cout << myCar->info() << endl; // Ford의 내용 출력
}

어댑터 패턴(adaper pattern)

  • 추상화가 현재 설계화가 맞지 않은 상황에서 변경이 어려운 경우가 있다.
  • adapter 혹은 wrapper 클래스를 활용하면 해결 가능하다.
  • 어떤 기능의 구현 코드에 대한 바람직한 추상화를 제공하면서 사용자와 구현 코드를 연결해주는 역할을 한다.
  • 예를 들어 표준 라이브러리의 stack, queue 는 deque이나 list와 같은 다른 컨테이너로 구현한다.
  • 다음과 같이 활용 가능하다.
    • 기존 구현을 재활용하는 방식으로 특정 인터페이스를 구현하는 경우. 이때 주로 내부에서 구현의 인스턴스를 생성함
    • 기존 기능을 새로운 인터페이스를 통해 사용할 수 있게 만드는 경우. 어댑터의 생성자는 일반적으로 내부 객체의 인스턴스를 인수로 받음

구현 예시

class Logger
{
public:
	enum class LogLevel {
		Error,
		Info,
		Debug
	};
	Logger()
	{
		cout << "Logger Constructor" << endl;
	}
	void log(LogLevel level, const std::string& message);
public:
	std::string_view getLogLevelString(Loglevel level) const
	{
		// return name of level
	}
};

Logger::log(LogLevel level, const std::string& message)
{
	cout << getLogLevelString(level) << " : " << message << endl;
}

// 내부 기능에 대한 인터페이스를 새로 정의하는 방법
class INewLoggerInterface
{
public:
	virtual void log(std::string_view, message) = 0;
};

class NewLoggerAdapter : public INewLoggerInterface
{
public:
	NewLoggerAdapter()
	{
		cout << "NewLoggerAdapter" << endl;
	}
	void log(std::string_view message) override;
private:
	Logger m_logger;
};

void NewLoggerAdapter::log(string_view message)
{
	m_logger.log(Logger::LogLevel::Info, message.data());
}

int main()
{
	NewLoggerAdapter logger;
	logger.log("Testing the logger.");
}

/* 출력 내용
Logger constructor
NewLoggerAdapter constructor
INFO : Testing the logger.
*/

프록시 패턴(proxy pattern)

  • 클래스 추상화를 내부 표현과 분리하는 패턴
  • 단어 proxy의 의미처럼 실제 객체에 대한 대리인 역할을 함
  • 객체를 직접 다룰 수 없거나 그렇게 하기엔 시간이 너무 오래 걸릴 때 활용하면 좋음
  • 특정한 기능을 클라이언트로부터 보호하면서 이를 뚫을 수 없도록 보장하는 용도로 사용됨

활용 예

  • 네트워크 기반 게임에서 인터넷에 연결된 게임 사용자를 Player 클래스로 표현한다 해본다.
  • 네트워크 문제를 사용자에게 곧바로 드러내지 않게 하기 위해 Player에서 네트워크 연결을 다루는 부분을 다른 클래스로 분리해 주는데,
  • 이 분리한 부분을 PlayerProxy 객체로 만들어 실제 Player 객체의 역할을 대신하게 만든다.
  • 이 PlayerProxy만 사용하거나, Player를 사용할 수 없게 된 경우 PlayerProxy 대체하는 방법이 있다.
  • 그러면 네트워크 문제로 Player를 사용할 수 없게 되어도 PlayerProxy를 통해 하던 일을 계속 할 수 있게 된다.

구현 방법

Leave a comment