[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

💻 CS/객체지향

SOLID in Swift (2): OCP(Open-Closed Principle)

inu 2021. 10. 29. 21:35
반응형

해당 게시글은 아래 Article을 참고하여 작성되었습니다.
https://medium.com/movile-tech/open-closed-principle-in-swift-6d666270953d

 

Open-Closed Principle in Swift

Second article of the series of five about SOLID and its use in Swift

medium.com

SOLID란?

SOLID는 5개의 프로그래밍 디자인 원칙의 앞글자를 딴 합성어이다. 각 디자인 원칙들은 소프트웨어의 이해와 발전뿐 아니라 유연성과 유지보수성을 높여준다. 이러한 원칙들은 교수이자 소프트웨어 엔지니어인 Robert C. Martin(Uncle Bob으로 많이 알려진)으로부터 소개되었다.

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle.

오늘은 이중 OCP(Open-Closed Principle)에 대해 알아보겠다.


OCP(Open-Closed Principle)

OCP는 "Software Entity들(class, module, function 등)이 extension(확장)에는 열려있고, modification(수정)에는 닫혀있어야 한다"는 원칙이다.

 

좀 이상하게 느껴질 수도 있는데, 확장을 원한다면 자연스럽게 특정 코드를 수정하게 될 수도 있지 않을까? 아직은 좀 이해가 되지 않는다.

 

예시를 보자.

class Person {
    private let name: String
    private let age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class House {
    private var residents: [Person]

    init(residents: [Person]) {
        self.residents = residents
    }

    func add(_ resident: Person) {
        residents.append(resident)
    }
}

// 코드출처 :  https://medium.com/movile-tech/open-closed-principle-in-swift-6d666270953d
  • House class의 객체가 Person class의 객체를 포함하는 매우 단순한 구조이다.

이는 OCP를 지키지 않은 것이다. 예를 들어 Person class와 유사한 역할의 새로운 class인 NewPerson 같은 객체가 새로 생겼다고 가정해보자. 그럼 House 객체는 NewPerson 타입을 받아들일 수 없어 어쩔 수 없이 House를 수정해야 한다. 이것은 수정에 대해 닫혀있어야 한다는 OCP의 원칙에 위배된다.

 

이렇게 작은 예시만 보면 별게 아니라고 느낄수도 있지만, 큰 프로젝트일수록 연결된 Software Entity가 많아질 것이므로 치명적이다. 특정 모듈의 변경이 다른 모듈의 변경을 강제하지 않는 편이 코드 관리에 훨씬 유리하다.

 

이 코드를 OCP를 지키도록 한번 바꿔보자.

protocol Resident {}

class Person: Resident {
    let name: String
    let age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class House {
    var residents: [Resident]

    init(residents: [Resident]) {
        self.residents = residents
    }

    func add(_ resident: Resident) {
        residents.append(resident)
    }
}

// 코드출처 :  https://medium.com/movile-tech/open-closed-principle-in-swift-6d666270953d

Resident Protocol을 생성하고 Person class가 이를 따르도록 하며, House 클래스는 Person에 의존하지 않고 Resident Protocol에 의존하도록 한다. 이렇게 되면 NewPerson이 새롭게 추가되어도 Resident protocol을 따르기만 하면 문제가 발생하지 않는다.

 

예시를 하나만 더 살펴보자.

이번엔 enum으로 선언한 타입들에 대한 예시이다.

enum DeeplinkType {
    case home
    case profile
}

protocol Deeplink {
    var type: DeeplinkType { get }
}

class HomeDeeplink: Deeplink {
    let type: DeeplinkType = .home

    func executeHome() {
        // Presents the main screen
    }
}

class ProfileDeeplink: Deeplink {
    let type: DeeplinkType = .profile

    func executeProfile() {
        // Presents the profile screen
    }
}

// 코드출처 :  https://medium.com/movile-tech/open-closed-principle-in-swift-6d666270953d
  • DeeplinkType : link의 타입을 나타내는 enum
  • HomeDeeplink, ProfileDeeplink : DeeplinkType으로 구성된 type을 가지며, executeProfile로 screen을 띄우는 class
class Router {
    func execute(_ deeplink: Deeplink) {
        switch deeplink.type {
        case .home:
            (deeplink as? HomeDeeplink)?.executeHome()
        case .profile:
            (deeplink as? ProfileDeeplink)?.executeProfile()
        }
    }
}

// 코드출처 :  https://medium.com/movile-tech/open-closed-principle-in-swift-6d666270953d
  • Router : 링크를 받아 이를 통해 실질적인 화면표시를 수행하는 class

이 역시 OCP를 지키지 않은 것이다. 갑자기 새로운 종류의 DeepLink를 생성하면, Router의 switch문도 수정해야 하기 때문이다. 그리고 DeepLinkType의 enum도 수정해야 한다. 이는 확장에 대해 전혀 수정이 닫혀있지 않음을 반증한다.

이를 한번 개선해보자.

protocol Deeplink {
    func execute()
}

class HomeDeeplink: Deeplink {
    func execute() {
        // Presents the main screen
    }
}

class ProfileDeeplink: Deeplink {
    func execute() {
        // Presents the Profile screen
    }
}

class Router {
    func execute(_ deeplink: Deeplink) {
        deeplink.execute()
    }
}

// 코드출처 :  https://medium.com/movile-tech/open-closed-principle-in-swift-6d666270953d

enum 타입을 삭제하고 protocol을 통해 다형성을 만들고 이를 활용하도록 했다. switch문과 같은 복잡한 처리가 없어졌다. (cf. 많은 경우에 enum은 OCP를 깨뜨릴 위험을 준다. 따라서 조심히 사용할 필요가 있다.) 이렇게 될 경우 새로운 Link가 추가되더라도 Router에 추가적인 처리를 하거나 enum을 관리할 필요가 없다.

 

사실 항상 이런식의 수정을 닫아놓는 것은 불가능에 가깝다. 억지로라도 닫을 수는 있지만 닫지 않았을 때의 이득이 더 큰 경우도 많다. 이러한 판단을 할 수 있는 능력은 많은 경험을 통해 만들어지는 것이다. 좀 더 많은 경험을 하여 올바른 판단을 할 수 있도록 해야겠다.

 

cf. OCP를 지키기 위해 유용한 관습으로는 되도록 모든 class의 property를 private로 관리하고, global 변수를 사용하지 않는 것이 있다. property를 private 처리하여 그 class 자체의 원자성을 지키도록 하며, global 변수 사용으로 생기는 의존성을 방지하는 차원이다. 물론 이 역시 예외는 있으니 잘 판단하여 사용해야 한다.

반응형