이전글들은 사실 이 원칙을 위해 달려온 것이 아닌가하는 생각이 든다. OOP의 정수로 불리우는 SOLID원칙에 대해서 깊게 알아보고, 실제 iOS Framework의 설계 방향에 대입하면서 보다 찐한 이해를 경험해보자.
Single Respoinsibility Principle (단일 책임 원칙)
- SRP
- 한줄 설명
- 하나의 클래스는 단일 책임을 가져야 한다.
- 단일
- 책임
- 변경을 위한 이유
- 특정 기능을 변경하기 위해 수정했는데, 클래스의 대부분이 수정되지 않은 경우
- 클래스가 지나치게 많은 기능을 가지고 있는 것은 아닌가?
- 기능 한 개에 하나의 클래스라고 가정한다면, 하나의 기능 수정할 경우 많은 부분 수정이 가해져야 함
- 논리적 관점에서 하나의 기능이라 생각되지만, 차후 변경 관점에서 변경될 가능성이 높거나 많다면 변경부만 별도의 클래스로 분리하여야 함
- 특정 기능을 변경하기 위해 수정했는데, 여러 클래스가 수정된 경우
- 하나의 기능에 하나의 클래스로 묶을 수 있도록 해야 함
- 변경의 관점에서 분리될 이유가 없는데 분리가 된 경우, 불필요한 복잡성을 가질 수 있음
- SRP 준수 클래스의 특징
- 응집도가 높음
- 결합도가 낮음
- 특정 기능 변경을 위한 수정이 한 곳에 집중되어 있음
- SRP 준수 클래스 응용
- Design Pattern
- Abstract Factory
- Bridge
- State
- Strategy
- Command
- (S)OLID 원칙들
- SRP 위반 사례
- ViewController
- Massive ViewController
- 해결책: MVP, MVVM, VIPER
- 위반 근거
- VC 정의: Provides the infrastructure for managing the views of your UIKit app
- 이미 view를 관리한다고 명시되어 있음
- 하지만 실제 만들다보면 view만 관리하지 않는 부분이 상당이 많음 (예: network 처리)
- Apple MVC
- 기존 MVC에서 Controller는 View와 Model을 이어주는 역할
- 하지만 VC는 View를 관리하는 역할을 하고 있음
- 또한 Model을 처리하는 로직까지 들어가 있음
- MVP, MVVM, VIPER는 모두 이 문제를 해결하기 위해 제시됨
Open / Closed Principle (개방 폐쇄 원칙)
- OCP
- 한줄 설명
- 소프트웨어 개체 (Class, function, etc)는 확장에 대해 열려있고 수정에 대해서는 닫혀있어야 한다.
- 확장에 대해 열려있다.
- 새로운 기능 확장을 위해 기존 코드를 손대지 않고 새로운 코드 추가만으로 가능해야 한다.
- 수정에 대해 닫혀있다.
- 새로 추가되는 코드 혹은 수정되는 코드는 기존 코드의 변경을 일으키지 않는다.
- OCP 준수 효과
- 경직성이 줄어듦
- 경식성
- 코드를 수정하기 어렵게 하고, 수정하더라도 부작용을 일으키기 쉬운 속성을 말함
- 빠르고 안정적인 수정
- 부작용의 최소화
- 방법
- OCP는 추상화에 크게 의존
- 추상화된 인터페이스 선언
- 인터페이스를 상속받아 구체적인 행위 구현
- 해당 모듈을 사용하는 코드는 구체화된 클래스에 의존하지 않고 추상화된 인터페이스만을 사용해서 동작
- 새로운 클래스가 추가되어도 기존 코드 손댈 이유가 없음
- OCP 적용하지 않았을 때 Code Smell
- 어떤 타입에 대한 반복적인 분기문
- Enum 값을 switch나 if문으로 반복적으로 판단하는 경우
- 새로운 case 추가는 쉬우나 모든 분기문들을 확인해야 함
- 수정에 닫혀있지 않음
- 분기문 같은 경우 새로운 클래스 추가시 새로 작성해주어야 함
- 실제 예
- switch 문을 사용하지 않고, protocol을 선언한 뒤, 각 클래스에서 이를 채택한다음, 다형성으로 동작하게 만든다.
- OCP는 만능인가?
- 만능인 것은 없다.
- 새로운 case, 기능 추가가 있는 경우에 남용되면 불필요한 복잡함을 가지게 됨
- greedy하게 해당 문제가 발생했을 때, 빠르게 대응하는 것이 보다 좋지 않을까?
- 언제 사용할까?
- Type에 새로운 멤버들이 계속 추가될 가능성이 클 때
- Type의 멤버로 분기처리되는 곳이 매우 많을때
- 더 생각해보아야 할 것들
- 어떤 부분을 Open, 어떤 부분을 Close할 것인가?
- == 어떤 부분을 추상화할 것이고 어떤 부분을 구체화할 것인가?
- 판단이 어려운 경우, 변경이 없을 것이다 가정하고 개발한다.
- 변경이 발생할 때
- 이 변경이 왜 발생하는가?
- 어떤 곳에 영향을 미치는가?
- 변경일어난 부분이 앞으로도 변경을 유발할 것 같다 -> 리팩토링
- 종류의 추가보다 인터페이스 변화가 더 자주 일어난다면
- 잘 추상화하면 interface는 가만히 있고 구현부만 추가하는 것으로 해결된다.
- 연습 방법
- 의도적 사용
- if/switch를 극도로 제한하는 코딩
Liskov Subsitution Principle (리스코프 치환 원칙)
- LSP
- 한줄 설명
- 자료형 S가 자료형 T의 하위형이라면, 프로그램의 속성의 변경없이 T 객체를 S 객체로 교체할 수 있어야 한다.
- 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 의의
- 상속에 있어서 가장 중요한 기본 원칙 제시
- OCP를 가능하게 만들어주는 원칙
- 상속할 때 해서는 안되는 행위
- 난해한 예시
- 직사각형을 상속받은 정사각형?
- 정사각형을 상속받은 직사각형?
- 둘 사이에는 상속관계가 있으면 안됨
- 일반적인 관념과 상충됨
- 직사각형을 상속받은 정사각형은 너무나 당연해 보임. 하지만..
- 만일
setWidth()
함수가 있다면
- 정사각형의 경우 height도 같이 바꿔야 한다.
- 직사각형은
setWidth()
로 폭을 2배로 하면 면적이 2배가 됨
- 정사각형은
setWidth()
로 폭을 2배로 하면 면적이 4배가 됨
- 부모의 정합성을 깨버림
- 자식이 거부할 수 밖에 없는 기능을 부모가 제공하고 있음
- 상속에 관해 깊이 생각해보기
- 개, 고양이에 "걷기", "뛰기" 행위를 추가하면 그 행위를 동물레벨로 올릴 수 없음
- 만약 동물 레벨에 해당 메서드를 추가한다면, 어류의 경우 퇴화함수가 발생 (걷기, 뛰기 불가)
- 즉, 동물 상속 구조에서 "걷기", "뛰기"는 넣을 수 없음
- 상속과 별도로 존재해야 함
- UIKit에서 LSP를 위반하는 경우
- VC에 여러가지의 View 요소를 변수로 가지고 있다고 가정하자. (10개)
- 해당 view들의 height를 모두 30으로 변경하고, 모든 뷰들의 height의 합계를 변수로 가지고 있고 싶다.
- 당연히 300이라는 값이 출력될 것이라 예상했지만 그보다 작은 값이 도출된다.
- 그 이유는 UIView라는 클래스를 상속받아 View 클래스들이 만들어지나, 하위 클래스에서 높이에 제한을 두는 것들이 있기 때문
- 부모의 행위를 자식이 거부하고 있는 상황
- 물론, 경험으로 이러한 부분에 대해 인지하고 있으나, 이러한 부분이 많다면 작업이 어려워짐
- LSP 위반시 문제
- 모든 클래스에서 하위 클래스를 명시적으로 지정해서 코딩해야 함
- OCP를 사용할 수 없게 됨
- 인터페이스를 만들어두고, 이 인터페이스로 특정 클래스에서 사용할 것이기 때문
- 하위 클래스를 명시적으로 선언하여 사용할 수 밖에 없기 때문에 발생하는 문제
- 코드의 복잡도를 높임
- 인터페이스를 통한 추상화로 코드 작업이 불가함
- 부모 클래스가 자식 클래스를 알아야 하는 경우도 발생
- 대부분은 LSP를 준수하기 때문에 믿어도 된다.
- LSP 준수시 효과
- 상위 클래스를 기준으로 작성된 코드가 문제없이 동작(다형성)
- 추상화된 인터페이스 하나로 공통코드 작성 가능
- 상속된 수많은 클래스를 일일이 고민하지 않음
- 확장을 위해 사용도 가능함
- 기능 확장을 위해 상속을 사용하는 것이 아님! 가능한 옵션일 뿐
Protocol extension
이라는 방법도 있음
- 현실적인 이야기
- 모든 코드에서 LSP 지키기는 어려움
- 적정선에서 Trade-Off
- LSP를 이해하고 따르면
- 설계에 도움이 됨
- 발생할 문제를 사전에 막을 수 있음
- 복잡하고 이해할 수 없는 상속을 만들지 않게 함
- 덩치큰 코드들에서 상황을 다각적이고 깔끔하게 정제하는데 도움을 줌
Interface Sergregation Principle (인터페이스 분리 원칙)
Dependency Inversion Principle (의존 관계 역전 원칙)
- DIP
- 한줄 설명
- 소프트웨어 모듈을 분리하는 원칙
- 상위 모듈은 하위 모듈에 의존해서는 안된다.
- 상위 모듈과 하위모듈 모두 추상화에 의존해야 한다.
- 상위 계층이 하위 계층에 의존하는 전통적인 의존관계를 역전시켜 상위계층이 하위계층의 구현으로부터 독립되도록 한다.
- 추상화는 세부사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
- 풀고 싶은 상황
- 상위 수준 모듈이 하위 수준 모듈에 의존성을 가지는 경향
- 정책이 구체적인 것에 의존하는 경향
- 목적
- 모듈간의 의존 관계를 끊는 방법 제시
- 변경시 다른 코드에 영향을 최소화할 수 있는 방법 제시
- 모듈의 의존 관계
- 코드는 어떻게든 의존관계를 가짐
- 의존 관계 자체를 없앨 수는 없음
- 하지만 의존 관계를 잘 정리하지 않으면 코드는 경직성을 가짐
- 무엇이 무엇에 의존하냐에 따라 다른 결과를 만들어 냄
- 문제 상황 예시
- 서로 의존관계를 가지는 모듈
- Class A 와 Class B가 서로가 있어야 존재 성립
- 아키텍쳐 패턴에서 중간에 있는 클래스가 이런 경우
- 만약 이런 상황이 있다면 weak으로 관계를 끊어주는 것이 좋음
- Class B만 사용한다면 Class A도 함께 가야만 하는 상황임
- 의존 관계가 순환을 만드는 경우
- Class A -> Class B -> Class C -> Class A
- 어느 클래스도 독립적으로 사용할 수 없음
- 단방향으로 흘러가는 의존 관계
- Class A -> Class B -> Class C
- 최선
- 현실적으로 어려움
- 그런데 만약 Class C가 B 혹은 A에 의존해야 하는 상황이 있다면?
- 의존의 방향
- 의존 관계는 어떤 방향으로 흘러가야 하는가?
- 구체적인 부분에서 추상적인 방향으로 의존해야 함
- 추상적인 것이 구체적인 것에 의존하지 않아야 함
- 정책이 구체적인 것에 의존하는 경향
- 상위 수준은 하위 수준에 의존하면 안됨
- 상위 수준: 추상적인 부분
- 하위 수준: 구체적인 부분
- iOS 예시
- UITableView
- UITableView의 동작은 DataSource, Delegate와 같은 방식으로 구체적인 동작을 주입받아서 동작함
- 추상적인 부분(UITableView)이 구체적인 부분(DataSource, Delegate에 구현된 코드)에 의존하지 않음
- 직접적으로 의존하지 않고 인터페이스(DataSource, Delegate)에 의존함
- UIViewController, CustomViewController
- UIViewController는 View life-cycle에 대한 정책 결정
- CustomViewController는 구체적인 구현을 수행
- 효과
- 두 모듈간의 의존관계를 단방향으로 만들어줌
- 추상화된 부분의 코드는 재활용성이 증가함
- 고민 사항들
- 레이어의 구분
- 추상적인 부분 (정책)
- 구체적인 부분 (세부)
- 의존성의 이행
- 특정 객체를 사용하기 위해 여러객체가 필요한 경우를 말함
- 의존성의 이행은 스파게티를 만듦
- 적합한 위치에서 DIP를 이용해서 의존성 이행을 막아주어야 함
- 각 레이어와 인터페이스
- 인터페이스: 가장 변화가 적어야 함
- 상위 레이어
- 하위 레이어
- 인터페이스의 소유
- 누가 소유하고 제공하는가?
- Class A - Class B는 서로 참조중
- Class A가 Class B를 가지고 있는 상황
- Class A가 interface 제공, Class B가 사용
- Class B가 interface 제공, Class A가 사용: iOS 채택
- ViewController(A)가 UITableView(B)를 가지고 있는 상황
- UITableView가 DataSource, Delegate를 제공하고, ViewController가 사용
- DIP 개념으로 클라이언트 클래스와 서버 클래스를 나눠본다면,
- UIViewController: 서버 클래스
- 구체적인 동작 (DataSource, Delegate)를 제공
- 해당 동작이 사용됨(수동)
- 요청에 따른 동작을 응답해줌
- UITableView: 클라이언트 클래스
- 즉, iOS에서 채택하는 interface 제공 방식은 클라이언트가 interface를 소유하고 있는 방식임
- 이유
- 인터페이스 변경은, 클라이언트 요구에 의해서 발생
- 변경을 유발한 쪽에서 인터페이스를 가지고 있는 것이 유리
- 즉, 어떻게 요청을 줘! 라고 하니 클라이언트가 가지고 있는 것이 낫다는 말
- 클래스의 휘발성에 따른 적용
- 휘발성
- 변경이 거의 없는 클래스
- String, Data etc
- 비휘발적 클래스 경우 강하게 적용할 필요 없음
- 비휘발적 클래스에 의존하는 것은 큰 해가 되지 않음
- 추상화된 클래스(DataSource, Delegate)는 비휘발적 클래스에 가깝기 때문에, 해당 클래스를 참조하거나 소유하는 것은 큰 문제가 아님