클래스를 잘 설계하는 것은 무엇이고, 어떤 원칙에 따라야 하는가?
해당 포스팅은 유튜브 “베어코드” 님의 Swift OOP 영상을 뼈대로, 다른 포스팅을 참고해서 짜집기 했음을 미리 밝힙니다. 🙏🏻
가치
- 커뮤니케이션
- 책처럼 술술 읽히는 코드는 중요함!
- 단순성
- 커뮤니케이션에 도움.
- 버그가 적음
- 복잡한 문제를 단순한 레벨로 낮춤(추상화)
- 유연성
- 커뮤니케이션/단순성이 우선! 단순성을 해치지 않는 범위에서 유연한 구조
- => 의존성이 적은 함수화 형태로 코드를 작성하는 것은 유연성이 높아지는 길이겠다
원칙
코드 수정시 함께 수정되는 부분을 지역화시킴
- => 모듈화!
코드 중복 최소화
- => 의존성이 커지는 단점이 생긴다.
로직과 데이터 결합
- => 지역화와 연결
- 다른 모듈간 의존성이 적은 형태로 OOP
- 또는 순수함수형태로 작성
선언적표현
- 어떻게하는 냐보다, “무엇을 하느냐”
참고 : SOLID 원칙(로버트 C. 마틴)
- 단일 책임의 원칙
- 하나의 클래스는 하나의 책임만 가져야 한다.
- 개방폐쇄의 원칙
- 기능확장은 쉬워야 하고, 변경은 닫혀있는 좁은 범위에서 되도록 설계해야함.
- 리스코프 치환 원칙
- 상속관계에서 부모 클래스를 수정했을때 자식 클래스도 문제없이 작동해야 한다.
- 상속관계에서 “is vs have 관계” 검증해봐야함.
- 인터페이스 분리 원칙
- 특정 기능 인터페이스 여러개가 범용 인터페이스 하나보다 낫다
- 단일책임 원칙의 여러개로 분리
- 의존관계 역전 원칙
- 추상화에 의존해야지 구체화에 의존하면 안된다?
- 단일 책임의 원칙
SOLID 원칙은 하단에서 다시 서술
잘못된 상속을 피하기
상속자체의 단점
중복코드를 줄이는 장점이 있지만…
- 현재코드와 부모클래스 코드를 오가면서 읽어야함.
- override된 함수와 아닌 함수를 구분해서 읽어야함
- 커플링(결합도: 다른 모듈관의 의존성)이 높음
- 모듈의 내부 요소들이 하나의 책임/목적을 위해 연결되어 있는 연관정도 : 결합도가 낮을 수록 검토해야하는 소수의 수가 적어져서 코드 수정하기가 쉬어짐
- => 수정시마다 고려해야할게 많아지며
- => 한번 상속으로 처리한 해법은 바꾸는데 매우 큰 비용이듬.
- feature envy(기능에 대한 욕심)
- 클래스 본연의 임무 이외의 기능이 클래스에 들어가고, 상속과 만나면..
- UIViewController가 상속받은 클래스에서 network요청을 하는 코드를 넣었다면, 상속 받은 클래스에서 왜 network가 호출되는지 알 수 없음
상속의 대안
가급적 상속대신 합성(Composition) 사용
위에서 서술한 이유 때문에, 합성을 사용한다. 상속을 특히 사용하지 말아야 하는 경우는 is 관계가 맞는지 have 관계가 맞는지 비교해보자. 주로 have관계에서 상속을 쓰는 오류를 범한다!
Junho is a person. => ok.
Junho is eyes. => No!
Junho have eyes. => ok.
합성이란 클래스 안에서, 다른 클래스를 변수로 선언하는 것을 말한다.
1 | class Junho { |
상속을 기능의 확장으로 볼것이 아닌, 추상의 구체화 도구로 볼것!
추상적으로 View를 정의하고 있는 추상클래스를 상속받아 MyCustomView를 구체화시킨다는 개념으로 접근
이때 View는 범용적으로 사용될 수 있도록 추상화되어야 할 것이다.
추상화
- 라면 끓이는 법
- 1/2/3 단계
- 각 단계에서 더 디테일한 설명이 없음 : 물은 어떤 물인지, 봉투를 어떻게 뜯는다던지
- 생략/그룹핑/동일한 레벨의 추상화
객체 지향은 추상화하여 불필요한 내용을 단순화 시키고, 비슷한 내용끼리 그룹핑하는 것이 중요하겠다
Solid 원칙
단일책임원칙(SRP)
하나의 클래스는 단일 책임을 가져야 한다.
단일?
- 한가지
- 되도록 작은 범위
책임?
- 변경시, 클래스 대부분이 수정되지 않는다면 => 분리될 이유가 없는
- 변경시, 클래스 수정되는 부분 존재 => 분리!
1 | Modem |
=> 결과적으로 높은 응집도 + 낮은 결합도
=> 특정 기능의 변경시, 수정할 곳이 집중됌.
예시
MVC패턴에서 Model을 View로 바로 바인딩할 경우.
- 기존 Model과 View 사이에 의존성이 커지기 때문에, Model이 여러곳에서 쓰였다면, 모든 View가 수정되어야 한다. 하지만 presenter 또는 viewModel 로 책임을 나눈다면 해당 부분만 수정하면 된다!!
개방폐쇄원칙(OCP)
- 확장에 대해서 열려있다.
- 새로운 기능을 확장하기 위해서 잘 동작하는 기존 코드를 손대지 않고 새로운 코드를 추가하는 것만으로 가능
- 수정에 대해 닫혀 있다.
- 새로 추가되는 코드 혹은 수정 되는 코드는 기존 코드의 변경을 초래하지 않음
OCP를 따라야 할때
전체 코드에 같은 타입에 대해 enum/if 분기문이 있고, 모든 분기문을 다 확인해야함
=> 수정에 대해 닫혀있지 않음
1 | class Vehicle { |
- 인터페이스를 상속받아 구체적인 행위를 구현 => 구체화된 클래스와 인터페이스는 의존성X
1 | interface Vehicle { |
위 코드는 새로운 타입의 유형을 만들어도, 기존코드를 적게 수정하면서 확장이 가능!
어떤 부분을 open 어떤부분을 close?
= 어떤 부분을 추상화/ 어떤 부분을 구체화?
- 미래의 가정을 위해, 단순성을 해칠 필요가 없다!(유연성보다는 단순성이 우선)
- 변경이 발생할때 계속 변경을 유발할 것 같다면 리팩터링!
- => 타입이 계속 생긴다거나..
- 타입의 추가보다 인터페이스의 변화가 더 자주 일어난다면, close 시켜야 하는 위치 잘못 잡은것
OCP는 남용해선 안된다!
- 클래스 설계 가치중 “단순성”을 위배하게 될 수 있다.
- 코드 수정시, 다른 부분에 변경이 없다면 그대로 유지하는 것이 복잡성을 줄임
OCP를 사용하는 디자인패턴
- Abstract Factory
- Command
- State
- Strategy
추후에 자세히 공부해보자!
리스코프치환원칙(LSP)
- 상속관계에서 부모 클래스를 수정했을때 자식 클래스도 문제없이 작동해야 한다.
- 상속관계에서 “is vs have 관계” 검증해봐야함.
- 부모의 행위를 자식이 거부한다면, 상속받아서는 안된다.
예시
- 직사각형을 상속받은 정사각형 vs 정사각형을 상속받은 직사각형
어떤 상속관계가 맞는가?
=> 상속을 해서는 안된다
정사각형은 width-height가 같이 바뀌기 때문에, 부모의 행위를 자식이 거부한다.
- 동물 인터페이스에 “걷기/뛰기” 메서드 추가
붕어나 고래는 걷기 뛰기가 불가
걷기/뛰기/헤엄치기 행위는 상속과 별도로 존재해야함
=> 해당 메서드가 사용된다고 가정하기 때문에, 구현하지 않을 경우 문제가 발생
인터페이스 분리원칙(ISP)
큰 덩어리의 인터페이스를 구체적이고 작은 단위들로 분리시켜 클라이언트들이 꼭 필요한 메서드만 사용할 수 있게 해야함
=> 한 기능에 대한 변경 여파 최소화
예시
- ISP 적용전
1 | // ISP를 적용하지 않은 예제 |
- ISP 적용 후
1 | // ISP가 적용된 예제 |
언제 사용해야 하는가?
- 항상 분리하는 것이 최선이 아님!
- 클래스의 인터페이스가 비대한 경우에 사용 => 재사용성이 높아짐
의존관계 역전원칙(DIP)
상위 수준(추상적)의 모듈이 하위 수준(구체적)의 모듈에 의존성을 가지는 경향
=> 하위 모듈 수정시 상위 모듈이 수정됌
추상적 : 자주 변경되지 않는/뼈대/인터페이스 및 추상클래스
구체적 : 자꾸 변경/추가 되는/구현체
예시
- 겨울이 지나면, 스노우 타이어를 변경해야 하기 때문에, 자동차가 => 스노우타이어(하위수준) 에 의존하는 형태
- 자동차가 => 타이어(추상화된 상위수준)에 의존하는형태
=> 타이어 종류가 변경되도, 자동차는 그 영향을 받지 않는다
자신보다 변하기 쉬운 것에 의존하던 것을, 인터페이스나 상위 클래스를 두어, 변하기 쉬운 것에 영향 받지 않게 하는 것이 의존 역전 원칙
Reference
MVC, MVP, MVVM 비교 https://beomy.tistory.com/43