C++ 디자인 패턴

2025. 1. 6. 17:29·C++

수업시간에 배운 디자인 패턴에 대해 공부하고, 추가적인 내용도 정리해 보았다.

참고한 자료는 아래와 같다.

https://refactoring.guru/design-patterns/cpp

 

Design Patterns in C++

Turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as a method arguments, delay or queue a request's execution, and support undoable operations.

refactoring.guru

https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823

 

[Design pattern] 많이 쓰는 14가지 핵심 GoF 디자인 패턴의 종류

디자인 패턴을 활용하면 단지 코드만 ‘재사용’하는 것이 아니라, 더 큰 그림을 그리기 위한 디자인도 재사용할 수 있습니다. 우리가 일상적으로 접하는 문제 중 상당수는 다른 많은 이들이 접

www.hanbit.co.kr

1. 디자인 패턴이란

디자인 패턴은 "개발 시 반복적으로 등장하는 문제를 해결하기 위한 일반화 된 솔루션" 이라고 한다.

디자인 패턴은 크게 생성 패턴, 구조 패턴, 행동 패턴으로 분류할 수 있다.

  • 생성 패턴(Creational Pattern)
    • 객체 인스턴스를 생성하는 패턴
    • 클라이언트와 그 클라이언트가 생성해야 하는 객체 인스턴스 사이의 연결을 끊어주는 패턴
    • 종류
      • 추상 팩토리
        • 구체적인 클래스를 지정하지 않고도, 관련 오브젝트 패밀리 생성 가능
      • 빌더 (Builder)
        • 복잡한 객체를 단계별로 구성
        • 동일한 construction 코드를 사용해서 객체의 다양한 유형과 표현 생성 가능
      • 팩토리 메서드
        • 객체 생성 코드를 캡슐화해서 객체 생성 방식을 동적으로 결정 
        • 객체를 생성할 때 필요한 인터페이스 만들기
        • 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정
      • 프로토타입
        • 클래스에 종속된 코드를 만들지 않고, 기존 객체를 복사할 수 있음
      • 싱글턴
        • 클래스의 인스턴스가 오직 하나만 생성되도록 보장
  • 구조 패턴(Structural Pattern)
    • 클래스와 객체를 더 큰 구조로 만들 수 있게 구성을 사용하는 패턴
    • 종류
      • 어댑터 (Adapter)
        • 기존 인터페이스를 변경하여 호환성을 제공
      • 브리지
        • 대규모 클래스 또는 밀접하게 관련된 클래스집합을 추상화와 구현이라는 두개의 개별 계층으로 분할하여 서로 독립적으로 개발
      • 컴포지트 (Composite)
        • 개체를 트리 구조로 구성한 다음 개별 객체와 그룹 객체를 동일하게 처리
      • 데코레이터
        • 객체에 추가 요소를 동적으로 더할 수 있으며, 서브클래스를 만들 때 보다 더욱 유연하게 동작
      • 파사드
        • 라이브러리, 프레임워크 등 복잡한 클래스 집합을 숨기고, 단순화된 인터페이스를 제공
      • 플라이 웨이트 (Flyweight)
        • 각 객체에 모든 데이터를 보관하는 대신 여러 객체 간에 상태의 공통 부분을 공유하여 사용 가능한 RAM 용량에 더 많은 객체를 넣을 수 있다.
      • 프록시
        • 다른 객체에 대한 대체 또는 placeholder를 제공할 수 있다.
        • 프록시는 원본 객체에 대한 엑세스를 제어하여 요청이 원본 객체에 전달되기 전이나 후에 어떤 작업을 수행할 수 있도록 한다.
  • 행동 패턴(Behavioral Pattern)
    • 클래스와 객체들이 상호작용하는 방법과 역할을 분담하는 방법을 다루는 패턴
    • 종류
      • Chain of Responsibility 
        • 핸들러 체인을 따라 요청을 전달할 수 있다.
        • 요청을 받으면 각 핸들러는 요청을 처리할지 다음 핸들러로 요청을 전달할지 결정한다.
      • 커맨드
        • 요청을 객체의 형태로 캡슐화하고, 사용자가 보낸 요청을 나중에 이용할 있도록 저장하는 방식
        • 요청을 매서드 인수로 전달하거 요청 실행을 지연, 큐에 대기,  실행 취소 등의 작업이 가능하다.
      • 반복자 (Iterator)
        • 컬렉션의 구현 방법(list, stack, tree)을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공하는 방식
      • 중재자 (Mediator)
        • 객체간의 직접적인 통신을 제한하고, 중재자를 통해서만 공동 작업을 하는 방식으로, 객체 간의 혼란스러운 종속성을 줄일 수 있다.
      • 메멘토 (Memento)
        • 구현의 세부 사항을 공개하지 않고, 객체의 이전 상태를 저장하고 복원할 수 있다.
      • 옵저버
        • 관찰 중인 객체에서 이벤트가 발생하면, 그 객체에 의존하는 다른 여러 객체에 알림이 가고 자동으로 내용이 갱신되는 방식
      • 상태 (State)
        • 객체 내부의 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있는 방식
        • 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.
      • 전략 (Strategy) 
        • 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.
        • 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.
      • 템플릿 메소드
        • 알고리즘의 골격을 정의하는 방식(super class에 정의)
        • 서브클래스가 구조를 변경하지 않고, 알고리즘의 특정 단계를 재정의할 수 있다.
      • 비지터 (Visitor)
        • 알고리즘을 알고리즘이 작동하는 객체와 분리하는 방식
      • 인터프리터 (Interpreter)
        • 자주 사용되는 특정한 문법적 규칙이 존재한다면, 이를 규칙에 따라 규격화하고 해석하는 방식

