SW Engineering - 디자인 패턴(행동 패턴)

윤형·2025년 6월 6일

Sofrware Engineering

목록 보기
9/9
post-thumbnail

서론

그동안 디자인 패턴중에 생성 패턴, 구조패턴에 대해서 배웠다. 그렇다면 마지막으로 행동 패턴에 대해서 배우면 디자인 패턴은 어느정도 알게 된것이다.

행동 패턴

객체간의 상호작용과 책임 분산을 다루는 디자인 패턴이다.

  • 객체 간 결합도를 줄이기
  • 재사용성과 유연성을 높인다.

패턴 #1 - Chain Of Responsibility

요청을 처리할 수 있는 객체들을 체인처럼 연결을하고, 각 객체가 자신이 처리할 수 없는 일이면 다음 객체로 넘긴다.

  • 요청을 보내는 쪽과 처리하는 쪽을 분리한다.
  • 클라이언트는 누가 요청을 처리하는지 몰라도 된다.

예시 - 위젯별 도움말

Client → Button → Dialog → Widget → Application

  • HelpHandler: 추상 핸들러
  • Application, Widget, Dialog, Button: 실제 요청 처리자
  • Successor: 다음 책임자로 넘기는 참조
class HelpHandler {
protected:
    HelpHandler* successor;
public:
    HelpHandler(HelpHandler* next) : successor(next) {}

    virtual void HandleHelp() {
        if (successor != nullptr) {
            successor->HandleHelp();  // 다음 책임자에게 넘김
        } else {
            cout << "[기본 도움말 없음]" << endl;
        }
    }

    virtual ~HelpHandler() {}
};
  • successor이 핵심이다.
  • 자기 자신의 클래스를 다시 참조한다.-> 존재한다면 다음 책임자에게 넘기게 된다. 만약 없다면 예외처리를 한다.
class Button : public HelpHandler {
public:
    Button(HelpHandler* next) : HelpHandler(next) {}

    void HandleHelp() override {
        cout << "[Button 도움말]" << endl;
    }
};

class Dialog : public HelpHandler {
public:
    Dialog(HelpHandler* next) : HelpHandler(next) {}

    void HandleHelp() override {
        cout << "[Dialog 도움말]" << endl;
    }
};

class Application : public HelpHandler {
public:
    Application(HelpHandler* next) : HelpHandler(next) {}

    void HandleHelp() override {
        cout << "[Application 도움말]" << endl;
    }
};
  • 원래는 widget클래스를 만들고, 그게 HelpHandler를 상속해야하지만, 이해를 위해 생략했다.
int main() {
    Application* app = new Application(nullptr);
    Dialog* dialog = new Dialog(app);
    Button* button = new Button(dialog);

    // 가장 하위 객체에서 요청 시작
    button->HandleHelp();

    delete button;
    delete dialog;
    delete app;
    return 0;
}
  • 상위의 객체를 먼저 생성하고 그 후에 하위 객체들을 순차적으로 연결해준다.
  • app - dialog - button 형태.
실행결과: [Button 도움말]

  • 다시 한번 그림을 확인해보자.

  • 최상위 HelpHandler가 successor으로 자신을 참조한다.

  • 정확히는 자신 보다는 다음 체인에 들어올 객체를 참조하는 것.

  • 만약 도움말이 없으면 다음 객체로 책임을 넘기려고 한다면, boolean값을 하나 만들어서 help가 있는지 유무를 확인하고 없으면 다음으로 넘기게 Button, Dialog 클래스에서 추가하면 된다.

패턴 #2 - Command(Action)

요청을 객체로 캡슐화 해서 요청자와 수행자를 분리하고 요청을 저장, 실행 할 수 있게 만드는 행동 패턴이다.

  • 뭔가 요청이 들어왔을때 실행하는 Event구조에서 유리하다.

  • Command: 모든 명령의 공통 인터페이스
  • ConcreateCommand: 구체적인 명령(copycommand)
  • MenuItem: 명령을 가지고 있는 버튼
class Command {
public:
    virtual void Execute() = 0;
    virtual ~Command() {}
};
  • Command에서는 Execute()메서드를 만든다.
class CopyCommand : public Command {
public:
    void Execute() override {
        cout << "복사 실행됨" << endl;
    }
};

class PasteCommand : public Command {
public:
    void Execute() override {
        cout << "붙여넣기 실행됨" << endl;
    }
};

class UndoCommand : public Command {
public:
    void Execute() override {
        cout << "실행 취소됨" << endl;
    }
};
  • Command를 상속받는 객체들이 Execute내용을 실제로 완성한다.
