티스토리 뷰

Programming

SOLID 원칙

군자동꽃미남 2019. 10. 29. 02:21

SOLID 원칙

SRP 단일 책임 원칙

OCP 개방-폐쇄 원칙

LSP 리스코프 치환 원칙

ISP 인터페이스 분리 원칙

DIP 의존 역전 원칙

 

 

= Single Responsiblity Principle 단일 책임 원칙 =

소프트웨어의 설계 부품(클래스, 함수 등)은 단 하나의 책임(기능)만을 가져야 한다.

 

응집도는 높고 결합도는 낮은 프로그램을 목표. 클래스가 수행할 수 있는 기능, 즉 책임이 많아지면 클래스 내부 함수끼리 강한 결합을 발생할 가능성이 높아진다. 이는 유지보수에 어려움이 따르므로 분리시킬 필요가 있다.

class cSocket
{
    ...
}

class cBind
{
    ...
}

class cServer
{
    cSocket m_socket;
    cBind   m_bind;
    ...
}

 

 

=Open-Closed Principle 개방-폐쇄 원칙=

기존의 코드를 변경하지 않고(Closed) 기능을 수정하거 추가할 수 있도록 (Open) 설계해야 한다.

 

클래스 설계시 자주 변경되는 것이 무엇인지에 초점을 맞춘다. 자주 변경되는 내용은 수정하기 쉽게 설계 하고, 변경되지 않아야 하는 것은 수정되는 내용에 영향을 받지 않게 하는 것이 중요하다. 이를 위해서, 가상 함수를 이용한 Interface에 의존하도록 코드를 작성한다. 즉, 추상화와 다형성을 적극 활용한다.

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

class cArcher : public cJob
{
public:
    void attack()
    {
    	//TODO:: 원거리 공격
    }
};

class cWarrior : public cJob
{
public:
    void attack()
    {
        //TODO:: 근접 공격
    }
};

class cUser
{
public:
    cJob* m_job;

    void set_user_job(cJob* _pJob)
    {
        m_job = _pJob;
    }
};

int main()
{
    cUser user;
    user.set_user_job(new cWarrior);
    user.m_job->attack();
}

 

 

= Liskov Substitution Principle 리스코프 치환 원칙 =

자식 클래스는 부모 클래스에서 가능한 행위를 수행할 수 있어야 한다.

 

즉, 기존에 A 클래스 객체를 통해 사용하던 함수 또는 기능들로 인해 A 클래스를 상속받은 B 클래스 객체를 사용할 때 제한사항이 없어야 한다.

 

리스코프 치환원칙에서 가장 유명한 직사각형과 정사각형 예제가 있다.

먼저, 직사각형을 생성해주는 클래스 개발을 요청받았다고 가정한다. 개발된 클래스는 다음과 같다.

class cRectangle
{
public:
    int m_width;
    int m_height;
    
    void set_width(int _w)
    {
    	m_width = _w;
    }
    
    void set_height(int _h)
    {
    	m_height = _h;
    }
    
    int get_width()
    {
    	return m_width;
    }
    
    int get_height()
    {
    	return m_height
    }
    
    int get_area()
    {
    	return m_width * m_height;
    }
};

int main()
{
    while (true)
    {
    	cRectangle* rect = new cRectangle();
    	rect.set_height(5);
    	rect.set_width(4);
    }
    return 0;
}

그다음, 직사각형의 면적이 20이 아닌 경우, 프로그램을 종료시키는 코드를 추가해 달라는 새로운 요청이 들어왔고, 이를 수용하기 위해 다음과 같이 코드를 수정하게 된다.

int main()
{
    while (true)
    {
    	cRectangle* rect = new cRectangle();
    	rect.set_height(5);
    	rect.set_width(4);
        
        if (rect.isCheck() != false)
        {
            exit(0);
        }
    }
    return 0;
}

 

여기까지는 아무 문제가 없다. 그러다 시간이 흘러 새롭게 개발 요청이 들어오게 되었다. 바로 정사각형을 생성할 수 있도록 해달라는 요청이다. 새로운 클래스를 설계하기 보다는 cRectangle을 상속받아 정사각형을 구현하도록 한다.

class cRectangle
{
public:
    int m_width;
    int m_height;
    
