[전문가를 위한 C++] 디자인 패턴
‘전문가를 위한 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