문제는 간단한 SIMDUCK 애플리케이션에서 시작했습니다.


상황 : 오리 연못 시뮬레이션의 초창기 버전에서 Duck이라는 슈퍼클래스를 상속하여 다양한 오리들을 만들었다. 

코드로 표현하면 다음과 같을 것이다.

class Duck
{
    void quack() {
        //모든 오리들이 꽥꽥거린다.
    }
    void swim() {
        //모든 오리가 헤엄친다.
    }
    virtual void display() {
        //오리의 모습은 모두 다르므로 가상함수로 만든다.
    }
    virtual ~Duck(){};
};
 
class RedheadDuck : public Duck
{
    virtual void display() {
        //빨간 머리 오리가 표시된다.
    };
};
 
class MallardDuck : public Duck
{
    virtual void display() {
        //청둥오리가 표시된다.
    };
};
cs



이제는 오리들이 날아다닐 수 있도록 해야합니다.


상황 : 회사의 높으신 분들이 경쟁사를 압도하려면 오리들이 날아다녀야 한다고 결정했다.

행동 : fly() 메서드를 추가해서 상속받게 했다.


class Duck
{
    void quack() {
        //모든 오리들이 꽥꽥거린다.
    }
    void swim() {
        //모든 오리가 헤엄친다.
    }
    void fly(){
        //모든 오리가 난다.
    }
    virtual void display() {
        //오리의 모습은 모두 다르므로 가상함수로 만든다.
    }
};
 
class RedheadDuck : public Duck
{
    virtual void display() {
        //빨간 머리 오리가 표시된다.
    };
};
 
class MallardDuck : public Duck
{
    virtual void display() {
        //청둥오리가 표시된다.
    };
};
cs



그런데 심각한 문제가 생겼습니다.


상황 : 이제 고무 오리와 모형 오리까지 날아다니는 시작했다..!

상속으로 코드를 재사용한다고 생각했지만, 실제 코드를 정비하는 데는 도움이 안되는 상황!

행동 : 

1. 고무 오리는 삑삑 거리므로 quack()을 오버라이드한다.

2. 마찬가지로 fly()도 오버라이드한다. 


class Duck
{
public:
    virtual void quack() {
        cout << "꽥꽥\n";
    }
    void swim() {
        cout << "오리가 헤엄친다.\n";
    }
    virtual void fly(){
        cout << "오리가 난다.\n";
    }
    virtual void display() {
        //오리의 모습은 모두 다르므로 가상함수로 만든다.
    }
};
 
class RedheadDuck : public Duck
{
public:
    virtual void display() {
        cout << "빨간 머리 오리\n";
    };
};
 
class MallardDuck : public Duck
{
public:
    virtual void display() {
        cout << "청둥오리\n";
    };
};
 
class RubberDuck : public Duck
{
public:
    virtual void quack(){
        cout << "삑삑\n";  //고무 오리는 꽥꽥이 아니라 삑삑 거린다.
    }
    virtual void display() {
        cout << "고무오리\n";
    };
    virtual void fly()    {
        cout << "이 오리는 날지 못합니다.\n";
    }
};
 
cs



조는 상속에 대해서 생각을 해봅니다...


상황 : 모형오리는 소리도 않나고 날지도 못한다.


class DecoyDuck : public Duck
{
public:
    virtual void quack() {
        //아무것도 하지 않도록 오버라이드
    }
    virtual void display() {
        cout << "모형 오리\n";
    };
    virtual void fly() {
        //아무것도 하지 않도록 오버라이드
    }
};
cs


Duck의 행동을 제공할 때 상속을 사용함에 있어 단점에 해당되는 사항은?

A. 서브클래스에서 코드가 중복된다.    X

-> C++에서는 부모의 함수를 바로 사용할 수도 있고, 중복되는 코드만 따로 함수로 만들어서 사용해도 된다.

B. 실행시에 특징을 바꾸기 힘들다.    O

