SOLID 디자인 법칙

Background

프레임워크를 바닥부터 새로 짜게 되었다.

이번엔 제대로 짜고 싶어서 공부를 해보다보니, Software Development, Clean Coders 정리 책에서 ‘개발자라는 사람이 SOLID 원칙도 모르는거 말이 안된다’ 하는걸 보고 충격먹어서 공부하기 시작했다.

 


SOLID 원칙

  • S : 단일 책임 원칙 (Single Responsibility Principle, SRP)
  • O : 열림-닫힘 원칙 (Open-Closed Principle, OCP)
  • L : 리스코프 치환 원칙 (Liskov Subsitution Principle, LSP)
  • I : 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
  • D : 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

이 원칙들은 2000년대 초 Clean Code의 저자인 Robert C. Martin에 의해 소개되었다.

 


단일 책임 원칙 (Single Responsibility Principle, SRP)

  • 각 클래스는 단 한가지의 책임을 부여받는다.
  • 클래스를 수정해야할 이유는 단 한가지여야한다 (i.e. 책임져야하는 기능에 대한 구현 수정).
  • 전지전능 객체는 꼭 피하자.

하나의 클래스가 거의 모든 기능을 담당하는 것을 본 적이 있을 것이다. 우리는 이것을 ‘전지전능 클래스‘라고 부른다. 전지전능 클래스는 처음 짤 때는 편하고 좋으나, 나중에 기능을 세분화하려고 하거나, 기능을 따로 떼려고 하거나, 새로운 기능을 추가하려고 하거나, 기존의 기능을 수정하려고 하거나 할 때 굉장히 골치아파진다. 전지전능 클래스 내부에 작은 수정을 하기 위해서는 해당 클래스에 정보를 주거나 받는 여러 다른 클래스들의 코드도 고쳐야하는 경우가 많다. 작은 수정을 여러 클래스에 걸쳐서 해야한다면 아키텍처에 뭔가 문제가 있는 것이다. 이처럼 코드를 수정하기 어렵게 하는 주 범인 중 하나가 전지전능 클래스이다.

예시로, ‘공책’ 이라는 클래스를 만들었다고 해보자. 우리는 이 클래스에 멤버변수로 ‘공책 제목’을 담는 string과 ‘노트 내용’을 담는 string을 모은 dynamic array를 멤버 변수로 가지고 있다. 그리고 ‘노트 작성’이라는 public 함수를 통해 그날의 일기를 쓸 수 있다고 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Notebook
{
std::string title;
std::vector<std::string> note_entries;

explicit Notebook(const std::string& title) : title{title} {}

void add(const std::string& note_entry)
{
static int count = 1;
note_entries.push_back(std::to_string(count++) + ": " + note_entry);
}
};

이 구조는 ‘공책’의 기능을 잘 표현하고 있다. 일기를 적는데에 필요한 정보가 클래스 멤버 변수로 들어있고, 적절한 메소드를 통해 멤버변수와 교류할 수 있다.

이제 이 ‘공책’에 적힌 정보를 파일로 저장하는 기능을 만든다고 해보자. 다음과 같은 코드를 짤 수 있다.

1
2
3
4
5
6
7
8
void Notebook::save(const std::string& filename)
{
std::ofstream ofs(filename);
for (const auto& entry : entries)
{
ofs << entry << std::endl;
}
}

전지전능 클래스라면 Notebook 클래스가 1. 내용 기입, 2. 저장 기능을 모두 다 가지고 있을 것이다. 호환성 좋게 2개 기능을 쓴다고 얘기할 수도 있겠지만, 기능이 조금만 세분화되고 많아지기만 해도 감당이 되지 않을 것이다. Notebook의 내용을 수정한다면? 내용을 셔플링한다면? 파일로 저장하는거 말고 클라우드에도 저장한다면? 프린트한다면? 이런것들도 다 Notebook에 들어가야하지 않을까?

Notebook의 책임은 ‘노트 항목들을 기입/관리’하는 것이지, 디스크에 쓰는 것이 아니다. 그러므로 save라는 기능은 처음부터 Notebook 클래스에 들어가면 안된다.

이러한 파일 저장 기능은 아예 별도로 취급하는 것이 좋다. 예를 들어, 다음과 같이 만들 수 있다.

1
2
3
4
5
6
7
struct DiskWriter
{
static void save(const Notebook& notebook, const std::string& filename)
{
// ...
}
}

 


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