패턴 정리 노트

정완훈·2025년 3월 25일

디자인 패턴 Wrap-Up

디자인 패턴은 소프트웨어 설계에서 자주 사용하는 패턴을 의미한다. 이는 프로그래밍 과정에서 반복적으로 나타나는 일반적인 문제에 대해 구체적인 해결책을 제시한다. 쉽게 말해, 디자인 패턴은 프로그래밍의 '족보'나 '답안지' 같은 역할을 한다. 자주 등장하는 문제 유형에 대해 정형화된 풀이법을 제공하며, 이는 문제를 해결하는 방법이자 개발자 간 소통의 도구로 활용된다. 게임 프로그래밍뿐만 아니라 일반적인 프로그래밍에서도 널리 사용되며, 이를 통해 코드의 품질을 높이고 작성 속도를 빠르게 개선한다.


디자인 패턴의 특징과 중요성

디자인 패턴은 개별 코드의 세부적인 구현보다는 프로그램의 전체 구조와 객체 간 상호작용에 초점을 맞춘다. 이는 객체 지향 프로그래밍의 기초를 기반으로 하지만, 그 사고방식은 특정 프로그래밍 패러다임에 얽매이지 않고 적용할 수 있다. 정확한 구현 방법을 외우는 것보다 구조를 이해하는 것이 핵심이다. 구조를 파악하면 어떤 프로그래밍 언어를 사용하더라도 자연스럽게 활용할 수 있으며, 나아가 자신만의 패턴을 창조하는 데도 도움이 된다.


디자인 패턴을 배우는 이유와 장점

프로그래밍을 하다 보면 설계상의 문제를 피할 수 없다. 예를 들어, 새로운 기능을 추가하려면 여러 파일을 수정해야 하거나, 특정 코드를 제거했는데 다른 곳에서 참조되고 있어 전체 시스템을 점검해야 하는 상황이 발생한다. 이런 경우 적절한 디자인 패턴을 적용하면 문제를 간단히 해결한다. 디자인 패턴은 수많은 개발자의 경험과 노하우를 정리한 도구로, 이를 통해 사고방식을 배우고 효율적으로 문제를 처리한다. 따라서 디자인 패턴을 잘 이해하고 상황에 맞게 활용하는 것은 좋은 소프트웨어를 개발하는 데 필수적이다.


디자인 패턴의 분류

디자인 패턴은 크게 세 가지로 나뉜다.

  • 생성 패턴(Creational Pattern): 객체를 생성하는 방법을 다룬다.
  • 구조 패턴(Structural Pattern): 객체 간의 구성과 관계를 설계한다.
  • 행동 패턴(Behavioral Pattern): 객체 간의 상호작용과 역할 분담을 정의한다.

이 분류는 패턴의 기능을 기준으로 구분한것으로, 필요한 패턴을 빠르게 찾는 데 유용하다.


도구로서의 디자인 패턴

디자인 패턴은 단순한 해결책을 넘어 도구로도 기능한다. 다른 개발자의 코드를 분석할 때 사용된 패턴을 파악하면 핵심 로직을 빠르게 이해한다. 또한, 자신이 과거에 작성한 복잡한 코드를 다시 볼 때도 길잡이 역할을 한다. 그러나 디자인 패턴을 과도하게 사용하면 코드의 가독성이 떨어지고 유지보수가 어려워진다. 필요한 상황에서만 적절히 사용하는 것이 중요하다.


게임 개발에서의 디자인 패턴 활용 사례

