프로그래밍 일반/객체 지향

SOLID 디자인 원칙

지노윈 2019. 11. 3. 23:28
반응형
객체지향 프로그래밍에서, SOLID는 5가지 디자인 원칙들의 첫 글자들을 결합입니다. 이 원칙들은 소프트웨어 디자인을 더욱 이해하기 쉽고 유연하고 유지보수 하기 쉽게하는 원칙들입니다. Robert C. Martin에의해 제안된 여러 원리들중의 일부입니다. SOLID 원칙은 많은 객체 지향 디자인에 적용 되지만 agile development 또는 adaptive software development 와 같은 개발 방법론의 핵심 철학이 됩니다.
 

SOLID - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Object-oriented programming design principles This article is about the SOLID principles of object-oriented programming. For the fundamental state of matter, see Solid. For other uses,

en.wikipedia.org

 

SOLID 디자인 원칙, 로버트 마틴(Uncle bob)에 의해 소개되었던 자료는 아래 링크에서 살펴볼 수 있습니다.

http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

 

ArticleS.UncleBob.PrinciplesOfOod

The Principles of OOD What is object oriented design? What is it all about? What are it's benefits? What are it's costs? It may seem silly to ask these questions in a day and age when virtually every software developer is using an object oriented language

butunclebob.com

좋은 객체 지향을 설계를 하기위해서는 SOLID 원칙을 따르는 것이 좋습니다.

디자인 패턴들을 공부하기 전데 이 원칙들을 안다면 디자인 패턴들의 좀더 깊게 이해 할 수 있습니다.

 

 

SOLID 원칙들을 아래와 같이 펼쳤을 때는 뭔가 복잡해 보이지만, 하나 하나 살펴보면 단순하여 알기 쉽습니다.

  • S - Single-responsiblity principle, 단일 책임 원칙
  • O - Open-closed principle, 열림-닫힘 원칙
  • L - Liskov substitution principle, 리스코프 치환 원칙
  • I - Interface segregation principle, 인터페이스 분리 원칙
  • D - Dependency Inversion Principle, 의존성 역전 원칙

 

S.R.P(Single responsibility principle) 단일 책임 원칙

하나의 클래스는 하나의 책임을 가지며 원경되는 이유는 하나여야 합니다. 즉, 하나의 클래스는 하나의 작업 만을 책임져야 합니다.

저널이 저널에 글 추가(add) 책임과 저장(save) 책임을 모두 가지면 S.R.P 위반입니다.

두 클래스(Journal, PersistenceManager)로 분리하여 각각이 하나의 책임을 갖도록 합니다.

  Journal journal("My Journal");
  journal.add("First Entry");
  journal.add("Second Entry");
  journal.add("Third Entry");

  // 저장의 책임을 Journal이 가지면 S.R.P 위반입니다.
  // journal.save("journal.txt");
  
  // 저장 기능을 위해 별도의 클래스로 분리합니다.
   PersistenceManager().save(journal, "journal.txt");

O.C.P(Open Closed Principle) 열림 - 닫힘 원칙

오브젝트들 또는 엔티티들은 확장에 열려 있어야 하지만 수정에는 닫혀야 합니다.

간단히 말하면, 한 클래스는 자신 클래스의 변경 없이 쉽게 확장 될 수 있어야합니다.

  • 수정에 닫힘 : 자신 클래스를 직접 수정하지 않고 
  • 확장에 열림 : 클래스 외부에서 쉽게 기능을 확장

그래픽 에디터가 있고 원과 사각형을 그리는 기능을 가지고 있습니다.

struct GraphicEditor 
{
	void drawShape(Shape s) 
	{
		if (s.type == 1)
			drawRectangle(s);
		else if (s.type == 2)
			drawCircle(s);
	}
	void drawCircle(Circle r) { .... }
	void drawRectangle(Rectangle r) { .... }
};

선을 그리는 기능을 추가 한다면 GraphicEditor 클래스 자신에 기능을 추가 해야합니다.

다음과 같이요. 이는 O.C.P 위반입니다.

struct GraphicEditor 
{
	void drawShape(Shape s) 
	{
		if (s.type == 1)
			drawRectangle(s);
		else if (s.type == 2)
			drawCircle(s);
		else if (s.type == 3)		// * 추가
			drawLine(s);
	}
	void drawCircle(Circle r) { .... }
	void drawRectangle(Rectangle r) { .... }
	void drawLine(Line r) { .... }		// * 추가
};

상속을 이용한 다형성과 Composition을 이용하여 다음과 같이 O.C.P를 준수하여 구현 할 수 있습니다.

struct GraphicEditor
{
	Shape& shape;
	GraphicEditor(Shape s) : shape(s) {}

	void drawShape()
	{
		shape.draw();
	}
};

struct Shape
{
	virtual void draw() = 0;
};

struct Rectangle : public Shape {
	void draw() { ... }
};

