디자인 패턴이 중요한 이유는
문제 해결의 일관성
문제가 생겼을 때 일관적으로 문제를 해결할 수 있다.
코드 가독성 및 유지보수성
코드의 구조가 명확해지고, 다른 사람이 코드를 이해하기 더 쉬워진다.
재사용성
재사용하기 용이하다.
확장성
소프트웨어 설계의 변화에 유연하게 대응할 수 있다.
공통 언어
개발자들 사이의 공통 언어로 사용되어 의사소통을 더 명확하고 간결하게 만든다.
디자인 패턴은 OOP의 개념과 밀접하게 연결되어있다.
단점으로는 아래와 같은 문제가 있다.
만약 오리를 제작해야 한다고 생각하자.
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 생성자 혹은 팩토리에서 어떤 행동을 쓸지 결정하는 과정이 필요하다.