2. 디자인 패턴 코드

어떤 방식인지 말로만 이해하려면, 잘 이해가 되지 않기 때문에 각 패턴별로 코드를 통해 이해를 해보자

  • 생성패턴 - 싱글턴
    •  이 패턴의 특징은 클래스 객체를 단 한 번만 만들도록 하는 방식이다.
      • 복사 생성자와 대입 연산다를 삭제해서 복사를 방지하며,
      • 생성자를 private 멤버함수로 만들면 된다.
      • GetInstance 함수를 통해 유일한 인스턴스 반환을 수행하면 된다.
      • 아래 코드의 실행 결과는 0 100 200 이 출력되게 된다.
    • 추가 설명
      • 싱글턴 패턴은 C++에서 사용이 줄고 있다. (antipattern)
      • 추가적으로 멀티스레드를 고려해서 코드를 짤 때 주의가 필요하다.
#include <iostream>

class Singleton
{
public:
	Singleton(const Singleton& other) = delete;
	Singleton& operator=(const Singleton&) = delete;

	static Singleton* GetInstance();

	void ValueChange(int num)
	{
		mValue += num;
	}

	int GetValue()
	{
		return mValue;
	}
private:
	static Singleton* instance;
	int mValue;

	Singleton()
		:mValue(0)
	{}
};

Singleton* Singleton::instance = nullptr; //정적 멤버 초기화

//static 메소드는 클래스 밖에서 정의
Singleton* Singleton::GetInstance()
{
	if (instance == nullptr)
	{
		instance = new Singleton();
	}
	return instance;
}

int main()
{
	Singleton* s = Singleton::GetInstance();
	std::cout << "before: " << s->GetValue() << " ";
	s->ValueChange(100);
	std::cout << "after: " << s->GetValue() << " ";

	Singleton* newS = Singleton::GetInstance();
	newS->ValueChange(100);
	std::cout << "new Value: " << s->GetValue() << "\n";
}
  • 구조 패턴 - 데코레이터 패턴
    • 이 패턴의 특징은 객체의 상태를 동적으로 업데이트 할 수 있다는 것이다.
      • Component라는 추상 클래스와 그 추상 클래스를 구체화한(상속받은) ConcreteComponent가 존재한다.
      • Decorator라는 추상클래스를 통해 ConcreteComponent의 기능을 확장시킨다.
      • 이후 각각의 ConcreteDecorator는 Decorator를 상속받아서 기능을 확장한다.
    • 추가 설명
      • C++에서 매우 표준적인 요소이다.
      • 상속을 사용하지 않고 객체의 행동을 확장할 수 있어서 OCP(Open-Closed Principle)를 준수한다.
        • 예를 들어 우리가 아래 코드의 내용을 데코레이터 패턴으로 구현하지 않았을 때를 생각해보자
          • simple이라는 컴포넌트에 DecoratorA와 DecoratorB의 특성을 모두 적용시키고 싶다면,
          • 새로운 클래스 DecoratorAB라는 클래스를 만들어서 적용해야 할 것이다.
            • 상속은 정적인 방법으로만 기능을 확장하기 때문에
            • Component* simple = new DecoratorAB; 이런 식으로 선언할 수 밖에 없다.
          • 그러나 데코레이터 패턴은 동적으로 객체의 행동 확장이 가능하기에 아래의 코드처럼 사용이 가능하다.
            • Compoent* simple = new Component;
            • Component* decorator1 = new ConcreteDecoratorA(simple);
            • Component* decorator2 = new ConcreteDecoratorB(decorator1);
            • 위의 코드의 결과 decorator2는 Component, DecoratorA, DecoratorB의 특성을 모두 가진다.