-> 모형오리가 못날다가 갑자기 날 수 있게 하려면 모형오리의 fly()의 코드가 복잡해질 것이다. 

C. 오리가 춤추게 만들 수 없다.     X

-> 하하

D. 모든 오리의 행동을 알기 힘들다.    O

-> 모든 자식의 fly()가 제각각으로 정의되어있어서 기억하기 힘들다.

E. 오리가 날면서 동시에 꽥꽥거릴 수 없다.    X

-> fly() 안에 quack()을 넣으면 되지 않나?

F. 코드를 변경했을 때 다른 오리들에게 원치 않는 영향을 끼칠 수 있다.    O

-> 부모의 메서드를 그대로 사용하는 오리들의 경우 부모 메서드의 코드를 변경했을 때 .영향이 간다.



인터페이스는 어떨까요?


#include <iostream>
using namespace std;
 
class Flyable
{
public:
    virtual void fly() = 0;
    virtual ~Flyable() {};
};
 
class Quackable
{
public:
    virtual void quack() = 0;
    virtual ~Quackable() {};
};
 
class Duck
{
public:
    void swim() {};
    virtual void display() {};
    virtual ~Duck() {};
};
 
class MallardDuck : public Duck, public Flyable, public Quackable
{
public:
    virtual void display(){
        //청동오리
    }
    virtual void fly() {
        cout << "청동오리가 난다.\n";
    }
    virtual void quack(){
        cout << "청동오리가 꽥꽥.\n";
    }
};
 
int main()
{
    Duck* duck = new MallardDuck();
    
    Flyable* fduck = dynamic_cast<Flyable*>(duck);
    
    if(fduck)
        fduck->fly();
 
    Quackable* qduck = dynamic_cast<Quackable*>(duck);
    if (qduck)
        qduck->quack();
 
    if (duck)
        delete duck;
}
cs


오우 . . . C++에서의 인터페이스는 영 아닌거 같다.

1. 오리집합이 있을 때 특정 오리가 날 수 있는지 없는지 런타임 체크를 해야함

2. 오리마다 나는 행동을 새로 정의해야함


그런데 C++에서는 만약 구체 클래스를 다중상속하면 어떨까?

예를들어 flyable에는 나는 속성과 행동이 정의되어있고

quack에는 꽥의 속성과 행동이 정의되어있다면 어떨까

-> 동일한 flyable을 상속하는 오리는 코드를 재사용할 수 있다.

-> 로켓으로 나는 오리는 Flyable을 상속한 FlyRocketPower를 상속받아 구현해봄직

하지만 여전히 런타임 체크를 해야한다는 단점이 있다.


class Flyable
{
public:
    virtual void fly() 
    {
        cout << "날 수 있어요~\n";
    };
    virtual ~Flyable() {};
};
 
class FlyRocketPower : public Flyable
{
public:
    virtual void fly()
    {
        cout << "로켓으로 나는 중~\n";
    };
    virtual ~FlyRocketPower() {};
};
class Quackable
{
public:
    virtual void quack()
    {
        cout << "꽥 꽥\n";
    }
    virtual ~Quackable() {};
};
class Duck
{
public:
 
    void swim() {
        cout << "오리가 헤엄친다.\n";
    }
 
    virtual void display() {
        //오리의 모습은 모두 다르므로 가상함수로 만든다.
    }
};
 
class RocketDuck : public Duck, public FlyRocketPower, public Quackable
{
public:
    void display() {
        cout << "나는 로켓 오리다.\n";
    }
};
 
int main()
{
    Duck* duck = new RocketDuck;
 
    duck->display();
    duck->swim();
    Flyable* fd = dynamic_cast<Flyable*>(duck);
    if (fd)
        fd->fly();
}
cs

그리고 코드가 점점 복잡해진다.


소프트웨어 개발에 있어서 바뀌지 않는 것


어디에 있던, 어떤 프로그래밍 언어를 써서 무엇을 만들던 항상 우리의 곁에 있는것


