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

💻 CS/객체지향

SOLID in Swift (5): DIP(Dependency Inversion Principle)

inu 2021. 11. 9. 01:23
반응형

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

 

Dependency Inversion Principle in Swift

Last 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.

오늘은 이중 마지막으로 DIP(Dependency Inversion Principle)에 대해 알아보겠다.

DIP(Dependency Inversion Principle)

DIP는 "상위 레벨의 모듈은 하위 레벨의 모듈에 의존해선 안되며, 둘 모두 추상화 수준에 의존해야 한다" 는 원칙이다. 이 원칙은 앞서 학습한 OCP(Open Closed Principle)와 LSP(Liskov Substitution Principle)를 엄격하게 지킨 결과이기도 하다. 따라서 OCP와 LSP에 대해서도 미리 알고 있으면 좋을 듯 하다.

 

OCP를 떠올려보자. 이는 코드가 확장은 가능하면서도 이에 대한 수정은 닫혀있어야 한다는 원칙이었다. 특정 모듈에 의존중인 모듈이 있을 때, 의존하는 모듈이 확장되어도 현재 모듈은 수정되어선 안된다는 것이다.

 

LSP를 떠올려보자. 이는 상위 자료형의 객체를 하위 자료형의 객체로 변경해도 어떠한 프로그래밍 변경이 없어야 한다는 원칙이었다. 상위의 자료형으로 처리된다면 하위의 자료형으로도 처리가 되어야 한다는 것이다.

 

내가 참고한 Article에서는 이 두 원칙의 해결방법으로 추상화(프로토콜 채택)을 선택했다. 즉, 두가지 원칙의 해결방법으로 DIP를 지키는 방향을 자연스럽게 선택한 것이다. (원 게시자는 이전 게시글을 참고해 다른 해결방법도 다시 살펴볼 것을 추천했다.)

 

예시를 보자

struct Product {
    let name: String
    let cost: Int
    let image: UIImage
}

// 코드출처 : https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5
final class Network {
    private let urlSession = URLSession(configuration: .default)

    func getProducts(for userId: String, completion: @escaping ([Product]) -> Void) {
        guard let url = URL(string: "baseURL/products/user/\(userId)") else {
            completion([])
            return
        }
        var request = URLRequest(url: url)
        request.httpMethod = "GET"

        urlSession.dataTask(with: request) { (data, response, error) in
            DispatchQueue.main.async {
                completion([Product(name: "Just an example", cost: 1000, image: UIImage())])
            }
        }
    }
}

// 코드출처 : https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5
final class ExampleScreenViewController: UIViewController {
    private let network: Network
    private var products: [Product]
    private let userId: String = "user-id"
    required init?(coder: NSCoder) {
        fatalError()
    }
    init(network: Network, products: [Product]) {
        self.network = network
        self.products = products
        super.init(nibName: nil, bundle: nil)
    }
    override func loadView() {
        view = UIView()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        getProducts()
    }
    private func getProducts() {
        network.getProducts(for: userId) { [weak self] products in
            self?.products = products
        }
    }
}

// 코드출처 : https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5
  • cf. 위 코드는 설명을 위한 코드로, 완벽하게 구현되지 않은 코드이다.
  • 여기서 주목해야할 점은 ExampleScreenViewControllerNetwork를 의존하고 있다는 점이다.

이는 DIP를 지키지 않는 코드이다. Network 모듈에 변경사항이 생길 경우 이는 바로 ExampleScreenViewController에 대한 영향으로 이어질 것이다. 이는 곧 결합도가 높다는 것을 의미한다. (사용자가 ExampleScreenViewController 사용을 원할 경우 반드시 Network도 함께 가져와야 함)

 

이 코드를 DIP를 지키도록 수정해보자.

protocol ProductProtocol {
    var name: String { get }
    var cost: Int { get }
    var image: UIImage { get }
}

// 코드출처 : https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5
protocol NetworkProtocol {
    func getProducts(for userId: String, completion: @escaping ([ProductProtocol]) -> Void)
}

// 코드출처 : https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5
struct Product: ProductProtocol {
    let name: String
    let cost: Int
    let image: UIImage
}

// 코드출처 : https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5
final class Network: NetworkProtocol {
    private let urlSession = URLSession(configuration: .default)

    func getProducts(for userId: String, completion: @escaping ([ProductProtocol]) -> Void) {
        guard let url = URL(string: "baseURL/products/user/\(userId)") else {
            completion([])
            return
        }
        var request = URLRequest(url: url)
        request.httpMethod = "GET"

        urlSession.dataTask(with: request) { (data, response, error) in
            completion([Product(name: "Just an example", cost: 1000, image: UIImage())])
        }
    }
}

// 코드출처 : https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5
  • 우선 추상화를 위한 protocol들을 만들고 실제 객체들이 이를 채택하도록 한다.
  • Network 모듈에서도 Product 객체에 직접 의존하기보단 ProductProtocol이라는 추상화 protocol에 의존한다.
final class ExampleScreenViewController: UIViewController {
    private let network: NetworkProtocol // Abstraction dependency
    private var products: [ProductProtocol] // Abstraction dependency
    private let userId: String = "user-id"

    required init?(coder: NSCoder) {
        fatalError()
    }

    init(network: NetworkProtocol, products: [ProductProtocol]) { // Abstraction dependency
        self.network = network
        self.products = products
        super.init(nibName: nil, bundle: nil)
    }

    override func loadView() {
        view = UIView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        getProducts()
    }

    private func getProducts() {
        network.getProducts(for: userId) { [weak self] products in
            self?.products = products
        }
    }
}

// 코드출처 : https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5
  • 그리고 최종 코드 또한 protocl들에 의존하도록 하였다.

이제 상위 모듈도 하위 모듈도 모두 추상화에만 의존한다. 모든 모듈이 추상화에 의존하기 때문에 이는 DIP를 지키는 코드이다.

 

추상화 처리를 해놓으면 이전 레거시를 새로운 구현으로 대체할 때도 용이하고, 테스트도 훨씬 용이해진다. 매번 protocol을 선언하고 처리해주는 것이 조금 귀찮긴하지만 DIP는 가능하다면 무조건 지키는 것이 좋겠다. (이를 지키면 자연스럽게 다른 SOLID 원칙들도 지킬 수 있기 때문이다.)

반응형