Post

OOP Basic - S.O.L.I.D Principles

객체 지향의 특징 및 개념을 설명 하기 어렵다면 OOP Feature & Keyword을 읽고 오길 바란다.

우리는 객체 지향을 더 깊이 있게 이해하고 이를 잘 활용 하기 위해서
S.O.L.I.D Principles(솔리드 원칙)이라는 것을 학습할 것이다.


S.O.L.I.D Principles

 ✅객체 지향 설계의 원칙

S.O.L.I.D(일명: 솔리드) 원칙이란 객체 지향 설계Object-Oriented-Design에서 지켜야할 원칙이다.
이 솔리드 원칙을 어느정도 이해만 해도 코드를 작성하기 전 설계가 꼭 필요하단 것을 알게될 것이다.

무엇보다 솔리드 원칙을 이해하고 지킬려고 노력하면 코드 스킬이나 설계 능력이 알아서 향상된다.

TitleDescription
SRP (Single Responsibility Principle)하나의 클래스는 하나의 책임만을 가져야 한다.
OCP (Open/Closed Principle)소프트웨어 확장은 열려 있지만 변경에 대해서는 닫혀 있어야한다.
LSP (Liskov Substitution Principle)객체는 프로그램 정확성을 깨지 않고
하위 타입 인스턴스로 바꿀 수 있어야 한다.
ISP (Interface Segregation Principle)특정 클라이언트를 위한 인터페이스 여러 개가
범용 인터페이스 하나 보다 낫다.
DIP (Dependancy Inversion Principle)프로그래머는 추상화에 의존. 구체화에 의존해서는 안된다.
관련된 내용으로는 의존성 주입 설계가 있다.

왜 이 내용을 꼭 알아야 할까?


솔리드 원칙은 객체 지향에 기본으로써 GoF에서 고안한 디자인 패턴(DesignPattern)같은 것 또한
SOLID 설계 원칙을 준수하며 만들어진 것이기 때문에 사용할려면 완벽히 이해하고 넘어가야한다.


 좋은 프로그램을 만들기 위해

좋은 프로그램이란 클라이언트가 요청하는 서비스를 보다 쉽게 제공해줄 수 있어야 한다.
시스템에 새로운 요구사항 또는 변경사항이 발생했을 때 다른 객체들에 영향이 적어야 한다.

SOLID 객체 지향 원칙을 준수하여 코드를 작성한다면 유지보수/확장성을 챙길 수 있다.
무엇보다 불필요한 코드에 대한 복잡성을 제거해 리팩토링 소요시간을 줄이는데,
이는 곧 개발 소요시간에 직접적인 영향을 끼친다. (개발의 생산성)

즉, 좋은 프로그램은 개발자가 예기치 못한 변경사항에 대해 유연하게 대처할 수 있고
이 후 클라이언트 요구에 따른 확장에도 어려움이 없는 소프트웨어를 말한다.

🏷️ 솔리드 원칙은 어떠한 라이브러리도 프레임워크도 아니다.
또한 디자인 패턴과 같은 것도 아니다. 특정 기술에 국한되는 개념이 아니라는 것이다.
즉, 어떤 프로그래밍 언어/프레임워크에서도 적용할 수 있는 설계 원칙이다.


 S.O.L.I.D 원칙 5가지 간단 정리

솔리드 원칙은 앞서 설명했던 내용들 그대로 객체-지향-설계에 있어서 매우 중요한 개념이다.
그래서 각 원칙별로 따로 포스팅을 할 예정인데, 그 전에 5가지가 어떤 원칙인지 간략하게 알아보자!

✨SRP - 단일 책임 원칙

  • 클래스를 변경해야 하는 이유는 단 하나여야 한다.
  • 모든 코드 모듈(클래스, 함수 등)이 소프트웨어 기능에서 유일한 목적을 가져야 한다.
  • 컴포넌트 기반 유니티에서는 모든 것을 관리하는 하나의 컴포넌트가 아닌,
    특정 기능을 지니는 여러 컴포넌트를 갖는 것이 선호되며 그룹화 하여 전체 객체를 형성한다.
  • 즉, Player.cs / Enemy.cs같이 통합된 것이 아닌 PlayerInput.cs WeaponHandler.cs로 나눠야한다.