#include <iostream>
#include <string>

class Component
{
public:
	virtual ~Component() {};
	virtual std::string Operation() const = 0;
};

class ConcreteComponent : public Component
{
public:
	std::string Operation() const override
	{
		return "ConcreteComponent";
	}
};

class Decorator : public Component
{
public:
	Decorator(Component* component)
		:mComponent(component)
	{}

	std::string Operation() const override
	{
		return this->mComponent->Operation();
	}
	
protected:
	Component* mComponent;
};

class ConcreteDecoratorA : public Decorator
{
public:
	ConcreteDecoratorA(Component* component)
		:Decorator(component)
	{}
	std::string Operation() const override
	{
		return "ConcreteDecoratorA(" + Decorator::Operation() + ")";
	}
};

class ConcreteDecoratorB : public Decorator
{
public:
	ConcreteDecoratorB(Component* component)
		:Decorator(component)
	{}
	std::string Operation() const override
	{
		return "ConcreteDecoratorB(" + Decorator::Operation() + ")";
	}
};

void ClientCode(Component* component)
{
	std::cout << "RESULT: " << component->Operation();
}

int main()
{
	Component* simple = new ConcreteComponent;
	ClientCode(simple);
	std::cout << "\n\n";

	Component* decorator1 = new ConcreteDecoratorA(simple);
	Component* decorator2 = new ConcreteDecoratorB(decorator1);
	ClientCode(decorator2);
	std::cout << "\n";

	delete simple;
	delete decorator1;
	delete decorator2;
}

 

실행 결과

  • 행동 패턴 - 옵저버 패턴
    • 이 패턴의 가장 큰 장점은 한 객체가 상태가 변경되면, 그 객체에 의존되는 다른 객체에 자동으로 알림이 가게되고 상태가 갱신된다는 점이다.
      • Observer 클래스
        • 인터페이스 클래스며, 각각의 객체들은 이 클래스를 상속받아 구현한다.
      • Subject 클래스
        • 상태를 관리하며, 상태가 변하면 Observer에게 알려주는 역할을 한다.
        • 아래 예시 코드에서는 Observer* 타입의 벡터를 멤버 변수로 가지고 있다.
        • 그래서 처음에 Observer의 자식 객체를 생성하면
          • Attach를 통해 Subject 클래스에 넣어주고,
          • 이후 값을 바꾸는 경우가 있다면(SetData), 자동으로 Notify 함수가 call되고
          • 모든 Observer 객체들을 순환하면서 Observer 클래스의 Update 함수가 call되므로 모든 객체들의 값이 업데이트 된다.
    • 추가 설명
      • C++코드에서 자주 사용하고 특히 GUI 컴포넌트에서 매우 흔하게 볼 수 있고
      • 클래스에 커플링하지 않고도 다른 객체에서 발생하는 이벤트에 반응을 할 수 있다는 장점이 있다.
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Observer 
{
public:
    virtual ~Observer() = default;     
    virtual void Update(int data) = 0;
};

//Subject 클래스
class ExcelSheet 
{
private:
    vector<Observer*> observers;               
    int data;                                  

public:
    ExcelSheet() : data(0) {}                    


    void Attach(Observer* observer) 
    {
        observers.push_back(observer);
    }
    
    void Notify() 
    {
        for (Observer* observer : observers) 
        {
            observer->Update(data);
        }
    }

