관리 메뉴

기억을 위한 기록들

SOLID 디자인 원칙에 관해 본문

디자인 패턴 ( Design Pattern )

SOLID 디자인 원칙에 관해

에드윈H 2023. 6. 17. 18:11

Solid 디자인 원칙은 무조건 이렇게 해야한다! 이렇게 하지 않으면 틀린다! 가 아니라 이 원칙들을 지키면 직관적인 코드가 되고 코드들의 유지보수가 쉬워진다고 한다.

 

S (Single Responsibility Principle / SRP) - 단일 책임 원칙

- 각 클래스는 단 한 가지의 책임을 부여받아, 수정할 이유가 단 한 가지여야 한다.

ex) 어떤 몬스터 클래스가 있다고 하자.

class Monster{
    int Hp;
    int Damage;
   	//..기타등등 변수들
    
    void Attack(); //공격하기 위해
    void Death(); //죽었을때 호출하기 위해    
    
    void PrintState(); // 해당 함수를 호출하면 이 몬스터 객체에 대해 로그를 남겨준다.
};

예와 같이 이런식의 클래스가 있다고 보면, 해당 클래스를 그대로 써도 무방하다. 하지만 PrintState라는 함수는 몬스터의 기능이라기보단 개발상의 상태를 출력해주기위한 함수로 단일 책임 원칙에 정당하지 않는다. 그래서 아래와 같이 분리를 해준다.

class Monster{
    int Hp;
    int Damage;
   	//..기타등등 변수들
    
    void Attack(); //공격하기 위해
    void Death(); //죽었을때 호출하기 위해        
};


class RepresentationMonster{
     void PrintState(const Monster& ref); 
     // 해당 함수를 호출하면 파라미터로 받은 몬스터 객체에 대해 로그를 남겨준다.
};

이런식으로  (이름은 길어서 마음에 당장은 안들지만) RepresentationMonster 라는 클래스로 몬스터 객체의 파라미터를 받아 출력해주어 클래스 분리가 가능하다.

 

O(Open-Closed Principle / OCP) - 열림-닫힘 원칙

1. 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소에는 수정이 일어나지 않고, 기존 구성요소를 쉽게 확장해서 재사용한다.

2. 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 해야 한다. (추상화/다형성 활용)

ex) 어떤 탈것 클래스가 있다고 치자. 

class Vehicle{
	EVehicleType type;
    //기타 등등 정보..
};



void Player::Ride(const Vehicle& target)
{	
    switch(target.type)
    {
    case EVehicleType::Car:
    	//자동차를 탄다
    	break;
    case EVehicleType::Airplane:
    	//비행기를 탄다
    	break;
    }
}

Vehicle 클래스 타입 enum에 맞춰서 Player클래스의 Ride 함수가 동작한다고 하는데, 여기까진 문제 없는데 어느날 요청에 의해 탈것 58000개가 추가 된다고하면 enum은 버티지 못할것이다...

 

여기서 이제 OCP에 기반에 의해 수정해준다고 하면, Interface클래스나 abstract 클래스로 바꿔주어야한다.

class Vehicle{
	virtual void Ride();
};


class Car : public Vehicle
{
	virtual void Ride() override; //Car의 Ride 함수정의
};

class Airplane : public Vehicle
{
	virtual void Ride() override; //Airplane의 Ride 함수정의
};

void Player::Ride(const Vehicle& target)
{
	target.Ride();//원하는 탈것의 동작을 이루어지게 된다.
}

위와같이 변경하게 되면 탈것이 다른것이 추가 되더라도 Ride 함수를 수정 할 필요 없고, Vehicle을 상속받는 클래스만 추가 해주고 원하는 Ride함수의 동작을 넣어주면 된다.

 

L (Liskov Substitution Principle / LSP) - 리스코프 치환 원칙

* 이름의 유래는 바바라 리스코프의 이름을 따서 만들었다.

- 어떤 자식 객체에 접근할 때 그 부모의 객체의 인터페이스로 접근하더라도 아무런 문제가 없어야 한다는 것을 의미. 즉, 자식 객체를 부모 객체와 동등하게 취급할 수 있어야 한다.

 

예를 들어 어떤 Monster 클래스가 있고 몬스터가 공격 할 수 있는 Attack이라는 함수가 있다고 해보자. 그리고 GhostMonster가 해당 클래스를 상속받고 동일하게 Attack을 할수 있게 되었다.

class Monster
{
	virtual void Attack(); //몬스터가 공격한다.
}


class GhostMonster : public Monster
{
	virtual void Attack() override; //고스트 몬스터가 공격한다.
}

이렇게 치환해도 가능하다.

//
Montser* monster1 = new Montser();
monster1->Attack() //공격!