struct Circle : public Shape {
	void draw() { ... }
};

struct Line : public Shape {
	void draw() { ... }
};

GraphicEditor에 어떠한 수정도 없이 Line 클래스 구현 만으로 기능을 확장 하였습니다.

이 예제의 패턴은 앞으로도 디자인 패턴을 공부하면서도 많이 나오고 이러한 형태의 구현을 Composition이라고 합니다. 이러한 구현이 생소 하다면 꼭 직접 타이핑하여 자기 것으로 만들고 자신의 실제 구현에서도 꼭 응용하여 자기 것으로 만들기를 추천 드립니다. Composition은 아주 아주 중요합니다.

L.S.P(Liskov Substitusion Principle) 리스코프 치환 원칙

자식 클래스는 언제나 자신의 부모클래스를 교체할 수 있다는 원칙입니다. 즉, 부모클래스 위치에 자식 클래스를 넣어도 동일하게 동작해야 합니다.

가장 전형적인 예로, 너비와 높이를 얻는 직사각형 부모 클래스로 부터 파생된 정사각형 자식 클래스의 예입니다.

class Square : public Rectangle {
 public:
  explicit Square(int size) : Rectangle{size, size} {}

  void SetWidth(const int width) override { this->width = height = width; }	// width, height 함께 변경
  void SetHeight(const int height) override { this->height = width = height; } // width, height 함께 변경
};

void process(Rectangle& r) {
  int w = r.GetWidth();
  r.SetHeight(10);

  std::cout << "expect area = " << (w * 10) << ", got " << r.Area() << std::endl;
}
  Rectangle r{5, 5};
  process(r);

  Square s{5}; // L.S.P 위반
  process(s);

부모 클래스 Rectangle을 자식 클래스 Square로 치환하면 문제가 발생한다. Square 클래스의 set_hieght 함수가 width 값도 함께 변경하기 때문입니다. 두 process로 얻어 지는 면적의 결과가 다릅니다.

I.S.P(Interface Segregation Principle) 인터페이스 분리 원칙

클라이언트는 사용하지 않는 인터페이스 구현을 강제하지 말아야 합니다. 혹은, 클라이언트들은 사용하지 않는 메서드에 의존을 강제하지 말아야 합니다.

아주 단순하고 명확안 원칙이며 당연한 말이죠.

  Printer printer;
  Scanner scanner;
  Machine machine(printer, scanner);
  std::vector<Document> documents{Document(std::string("Hello")),
                                  Document(std::string("Hello"))};
  machine.print(documents);
  machine.scan(documents);

Printer에게 scan을 구현 하도록 또는 Scanner에게 print를 구현 하도록 강제하지 않았습니다.

각각의 인터페이스를 분리하여 구현 하였으며 Machine이 이 두 오브젝트를 갖도록 하여 복합기 처럼 scan과 print 기능이 모두 동작하도록 하였습니다. 

D.I.P(Dependency Inversion Principle) 의존성 역전 원칙

엔티티들은 구상(Concretions)이 아니라 추상(Abstractions)에 의존해야 합니다. 높은 레벨의 모듈은 낮은 레벨의 모듈에 의존하지 말아야하며 이 모듈들은 추상에 의존해야 합니다.

프로그래밍에서, D.I.P는 소프트웨어 모듈의 결합도를 낮추는 하나의 방법입니다.

  • 높은 레벨 모듈들은 낮은 레벨 모듈들에 의존하지 말아야 합니다. 이 둘은 추상에 의존해야 합니다.
  • 추상은 구상에 의존하지 말아야 합니다. 구상은 추상에 의존해야합니다.

추상은 인터페이스이며 어떠한 기능이 동작하는 순서 즉 알고리즘을 의미 합니다. 구상은 인터페이스의 기능을 상세하게 구현한 구상 클래스를 의미 합니다. 즉, 기능의 호출의 나열은 추상에 호출은 기능의 상세 구현은 구상에 맞기는 것입니다.

[객체 지향 프로그래밍] - 구상(Concrete) 클래스, 추상(Abstract) 클래스

 

이는 아주 간단한 예지만 D.I.P를 아주 잘 표현합니다. SqlConnection이라는 구상 클래스에 직접적으로 의존하지 않고 IDbConnection이라는 인터페이스에 의존합니다.

void ClientCode(SqlConnection sqlConnection) {
   sqlConnection.Open();
}

void ClientCode(IDbConnection dbConnection) {
   dbConnection.Open();
}

 

D.I.P를 설명하는 구체적인 예는 이것 보다 훨씬더 복잡하고 설명할 것도 많이만 이 정도의 개념만 갖고 있어도 충분하기에 더 이상 복잡한 설명은 생략합니다. D.I.P로 부터 파생되어 Dependency Injection(의존성 주입)과 Inversion of control(IoC)가 있으며 따로 내용 찾아 보길 권장드립니다.