class MenuItem {
private:
    Command* command;
public:
    MenuItem(Command* cmd) : command(cmd) {}

    void OnClicked() {
        command->Execute();  // 버튼이 클릭되면 명령 실행
    }
};
  • MenuItem에는 Command들을 가지고 있는다.
  • Command를 호출하는 이벤트의 역할을 하게 된다.
int main() {
    Command* copy = new CopyCommand();
    Command* paste = new PasteCommand();

    MenuItem copyItem(copy);
    MenuItem pasteItem(paste);

    copyItem.OnClicked();   // 복사 실행됨
    pasteItem.OnClicked();  // 붙여넣기 실행됨

    delete copy;
    delete paste;
    return 0;
}
  • 메인에서는 버튼과 command를 만든다.
  • 원하는 command를 실행한다.
[실행 결과]: 복사 실행됨  , 붙여넣기 실행됨

게임으로 예시를 들어보자면, 팝업창을 애니메이션을 넣어서 띄우게 되는데 여러 애니메이션 중에서 선택해서 실행시키면 action하게 만들 수 있다.

✅ Command 패턴은 명령을 객체로 만들어서 관리한다는 점이 핵심!!

패턴 #3 - Iterator(Cursor)

컬렉션 내부 구조를 노출하지 않고, 요소를 하나씩 순차적으로 접근할 수 있는 방법을 제공하는 패턴

  • Iterator: 반복자의 인터페이스
  • ByValueIterator: 다양한 탐색 방식의 반복자 구현
  • AbstractList: 반복자를 생성하는 컬렉션
  • ByValueList: 반복자 생성 방식이 다른 컬렉션

목적: ByValueList라는 컬렉션을 직접 순회하지 않고 Iterator를 이용해서 추상적으로 하나씩 접근할 수 있게 만든다.

class MyList {
private:
    int data[3] = {10, 20, 30};
public:
    int get(int index) { return data[index]; }
    int size() { return 3; }
};
  • 먼저 추상 List를 만든다.
class Iterator {
private:
    MyList* list;
    int index = 0;
public:
    Iterator(MyList* l) : list(l) {}

    void first() { index = 0; }
    void next() { index++; }
    bool isDone() { return index >= list->size(); }
    int currentItem() { return list->get(index); }
};
  • 그다음 그 리스트를 순서대로 접근할 수 있게 해주는 클래스를 만든다.
int main() {
    MyList list;
    Iterator it(&list);

    for (it.first(); !it.isDone(); it.next()) {
        cout << it.currentItem() << " ";
    }

    return 0;
}
출력 결과: 10 20 30

패턴 #4 - Mediator

여러 객체 간의 복잡한 상호작용을 중앙 집중식으로 조율하는 "중재자" 객체를 만들어, 객체 간 직접 통신을 막고, 결합도를 낮추는 행동 패턴.

  • 객체들끼리 직접적으로 통신하게 되면 코드가 지저분해진다.
  • Mediator를 사용하면 객체들은 이것만 알면됨.
  • 이런식으로 Dialog를 통해서만 서로 통신을 진행하는 것이다.

예시 - UI 컴포넌트 중재

class Mediator {
public:
    virtual void Notify(const string& sender, const string& event) = 0;
    virtual ~Mediator() {}
};
class Component {
protected:
    Mediator* mediator;
public:
    void SetMediator(Mediator* m) { mediator = m; }
};

class Button : public Component {
public:
    void Click() {
        cout << "[Button 클릭됨]\n";
        mediator->Notify("Button", "Click");
    }
};

class TextBox : public Component {
public:
    void ShowText(const string& msg) {
        cout << "[TextBox 출력] " << msg << endl;
    }
};
  • 각각의 클래스를 만든다.
class DialogMediator : public Mediator {
private:
    Button* button;
    TextBox* textbox;
public:
    DialogMediator(Button* b, TextBox* t) : button(b), textbox(t) {
        button->SetMediator(this);
        textbox->SetMediator(this);
    }

    void Notify(const string& sender, const string& event) override {
        if (sender == "Button" && event == "Click") {
            textbox->ShowText("버튼이 눌렸습니다");
        }
    }
};
  • Dialog를 만들어서 중간에서 중재할 클래스를 생성한다.
  • 각 클래스에는 중재자(자신)을 넘겨주고, 각 클래스들을 소유한다.
  • 이후 버튼이 클릭되는 이벤트 발생 시 특정 행동을 하게 넘겨주면 된다.
