- 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 함수를 포함하기 때문이다.