25.06.13 (2) - 디자인 패턴

김영하·2025년 6월 13일

C++

목록 보기
27/32

C++ 문법 3주차


디자인 패턴

"개발 시 반복적으로 등장하는 문제를 해결하기 위한
일반화된 솔루션 "

객체지향 프로그래밍을 하다 보면
당연히 문제가 발생을 할 텐데
다들 비슷비슷한 문제가 생길 확률도 높다
=>
그래서 이럴 때 어떻게 해결해야 하는지
그 솔루션들을 패턴으로 정리해 놓고
쉽게 사용할 수 있도록 만든 게 = 디자인 패턴


'생성 패턴' '구조 패턴' '행동 패턴'


디자인 패턴의 구분

  1. 생성 패턴 (Creational Patterns)
    : 객체를 어떤 방식으로 생성할지
    객체 생성에 제한을 두거나, 특정 방법으로 객체를 생산하고 싶거나

'새로운 것을 만들어내는 방법' 과 관련된 패턴으로,
예를들어 공장에서 물건을 찍어내는 느낌인데
이 '물건' 이 프로그래밍의 "객체" 가 되는 것

  1. 구조 패턴 (Structual Patterns)
    객체가 여러 가지 있을 떄
    이 객체들끼리 어떻게 연동시킬지
    이 객체들의 구조를 어떻게 잡아갈지.

아까의 공장에서 여러 부품을 어떻게 조립하고 연결하는가
여러 객체들의 구조를 어떻게 구성할지.

  1. 행동 패턴 (Behavioral Patterns)
    객체의 어떤 상태가 변할 때
    다른 객체들에게 이 상태와 변화를 어떻게 전달할지.
    초점이 객체의 "상태" 에 있는 것.

객체에 뭔가 변화가 일어날 때
어떤 방식으로 다른 객체에게 상태를 전달하거나
어떤 식으로 동작을 하는 것.
= 객체의 상태가 변경될 떄 어떻게 할 것인가

부품이 서로 어떻게 상호작용할지
특정 객체가 변할 떄 다른 객체들에 이 상태를 어떻게 전달할지


싱글톤 패턴 (생성패턴)

아래와 같은 요구사항이 있을 때:

  • 비행기 관리
    1) 게임 내 비행기는 오직 하나
    2) 비행기 초기 위치는 (0,0) 좌표

  • 비행기 이동
    1) 비행기는 move(int X, int Y) 메서드를 통해 이동하고
    위치를 업데이트한다.

2) 현재 비행기 좌표를 확인할 수 있는
getPosition()을 제공한다.

"싱글톤 패턴" 을 배우는 것이기 때문에 중요한건
"게임 내 비행기는 오직 하나" 라는 요구사항


싱글톤 패턴 구현

어떻게 하면 프로그램에서 비행기가 '하나만' 존재하도록 구현할 수 있을까?
= "객체의 개수를 제한"
=> 내가 원하는 객체를 생산하는 방법을 제외하고는,
어떤 방법으로도 객체를 생산할 수 없도록 하면 되지 않을까.


클래스다이어그램을 보면,
생성자도 private, 복사 생성자도 private, 대입연산자도 private
이렇게 지정해주면서 일반적인 객체 생성 방법을 막아놓고
static getInstance() 로만 객체를 반환받도록 설정

  • 싱글톤 패턴 코드
#include <iostream>
using namespace std;

class Airplane {
private:
    static Airplane* instance; // 유일한 비행기 객체를 가리킬 정적 포인터
    int positionX;             // 비행기의 X 위치
    int positionY;             // 비행기의 Y 위치

    // private 생성자: 외부에서 객체 생성 금지
    Airplane() : positionX(0), positionY(0) {
        cout << "Airplane Created at (" << positionX << ", " << positionY << ")" << endl;
    }

public:
    // 복사 생성자와 대입 연산자를 삭제하여 복사 방지
    Airplane(const Airplane&) = delete;
    Airplane& operator=(const Airplane&) = delete;

    // 정적 메서드: 유일한 비행기 인스턴스를 반환
    static Airplane* getInstance() {
        if (instance == nullptr) {
            instance = new Airplane();
        }
        return instance;
    }
	...
};

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