int main() {
    Button* btn = new Button();
    TextBox* txt = new TextBox();

    DialogMediator* mediator = new DialogMediator(btn, txt);

    btn->Click();  // → TextBox가 자동으로 반응

    delete mediator;
    delete btn;
    delete txt;
    return 0;
}
  • mediator를 사용하면 나중에 이것만 바꾸면 되기 때문에 상호 작용 방식을 손쉽게 바꿀 수 있다.

패턴 #5 - Memento

메멘토는 객체의 내부 상태를 저장해두었다가 나중에 그 상태로 되돌릴 수 있게 하는 패턴이다.

  • 보통 Undo를 위해 사용된다.

  • 외부에서는 Opaque한 상태다(불투명)

패턴 #6 - Observer

어떤 객체가 상태가 바뀌면 그와 의존하고 있던 옵저버에게 알림을 보내는 구조.
보통 이벤트를 다루기 위해서 많이 사용한다.

  • 게임에서는 알림 시스템이나 업적에 자주 사용한다.

예시 - 뉴스 발행 시스템

class Observer {
public:
    virtual void Update(const string& message) = 0;
    virtual ~Observer() {}
};

먼저 옵저버 인터페이스를 만든다.

class Subject {
public:
    virtual void Attach(Observer* o) = 0;
    virtual void Detach(Observer* o) = 0;
    virtual void Notify() = 0;
    virtual ~Subject() {}
};
  • 그다음 subject를 만든다.
class NewsAgency : public Subject {
private:
    vector<Observer*> observers;
    string latestNews;
public:
    void Attach(Observer* o) override {
        observers.push_back(o);
    }

    void Detach(Observer* o) override {
        observers.erase(remove(observers.begin(), observers.end(), o), observers.end());
    }

    void SetNews(const string& news) {
        latestNews = news;
        Notify();  // 상태가 바뀌면 자동 알림
    }

    void Notify() override {
        for (Observer* o : observers) {
            o->Update(latestNews);
        }
    }
};
  • NewsAgency는 Subject를 상속받고 세부 내용을 구현한다.
  • 여기서 알릴 상황이 생기면 observer의 update를 통해 알린다.
  • 여기서 옵저버들을 리스트로 가지고 있다는 것이 중요하다.
class Subscriber : public Observer {
private:
    string name;
public:
    Subscriber(const string& n) : name(n) {}

    void Update(const string& message) override {
        cout << name << "님에게 뉴스 도착: " << message << endl;
    }
};
  • Subscriber는 observer를 상속받고, 업데이트 시 메시지를 출력할 수 있게 한다.
int main() {
    NewsAgency agency;

    Subscriber s1("홍길동");
    Subscriber s2("김철수");

    agency.Attach(&s1);
    agency.Attach(&s2);

    agency.SetNews("🚨 속보: 디자인 패턴 시험 출제!");
    // 둘 다 알림 받음

    agency.Detach(&s1);
    agency.SetNews("📢 공지: 점심시간 연장됨");
    //S2만받음

    return 0;
}
홍길동님에게 뉴스 도착: 🚨 속보: 디자인 패턴 시험 출제!  
김철수님에게 뉴스 도착: 🚨 속보: 디자인 패턴 시험 출제!  
김철수님에게 뉴스 도착: 📢 공지: 점심시간 연장됨
  • main에서는 이런식으로 Attach를 통해 리스트에 각 Subscriber들을 넣을 수 있게 한다.
  • SetNews()메서드를 호출하게 되면 들어있는 모든 observer들에게 내용이 전달되게 된다.
  • subject와 여러 observer들이 연결되어 있는 구조.

패턴 #7 - State

객체의 상태에 따라 행동이 달라질 때, 그 상태를 객체로 분리해서 상태 전환과 행동을 깔끔하게 관리하는 패턴이다.

  • 하나의 객체가 상태에 따라 행동이 달라질때 사용.

  • State 추상 클래스를 만든다.
  • 유저 상태에 따라 적적한 서브클래스 객체를 만들어 state멤버로 저장한다.

예시 - 전등 스위치

class LightState {
public:
    virtual void Toggle(class Light* light) = 0;
    virtual ~LightState() {}
};
  • LightState라는 상태 클래스를 만든다.
class Light {
private:
    LightState* state;
public:
    Light(LightState* initialState) : state(initialState) {}

    void SetState(LightState* newState) {
        state = newState;
    }

