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