SOLID 원칙이란?
- 함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 의미한다.
- SRP, OCP, LSP, ISP, DIP 의 앞글자를 따서 만들어졌다.
SOLID 원칙의 목적
- 변경에 유연하다.
- 이해하기 쉽다.
- 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트 기반이 된다.
SRP (Single Responsibility Principle) - 단일 책임 원칙
- 단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다.
- 아키텍처 수준에서는 아키텍처 경계의 생성을 책임지는 변경의 축(Axis of Change)이 된다.
SRP 위배 사례
- 아래 Employee 클래스 SRP를 위반하고 있다.
- 한 클래스 내 세 가지 메서드가 서로 다른 세 명의 엑터(회계 팀, 인사 팀, DBA)를 전부 책임지고 있기 때문이다.
초과 근무를 제외한 업무 시간을 계산하는 알고리즘인 새로 생성한 메서드 regularHours()을 calculatePay() 메서드와 reportHours() 메서드에서도 사용한다고 가정하고 아래와 같은 상황이 생겼다고 해보자.
- CFO 팀에서 초과 근무를 제외한 업무 시간을 계산하는 방식을 수정하길 원해서 개발자가 regularHours() 메서드를 수정하였다.
- COO 팀에선 CFO팀과는 다른 목적으로 사용하기에 변경을 원치 않는다.
- 개발자가 뒤늦게 COO 팀에서 사용하는reportHours()에서도 regularHours()메서드를 사용하는 것을 발견하였다.
- COO 팀에선 개발자가 발견하기까지 엉터리 보고서로 인한 잘못된 데이터로 회사에 수백만 달러의 예산이 지출되었다.
위 상황은 한 모듈 내 여러 엑터의 책임을 지고 있기 때문에 발생한 일이다.
해결책
- 엑터 단위로 책임을 분리한다.
- 퍼스드(Facade) 패턴을 사용해 개발자가 클래스를 인스턴스해 추적하는 단점을 해결한다.
- 아래 EmployeeFacade에 코드는 거의 없다.
- EmployeeFacade는 어떠한 엑터도 담당하지 않는다.
- 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.
위와 같이 변경 후 CFO 팀에서 초과 근무를 제외한 업무 시간을 계산하는 방식을 변경한다면 PayCalulator 클래스의 calculatePay() 메서드만 수정해주면 되므로 HourReporter 클래스의 reportHours() 메서드는 어떠한 영향도 받지 않는다.
OCP (Open-Closed Principle) - 개방 폐쇄 원칙
- 기존 코드를 수정하기보다는 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변결할 수 있도록 설계해야만 한다.
- 소프트웨어 개체(artifact)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야한다.
만약 요구사항을 살짝 확장하는 데 소프트웨어를 엄청나게 수정해야 한다면, 그 소프트웨어 시스템을 설계한 아키텍트는 엄청난 실패에 맞닥뜨린 것이다.
OCP 위배 사례
위 소스에서 소, 돼지를 추가한다면 Animal 클래스의 speak() 메서드에서 type을 분류되지 않아 원하는 결과를 얻지 못한다.
만일 speak() 메서드에서 type을 추가한다면 원하는 결과는 나오지만 아래와 같은 원칙이 지켜지지 않는다.
- 기존 speak() 메서드 변경 (변경에는 닫혀 있어야한다. X)
- speak() 메서드의 변경 없이는 추가가 제약된다. (확장에는 열려 있어야한다. X)
해결책
- 객체 지향에서 기본적으로 제공되는 추상화와 다형성을 사용한다. (Abstract, Interface)
- 변경되는 부분과 변경되지 않는 부분을 분리한다.
주의점
- 추이 종속성이 될 경우 '자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안 된다'는 원칙을 위반하게된다.
추이 종속성이란?
- 클래스 A가 클래스 B에 의존하고, 다시 클래스 B가 클래스 C에 의존한다면, 클래스 A는 클래스 C에 의존하게 되는 경우를 의미한다.
- 클래스 의존성이 순환적(cyclic)이라면 모든 클래스가 서로 의존하게 되는 문제가 있다.
LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
- 다형성을 지원하기 위한 원칙
- 서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다.
- 상속을 통한 재사용은 기반(부모) 클래스와 서브 클래스 사이에IS-A관계가 있을 경우로만 제한, 그 외는 합성(composition)을 사용해야 한다.
LSP 위배 사례
- Square(정사각형)은 Rectangle(직사각형)의 하위 타입으로 적합하지 않다.
- Rectangle은 높이와 너비가 서로 독립적으로 변경될 수 있는 반면, Square는 반드시 높이와 너비가 함께 변경되기 때문이다.
- LSP 위반을 막기 위한 방법은 비교문을 통해 타입 검사를 하는 것 뿐이다. 하지만 그럴 경우 사용하는 타입에 의존하게 되므로 결국 타입을 치환할 수 없다.
치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있다.
ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
- 사용하지 않는 것에 의존하면 안된다.
- 큰 인터페이스를 작은 단위로 나누는 것이 좋다.
- SRP는 클래스의 단일책임을 강조한다면, ISP는 인터페이스의 단일책임을 강조한다.
ISP 위배 사례
- 인터페이스로 차를 추상화하였다.
- 휘발류, 경유, 전기를 주유/충전하는 기능이 있다. 하지만 차량 별 연료가 다르다.
- Avente는 휘발유, Tucson은 경유를 연료만을 사용하지만 지원하지 않는 연료 충전 기능도 재정의 해야한다.
해결책
- 각 기능에 맞게 인터페이스를 분리한다.
- 사용하는 기능만을 인터페이스만 implements 하여 구현한다.
- 만약 연료가 하이브리드라면 Car, ElectroRefuelAble, GasolineRefuelAble 인터페이스를 implements 한다.
주의점
- 인터페이스는 구성된 이후 나중에 수정사항이 생긴다면 인터페이스 분리, 추상 메서드의 추가 등의 행위를 하지 말아야한다.
- ISP는 어떤 클래스 혹은 인터페이스가 여러 책임 혹은 역할을 갖는 경우를 인정한다.
DIP (Dependency Inversion Principle) - 의존성 역전 원칙
- 고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안된다. 대신 세부사항이 정책에 의존해야 한다.
- 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템 즉, 구현 클래스가 아니라 인터페이스 혹은 추상 클래스에 의존해야한다.
DIP 위배 사례
- Cafe 클래스(고수준)이 Americano, CafeMocha, VanillaLatte 클래스(저수준)에 의존되어 있다.
- 만약 Coffee 종류가 추가 되면 고수준의 의존성이 많아져 추후 수정, 관리가 힘들어진다.
해결책
- 새로 생성한 추상 클래스 혹은 인터페이스에 의존하도록 한다.
- Cafe 클래스(고수준)이 Coffee 인터페이스에 의존
Reference
Clean Architecture - 로버트C.마틴
https://www.youtube.com/@nullnull_not_eq_null - 널널한 개발자 TV
https://www.youtube.com/@user-pw9fm4gc7e - 코드없는 프로그래밍