SOLID 원칙
이란?
- S (Single Responsibility Principle, 단일책임원칙, SRP)
— 한 클래스는 하나의 책임만을 가져야 한다 - O (Open-Closed Principle, 개방-폐쇄 원칙, OCP)
— 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀 있어야 한다 - L (Liskov Substitution Principle, 리스코프 치환 원칙, LSP)
— 프로그램 객체는 프로그램 정확성을 깨지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다 - I (Interface Segregation Principle, 인터페이스 분리 원칙, ISP)
— 범용 인터페이스보다, 여러개의 잘게 쪼개진 인터페이스로 분리 해야 한다. - D (Dependency Inversion Principle, 의존성 역전 원칙, DIP)
— 프로그래머는 구현에 의존하기 보다는 추상화에 의존하여야 한다.
SOLID 원칙의 장점
- 변경에 유연
- 이해하기 쉬움
- 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반
- 코드 수준보다는 조금 상위에서 적용
- 모듈과 컴포넌트 내부에서 사용되는 소프트웨어 구조를 정의하는데 도움이 됨
S (Single Responsibility Principle, 단일책임원칙, SRP)
- 하나의 모듈은 하나의, 오직
하나의 액터
에 대해서만 책임져야 한다 - 응집된 (Cohensive) 이라는 단어는 SRP를 암시해 주는 단어이다.
- Access Control 을 활용하자!
- 메서드와 클래스 수준의 원칙!
ex) Business Logic의 분리, View Controller는 UI 바인딩에 대한 책임, Navigation은 Coordinator에서만 책임 etc…
O (Open-Closed Principle, 개방-폐쇄 원칙, OCP)
- 훌륭한 아키텍처는 유지보수시에 변경되는 코드의 양이 가능한 한
최소화
될 것이다 (이상적인 변경량은 0) - 이상적인 목표는 확장하기 쉬운 동시에, 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다
— 컴포넌트 단위로 분리, 저수준 컴포넌트에서 발생한 변경에서 고수준 컴포넌트를 보호 - ex) Controller, Presenter, Interactor에서의 가장 고수준은 Interactor 이므로, Controller에서 변경된 저수준 컴포넌트가 Interactor에 영향을 미치면 안된다. —
컴포넌트의 확실한 분리가 필요
L (Liskov Substitution Principle, 리스코프 치환 원칙, LSP)
- 자식 클래스는 부모클래스의 역할을 충분히 수행하여야 하고 이에 대한 확장을 할 수 있어야 한다
- LSP 위반사례
— ex) 정사각형(square) 직사각형(rectangle) 문제
Rectangle
의 높이와 너비는 서로 독립적으로 변경 가능Square
는 높이와 너비가 서로 독립적으로 변경 불가능testSize
함수에서width
를 4로,height
를 5로 할당해준다면
testSize(rectangle: Rectangle()) // SUCCESS
testSize(rectangle: Square()) // FAIL
와 같은 LSP 위반 사례가 나오게 된다.
LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 한다. 치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문이다.
- I (Interface Segregation Principle, 인터페이스 분리 원칙, ISP)
가정:
- OPS클래스는 정적 타입 언어로 작성된 클래스이다.
- User1은 op1만을, User2는 op2만을, User3는 op3만을 사용한다.
결과:
- User1은 op2와 op3를 전혀사용하지 않음에도, User1의 소스 코드는 이 두가지 메서드에 의존하게 된다
→ 이러한 의존성으로 인해 OPS 클래스에서 op2의 소스코드가 변경된다면, User1도 재 컴파일 후에 배포되어야하는 현상
→ 다행히도인터페이스 단위로 분리
를 하여 해결 가능 그림 10.2 참조
- 불필요한 재컴파일과 재배포를 강제하지 않아도 됨 (동적언어 vs 정적언어)
- 저는 이와같이 범용적으로 사용할
ServiceProviderType
이라는 프로토콜(인터페이스)을 만들어 하위의 프로토콜(인터페이스)로 쪼개는 방식으로 구현합니다
- D (Dependency Inversion Principle, 의존성 역전 원칙, DIP)
- 유연성이 극대화된 시스템
→ 소스코드 의존성이 추상(abstraction)에 의존, 구체(concretion)에는 의존 X - 의존하지 않도록 피하고자 하는 것이 바로 변동성이 큰 (volatile) 구체적 요소
→ 아키텍트는 인터페이스의 변동성을 낮추기 위해 노력
→ 변동성이 큰 구체 클래스를 참조하지 말자 (=추상 인터페이스를 참조하자)
→ 변동성이 큰 구체 클래스로부터 파생하지 말자(= 상속은 신중히 하자. 특히 정적타입언어라면 더더욱, 이유는 강력한 동시에 뻣뻣해서 변경이 어렵기 때문)
→ 구체 함수를 오버라이드 하지 말자 (= 구체 함수는 소스 코드 의존성이 필요함) - 고수준 -> 저수준으로의 설계방식이 아닌, 저수준 -> 고수준으로 설계하자!
- ex)
자동차 + 스노우타이어- 자동차는 눈이 오면 스노우타이어를 이용하는데, 이는 고수준 모듈인 (자동차) 가 저수준 모듈인 (스노우타이어) 에 의존하는 상황입니다.
시간이 흘러 겨울이 지나 봄이 와, 스노우 타이어를 교체해야합니다.
이 스노우타이어를 그냥 교체하게된다면 스노우 타이어와 연계되어있는 모든 객체들이 영향을 받게 됩니다.
(이는 또한, 우리가 SOLID원칙에서 말하는 OCP도 위배하게 됩니다. 이는 추상화나 다형성을 통해 해결해야합니다.)DIP는 추상화를 통해 해결합니다.
그러므로, 스노우타이어보다는, 타이어로 추상화 한 다음, 고수준 모듈이 저수준 모듈에 의존하지말고, 저수준 모듈이 고수준모델에 의존하게 하여, 확장성을 높힙니다.
DIP를 위해 DI (Dependency Injection, 의존성 주입)를 많이 사용하게 되는데, 이를통해
- 높은 응집성과 낮은 결합성
- 내부의 접근성이 차단되고, 인터페이스를 통한 구현이 가능
- 의존관계를 인터페이스로 추상화하여 저수준 -> 고수준의 설계가 가능하게 됩니다.