    virtual void set_width(int _w)
    {
    	m_width = _w;
    }
    
    virtual void set_height(int _h)
    {
    	m_height = _h;
    }
    
    int get_width()
    {
    	return m_width;
    }
    
    int get_height()
    {
    	return m_height
    }
    
    int get_area()
    {
    	return m_width * m_height;
    }
    
    bool isCheck()
    {
        if (get_area() == 20)
            return true;
        return false;
    }
};

class cSquare : public cRectangle
{
    void set_width(int _w)
    {
    	m_width = _w;
        m_height = _w;
    }
    
    void set_height(int _h)
    {
    	m_width = _h;
        m_height = h;
    }
}

int main()
{
    while (true)
    {
    	cRectangle* rect = new cSquare();
    	rect.set_height(5);
    	rect.set_width(4);
        
        if (rect.isCheck() != false)
        {
            exit(0);
        }
    }
    return 0;
}

정사각형을 만들어야 하는 클래스이므로, set_width(...) 함수와 set_height(...) 함수를 오버라이딩하였다. 그리고 나서 직사각형 생성에서 정사각형 생성으로 변경하기 위해 main 함수의 코드를 수정하였다.

 

하지만, 원하던 결과를 얻지 못하고 기존 cRectangel::isCheck() 함수를 사용하여 면적을 확인하는 과정으로 인해 프로그램이 계속해서 종료가 된다. 이것이 바로 리스코프 치환의 원칙을 어겨서 일어난 일이다.

 

자식 클래스는 부모 클래스에서 가능한 행위를 수행할 수 있어야 한다.

 

다시 한번 정의를 살펴보면, 기존 cRectangle을 사용할 때 정상적으로 수행했던 프로그램 실행 과정이 이를 상속받아 cSquare를 사용하여 프로그램을 실행하여도 전혀 상관없이 정상적으로 실행이 되어야 한다는 뜻이다.

 

위의 문제를 해결하기 위해서는, cRectangle::isCheck()를 자식 클래스에서 오버라이딩할 수 있도록 virtual 키워드를 추가해 주는 방식과 상속 관계를 제거하는 방법이 있다.

 

결과적으로 LSP를 유의하여 클래스 설계를 하면, 무분별한 자식 클래스의 생성을 막을 수 있고, 부모 자식간 코드가 뒤엉키는 사태를 사전에 방지할 수 있다.

 

 

= Interface Segregation Principle 인터페이스 분리 원칙 =

한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스보다는, 여러개의 구체적인 인터페이스가 낫다.

 

다시 말해, 자신이 사용하지 않는 기능(인터페이스)에는 영향을 받지 말아야 한다는 의미이다.

class cNPC
{
public:
    bool m_enable;
    int m_hp;
    int m_mp;
    int m_id;
    
    virtual void init(int _hp, int _mp, int _id) = 0;
    virtual void conversation() = 0;
    virtual void attack_to_player() = 0;
}

class cStore : public cNPC
{
public:
    vector<int> m_item_list;
    
    void init()
    {
    	//TODO:: init Store NPC.
    }
    
    void conversation()
    {
    	//TODO:: open store and item list.
    }
    
    void attack_to_player()
    {
    	//TODO:: empty.
    }
}

class cMonster : public cNPC
{
public:
    int m_def;
    int m_att;
    
    void init()
    {
    	//TODO:: init Monster.
    }
    
    void conversation()
    {
    	//TODO:: empty.
    }
    
    void attack_to_player()
    {
    	//TODO:: attack users nearby.
    }
}

위의 코드를 보면 cNPC를 부모 클래스로, cStore와 cMonster 이름의 자식 클래스가 있다. cNPC를 인터페이스로 상속을 받아 사용하기 때문에 자식 클래스는 cNPC::init, cNPC::conversation, cNPC::attack_to_player 함수의 내부를 필수적으로 구현해 주어야 한다. 그러나 cStore 클래스에서는 cNPC::attack_to_player 함수의 사용이 필요가 없다. 또한 cMonster 클래스에서 cNPC::conversation 함수도 마찬가지이다.

 

이는 ISP 원칙에 위배가되며, 인터페이스로 사용한 cNPC 클래스에서 해당 두 함수를 다른 인터페이스 클래스로 구현을 하거나, 자식 클래스에서 내부 함수로 구현하는 방향으로 수정을 해야 한다.

 