    void SetData(int newData) 
    {
        data = newData;                       
        cout << "ExcelSheet: Data updated to " << data << endl;
        Notify();
        cout << "\n";
    }
};

class BarChart : public Observer 
{
public:
    void Update(int data) 
    {                     
        cout << "BarChart: Displaying data as vertical bars: ";
        for (int i = 0; i < data; ++i) 
        {
            cout << "|";                        
        }
        cout << " (" << data << ")" << endl;
    }
};


class LineChart : public Observer 
{
public:
    void Update(int data) 
    {
        cout << "LineChart: Plotting data as a line: ";
        for (int i = 0; i < data; ++i) 
        {
            cout << "-";                        
        }
        cout << " (" << data << ")" << endl;
    }
};

class PieChart : public Observer 
{
public:
    void Update(int data) 
    {       
        cout << "PieChart: Displaying data as a pie chart slice: ";
        cout << "Pie [" << data << "%]" << endl;
    }
};

int main() 
{
    ExcelSheet excelSheet;                      

    BarChart* barChart = new BarChart();       
    LineChart* lineChart = new LineChart();    
    PieChart* pieChart = new PieChart();       

    excelSheet.Attach(barChart);
    excelSheet.Attach(lineChart);
    excelSheet.Attach(pieChart);

    excelSheet.SetData(5);                       
    excelSheet.SetData(10);                     

    delete barChart;
    delete lineChart;
    delete pieChart;

    return 0;
}

data를 수정하면, 세가지 차트의 값이 모두 수정된다.

3. 숙제

  • OCP 원칙을 적용된 Animal 클래스에 기능 추가하기
    • 아래 UML을 참고하여, 기존 코드는 수정하지 않고,
    • 새로운 동물을 추가했을 때 원하는 방식대로 동작하도록 구현해야 한다.

  • 정답 코드
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Animal
{
public:
	virtual ~Animal() = default;
	virtual void Speak() = 0;
};

class Dog : public Animal
{
public:
	void Speak() override
	{
		cout << "dog speak\n";
	}
};

class Cat : public Animal
{
public:
	void Speak() override
	{
		cout << "cat speak\n";
	}
};

//new class
class Cow : public Animal
{
public:
	void Speak() override
	{
		cout << "cow speak\n";
	}
};

//not fixed
void SpeakAllAnimals(vector<Animal*> animals)
{
	for (auto animal : animals)
	{
		animal->Speak();
	}
}
int main()
{
	vector<Animal*> animals;
	animals.push_back(new Dog());
	animals.push_back(new Cat());

	//new class
	animals.push_back(new Cow());

	SpeakAllAnimals(animals);

	for (auto animal : animals)
	{
		delete animal;
	}
}

 

'C++' 카테고리의 다른 글

텍스트 RPG 게임 만들기 (C++)  (1) 2025.01.16
C++ 면접 대비 정리  (0) 2025.01.09
C++ TIL day 13  (1) 2025.01.03
Lambda Expressions  (0) 2025.01.03
C++ TIL day 12  (1) 2025.01.02
'C++' 카테고리의 다른 글
  • 텍스트 RPG 게임 만들기 (C++)
  • C++ 면접 대비 정리
  • C++ TIL day 13
  • Lambda Expressions
gbleem
gbleem
gbleem 님의 블로그 입니다.
  • gbleem
    gbleem 님의 블로그
    gbleem
  • 전체
    오늘
    어제
    • 분류 전체보기 (184)
      • Unreal Engine (73)
      • C++ (19)
      • 알고리즘(코딩테스트) (27)
      • TIL (60)
      • CS (4)
      • 툴 (1)
  • 블로그 메뉴

    • 홈
    • 카테고리
  • 링크

    • 과제용 깃허브
    • 깃허브
    • velog
  • 공지사항

  • 인기 글

  • 태그

    blend pose
    character animation
    매크로 지정자
    BFS
    actor 클래스
    map을 vector로 복사
    const
    DP
    C++
    Vector
    cin함수
    상속
    applydamage
    additive animation
    gamestate
    enhanced input system
    템플릿
    싱글턴
    addonscreendebugmessage
    motion matching
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
gbleem
C++ 디자인 패턴
상단으로

티스토리툴바