Swift OOP) SRP - 단일 책임 원칙(Single Responsibility Principle)

오늘 공부해볼 내용은 모든 OOP 설계 원칙의 근간이 되는 SRP - 단일 책임 원칙 입니다.

SRP를 지켜야하는 이유 선요약을 하자면 .. 이래서 안되고..

image

이래서도 안된다! 입니다.

image

책임이 없다면 필요 없는 객체이다

“단일” 과 “책임”의 의미 우선 책임의 의미부터 생각해보겠습니다.

“객체가 존재하는 이유는 책임 없는 객체는 존재할 이유가 없다”. 여기서말하는 책임이란건 어떤 것을 말하는 걸까요? 객체의 책임은 곧 객체가 수행하는 역할을 뜻하게 됩니다. 즉 역할이 없는 객체는 필요가 없다는 소리가 되는 것입니다.

만약 클래스가 여러개의 책임(역할)을 가지게 되면 어떻게 될까요?

우리는 앞에서 좋은 소프트웨어 설계는 “높은 응집력과 낮은 결합력”이라고 하였습니다. 결국 여러개의 책임을 가지게 된다는 것은 서로 관련성이 없는 두 코드들이 강하게 결합되어있다는 것을 의미하게 됩니다. 마찬가지로 테스팅의 관점에서 보았을때도 변경에 따른 테스트의 갯수 또한 많아지게 됩니다.

“클래스는 단 한개의 책임을 가져야한다” 이말은 “클래스를 변경하는 이유는 단 한개” 한다는 말과 동일합니다.

SRP의 기본적인 정의 입니다. SRP는 이런 변경의 관점에서 분리가 되어야 합니다. 아래의 상황을 고려해 보겠습니다.

  • 특정 기능을 변경하기 위해 수정했는데 여러 클래스가 수정되었다.
    • 클래스의 기능들이 여기저기 흩어져있다는 소리고 결국 이는 클래스의 응집력이 약하다는 것을 의미하게됩니다.
  • 특정 기능을 변경하기 위해 수정했는데 클래스의 대부분이 수정 되지 않았다.
    • 위와 반대되는 경우로 마찬가지로 클래스의 역할이 수정한 곳 이외의 다른 곳에 존재한다는 것을 의미 합니다.

SRP는 이런 변경의 관점에서 분리될 이유가 없는데 분리가 되었다는 것은 내가 객체의 역할/책임을 잘못 설정해 준 것을 의미합니다. 이는 불필요한 복잡성을 야기하게 됩니다. 간단한 해결책으로 분리될 수 있는 부분을 별도의 클래스로 두어서 해결하는 방법이 있습니다.

예제를 통해 조금더 알아 보겠습니다.

SRP를 준수하지 않는 클래스

건설회사 클래스는 안전한빌딩 을 건설하기 위해 설계도그리기,건축공사하기,안전도테스트 를 완료하여 반환해주는 클래스입니다.

class 건설회사 {
    func 건설하기() -> 안전한건축? {
        let 건축설계도 = 설계도그리기()
        let 건축물 = 건축공사하기(건축설계도: 건축설계도)
        let 입주가능건물 = 안전도테스트(건축물: 건축물) ? 건축물 : nil
        return 입주가능건물
    }
    
    func 설계도그리기() -> 건축설계도 {
        return 건축설계도()
    }
    
    func 건축공사하기(건축설계도: 건축설계도) -> 건축물 {
        return 건축물()
    }
    
    func 안전도테스트(건축물: 건축물) -> Bool {
        return 건축물.안전도 == 100 ? true : false
    }
}

우선 위의 예제의 경우 여러개의 책임을 가지고 있기 때문에 특정기능을 변경해야하는 상황이 발생하는 경우 불필요한 작업을 진행해야합니다.

변경이 발생!

가령 예를 들어 설계도그리기 작업이 상황에 따라 다른 설계도를 반환해야하게 기능이 변경이 된다고 생각해보겠습니다.

func 설계도그리기(타입: 건축타입) -> 건축설계도 {
    switch type {
    case .빌딩:
        return 빌딩건축설계도()
    case .아파트:
        return 아파트건축설계도()
    case .주택:
        return 주택건축설계도()
        ...
    }
}