변화

변하지 않는 소프트웨어는 죽은 소프트웨어 뿐


변화를 유발하는 것에는 어떤 것이 있을까?

1. 고객이나 사용자가 다른 것, 또는 새로운 기능을 요구하는 경우

2. 회사에서 데이터베이스를 바꾸고 데이터도 바꾸려는데 새 회사에서 다른 데이터 형식을 사용하는 경우

3. 안드로이드에서 개발해왔는데  ios에서도 동작해야하는 경우 즉 플래폼이 바뀌는 경우

4. 프로그래밍 언어에 더 효율적인 기능이 생겨서 바꾸고 싶은 경우

5. 코드를 유지보수하기 쉽게 리팩토링 하고 싶을 때



문제를 명확하게 파악하기


상속을 사용할 때의 문제점 : 

1. 실행시에 특징을 바꾸기 힘들다.

2. 모든 오리의 행동을 알기 힘들다.

3. 코드를 변경했을 때 다른 오리들에게 원치 않은 영향을 끼칠 수 있다.


인터페이스를 사용할 때의 문제점:

1. 오리마다 어떤 인터페이스가 있는지 체크하기 힘들다.


첫 번째 디자인 원칙

애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.


코드에 요구사항이 있을 때마다 바뀌는 부분이 있다면 그 행동을 바뀌지 않는 다른 부분으로부터 분리해야 한다

바뀌는 부분은 따로 뽑아서 캡슐화시킨다. 그러면 바뀌지 않는 부분에는 영향을 주지 않고 고치거나 확장할 수 있다."

이 개념은 모든 디자인 패턴의 기반을 이루는 원칙이다. 모든 패턴은 "시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는 방법을 제공하기 위함"이기 때문이다.



바뀌는 부분과 그렇지 않은 부분 분리하기


현재 상황에서 fly()와 quack()문제를 제외하면 Duck의 다른 부분은 잘 작동하고 있으므로 그대로 두자. 이들은 "달라지지 않는 부분" 이다.

이제 "변화하는 부분"인 두 개의 클래스 집합을 만들어야 한다. 나는 행동 집합과 꽥꽥거리는 행동 집합이다.

각 클래스 집합에는 해당 개념에 대한 모든 구현을 다 집어넣는다. 꽥꽥 거리는 것을 구현한 클래스, 삑삑 거리는 것을 구현한 클래스, 아무 소리도 내지 않는 것을 구현한 클래스 등등. 



오리의 행동 디자인


Duck 인스턴스에는 행동을 할당할 수 있어야 하고, 동적으로도 행동을 바꿀 수 있어야 한다. 즉 Duck의 행동에 대한 Setter가 있어서 런타임에 오리의 행동방식을 바꿀 수 있게 하자는 것이다. 


두 번째 디자인 원칙

구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.


자바는 몰라서.. 표현이 햇갈린다.

오리의 각 행동은 인터페이스(FlyBehavior, QuackBehavior)로 표현하고, 이 인터페이스를 상속받아 구현함으로써 하나의 행위를 표현한다. 이 때 인터페이스라는 용어가 중요한게 아니고, 다형성을 구현할 수 있는 부모클래스로 행동을 표현한다는 것이다. 즉 QuackBehavior '인터페이스'를 상속받아 구현한 클래스들이 있을 때, QuackBehavior의 quack() 메소드 하나로 꽥꽥이나 끽끽 삑삑등을 모두 표현한다는 것이다. 

C++에선 포인터로 다형성을 구현하니까 QuackBehavior* qb가 있다면 qb->quack() 하나로 끝이라는 이야기. 

구현에 맞춰서 프로그래밍을 한다는 건 Duck클래스에서 직접 구현을 하거나 자식 클래스에서 구현을 하는 방법을 말하는 것 같다. 이 경우 행동을 재사용할 수도 없고, 무슨 행동이 있는지 알기도 힘들다.