// 메인 함수 (사용 예시)
int main() {
    // 유일한 비행기 인스턴스를 가져옴
    Airplane* airplane = Airplane::getInstance();
    airplane->move(10, 20);  // 비행기 이동
    airplane->getPosition();

    // 또 다른 요청도 같은 인스턴스를 반환
    Airplane* sameAirplane = Airplane::getInstance();
    sameAirplane->move(-5, 10); // 비행기 이동
    sameAirplane->getPosition();

    return 0;
}

기본 생성자는 private 으로 막아놓고
public에 있는 복사 생성자 와 대입 연산자는 = delete; 를 통해 막아놓은 패턴

물론 3개 다 private 처리하면서 막아두는 것도 같은 동작이다.

여기서 중요한 건 Airplane 포인터가
static 으로 되어있다는 건데
`static Airplane
instance;`

static = 정적, 고정된

이 static 걸어둔 변수가 한번 초기화되면
모든 해당 클래스 객체에서 이 변수를 공유한다!

거기에 "정적 메서드" 라는 것도 있는데
이걸 통해 이 유일한 객체만 반환하도록 구성한다.

instance 가 nullptr 이면
instance = new Airplane() 해서 새 인스턴스 만들고

이미 있으면 return instance
=> 객체는 이 instance 하나만 있게 되는 것


데코레이터 패턴 (구조패턴)

아래 같은 요구사항이 있을 때:

  • 기본 피자
    1) 피자의 기본 베이스가 반드시 필요하다
    2) 기본 피자에는 이름과 가격이 정의되어 있다

  • 토핑 추가
    1) 피자에 토핑(치즈, 페퍼로니, 올리브)를 동적으로 추가 가능
    2) 토핑은 이름과 추가비용 을 갖는다

  • 피자 정보 제공
    최종 피자의 이름(기본 피자 + 추가 토핑) 과 가격을 제공

"이 동적 토핑들을 피자와 어떻게 연결할 것인가"


데코레이터 패턴 구현

"어떻게 하면 객체의 상태를 동적으로 업데이트 할 수 있을까"

흰색 세모가 있는 화살표 = 화살표 방향에 있는 걸 상속
Pizza 에 I 가 붙어있는건 = Interface, 순수가상함수 때문

#include <iostream>
#include <string>

using namespace std;

// **추상 컴포넌트 (Component): Pizza**
// - 피자 객체의 기본 구조를 정의하는 인터페이스입니다.
// - 모든 피자는 이름(`getName`)과 가격(`getPrice`)을 가져야 합니다.
class Pizza {
public:
    virtual ~Pizza() {}
    virtual string getName() const = 0;  // 피자의 이름 반환
    virtual double getPrice() const = 0; // 피자의 가격 반환
};

// **구체 컴포넌트 (Concrete Component): BasicPizza**
// - 기본 피자 클래스입니다.
// - 피자의 기본 베이스(이름과 가격)를 구현합니다.
class BasicPizza : public Pizza {
public:
    string getName() const {
        return "Basic Pizza"; // 기본 피자의 이름
    }
    double getPrice() const {
        return 5.0; // 기본 피자의 가격
    }
};

// **데코레이터 추상 클래스 (Decorator): PizzaDecorator**
// - 기존 피자의 기능을 확장하기 위한 데코레이터의 기본 구조를 정의합니다.
// - 내부적으로 `Pizza` 객체를 감싸며, 이름과 가격에 추가적인 기능을 제공합니다.
class PizzaDecorator : public Pizza {
protected:
    Pizza* pizza; // 기존의 피자 객체를 참조합니다.
public:
    // 데코레이터는 피자 객체를 받아서 감쌉니다.
    PizzaDecorator(Pizza* p) : pizza(p) {}
    
    // 소멸자에서 내부 피자 객체를 삭제합니다.
    virtual ~PizzaDecorator() {
        delete pizza;
    }
};

// **구체 데코레이터 (Concrete Decorators): Cheese, Pepperoni, Olive**
// - 각각의 토핑 데코레이터는 `PizzaDecorator`를 상속받아 이름과 가격을 확장합니다.

// 치즈 토핑 데코레이터
class CheeseDecorator : public PizzaDecorator {
public:
    CheeseDecorator(Pizza* p) : PizzaDecorator(p) {}
    string getName() const {
        // 기존 피자의 이름에 " + Cheese"를 추가
        return pizza->getName() + " + Cheese";
    }
    double getPrice() const {
        // 기존 피자의 가격에 치즈 추가 비용 1.5를 더함
        return pizza->getPrice() + 1.5;
    }
};

