객체 지향 설계(SOLID)

Understanding SOLID for object oriented programming

Posted by dydtjr1128 on September 09, 2019 · 6 mins read OOP

1. Intro

객체 지향의 4대원칙(캡슐화, 상속, 추상화, 다형성)과 더불어 객체 지향의 원칙을 살려 설계 하는 방법을 가진 SOLID가 존재한다. 5가지의 내용의 앞 단어를 따서 만들어 진 내용이다.

SOLID 원칙들은 결국 자기 자신 클래스 안에 응집도는 내부적으로 높이고, 타 클래스들 간 결합도는 낮추는 High Cohesion - Loose Coupling 원칙을 객체 지향의 관점에서 도입한 것이다.

좋은 소프트웨어는 응집도가 높고 결합도가 낮다(High Cohesion - Loose Coupling). 클래스 당 하나의 책임을 주어 결합도를 낮추는 것인데, 이러한 방식으로 되어 있어야 소프트웨어의 재 사용성이 높아지고 유지보수 하기가 쉬워진다.

2. SOLID

SOLID 설계원칙은 다음과 같이 5가지로 나뉘어져 있다.

  • SRP(Single Responsibility principle) : 단일 책임 원칙
  • OCP(Open Closed Principle) : 개방 폐쇄 원칙
  • LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle) : 의존 역전 원칙

2.1 SRP(Single Responsibility principle) : 단일 책임 원칙

단일 책임 원칙은 하나의 클래스에 다양한 기능을 주는것이 아니라 하나의 기능을 하도록 만드는 것이다.

SRP

위의 그림처럼 개발자가 먹는 기능과 코딩하는 기능 모두 하는 것이 아니라 사람은 먹는 기능을 하고, 개발자는 코딩 기능을 하는 구조로 구현해야 한다.

개발자가 먹는 기능도 하고 코딩하는 기능도 한다면, 변경의 여지가 있기 때문에 SRP에 위반된다.

만약 개발자가 아닌 기획자가 추가된다면 어떻게 될까?

그렇다면 또다른 eat을 가진 기획자를 만들어야 한다. 그렇기 때문에 하나의 클래스가 하나의 책임을 하도록 만드는 것이 중요하다.

2.2 OCP(Open Closed Principle) : 개방 폐쇄 원칙

SRP

개방 폐쇄 원칙은 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다는 것을 뜻한다. 위와 같은 그림이 존재할때, 개발자의 달리는 기능과 디자이너의 달리는 기능이 같게 만들고 싶을 때 하나를 수정하면 나머지도 수정해야한다.

하지만 OCP(개방 패쇄 원칙) 의거하여 수정하면 우측과 같이 Human이라는 부모 클래스를 두고 공통된 내용을 구현하면된다.

그렇게 되면 따로 코드 수정이 없으면서도 다른 형태의 클래스가 추가되더라고 문제없이 확장적으로 추가할 수 있다.

위의 예시 외에도 대표적으로 OCP를 지키는것에는 JDBC가 있다.
JDBC는 Oracle, MySQL 등 타 DB 종류와 상관없이 공통된 인터페이스를 이용해서 접근 할 수 있도록 구현되어있다. 이러한 것은 새로운 DB가 나타나더라도 확장적으로 사용이 가능하다.

2.3 LSP(Liskov Substitution Principle) : 리스코프 치환 원칙

리스코프 치환 법칙의 정의는 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다는 것이다.

즉, 다시 말하자면 업캐스팅(Upcasting)된 객체가 그 객체의 기능을 문제 없이 실행 할 수 있어야 한다는 것이다.

위의 2.2 OCP에서 나온 예제를 예로 들어서 설명해 보려고 한다.

int main(){
  Human *human = new Developer();
  human->eat();
}

위와 같은 코드가 존재할 때, human은 상위 객체이지만 문제없이 본인의 일을 충실히 수행한다. 마찬가지로 Designer로 변경하더라도 LSP를 위반하지 않는다.

하지만 하위 자식으로 Toy가 존재한다고 할 때, 컴파일 상에는 문제가 생기지 않지만 Toy는 eat이라는 기능을 수행하지 못하지만 연결된 상태로 있으므로 본인의 일을 수행할수 없어 LSP에 위반하는 결과를 발생시킨다.

즉, LSP(리스코프 치환 원칙)은 하위클래스가 상위클래스 역할을 대신할 때 논리적으로 맞아 떨어져야 한다.

2.4 ISP(Interface Segregation Principle) : 인터페이스 분리 원칙

인터페이스 분리 원칙은 클라이언트에 특화되도록 기능을 분리시키라는 설계 원칙이다.

SRP

위의 그림처럼 개발자가 상황에 맞게 앱 개발 혹은 웹 개발을 해야 할 수 있다. 그렇기 때문에 이 기능들을 Developer안에 모두 넣기보다는 인터페이스로 분리하고, 안드로이드 개발 기능이 필요하면 AndroidDevelopable을 구현해야한다.

즉 ISP(인터페이스 분할 원칙)을 적용하여, 각 역할에 맞는 메서드만 제공하도록 수정해야한다.

#include <iostream>

class Eatable {
public:
  virtual void eat() = 0;
};

class WebDevelopable {
public:
  virtual void useCSS() = 0;
};

class AndroidDevelopable {
public:
  virtual void developAndroidLayout() = 0;
};

class Developer : public Eatable, public WebDevelopable, public AndroidDevelopable {
public:
  void eat() {
    std::cout << "Developer::eat()" << std::endl;
  }
  void developAndroidLayout() {
    std::cout << "Developer::developAndroidLayout()" << std::endl;
  }
  void useCSS() {
    std::cout << "Developer::useCSS()" << std::endl;
  }
};

int main() {
  WebDevelopable* webDeveloper = new Developer();
  webDeveloper->useCSS();

}

위의 코드는 그림을 코드로 구현한 예이다.

상황에 맞게 Web 개발자라면 WebDeveloper로 업캐스팅을 하여 WebDeveloper에 필요한 기능만 사용하도록 제한 할 수 있다.

2.5 DIP(Dependency Inversion Principle) : 의존 역전 원칙

의존 역전 원칙은 자신보다 변하기 쉬운 것에 의존해서는 안된다는 의미를 가지고 있다.

쉽게 생각하면 하위클래스나 구체 클래스와 같이 바뀌기 쉬운 부분에 의존해서는 안된다는 의미이다.

SRP

위의 그림은 DIP를 위반하는 그림이다.
왜냐하면 타이어는 계절에 맞게 상황에 맞게 광폭타이어로 바뀔수도, 일반 타이어일수도, 스노우 타이어 일수도 있다.

즉, 현재에는 타이어에 의존적인 형태를 띄고 있다.

그렇기 때문에 이렇게 바뀌기 쉬운 부분은 의존성을 줄여주어야 한다.(Loose Coupling)

SRP

위처럼 변경한다면 인터페이스로 타이어를 가지고 있기 때문에 타이어에 의존하지 않아도 된다. 타이어가 추상화된 Tire에 의존적이어야 하기 때문에 DIP(의존 역전 원칙)이 성립하게 된다.

위의 그림은 어떤 타이어를 사용하던 타이어에 의존을 피했기 때문에 문제가 발생하지 않는다.

Review

이처럼 5가지의 SOLID 설계 원칙에 대해서 알아보았다. 공부하면서 글을 작성했지만 아직도 햇갈리는부분이 많다고 생각한다. 하지만 이러한 내용을 잘 이해하고 실전에서 사용한다면 프로그램의 유지보수 측면에서 굉장한 이득을 볼 수 있다고 생각한다.