게임 개발에서는 디자인 패턴을 활용해 신규 콘텐츠 추가나 기획 변경에 유연하게 대응한다. 자주 사용되는 패턴의 예시는 다음과 같다.

  • 싱글톤 패턴(Singleton Pattern):
    리소스 관리나 게임 매니저처럼 단일 인스턴스가 필요한 경우 사용한다.

    class GameManager {
    private:
        static GameManager* instance;
        GameManager() {}
    public:
        static GameManager* getInstance() {
            if (!instance) instance = new GameManager();
            return instance;
        }
    };
    GameManager* GameManager::instance = nullptr;
  • 옵저버 패턴(Observer Pattern):
    객체 간 데이터 전달이 필요한 경우, 예를 들어 업적 시스템에 활용한다.

    class Observer {
    public:
        virtual void update() = 0;
    };
    
    class Subject {
    private:
        std::vector<Observer*> observers;
    public:
        void addObserver(Observer* obs) { observers.push_back(obs); }
        void notify() { for (auto obs : observers) obs->update(); }
    };
  • 상태 패턴(State Pattern):
    캐릭터의 상태(걷기, 뛰기 등)를 관리한다.

    class State {
    public:
        virtual void handle() = 0;
    };
    
    class Walking : public State {
    public:
        void handle() override { std::cout << "Walking\n"; }
    };
    
    class Character {
    private:
        State* currentState;
    public:
        void setState(State* state) { currentState = state; }
        void action() { currentState->handle(); }
    };
  • 반복자 패턴(Iterator Pattern):
    아이템 목록 같은 객체 집합을 순회한다.

    class Iterator {
    public:
        virtual bool hasNext() = 0;
        virtual int next() = 0;
    };
    
    class ItemList {
    private:
        std::vector<int> items;
    public:
        Iterator* createIterator();
    };
  • 전략 패턴(Strategy Pattern):
    무기 선택이나 AI 전술을 유연하게 변경한다.

    class Strategy {
    public:
        virtual void execute() = 0;
    };
    
    class SwordAttack : public Strategy {
    public:
        void execute() override { std::cout << "Sword Attack\n"; }
    };
    
    class Context {
    private:
        Strategy* strategy;
    public:
        void setStrategy(Strategy* strat) { strategy = strat; }
        void perform() { strategy->execute(); }
    };

안티 패턴

안티 패턴은 피해야 할 잘못된 프로그래밍 습관이나 설계를 의미한다. 이는 코드의 품질을 떨어뜨리고 유지보수를 어렵게 한다. 안티 패턴을 학습하면 실수를 방지하고, 기존 코딩 습관을 점검하는 데 도움이 된다.


불필요한 주석은 삭제한다

주석은 코드의 의도를 설명하는 데 유용하지만, 불필요하거나 잘못 사용되면 가독성을 해친다. 예를 들어, 아래 코드는 주석이 과도하다.

// 공격 함수
void attack(int power) {
    // 데미지 계산
    int damage = power * 2;
    std::cout << damage << "\n";
}

대신, 코드 자체를 명확히 작성한다.

void attack(int weaponPower) {
    int damage = weaponPower * 2;
    std::cout << damage << "\n";
}

주석 대신 코드 자체를 명확히 작성한다

변수와 메서드 이름을 명확히 하면 주석 없이도 코드가 읽힌다. 예를 들어:

// 나쁜 예
int d = p - def; // 데미지 계산

// 좋은 예
int damageAfterDefence = power - defence;

또한, 메서드를 작게 나누면 가독성과 재사용성이 높아진다.

bool isAttackBlocked(int damage) {
    return damage <= 0;
}

void applyDamage(int power, int defence) {
    int damage = power - defence;
    if (!isAttackBlocked(damage)) {
        std::cout << "Damage: " << damage << "\n";
    }
}

예외를 올바르게 사용한다

예외는 예상치 못한 문제를 처리하는 안전장치다. 버그와 달리 의도적으로 관리할 수 있다.

#include <stdexcept>

class Character {
private:
    int hp = 100;
public:
    void takeDamage(int damage) {
        if (damage > hp) {
            throw std::invalid_argument("Damage exceeds HP");
        }
        hp -= damage;
    }
};

void attack(Character& target) {
    try {
        target.takeDamage(200);
        std::cout << "Attack succeeded\n";
    } catch (const std::invalid_argument& e) {
        std::cerr << "Error: " << e.what() << "\n";
    }
}

구체적인 예외를 개별적으로 처리하면 문제 원인을 명확히 파악한다.


상속 대신 인터페이스를 사용한다

상속은 "is-a" 관계를 나타내지만, 다중 상속의 한계가 있다. 인터페이스는 "can-do" 관계를 정의하며 유연성을 제공한다.

class IAttackable {
public:
    virtual void attack() = 0;
};

class LongSword : public IAttackable {
public:
    void attack() override { std::cout << "LongSword Attack\n"; }
};

인터페이스를 사용하면 유지보수가 쉬운 구조를 만든다. 상속은 계층 구조에 적합하지만, 유연성이 필요한 경우 인터페이스가 더 효과적이다.

0개의 댓글