// 페퍼로니 토핑 데코레이터
class PepperoniDecorator : public PizzaDecorator {
public:
    PepperoniDecorator(Pizza* p) : PizzaDecorator(p) {}
    string getName() const {
        // 기존 피자의 이름에 " + Pepperoni"를 추가
        return pizza->getName() + " + Pepperoni";
    }
    double getPrice() const {
        // 기존 피자의 가격에 페퍼로니 추가 비용 2.0을 더함
        return pizza->getPrice() + 2.0;
    }
};

// 올리브 토핑 데코레이터
class OliveDecorator : public PizzaDecorator {
public:
    OliveDecorator(Pizza* p) : PizzaDecorator(p) {}
    string getName() const {
        // 기존 피자의 이름에 " + Olive"를 추가
        return pizza->getName() + " + Olive";
    }
    double getPrice() const {
        // 기존 피자의 가격에 올리브 추가 비용 0.7을 더함
        return pizza->getPrice() + 0.7;
    }
};

// **클라이언트 코드**
// - 피자와 데코레이터를 조합하여 최종 피자를 생성하고, 정보를 출력합니다.
int main() {
    // 1. 기본 피자를 생성합니다.
    Pizza* pizza = new BasicPizza();

    // 2. 치즈 토핑을 추가합니다.
    pizza = new CheeseDecorator(pizza);

    // 3. 페퍼로니 토핑을 추가합니다.
    pizza = new PepperoniDecorator(pizza);

    // 4. 올리브 토핑을 추가합니다.
    pizza = new OliveDecorator(pizza);

    // 5. 최종 피자 정보 출력
    cout << "Pizza: " << pizza->getName() << endl; // 피자의 이름 출력
    cout << "Price: $" << pizza->getPrice() << endl; // 피자의 가격 출력

    // 6. 메모리 해제
    delete pizza;

    return 0;
}
  • 추상 클래스(컴포넌트?) Pizza

  • 기본 피자 BasicPizza (구체 컴포넌트)

  • 데코레이터 추상 클래스

    "피자 객체를 받아 내부적으로 감싸준다"

  • 구체적 데코레이터

    데코레이터 추상 클래스를 상속받아서
    이름과 가격을 확장
    pizza->getName() 피자 객체의 getName 해서
    토핑 이름 더해주고, 마찬가지로 가격도 pizza->getPrice() 해서 가격 추가


결과적으로,
연쇄적으로 맨 아래 올리브 부터 getName을 하면
타고 올라가면서 페퍼로니, 치즈 getName 호출하고
마지막으로 기본 피자 getName 까지 호출하는 (getPrice도 마찬가지)
그런 구조인건데


계속 new ~ 로 대입해주는데
덮어쓰기 되는 게 아니라 "추가"가 되는 판정인 건 왜일까?

그러니까 Pizza* pizza = new BasicPizza(); 해서 기본피자의 포인터를 만들고
데코레이터들 형태는 다 동일하니 추상 클래스 거로 보자면

protected:
	Pizza* pizza;
public:
	PizzaDecorator(Pizza* p) : pizza(p) {}

이렇게 피자 포인터를 받아서 또 그거에 대한 포인터를 만든다
=> 그래서 "포장" 이라고 표현하는 듯

그리고 포인터인 pizza 에 계속 = 대입을 해줘도
포인터가 가리키는 대상이 바뀌는거지
"가리키던 객체가, 메모리의 값이 없어지는 게 아니기 때문에"
계속 중첩해서 화살표가 생기는 느낌
pizza<-치즈<-페퍼로니<-올리브

그래서 계속 대입연산을 해준다고 해서 '덮어쓰기' 되는게 아니라
"포장", "연결" 이 되는 것이다.



옵저버 패턴 (행동패턴)

요구사항:
엑셀 프로그램,
엑셀에 데이터를 입력하고 변경했을 때
엑셀에 있는 다양한 차트들이 이 데이터의 수정에 따라서
자동으로 다수의 차트들의 수치가 업데이트 되도록 만들고 싶다.


옵저버 패턴 구현

  • 엑셀 시트와 차트 관리
    1) 엑셀 시트는 데이터를 입력받을 수 있다
    2) 여러 차트들은 엑셀 데이터를 기반으로 업데이트된다
    3) 엑셀에 새로운 값이 입력되면
    연결된 모든 차트가 자동으로 업데이트된다

