[C++] 디자인 패턴, 전략 패턴

김영웅·2025년 2월 25일

디자인 패턴이 중요한 이유는

  1. 문제 해결의 일관성
    문제가 생겼을 때 일관적으로 문제를 해결할 수 있다.

  2. 코드 가독성 및 유지보수성
    코드의 구조가 명확해지고, 다른 사람이 코드를 이해하기 더 쉬워진다.

  3. 재사용성
    재사용하기 용이하다.

  4. 확장성
    소프트웨어 설계의 변화에 유연하게 대응할 수 있다.

  5. 공통 언어
    개발자들 사이의 공통 언어로 사용되어 의사소통을 더 명확하고 간결하게 만든다.


디자인 패턴은 OOP의 개념과 밀접하게 연결되어있다.

1. 캡슐화 (Encapsulation)

  • 문제 : 데이터를 외부에서 직접 조작하면 의도치 않은 상태 변경이 발생.
  • 해결 : 데이터를 객체 내부로 숨기고, 공개된 인터페이스로만 접근하도록 설계.
  • 예 : Singleton 패턴, Factory 패턴.

2. 코드 중복 방지 (DRY - Don't Repeat Yourself)

  • 문제 : 동일한 코드를 여러 곳에서 반복적으로 작성하면 유지보수성이 떨어짐.
  • 해결 : 패턴을 사용하여 중복을 최소화.
  • 예 : Template Method, Strategy 패턴.

3. 의존성 최소화 (Dependency Minimization)

  • 문제 : 하나의 변경이 다른 모듈에 영향을 주는 의존성 문제.
  • 해결 : 느슨한 결합(loose coupling)을 유지하도록 설계.
  • 예 : Observer 패턴, Dependency Injection.

4. 변화에 열린 설계 (Open/Closed Principle)

  • 문제 : 기존 코드를 수정하지 않고 새로운 기능을 추가해야 할 때.
  • 해결 : 확장은 쉽고, 변경은 최소화하는 설계.
  • 예 : Decorator 패턴, Factory 패턴.

단점으로는 아래와 같은 문제가 있다.

  1. 복잡성 증가
    잘못 사용하면 불필요한 복잡성을 초래할 수 있다.
  2. 학습 곡선
    패턴을 제대로 이해하고 활용하려면 경험과 학습이 필요하다.
  3. 과잉 설계
    간단한 문제에 과도하게 적용하면 오히려 비효율적일 수 있다.
  4. 언어 특화 기능의 대체
    일부 언어에서는 자체 기능으로 디자인 패턴이 필요 없거나 자동화된다.
    예: Python에서는 Singleton이 메타클래스로 구현 가능.


전략 패턴

만약 오리를 제작해야 한다고 생각하자.
Duck이라는 클래스를 부모로 두고 MallardDuck, RedheadDuck, RubberDuck이 상속을 받아 클래스를 구현했다고 생각해보자


class Duck
{
public:
	virtual void Quack()
	{
		cout << "꽥꽥" << endl;
	}

	virtual void Swim()
	{
		cout << "첨벙첨벙"  << endl;
	}

	virtual void Display() = 0;
};

class MallardDuck : public Duck
{
public:
	void Display() override
	{
		cout << "청둥오리" << endl;
	}
};

class RedheadDuck : public Duck
{
public:
	void Display() override
	{
		cout << "홍머리오리" << endl;
	}
};

class RubberDuck : public Duck
{
public:
	void Display() override
	{
		cout << "고무오리" << endl;
	}
};

만약 오리에 Fly()가 들어온다고 생각해보자.
RubberDuck은 고무오리기 때문에 날 수 없으니 Fly()를 오버라이딩해 날지 않는다고 해야할 것이다.
이처럼 부모에서 변경해서 서브클래스에 원치 않았던 움직임이 생겨서 그것을 막느라 더 많은 일을 하게 되버린다.


class Duck
{
public:
	virtual void Quack()
	{
		cout << "꽥꽥" << endl;
	}

	virtual void Swim()
	{
		cout << "첨벙첨벙"  << endl;
	}
    
    virtual void Fly()
    {
    	cout << "날아요" << endl;
    }

	virtual void Display() = 0;
};

class MallardDuck : public Duck
{
public:
	void Display() override
	{
		cout << "청둥오리" << endl;
	}
};

class RedheadDuck : public Duck
{
public:
	void Display() override
	{
		cout << "홍머리오리" << endl;
	}
};

class RubberDuck : public Duck
{
public:
	void Display() override
	{
		cout << "고무오리" << endl;
	}
    
    void Fly() override
    {
    	cout << "못날아요" << endl;
    }
};

이 문제를 패턴으로 해결하기 위해선 interface로 만들면 된다.
공통되는 점은 상속으로 해결하고, 다른 점은 interface로 나누어 해결한다.
이 패턴에서는 그 interface로 나눈 것을 클래스로 구현하여 이렇게 생성한 클래스를 마치 요소처럼 사용한다.


class IFlyBehavior
{
public:
	virtual void Fly() = 0;
};

class IQuackBehavior
{
public:
	virtual void Quack() = 0;
};

// 날 수 있는 행동 구현
class FlyWithWings : public IFlyBehavior
{
public:
    void Fly() override
    {
        cout << "날개로 납니다." << endl;
    }
};

// 날지 못하는 행동 구현
class FlyNoWay : public IFlyBehavior
{
public:
    void Fly() override
    {
        cout << "날지 못합니다." << endl;
    }
};


// 꽥꽥 소리를 내는 행동 구현
class RealQuack : public IQuackBehavior
{
public:
	void Quack() override
	{
		cout << "꽥꽥 소리를 냅니다." << endl;
	}
};

class MuteQuack : public IQuackBehavior
{
public:
	void Quack() override
	{
		cout << "..." << endl;
	}
};

클래스 구현도 다음과 같이 달라져야한다.

class Duck
{
protected:
    IFlyBehavior* flyBehavior;
    IQuackBehavior* quackBehavior;

public:

    void PerformFly()
    {
        flyBehavior->Fly();
    }

    void PerformQuack()
    {
        quackBehavior->Quack();
    }

    void Swim()
    {
        cout << "모든 오리는 물에 뜹니다." << endl;
    }

    void SetFlyBehavior(IFlyBehavior* fb)
    {
        flyBehavior = fb;
    }

    void SetQuackBehavior(IQuackBehavior* qb)
    {
        quackBehavior = qb;
    }

    virtual void Display() = 0;
};

class MallardDuck : public Duck
{
    MallardDuck()
    {
        flyBehavior = new FlyWithWings();
        quackBehavior = new RealQuack();
    }

    void Display() override
    {
        cout << "청둥오리" << endl;
    }
};

class RedheadDuck : public Duck
{
    RedheadDuck()
    {
        flyBehavior = new FlyWithWings();
        quackBehavior = new RealQuack();
    }

    void Display() override
    {
        cout << "홍머리오리" << endl;
    }
};

class RubberDuck : public Duck
{
    RubberDuck()
    {
        flyBehavior = new FlyNoWay();
        quackBehavior = new MuteQuack();
    }

    void Display() override
    {
        cout << "고무오리" << endl;
    }
};

실제 사용

int main()
{
    Duck* mallardDuck = new MallardDuck();
    Duck* rubberDuck = new RubberDuck();

    mallardDuck->Display();
    mallardDuck->PerformFly();
    mallardDuck->PerformQuack();

    rubberDuck->Display();
    rubberDuck->PerformFly();
    rubberDuck->PerformQuack();
}

전략 패턴 장단점 및 활용 방안

장점

  • 유연성
    새로운 행동 클래스만 추가하면 되므로, 기존 코드 영향이 적다.

  • 결합도 감소
    Duck은 IFlyBehavior, IQuackBehavior 인터페이스만 알면 되므로, 구체 구현 변경 시 수정 범위가 최소화된다.

  • 런타임 교체
    SetFlyBehavior, SetQuackBehavior 등을 통해 실행 중에도 행동을 교체할 수 있다.

단점

  • 클래스 증가
    행동마다 별도 클래스를 만들어야 하므로, 파일이 늘어날 수 있다.

  • 행동 설정
    Duck 생성자 혹은 팩토리에서 어떤 행동을 쓸지 결정하는 과정이 필요하다.

활용 방안

  • 자주 바뀌는 로직(할인 정책, 무기 시스템, 정렬 알고리즘 등)에 대해서 전략 패턴을 적용하면 유리하다.
  • 복잡한 if-else 또는 switch문으로 분기 처리하던 로직을 “행동” 객체로 분리함으로써 코드 가독성과 유지보수성을 높일 수 있다.
profile
게임 프로그래머

0개의 댓글