수업시간에 배운 디자인 패턴에 대해 공부하고, 추가적인 내용도 정리해 보았다.
참고한 자료는 아래와 같다.
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를 제공할 수 있다.
- 프록시는 원본 객체에 대한 엑세스를 제어하여 요청이 원본 객체에 전달되기 전이나 후에 어떤 작업을 수행할 수 있도록 한다.
- 어댑터 (Adapter)
- 행동 패턴(Behavioral Pattern)
- 클래스와 객체들이 상호작용하는 방법과 역할을 분담하는 방법을 다루는 패턴
- 종류
- Chain of Responsibility
- 핸들러 체인을 따라 요청을 전달할 수 있다.
- 요청을 받으면 각 핸들러는 요청을 처리할지 다음 핸들러로 요청을 전달할지 결정한다.
- 커맨드
- 요청을 객체의 형태로 캡슐화하고, 사용자가 보낸 요청을 나중에 이용할 있도록 저장하는 방식
- 요청을 매서드 인수로 전달하거 요청 실행을 지연, 큐에 대기, 실행 취소 등의 작업이 가능하다.
- 반복자 (Iterator)
- 컬렉션의 구현 방법(list, stack, tree)을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공하는 방식
- 중재자 (Mediator)
- 객체간의 직접적인 통신을 제한하고, 중재자를 통해서만 공동 작업을 하는 방식으로, 객체 간의 혼란스러운 종속성을 줄일 수 있다.
- 메멘토 (Memento)
- 구현의 세부 사항을 공개하지 않고, 객체의 이전 상태를 저장하고 복원할 수 있다.
- 옵저버
- 관찰 중인 객체에서 이벤트가 발생하면, 그 객체에 의존하는 다른 여러 객체에 알림이 가고 자동으로 내용이 갱신되는 방식
- 상태 (State)
- 객체 내부의 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있는 방식
- 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.
- 전략 (Strategy)
- 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.
- 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.
- 템플릿 메소드
- 알고리즘의 골격을 정의하는 방식(super class에 정의)
- 서브클래스가 구조를 변경하지 않고, 알고리즘의 특정 단계를 재정의할 수 있다.
- 비지터 (Visitor)
- 알고리즘을 알고리즘이 작동하는 객체와 분리하는 방식
- 인터프리터 (Interpreter)
- 자주 사용되는 특정한 문법적 규칙이 존재한다면, 이를 규칙에 따라 규격화하고 해석하는 방식
- Chain of Responsibility
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되므로 모든 객체들의 값이 업데이트 된다.
- Observer 클래스
- 추가 설명
- 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;
}
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 |