    void PressButton() {
        state->Toggle(this);  // 현재 상태에 동작 위임
    }
};
  • LightState를 가지고 있는 Light를 만든다. (유저)
class OnState : public LightState {
public:
    void Toggle(Light* light) override {
        cout << "💡 전등을 끕니다.\n";
        static OffState off;  // 전환할 상태를 정적 객체로 생성
        light->SetState(&off);
    }
};

class OffState : public LightState {
public:
    void Toggle(Light* light) override {
        cout << "🔆 전등을 켭니다.\n";
        static OnState on;
        light->SetState(&on);
    }
};
  • 각 상태별로 구체적으로 구현한다.
int main() {
    static OffState off;  // 초기 상태는 꺼짐
    Light light(&off);

    light.PressButton();  // 🔆 전등을 켭니다.
    light.PressButton();  // 💡 전등을 끕니다.
    light.PressButton();  // 🔆 전등을 켭니다.

    return 0;
}
  • 이렇게 상태를 바꾸면 되는 간단한 패턴이다.

패턴 #8 - Strategy

행동을 객체로 캡슐화해서, 동적으로 알고리즘을 교체할 수 있도록 만드는 패턴이다.

  • 그림에서는 sorting 알고리즘을 세분화 하고 그것을 client가 소유하고 있게 구상했다.

예시 - 캐릭터 무기 전략

class WeaponStrategy {
public:
    virtual void Attack() = 0;
    virtual ~WeaponStrategy() {}
};
  • Strategy 인터페이스를 만든다.
class Sword : public WeaponStrategy {
public:
    void Attack() override {
        cout << "⚔️ 검으로 공격!" << endl;
    }
};

class Bow : public WeaponStrategy {
public:
    void Attack() override {
        cout << "🏹 활로 공격!" << endl;
    }
};
  • 구체 전략을 만들고
class Character {
private:
    WeaponStrategy* weapon;
public:
    Character(WeaponStrategy* w) : weapon(w) {}

    void SetWeapon(WeaponStrategy* w) {
        weapon = w;
    }

    void Attack() {
        weapon->Attack();  // 전략에 따라 공격 방식 결정
    }
};
  • 캐릭터가 이를 다루게 한다.
int main() {
    Sword sword;
    Bow bow;

    Character hero(&sword);  // 처음엔 검
    hero.Attack();           // ⚔️ 검으로 공격!

    hero.SetWeapon(&bow);    // 활로 교체
    hero.Attack();           // 🏹 활로 공격!

    return 0;
}
  • 메인에서는 이렇게 캐릭터의 전략 클래스를 구현한다.
  • 이는 게임에서 자주 사용하는 기법이기 때문에 꼭 알아야 하는 패턴중에 하나다.

패턴 #9 - Template Method

알고리즘의 뼈대(골격)는 상위 클래스에서 정의하고, 그 구체적인 세부 단계는 하위 클래스에서 구현하도록 하는 디자인 패턴이다.

예시 - 문서 출력 템플릿

class Document {
public:
    void Print() {  // Template Method
        Header();
        Body();
        Footer();
    }

    virtual void Header() {
        cout << "[기본 헤더]\n";
    }

    virtual void Footer() {
        cout << "[기본 푸터]\n";
    }

    virtual void Body() = 0;  // 꼭 자식이 구현해야 함

    virtual ~Document() {}
};
  • 이렇게 문서의 틀을 만들어 둔다.
class Report : public Document {
public:
    void Body() override {
        cout << "📄 리포트 본문 출력\n";
    }
};

class Invoice : public Document {
public:
    void Header() override {
        cout << "[💼 인보이스 헤더]\n";
    }

    void Body() override {
        cout << "💰 청구서 본문 출력\n";
    }

    void Footer() override {
        cout << "[📌 인보이스 푸터]\n";
    }
};
  • 그리고 그 문서 클래스를 상속받은 자식 클래스에서 이를 구체화 한다.
int main() {
    Report report;
    Invoice invoice;

    cout << "=== 리포트 출력 ===\n";
    report.Print();

    cout << "\n=== 인보이스 출력 ===\n";
    invoice.Print();

    return 0;
}
  • 메인에서는 각 객체를 생성해 사용하면 된다.

  • 우리가 많은 패턴을 배웠는데 자식이 부모 클래스를 구체화 하는 흐름은 사실 많이 봐왔다.

  • Template Method는 보통 다른 디자인 패턴에 많이 붙어서 사용된다.

profile
제가 관심있고 공부하고 싶은걸 정리하는 저만의 노트입니다.

0개의 댓글