중요한 건 크게 두가지인데,
1. Subject : 상태를 관리하고, 변경되었음을 옵저버에 알림
2. Observer : Subject를 관찰하며 상태 변경 시 반응함
=> 옵저버들은 서브젝트(어떠한 주제)에 등록하고 변경이 발생하면 알람을 받음

엑셀 시트 클래스에 보면
옵저버에 관한 벡터를 가지고 있고
옵저버에는 update라는 함수가 있는 걸 볼 수 있다.

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// Observer 인터페이스
// - Observer 패턴에서 상태 변화를 알림받는 객체들의 공통 인터페이스
// - Observer들은 이 인터페이스를 구현하여 `update` 메서드를 통해 데이터를 전달받음
class Observer {
public:
    virtual ~Observer() = default;               // 가상 소멸자
    virtual void update(int data) = 0;           // 데이터 업데이트 메서드 (순수 가상 함수)
};

// Subject 클래스 (엑셀 시트 역할)
// - 데이터의 상태 변화를 관리하며, 모든 등록된 Observer들에게 변경 사항을 알림
class ExcelSheet {
private:
    vector<Observer*> observers;                 // Observer들을 저장하는 리스트
    int data;                                    // 현재 데이터 상태

public:
    ExcelSheet() : data(0) {}                    // 생성자: 초기 데이터 값은 0

    // Observer 등록 메서드
    // - 새로운 Observer를 등록하여 변경 사항 알림을 받을 수 있도록 추가
    void attach(Observer* observer) {
        observers.push_back(observer);
    }

    // 데이터 변경 알림 메서드
    // - 등록된 모든 Observer들의 `update` 메서드를 호출하여 데이터 변경 사항을 알림
    void notify() {
        for (Observer* observer : observers) {
            observer->update(data);              // 각 Observer에게 데이터를 전달
        }
    }

    // 데이터 설정 메서드
    // - 데이터를 변경하고 변경 사항을 모든 Observer에게 알림
    void setData(int newData) {
        data = newData;                          // 새로운 데이터로 갱신
        cout << "ExcelSheet: Data updated to " << data << endl;
        notify();                                // Observer들에게 알림
    }
};

// 구체적인 Observer 클래스: BarChart (막대 차트)
// - 데이터를 막대 그래프로 표현
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;
    }
};

// 구체적인 Observer 클래스: LineChart (라인 차트)
// - 데이터를 선형 그래프로 표현
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;
    }
};

// 구체적인 Observer 클래스: PieChart (파이 차트)
// - 데이터를 파이 그래프로 표현
class PieChart : public Observer {
public:
    void update(int data) {                      // 데이터 업데이트 시 호출됨
        cout << "PieChart: Displaying data as a pie chart slice: ";
        cout << "Pie [" << data << "%]" << endl; // 데이터 값 출력 (가정: % 비율로 표현)
    }
};

// 메인 함수
int main() {
    // Subject 생성
    ExcelSheet excelSheet;                       // 데이터를 관리하는 엑셀 시트 객체 생성

    // Observer 객체 생성 (각 차트 객체)
    BarChart* barChart = new BarChart();         // 막대 차트 생성
    LineChart* lineChart = new LineChart();      // 라인 차트 생성
    PieChart* pieChart = new PieChart();         // 파이 차트 생성

    // Observer 등록
    // - 각 차트(Observer)를 엑셀 시트(Subject)에 등록
    excelSheet.attach(barChart);
    excelSheet.attach(lineChart);
    excelSheet.attach(pieChart);

    // 데이터 변경 테스트
    // - 데이터를 변경하면 등록된 모든 Observer들이 알림을 받고 화면에 갱신
    excelSheet.setData(5);                       // 데이터 변경: 5
    excelSheet.setData(10);                      // 데이터 변경: 10

    // 메모리 해제
    // - 동적 할당된 Observer(차트) 객체 삭제
    delete barChart;
    delete lineChart;
    delete pieChart;

    return 0;
}

옵저버 인터페이스가 있고

서브젝트인 엑셀 시트 클래스가 있다

attach 로 옵저버 등록하고
notify 로 알리면 -> 각 옵저버들에게 update(data) 되고
setData는 엑셀시트에서 data 값을 새로 설정해줌과 동시에 notify 해주는 것

그리고 옵저버 인터페이스가 구체화된 옵저버 클래스들

알림 받아서 update 하면 차트 모양이 바뀌는 모습

그리고 메인에서 차트랑
각 옵저버 객체를 만들어서
attach 해주고

profile
내일배움캠프 Unreal 3기

0개의 댓글