여러개의 타입별로 별도의 수정이 발생을 하게됩니다.

이거 외에도 건축공사하기 에도 위와 같이 기능적 추가로 빌딩, 아파트, 주택등에 따라 다른 건축물을 반환한다고 했을때 그 변경이 건설회사 클래스에서 일어난다는 것 입니다.

이 같은 문제는 건설회사 클래스가 너무 많은 책임(역할)을 갖고 있다는 소리가 되는 것이고 잘못된 설계라고 할 수 있습니다. 이를 해결하기 위해 다중으로 된 책임은 별도의 클래스를 두어 건설회사는 건설하기 만을 할 수 있게 만드느 것입니다.

SRP를 준수하게 변경

class 건설회사 {
    let 설계자: 설계자
    let 건축공: 건축공
    let 건축안전기사: 건축안전기사
    
    func 건설하기() -> 인전한건축? {
        let 건축설계도 = self.설계자.설계도그리기()
        let 건축물 = self.건축공.건축공사하기(건축설계도: 건축설계도)
        let 입주가능건물 = self.건축안전기사(건축물: 건축물) ? 건축물 : nil
        return 입주가능건물
    }
}

건설회사 클래스가 이제 단 하나의 책임, “건설하기” 를 책임지고 있어서 SRP를 준수한다고 할 수 있습니다.

결과적으로 앞에서 기능에 대한 추가가 발생한 변경은 각 클래스에 맞게 변경할 수 있습니다. 설계에 대한 변경은 설계자 클래스에서 수행하고 건축안전기사는 건축안전기사 클래스에서 기능에 대한 추가나 수정을 할 수 있습니다.

SRP를 준수하기 때문에 클래스는 본인이 수행해야할 업무를 명확하게 할 수 있어서 응집력을 높이고 또 다른 클래스에 영향을 끼치지 않게 즉 변경사항은 해당 클래스에서 수행할 수 있게 결합력이 낮아졌다고 할 수있습니다.

iOS에서의 대표적인 SRP위반 사례

우선 ViewController가 비대해지는 Massive ViewController가 발생하는 것이 SRP위반의 대표적인 사례라 할 수 있습니다.

image

그럼 ViewController가 SRP를 위반하고 있다고 판단하는 근거는 무엇일까요?

  • ViewController가 데이터 패치 및 패치형 데이터를 관리하고 있을때
  • View의 Layout을 관여하고 있을때

자 그럼 왜? 애플은 애초에 이 원칙을 어긋나게 만들게 된걸까요?

애플의 MVC 설계를 우선 보고 가겠습니다.

  • 애플이 원하던 MVC의 모습

image

애플이 본래 MVC를 설계할때 Controller는 Model과 View를 연결시켜주는 역할을 하게 되므로 서로 알필요가 없다고 판단하였습니다. 그리고 Controller의 경우 재사용이 불가능 하기 때문에 가능한 모든 로직을 Model이 아닌 ViewController에 넣어주어야 하고 이는 앞서 말한 Massive View Controller를 발생하게 되는 원인이 되게 됩니다.

  • 현실..

image

Cocoa MVC는 위와 같이 Massive ViewController를 야기하게 됩니다. 그 이유는 View와 Controller가 뒤 엉켜 버리는 문제가 발생하게 됩니다. 그 이유는 View에서 발생하는 액션을 Controller에서 처리하도록 보내기 때문입니다. TableView를 예로 들어보면 TableView Delegate나 Datasource를 Controller에서 처리하게 되고 또 추가적으로 네트워크 요청과 같은 로직이 Controller에 존재할 수도 있습니다(극단적인 예시..)

그러므로 Cocoa MVC의 Massive VC가 SRP를 위반하고 있습니다.

이를 해결하기 위해 다양한 Architecture Pattern이 나오게 된 계기가 되었습니다.

  • MVP
  • MVVM
  • VIPER

추가적으로 앱 아키텍쳐에 대해서 SRP를 한번더 언급하여서 정리해보도록 하겠습니다.

Reference





© 2020. by Gaki

Powered by gaki