좋은 소프트웨어 시스템은 깔끔한 코드(clean code)로부터 시작한다. 하지만 좋은 코드를 사용하더라도 아키텍처를 엉망으로 만들 수도 있다. 그 반대도 가능하다. 그래서 좋은 벽돌로 좋은 아키텍처를 정의하는 원칙이 필요한데 그것이 SOLID이다.

SOLID 원칙은 함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명해준다. SOLID 원칙의 목적은 중간 수준의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.

  • 변경에 유연하다.
  • 이해하기 쉽다.
  • 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.

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

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

  • 각 소프트웨어 모듈은 변경의 이유가 단 하나뿐이어야만 한다. 모든 모듈이 단 하나의 일만 해야 한다는 의미는 아니다. 하나의 모듈은 오직 하나의 액터에 대해서만 책임져야 한다는 것이다. 공유된 알고리즘을 서로 다른 이해관계자가 사용할 때 한쪽의 계산 방식이 수정되는 경우 다른 쪽의 로직에 영향을 줄 수가 있다. 즉 서로 다른 액터가 의존하는 코드는 서로 분리되어야 한다. 예) DateUtil : 해당 모듈을 여러 국가의 컴포넌트에서 사용할 경우 한쪽의 수정이 다른 한쪽의 데이터를 잘 못 나오게 하는 결과를 초래할 수 있다.
  • Facade Pattern (퍼사드 패턴) 대표 1개의 클래스를 생성하여 요청된 메서드를 가진 객체로 작업을 위임한다. 여기서 가장 중요한 메서드는 대표 클래스에서 처리하고 나머지는 각자의 클래스에서 처리하도록 수정해도 된다. 메서드가 하나의 가족을 이루고, 메서드의 가족을 포함하는 각 클래스는 하나의 유효 범위가 된다. 해당 유효 범위 바깥에서는 이 가족에게 감춰진 식구(private멤버)가 있는지를 전혀 알 수 없다.

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

  • 기존 코드를 수정하기보다는 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계해야만 소프트웨어 시스템을 쉽게 변경할 수 있다.
  • 소프트웨어의 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안된다. 예) 재무 재표를 표시해주는 웹페이지에 동일한 결과를 엑셀로 출력하는 기능 확장 시 원래 코드는 얼마나 수정해야 할까? 이상적인 변경량은 0이다. 재무 데이터를 검사한 후 보고서용 데이터를 생성한 다음 필요에 따라 두 가지 보고서 생성 절차 중 하나를 거쳐 적절히 포매팅한다. 이때 두가지 책임 중 하나에 변경이 발생하더라도 다른 하나는 변경되지 않도록 소스코드 의존성도 확실히 조직화해야 한다. 처리과정을 클래스로 분리하고 이들 클래스를 컴포넌트 단위로 구분해야 한다.
  • 서로 다른 목적으로 변경되는 요소를 적절하게 분리하고(SRP : 단일 책임원칙) 이들 요소 사이의 의존성을 체계화 함으로써(DIP : 의존성역전원칙) 변경량을 최소화할 수 있다.
  • A컴포넌트에서 발생한 변경으로부터 B컴포넌트를 보호하려면 반드시 A컴포넌트가 B컴포넌트에 의존해야 한다. 컴포넌트 계층구조를 이와 같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.

LSP(LIskov substitution principle : 리스코프 치환 원칙)

  • 상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 치환 가능해야 한다는 규약을 반드시 지켜야 한다. 예) 라이선스에는 퍼스널 라이선스와 비즈니스 라이선스의 두가지 하위타입이 존재한다. 퍼스널 라이선스에 비즈니스 라이선스를 치환하더라도 동일하게 동작한다. 이들 하위 타입은 모두 라이선스 타입을 치환할 수 있다.

ISP(interface segregation principle : 인터페이스 분리 원칙)

  • 소프트웨어 설계자는 사용하지 않는 것에 의존하도록 만들지 않아야 한다. 예) 하나의 클래스에 여러 사용자가 각각 사용하는 메서드가 기술되었다고 할 때 다른 하나의 사용자의 메서드를 수정하면 다른 사용자의 메서드는 수정되지 않았음에도 다시 컴파일돼야 한다. 이때는 인터페이스를 분리하여 각각의 의존성이 영향을 받지 않도록 해야 한다.
  • 정적 타입 언어는 소스에 타입 선언을 하기 때문에 소스 코드 의존성이 발생하고 재컴파일 또는 재배포가 강제되는 상황이 무조건 초래된다.
  • 필요이상으로 많은것을 포함하는 모듈에 의존하는 것은 위험한 일이다.

DIP(dependency inversion principle : 의존성 역전 원칙)

  • 고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안된다. 유연성이 극대화된 시스템이란 소스코드 의존성이 추상(abstract, interface)에 의존하며 구체화(concretion)에는 의존하지 않는 시스템이다.
  • 변동성이 큰 구체 클래스를 참조하지 말고 대신 추상 인터페이스를 참조하도록 구현한다. 변동성이 큰 구체 클래스로부터 파생하거나 구체 함수를 오버라이드 해서는 안된다. 예) UserService라는 구체 클래스가 있다. 해당 클래스는 페이스북 유저가 참조할 수도 있고 구글 유저가 참조할 수도 있다. 이런것을 고려하여 인터페이스를 만들고 참조하는 쪽에서는 변경사항이 없도록 해야 한다. 버전 1을 기반으로 버전 2를 만든다고 할 때 동일한 구체 클래스내에 오버라이드 하거나 클래스를 상속하면 의존성이 커진다. 역시 인터페이스로 만들고 구체 클래스는 두개로 나누어 의존성을 분리하는 것이 좋다.