• SOLID 원칙의 목적은 중간 수준의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.
    • 변경에 유연하다.
    • 이해하기 쉽다.
    • 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
  • SOLID 원칙
    • SRP(단일 책임 원칙): 하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.
    • OCP(개방-폐쇄 원칙): 기존 코드를 수정하기보다는 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계해야만 소프트웨어 시스템을 쉽게 변경할 수 있다.
    • LSP(리스코프 치환 원칙): 상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 상위 타입 자리에 하위 타입으로 치환할 수 있어야된다.
    • ISP(인터페이스 분리 원칙): 소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다.
    • DIP(의존성 역전 원칙): 고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안 된다.

SRP: 단일 책임 원칙

하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.

  • 액터: 해당 변경을 요청하는 한 명 이상의 집단
  • 모듈: 단순한 정의는 소스 파일. 또는 함수와 데이터 구조로 구서된 응집된 집합.
  • Employee 클래스에서 회계팀은 calculatePay() 메서드를, 인사팀에서는 reportHours() 메서드를, 데이터베이스 관리자는 save() 메서드를 사용하고 있으면 서로 다른 액터가 의존하는 코드를 너무 가까이 배치했기 떄문에 문제가 발생한다.
  • 단일 책임 원칙을 지키지 않으면 소스 파일에 병합이 자주 발생일어난다. 특히 이들 메서드가 서로 다른 액터를 책임진다면 병합이 발생할 가능성은 더 높다.
  • 가장 확실한 해결책은 데이터와 메서드를 분리하는 방식이다.
    • 아무런 메서드가 없는 간단한 데이터 구조인 EmployeeData클래스를 만들어, 세 개의 클래스가 공유하도록 한다. 각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만 포함하고, 서로 존재를 몰라야 한다.
    • 이 해결책은 개발자가 모든 클래스를 인스턴스화하고 추적해야 한다는 단점이 있다.
  • 이러한 난관에서 흔히 쓰는 기법으로 Facade 패턴이 있다.
    • EmployeeFacade클래스는 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.
    • 중요한 업무 규칙을 데이터와 가깝게 배치하는 방식을 선호할 경우, 가장 중요한 메서드는 기존 Employee 클래스에 유지하되, 덜 중요한 나머지 메서드들에 대한 퍼사드로 사용할 수도 있다.
  • 단일 책임 원칙은 메서드와 클래스 수준의 원칙이다. 하지만 이보다 상위 두 수준에서도 다른 형태로 다시 등장한다.
    • 컴포넌트 수준: 공통 폐쇄 원칙
    • 아키텍처 수준: 아키텍처 경계의 생성을 책임지는 변경의 축

OCP: 개방-폐쇄 원칙

기존 코드를 수정하기보다는 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계해야만 소프트웨어 시스템을 쉽게 변경할 수 있다.

  • 서로 다른 목적으로 변경되는 요소를 적절하게 분리하고(SRP), 이들 요소 사이의 의존성을 체계화함으로써(DIP) 변경량을 최소화할 수 있다.
  • 높은 수준의 정책을 포함한 컴포넌트를 변경으로 보호할 수 있도록 설계해야된다.
  • 인터페이스를 통해 컴포넌트 간 방향을 제어할 수 있다.
  • 인터페이스를 통해 추이 종속성을 막을 수 있다. 추이 종속성을 가지게 되면, 소프트웨어 엔티티는 ‘자신이 직접 사용하지 않는 요소에 절대로 의존해서는 안 된다’는 소프트웨어 원칙을 위반하게 된다.
  • OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다. 이 목표를 달성하려면 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 한다.

LSP: 리스코프 치환 원칙

상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 상위 타입 자리에 하위 타입으로 치환할 수 있어야된다.

  • 잘 정의된 인터페이스와 그 인터페이스의 구현체끼리의 상호 치환 가능성에 기대는 사용자들이 존재한다.
  • LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 한다. 치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문이다.

ISP: 인터페이스 분리 원칙

소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다.

  • ISP를 지키지 않는다면, 사용하지 않는 코드에서 변경이 일어나도 이를 의존하는 클래스를 다시 컴파일한 후 새로 배포해야된다.
  • 루비나 파이선 같은 동적 타입언어에서는 import, use 같은 선언문이 존재하지 않고 런타임에 추론이 발생한다.
    • 동적 타입 언어를 사용하면 정적 타입 언어를 사용할 때보다 유연하며 결합도가 낮은 시스템을 만들 수 있는 이유는 바로 이 때문이다.
  • ISP도 아키텍처까지 확장될 수 있다. 의존하는 아키텍처에서 사용하지 않는 기능의 변경 때문에 재배포가 필요해질 수 있다.

DIP: 의존성 역전 원칙

고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안 된다.

  • ‘유연성이 극대화된 시스템’이란 소스코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템이다.
    • 모든 경우에 이를 만족할 수는 없다. String 클래스, 운영체제, 플랫폼과 같이 변경이되지 않는 다는 것을 보장하는 경우는 염려할 필요가 없다.
    • 우리가 의존하지 않도록 피하고자 하는 것은 변동이 큰 구체적인 요소다.
  • 안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고 ,안전된 추상 인터페이스를 선호하는 아키텍처라는 뜻이다.
    • 변동성이 큰 구체를 참조하지 말라. 대신 추상 인터페이스를 참조하라. 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리를 사용하도록 강제한다.
    • 변동성이 큰 구체 클래스로부터 파생하지 말라. 상속은 소스 코드에 존재하는 모든 관계 중에서 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다.
    • 구체 함수를 오버라이드 하지 말라. 대체로 구체 함수는 소스 코드 의존성을 필요로한다. 따라서 구체 함수는 오버라이드 하면 이러한 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다. 의존성을 제거하려면, 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.
    • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.
  • 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생한다. 대다수의 객체 지향 언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용하곤 한다.
  • 추상 컴포넌트는 애플리케이션의 모든 고수준 업무 규칙을 포함한다. 구체 컴포넌트는 업무 규칙을 다루기 위해 필요한 모든 세부사항을 포함한다.
    • 소스 코드 의존성은 제어흐름과는 반대 방향으로 역전된다. 이러한 이유로 이 원칙을 의존성 역전이라고 부른다.
  • DIP 위배를 모두 없앨 수는 없다. 하지만 DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분과는 분리할 수 있따.
    • 대다수의 시스템은 이러한 구체 컴포넌트를 최소한 하나는 포함할 것이다. 이 컴포넌트를 메인이라고 부르는데, main 함수를 포함하기 때문이다.