다시 정리하면 '달라지는 부분'을 '인터페이스'로 만들어서 해당 '인터페이스'의 구현 하나당 달라지는 경우 하나를 표현 할 수 있게 한다는 것.

ex : 오리의 달라지는부분 하나인 '나는 행동'을 인터페이스로 만들어서 해당 '나는 행동'의 구현 하나당 나는 행위 하나를 표현하는 것.



Duck의 행동을 구현하는 방법


#include <iostream>
using namespace std;
 
class FlyBehavior
{
public:
 
    virtual void fly() = 0;
    virtual ~FlyBehavior() {};
};
 
class FlyWithWings : public FlyBehavior
{
public:
 
    void fly()
    {
        cout << "날개를 퍼덕거리면서 날고 있다.\n";
    }
};
class FlyWithRocket : public FlyBehavior
{
public:
 
    void fly()
    {
        cout << "로켓 콰아아ㅏ아아ㅏㄱ.\n";
    }
};
class Duck
{
public:
    virtual ~Duck() {}
    void performFly() 
    {
        if (fb)
            fb->fly();
    }
    void swim() 
    {
        cout << "오리가 헤엄친다.\n";
    }
    //오리의 모습은 모두 다르므로 순수가상함수로 만든다.
    virtual void display() = 0
 
    void setFb(FlyBehavior* b)
    {
        fb = b;
    }
    void releaseFb()
    {
        if (fb)
        {
            delete fb;
            fb = nullptr;
        }
    }
private:
    FlyBehavior* fb;
 
};
 
class MallardDuck : public Duck
{
public:
    MallardDuck()
    {
        setFb(new FlyWithRocket);
    }
    ~MallardDuck()
    {
        releaseFb();
    }
    void display()
    {
        cout << "나는 청둥오리\n";
    }
};
 
int main()
{
    Duck* duck = new MallardDuck;
    duck->swim();
    duck->display();
    duck->performFly();
}
 
cs

이런 느낌인 것인가..!


이런 식으로 디자인하면 다른 형식의 객체에서도 나는 행동과 꽥꽥거리는 행동을 재사용할 수 있다. 더이상 나는 행동이 오리 안에 있지 않으니.

또 기존의 나는 행동을 수정하거나 오리를 건들이거나 하지 않고도 새로운 행동을 추가할 수 있다.



바보같은 질문은 없습니다.


애플리케이션을 먼저 구현한 다음 바뀌는 부분을 찾아 분리해서 캡슐화해야하나?

-> 애플리케이션을 디자인 하는 과정에서 바뀔 수 있는 부분을 예측하고 미리 대처해 유연한 코드를 작성할 수 있다.


클래스에는 상태와 행동이 모두 들어있어야 하는 거 아닌가요?

-> 객체지향에서는 상태(인스턴스 변수)와 메소드를 가지고 있다. 여기서는 클래스가 "행동"을 가지고 있는 것이다. 그런데 행동에도 상태와 메소드가 있을 수 있다. 행동의 속성(얼마나 빨리 나는가)을 인스턴스 변수로 넣을 수도 있을 것이다.


Duck 인터페이스도 만들어야 할까?

-> 지금은 필요 없다. Duck을 구체 클래스로 만들면 공통적인 속성과 메소드를 상속해서 얻는 장점도 있다. Duck의 상속에서 바뀔 수 있는 것들은 캡슐화해서 제거 했기 때문에 장점만을 취할 수 있다.



캡슐화된 행동을 큰 그림으로 바라보기


오리의 한 가지 행위에 대한 여러 모습을 "알고리즘군(family of algorithms)"으로 생각해보자. 여기에서의 알고리즘은 오리가 하는 행위중 한가지 모습을 나타내는데, 똑같은 테크닉을 지역에 따라 달라지는 세금 계산 방식을 구현하는 클래스에서도 활용할 수 있을 것이다.

책의 클래스 사이 관계도를 살펴보면 여러 오리 자식클래스들과 부모 클래스는 (is a), 행동에 대해서는 (has a) 관계를 갖는다. 