다음 코드는 cNPC::conversation과 cNPC::attack_to_player 함수를 제거하고, 각각 자식 클래스의 내부 함수로 구현한 방식이다.

class cNPC
{
public:
    bool m_enable;
    int m_hp;
    int m_mp;
    int m_id;
    
    virtual void init(int _hp, int _mp, int _id) = 0;
}

class cStore : public cNPC
{
public:
    vector<int> m_item_list;
    
    void init()
    {
    	//TODO:: init Store NPC.
    }
    
    void conversation()
    {
    	//TODO:: open store and item list.
    }
}

class cMonster : public cNPC
{
public:
    int m_def;
    int m_att;
    
    void init()
    {
    	//TODO:: init Monster.
    }
    
    void attack_to_player()
    {
    	//TODO:: attack users nearby.
    }
}

 

 

= Dependency Inversion Principle 의존 역전 원칙 =

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

 

말이 너무 어렵지만, 간단하게 업캐스팅을 생각하면 된다.

 

MMORPG 게임 개발을 한다고 가정을 한다. 먼저, 유저가 플레이할 캐릭터에 대한 클래스를 설계하도록 한다. 이 클래스의 이름은 cPlayer로 정하도록 한다.

class cPlayer
{

...

};

게임을 하게 되면 캐릭터에 다양한 정보들이 있다는 것을 알 수 있다. 그 중 캐릭터의 직업은 플레이어마다 다르게 되어 있다. 따라서 캐릭터의 직업을 나타내는 cJob 클래스를 새롭게 만들어주고, cPlayer와 has a 관계로 설계한다.

class cPlayer
{
public:
    cJob m_job;		// has a
};

class cJob
{
public:
    int m_attack_range;
    int m_damge;
    int m_def;
    
    void attack();
};

그러나 직업에는 다양한 종류가 분포되어 있으므로, cJob 클래스를 상속받아 Is a 관계의 자식 클래스들을 생성해주도록 한다.

class cJob
{
public:
    int m_attack_range;
    int m_damge;
    int m_def;
    
    virtual void attack() = 0;
};

class cWarrior : public cJob
{
    void attack()
    {
        //근접 공격
    }
};

class cArcher : public cJob
{
    void attack()
    {
        //원거리 공격
    }
};

 

 

이제 플레이어가 다양한 직업을 선택할 수 있도록 cPlayer::m_job 멤버 변수를 변경해주고, 선택된 직업에 따라 정상적으로 작동할 수 있도록 수정해 주어야 한다. 여기서 정의를 한번 더 생각해보자.

 

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

 

고수준 모듈은 플레이어의 캐릭터를 나타내는 cPlayer가 되고, 저수준 모듈은 캐릭터의 직업을 나타내는 cWarrior, cArcher가 된다.

 

고수준 모듈이 저수준 모듈의 구현에 의존하게 되는 상황 다음과 같다. 플레이어의 캐릭터마다 직업이 다른데, cJob 클래스를 통한 has a 관계로 멤버변수를 선언하지 않고, 각 직업을 나타내는 cWarrior, cArcher 클래스의 객체를 has a 관계로 선언하게 되면, cPlayer 클래스로만 끝나는게 아닌 cPlayer_warrior, cPlayer_archer 같은 많은 클래스들이 필요로 하게 된다.

 

이는 DIP 원칙에 어긋나는 행위이며, 이를 방지하기 위해 cJob 클래스를 인터페이스 클래스로 개발하여 업캐스팅을 통한 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존할 수 있도록 개발한다.

 

다음은 최종적으로 개발이 끝난 cPlayer 클래스이며, 업캐스팅을 통해 cJob이 담고 있는 직업에 따라 정상적인 게임 플레이가 가능하도록 해준다.

class cPlayer
{
public:
    cJob* m_job;		// has a
    
    void set_job(cJob* _job)
    {
        m_job = _job;
    }
};

int main()
{
    cPlayer user;
    user.set_job(new cWarrior);	// up-casting
    
    return 0;
}

'Programming' 카테고리의 다른 글

OS - Context Switch(컨텍스트 스위치)가 무엇인가?  (0) 2019.11.04
Race Condition  (0) 2019.11.01
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함