OCP - 개방/폐쇄 원칙

  • 소프트웨어 엔티티(클래스, 모듈, 기능 등)은 확장을 위해서 열려야하지만,
    반대로 수정(변경)을 위해서는 폐쇄적으로 닫혀 있어야 한다.
  • 즉 확장을 위해 새로운 필드나 요소를 모듈에 추가하지만 이를 사용하는 다른 모듈에서
    구현을 변경할 필요가 없을 때 이 원칙이 적용된다.
  • Fire()가 있는 IWeapon 인터페이스를 구현하는 객체 Gun에 대한 참조를
    지니고 있는 WeaponHandler.cpp가 있다고 가정.
  • 해당 Fire 메서드에서 무슨 일이 발생하든 이를 구현하는 클래스가 변경되든
    WeaponHandler.cpp는 그대로이며 변경되어서는 안된다. 그대로 Fire()를 호출 할 수 있어야한다.
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
class IWeapon {
	virtual void Fire() = 0;
	virtual ~IWeapon() { }
};

class Gun : public IWeapon
{
public:
	void Fire() override
	{
		std::cout << "총을 쐈습니다." << std::endl;
		// Fire 메서드 변경
	}
};

class WeaponHandler
{
private:
	IWeapon* playerWeapon;

public:
	void Attack()
	{
		// 변경 내용을 알 필요없이 Fire만 쓰면 된다.
		playerWeapon->Fire();
	}
};

LSP - 리스코프 치환 원칙

  • 하위 유형의 인스턴스는 상위 클래스 유형의 인스턴스를 대체할 수 있어야한다.
  • 다른 클래스에서 상속받은 클래스는 결과를 변경하지 않고 기본 클래스처럼 사용 가능해야한다.
  • Reaction()가 있는 NPCAI 클래스가 있다고 가정, 거기에 EnemyAICitizenAI를 만든다.
  • 둘 다 NPCAI에서 상속되지만 Reation 메서드의 구현은 다르다.
    결국 플레이어가 이들과 충돌하면 NPCAI->Reaction을 호출 하지만
    반응하는 개체가 적인지 시민인지에 따라서 결과가 달라지는 것이다.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class NPCAI
{
public:
	virtual void Reaction() = 0;
};

class EnemyAI : public NPCAI
{
public:
	void Reaction() override
	{
		std::cout << "당신은 적입니다. 사살합니다." << std::endl;
	}	
};

class CitizenAI : public NPCAI
{
public:
	void Reaction() override
	{
		std::cout << "반갑습니다. 플레이어님" << std::endl;
	}
};

void ProcessReaction(NPCAI* npc)
{
	npc->Reaction();
}

int main()
{
	Player* player = new Player();
	std::vector<NPCAI*> npcs;
	for(int i = 0; i < 4; ++i)
	{
		NPCAI* npc;
		if(i % 2 ==0)
			npc = new CitizenAI();
		else
			npc = new EnemyAI();
		npcs.push_back(npc);
	}

	for(int i = 0; i < npcs.size(); ++i)
		ProcessReaction(&npcs[i]);
}

ISP - 인터페이스 분리 원칙

  • 클라이언트(사용자)가 사용하지 않는 인터페이스에 의존하도록 강요해선 안된다.
  • 클래스가 인터페이스를 구현해야하는 경우 선언해야 하는 모든 함수를 활용,
    모든 클래스에 필요하지 않은 함수가 있는 경우 인터페이스를 두 개 이상으로 나눠야함.
  • 예시)
    • Attack(), Turn()을 지니는 인터페이스 IEnemy
    • Vampire, Zombie클래스는 이를 가질 수 있지만 Orc는 Turn이 필요 없다.
    • Orc 클래스는 IEnemy인터페이스를 구현하면 빈 함수가 생긴다. (원칙 위반)
    • IEnemy인터페이스는 Attack()만을 남기고 ITurner라는 인터페이스는 Attack()을 지니게한다.
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
31
32
33
class IEnemy
{
public:
	virtual void Attack() = 0;
	virtual ~IEnemy() { }
};

class ITurner : public IEnemy
{
public:
	virtual void Turn() = 0;
	virtual ~ITurner() { }
};

class Vampire : public ITurner
{
public:
	void Attack() override;
	void Turn() override;
};

class Zombie : public ITurner
{
public:
	void Attack() override;
	void Turn() override;
};

class Orc : public IEnemy
{
public:
	void Attack() override;
};

✨DIP - 의존관계 역전 원칙

  • 종속성 반전은 “높은 수준의 모듈은 낮은 수준의 모듈에 의존해서는 안된다.”
  • 둘 다 추상화에 의존 해야 하는 것이 의존관계 역전 원칙이다.
  • 추상화는 세부 사항에 의존하지 않고(즉, 구체화) 세부 사항은 추상화에 따라 달라진다.
  • 관련된 내용으로 의존성 주입이라는 메커니즘이 있는데, DIP내용은 보다 복잡하고
    헷갈리는 내용이 많으니 DIP 포스팅에서 추가적으로 더 다룰 예정이다.
This post is licensed under CC BY 4.0 by the author.