monster1 = New GhostMonster();
monster1->Attack() //GhostMonster의 공격!

하지만.. 여기서 갑자기

 

class HPItem : public Monster
{
	//HP Item이.. 공격한다?...
	virtual void Attack() override
    {
    	//Exception 발생!
    }
}

 

//
Montser* monster1 = new Montser();
monster1->Attack() //공격!

monster1 = New HPItem();
monster1->Attack() //에러 발생!

이렇게 실행하면 Exception이 발생한다... 즉, 정리하자면 처음부터 클래스 구조를 짤때 전체적인 요소를 잘 확인하거나, 문제가 발생하지 않는지 확인한다.. 어찌보면 당연히 이렇게 할리 없겠지만은 여러 프로그래머가 클래스를 만들게 되면서 클래스가 많아지고 (어떤 클래스가 있는지 모르고) 구조가 복잡해지면 발생하지 않는다고 보장은 못할것이다...

 

I  (Interface Segregation Principle / ISP) - 인터페이스 분리 원칙

- 인터페이스 모든 항목에 대한 구현은 강제하지 않고, 실제 필요한 인터페이스만 구현할 수 있도록 하는 것. 즉, 가능한 최소한의 인터페이스만 구현한다로 사용하지 않는 인터페이스는 있지 않아야 한다는 것이다.

ex) 예를 들어 어떤 아이템 인터페이스가 있다고하자.

Interface IItem
{
    void Eat(); //먹다.
    void Throw(); //버리다.
};


class HPItem : public IItem
{
    void Eat(); //hp 먹다.
    void Throw(); //버리다.
};

그 인터페이스를 상속받는 HP Item 클래스가 있다. 먹고 버릴수 있는 기능까지는 문제가 없지만 예를 들어서, 해당 인터페이스를 상속받는 무기 클래스가 생겨서 Attack과 Broken이라는 함수가 추가 되어 버렸다.

 

Interface IItem
{
    void Eat(); //먹다.
    void Throw(); //버리다.
    
    
    void Attack(); //공격하다
    void Broken();//부숴지다.
};


class HPItem : public IItem
{
    void Eat(); //hp아이템 먹다.
    void Throw(); //버리다.
    
    //두개 함수는 안쓰게된다.
    //void Attack(); //공격하다 
    //void Broken();//부숴지다.
};

class WeaponItem : public IItem
{
    //두개 함수는 안쓰게된다.
    //void Eat(); //먹다.
    //void Throw(); //버리다.
    
    void Attack(); //공격하다
    void Broken();//부숴지다.
};

이렇게 되면 HP하고 Weapon 각각에서 사용하지 않는 함수들이 들어오게 되고, 인터페이스 분리 원칙에 어긋난다. 여기서는 인터페이스를 분리해주라는 것이다. 그 결과는 아래와 같다.

Interface IEatItem
{
    void Eat(); //먹다.
    void Throw(); //버리다.
};


Interface IWeaponItem
{
    void Attack(); //공격하다
    void Broken();//부숴지다.
};

class HPItem : public IEatItem
{
    void Eat(); //hp아이템 먹다.
    void Throw(); //버리다.
};

class WeaponItem : public IWeaponItem
{
    void Attack(); //공격하다
    void Broken();//부숴지다.
};

 

 

D (Dependency Inversion Principle / DIP ) - 의존성 역전 원칙

- 상위모델은 하위모델에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다.

- ex) 어떤 특정 Stage 공간을 관리하는 클래스가 있다고 하자

class Player{
	//플레이어 클래스의 기능들..
};

class Monster{
	//어떤 몬스터 클래스의 기능..
};

class StageManager
{
    Player* plyer;
    Monster* BossMonster;
    Monster* EasyMonster;
};

 

그런데 여기서 StageManager에 계속해서 플레이어들과 몬스터들이 추가 된다고 하면,  StageManager클래스 안에 계속해서 객체들을 추가해줘야한다. (실제론 동적으로 추가하면서 관리하겠지만 해당 원칙을 설명하기 위해 예시를 이렇게 들었다.)

DIP를 사용하게 된다고 하면,

 

 

class StageSpawnObj
{
};

class Player : public StageSpawnObj
{
	//플레이어 클래스의 기능들..
};

class Monster: public StageSpawnObj
{
	//어떤 몬스터 클래스의 기능..
};

class StageManager
{
	void AddObject(StageSpawnObj* newTarget) ;//추가 시켜주면서 관리한다...
    
};



StageManage* manager = StageManager();
manager.AddObject(Monster());
manager.AddObject(Player());

이렇게  StageSpawnObj 라는 임의 클래스를 추가 해줌으로써 새로운 Object가 추가되더라도 StageManage를 수정하는 일은 크게 없어진다.