이전글들은 사실 이 원칙을 위해 달려온 것이 아닌가하는 생각이 든다. 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는 모두 이 문제를 해결하기 위해 제시됨
        • VC를 View로 바라봄

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 (인터페이스 분리 원칙)

  • ISP

  • 한줄 설명

    • 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.
    • A라는 클래스가 B라는 클래스를 사용한다면, B 클래스의 모든 메서드를 사용하는 것이 좋다라는 의미
  • 의문과 고민

    • 그럼 그냥 인터페이스를 작게 만들면 되지 않을까?
    • 다시 한줄 설명의 단어들의 정의를 생각해보자.
    • 클라이언트
      • 어떤 다른 객체를 사용하는 쪽
    • 서버
      • 사용되는 쪽
    • 의존
      • 사용과 같은 말
    • 서버 클래스는 클라이언트 클래스가 필요로하는 최소한의 인터페이스만 제공해서 둘 간의 의존도를 낮춰야 함
  • 사용 방식

    • 서버 클래스를 상속을 해서 사용하는 경우
      • 클라이언트 클래스가 사용하지 않는 메소드의 선언을 강제하게 됨
      • 이는 상속받은 메서드를 퇴화시켜야 한다는 의미 = LSP 위반
      • 덩치가 큰 protocol을 상속 받으면 불필요한 메소드를 선언해야 할 가능성이 높음
        • Swift에서는 Protocol에 작성된 것은 모두 구현해야 함
        • 물론 Optional이라는 것이 있는데, 이 Optional을 따로 protocol로 모아서 관리하는 것이 훨씬 좋음
    • 내부에서 해당 서버 클래스의 인스턴스를 사용
      • 직접적으로 메소드를 사용하지 않으면 크게 영향을 주지는 않음
      • 하지만 컴파일시 의존관계에 의해 불필요한 컴파일이 요구됨
      • 불필요한 모듈의 업데이트 유발
  • 예시

    • Hashable

      • Protocol

      • String역시 hashable 채택하고 있기 때문에 동작은 같게 함

      • 하지만 만약 특정함수에서 Hashable 에 관련된 기능을 사용한다고 했을 때, String으로 변수를 잡기보다 Hashable로 받는 것이 좋음

        • String보다 Hashable이 더 작은 Interface이기 때문
        • 클라이언트와 String 사이의 의존관계를 한번 끊을 수 있음
      •   func myFunction(key: String) {
              let hashValue = key.hashValue
              // continue
          }
        
          func meyFunction(key: Hashable) {
              let hashValue = key.hashValue
          }
    • UITableViewDataSource & UITableViewDelegate 분리

      • 동작이 다른 코드를 분리하여 제공
    • Swift에서 잘게 분해된 Protocol들

      • Equatable, Comparable, Hashable etc
  • 통상적인 작업 방식

    • 보통 클라이언트 클래스 쪽이 서버 클래스와 강하게 커플링되어 있음
      • 별도로 분리된 인터페이스를 사용하지 않고 직접 서버클래스를 사용하기 때문
      • 서버 클래스 변경 사항이 클라이언트에 강하게 영향을 미침
    • ISP 적용
      • 서버 클래스를 SRP 준수하도록 잘게 분해
      • 인터페이스를 사용하는 그룹 별로 나누고, 이를 클라이언트가 사용
      • 남발할 경우 복잡도가 증가함
      • Protocol로 작업하면 간결함
  • Protocol Oriented Programming

    • POP 실천은, ISP 이해와 습관으로부터 출발함
    • 고전적인 ISP는 abstract class를 주로 사용하나 swift에서 protocol로 변화함
    • ISP에 따라 작게 분해된 Interface를 이용해 코딩하는 것이 POP
  • 요약

    • 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시켜 클라이언트들이 꼭 필요한 메소드만 사용할 수 있게 해야한다.

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에 의존해야 하는 상황이 있다면?
        • DIP가 해결책이 될 수 있음
  • 의존의 방향
    • 의존 관계는 어떤 방향으로 흘러가야 하는가?
      • 구체적인 부분에서 추상적인 방향으로 의존해야 함
      • 추상적인 것이 구체적인 것에 의존하지 않아야 함
    • 정책이 구체적인 것에 의존하는 경향
      • 정책은 추상적인 것임
    • 상위 수준은 하위 수준에 의존하면 안됨
      • 상위 수준: 추상적인 부분
      • 하위 수준: 구체적인 부분
  • 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)는 비휘발적 클래스에 가깝기 때문에, 해당 클래스를 참조하거나 소유하는 것은 큰 문제가 아님
profile
Goal, Plan, Execute.

0개의 댓글