"A는 B이다" 보다 "A에는 B가 있다"가 나을 수 있다.


오리에 FlyBehavior와 QuackBehavior가 있는 것같이 합치는 것을 구성(composition)이라고 한다. 오리는 행동을 상속하는대신 행동을 구성함으로써 행동을 부여받는다.


세 번째 디자인 원칙

상속보다는 구성을 활용한다.


구성을 활용하면 유연성이 크게 는다. 알고리즘군을 클래스의 집합으로 캡슐화하며, 런타임에 알고리즘을 바꿀 수도 있게 해준다. 


축하합니다! 드디어 한 가지 디자인 패턴을 배웠습니다.

이와 같은 패턴을 스트래티지 패턴strategy pattern 이라고 한다.

정확한 정의는

스트래티지 패턴은 알고리즘군을 정의하고 각각을 캡슐화하여 사용할 수 있도록 한다. 스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.



디자인 퍼즐



전문 용어의 위력


패턴 용어로 대화할 때 그 패턴이 내포하고 있는 내용, 특성, 제약등을 함께 이야기할 수 있다.

간단한 단어로 많은 것을 이야기할 수 있다.

패턴 수준에서 이야기하면 설계 그 자체에 더 집중할 수 있다. (자질구레한 구현에 대해 이야기 할 필요가 없다)

전문 용어로 이야기하면 개발 팀의 능력을 극대화할 수 있다. ( 오해의 소지를 최소화하고 빠르게 작업한다)

신참 개발자들에게 훌륭한 자극제가 된다.



디자인 패턴을 어떻게 사용하나요?


디자인패턴은 라이브러리처럼 바로 코드로 들어가는 것은 아니다. 디자인 패턴은 머리 속으로 들어간다. 패턴을 익히고 나면 새로운 디자인을 할 때, 또는 이전의 코드가 유연성이 없는 스파게티 코드라는 것을 알아내고 새 패턴을 적용할 수 있다.



바보같은 질문은 없습니다.

디자인 패턴 라이브러리 같은 건 왜 없을까?

디자인 패턴은 라이브러리보다 높은 단계에 있기 때문에, 클래스와 객체를 구성해서 활용하는 건 개발자들의 몫

라이브러리나 프레임워크도 디자인 패턴 아닌지?

프레임워크나 라이브러리는 특정 구현을 제공할 뿐이며, 구현과정에서 디자인 패턴을 사용할 수는 있다. 

디자인 패턴 라이브러리 같은 건 없을까?

없다. 하지만 패턴의 목록은 알 수 있다. (패턴 카탈로그)



왜 디자인 패턴인가?

유연하고, 재사용가능하고, 관리하기 쉬운 시스템을 개발 하는 건 단순히 객체지향의 캡슐화, 추상화, 상속, 다형성을 잘 알고있는 것만으로는 매우 힘들다.

디자인 패턴은 이미 고생을 한 사람들이 만들어놓은 패턴이다. 장점이 있으면 단점도 있다. 

알맞는 디자인 패턴을 찾을 수 없는 경우라면 원칙을 잘 알고 있는 것이 도움이 된다.



디자인 도구상자


객체지향의 기초

추상화

캡슐화

다형성

상속


객체지향 원칙

바뀌는 부분은 캡슐화 한다.

상속보다는 구성을 활용한다.

구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.


객체지향 패턴

스트래티지 패턴 - 알고리즘군을 정의하고 각각을 캡슐화하여 바꿔 쓸 수 있게 만든다. 스트래티지 패턴을 이용하면 알고리즘을 활용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.


'프로그래밍 책 공부' 카테고리의 다른 글

23장 길찾기  (0) 2018.04.02
2장 템플릿  (0) 2018.03.24
게임 프로그래머를 위한 자료구조와 알고리즘 - 목차  (0) 2018.03.23
목차와 서문  (0) 2018.03.19