ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아키텍처 & Composition
    ios/Architecture 2022. 8. 18. 16:50
    728x90

    학습 목표 :

    아무리 복잡한 기능도 작은 객체들로 나눌 수 있다면 특성있는 아키텍처의 기본기를 가질 수 있다.

     

     

    Composition(합성, 조립)

    A way to combine objects and data types into more complex ones - Wikipedia

    ( 객체와 데이터 타입을 조합해서 더 복잡하고 어려운 일을 객체로 만드는 방식 )

     

    Composition의 중요성

    - Composition은 다른 패턴들을 배우는 데 도움이 되는 기본기 패턴

    - Composition을 제대로 활용한다면 궁극적으로 SRP를 잘지킬 수 있게 됨

    - DRY원칙도 지킬 수 있게 된다.

     

    기존에 존재하던 패턴들은 각자의 문제점을 가지고 있다.

    MVC -> Massive ViewController

    MVVM -> Massive ViewModel

    NIBS -> Massive interrupt

    MVC의 VC가 커지는 것을 막기 위해 MVVM, NIBS 등을 도입해도 결국 아키텍처의 문제가 아니라는 것을 깨닫게 된다.

    따라서 Composition을 제대로 알고 활용하지 않으면 어떤 디자인 패턴을 사용하던지 Massive해 지는 것을 벗어날 수 없게 된다.

    

    작은 객체로 이루어진 코드는 재사용성과 유지보수가 굉장히 좋으며 TDD에도 유용해진다. 

     

     

    Favor object composition
    over class inheritance

    - Gang of Four, Design Pattern (1994)

     

    (상속은 코드에 유연성이 떨어지는 가장 강한 결합의 형태이기 때문에 상속 대신에 객체 합성을 더 선호해야 한다.)

     

    SOLID 원칙을 어겼을 때 생기는 부작용을 이해하고 그것을 굉장히 제한적으로 사용하는 것은 중요하다.

    코드를 재사용해야할 때 바로 상속부터 한다면 요구사항이 변할 때 민첩하게 대응할 수 없는 코드가 된다.

     

     

    Swift는 Composition을 잘지원하는 언어이다.

    대표적인 예로 Value 타입은 상속을 지원하지 않기 때문에 Value 타입의 기능을 확장하기 위해서는 Composition을 활용해야 한다.

     

    구체적으로 SwiftUI View들은 Value 타입이기 때문에 SwiftUI View들의 기능을 확장하기 위해서는 View Modifier를 활용해서 상속없이 기능을 확장할 수 있다. 

     

    더 나아가 Type, Interface, Module, 함수를 우리는 조합할 수 있다. 

     

    A라는 화면을 담당하는 객체가 있다고 하자

    class A: UIViewController {
        func setupViews() {
            // add sub views
        }
    }

     

    만약 A라는 뷰 안에 B 컴포넌트가 추가된 새로운 요구사항이 생겼다면 보통 A라는 객체를 재활용할 것이다.

     

    이때 상속을 사용한다면 다음과 같은 코드가 작성될 것이다.

    class B: A {
        override func setupViews() {
            super.setupViews()
            // add B View
        }
    }

     

    하지만 이런 코드는 A와 B가 서로 강한 의존성을 갖고 있기 때문에 중장기적으로 유지보수가 어렵다.

     

    이 코드를 Composition으로 해결한다면 다음과 같다.

    class A: UIViewController { }
    
    class B: UIViewController { }
    
    class C: UIViewController {
        private let a: UIViewController 
        private let b: UIViewController
        
        // add a, b to child viewcontroller
    }

     

    A와 B 영역을 담당하는 객체를 따로 만들어 이 두 객체를 Child로 가지고 있는 C라는 객체를 만드는 것이다. 

    그럼 A와 B가 서로 의존성 없이 새로운 C라는 객체에 조립하기만 하면 된다. 

     

    보통 Composition은 블랙박스라고 하고 상속은 화이트박스라고 한다.

    그 이유는 상속의 경우 부모의 모든 것을 알고있기 때문이다.  

     

    하나의 객체가 너무 많은 로직과 데이터를 갖는 것은 좋지 않다. 

    더 작게하고 적은 정보를 갖는 객체를 만드는 것이 그 코드를 이해하기 쉽기 때문에 유지보수 관점에서 좋다.

     

    UIKit을 보면 UINavigationController와 UITabBarController가 있는데, 이 두 가지는 화면 전체에 UI를 표시하는 것이 아니라 ChildViewController들을 화면에서 배치하고 서로 이동시키는 역할을 한다. 여러 뷰컨을 관리하는 만큼 복잡하고 여러 vc들을 관리하는역할을 하고 있는다. 우리가 NavigationController, TabBarController를 사용할 때 전혀 상속이 필요하지 않다. 그 이유는 UIViewController에 있는 navigatoin item이나 tabbar item의 값을 세팅해 놓으면 Contatiner ViewController가 그 값을 가지고 Composition해서 화면에 보이는 Tab Bar와 Navigation Bar를 만드는 것이다.

     

    이렇게 Composition을 활용하면 상속 없이도 복잡해보이는 기능을 단순한 API로 만들 수 있게 된다.  

     

    함수형에서 Composition을 활용한 예

     

    import Foundation
    
    let intResult = [1, 2, 3].map { $0 + 1 }
    
    let num: Int? = nil
    let optionalResult = num.map{ $0 + 1 }
    
    let result: Result<Int, Error> = .success(2)
    let resultResult = error.map { $0 + 1 }
    
    // 모든 map들이 이름은 같지만 각자 다른 타입의 값을 반환한다.
    
    // 공통점
    // 1. Generic 타입
    // Optional -> enum Optional<Wrapped>
    // Sequence -> associatedtype Element
    // Result -> enum Result<Success, Failure> where Failure : Error
    // Completion -> associatedtype Output
    
    // 2. transform 함수를 인자로 받음
    
    // 결론 
    // transform: A -> B 
    // A타입을 B타입으로 바꾸는 transform이라는 함수가 있을 때
    // F<A> -(map)-> F<B>
    // F<A> 타입을 매핑해서 F<B>타입으로 바꿀 수 있게 된다. 
    
    // flatMap을 조합해서 사용하는 경우
    
    let ageString: String? = "10"
    
    if case let x?? = ageString.map(Int.init) {
        print(x)
    }
    
    let ageResult = ageString.flatMap(Int.init)
    // ageResult: Optional<Int>
    // flatMap은 transform이 Optional<B>를 return하더라도 
    // map과 다르게 최종 return은 Optional<Optional<B>>가 아니라 Optional<B>이다.

     

    우리는 map과 flatMap을 활용하면 작은 함수들을 Composition해서 더 복잡한 작업을 파이프 라인처럼 구성할 수 있다.

     

     

    프로그래밍은 데이터를 다루는 일이기 때문에 데이터 타입 변환의 일련의 과정이라고 볼 수 있다. 

    앱을 살펴보면 UIEvent가 발생하면 TableView를 사용했다면 우리에게 IndexPath로 넘겨주게 된다. 우리는 이 IndePath에 해당하는 Model을 가져와서 서버URL을 받아오고 URL을 통해서 Data를 가져오게 된다. 그리고 이 Data를 통JSONDecoder를 통해서 새로운 Model로 전환할 수 있다.

      

    UIEvent ->  IndexPath -> Model -> URL -> Data -> Model -> ViewModel -> View

     

    예시 코드

    import UIKit
    
    struct MyModel: Decodable {
        let name: String
    }
    
    let myLabel = UILabel()
    
    if let data = UserDefaults.standard.data(forKey: "my_data_key") {
        if let model = try? JSONDecoder().decode(MyModel.self, from: data) {
            let welcomeMessage = "Hello \(model.name)"
            
        }
    }
    
    let welcomeMessage = UserDefaults.standard.data(forKey: "my_data_key")
        .flatMap { try? JSONDecoder().decode(MyModel.self, from: $0) }
        .map(\.name)
        .map { "Hello \($0)" }
    // if문이 없어서 코드의 depth가 1단계로 유지 가능
        
    myLabel.text = welcomeMesssage

     

     

    모듈 관점에서 Composition

     

    택시 호출 모듈은 상당히 복잡한 로직을 담고 있다. 이 로직들을 한 모듈 안에서 전부 담당한다면 서로 결합성이 깊어져 유지보수에 취약하게 된다. 

    The RED : 슈퍼앱 운영을 위한 확장성 높은 앱 아키텍처 구축 by 노수진

    하지만 이 모듈을 독립적인 모듈들로 나눈다면 복잡한 참조관계가 해소될 수 있다. Public이 아닌 부분은 다른 모듈에서 아애 접근조차 할 수 없게 막혀있어 sideEffect가 없을 것이고, 코드를 꼭 읽어보지 않더라도 무슨 역할을 하는지 파악이 가능하는 등 여러 장점들이 있다. 

     

    The RED : 슈퍼앱 운영을 위한 확장성 높은 앱 아키텍처 구축 by 노수진

     

     

    Interactor, Router 기반 아키텍처의 Composition

    - RIBs, VIPER

     

    The RED : 슈퍼앱 운영을 위한 확장성 높은 앱 아키텍처 구축 by 노수진

    Builder, Router, Interactor, Presenter, View가 하나의 묶음이며, 이 하나의 단위를 리블렛(Riblet)이라고 부른다. 

    리블렛은 한 화면 전체를 담당할 수 있고, 일부를 담당할 수도 있고 혹은 화면을 아애 담당하지 않을 수 있다. 

     

    리블렛은 tree구조로 시각화가 가능하다. 

    https://hithub.com/uber/RIBs/wiki

     

     

    마무리

    아키텍처에서 Composition이 중요한 이유는 앱이 처음에는 작게 시작하지만 나중에 복잡한 로직과 다양한 화면이 추가되면 코드가 많아지게 된다. Composition이 가능한 아키텍처는 앱이 아무리 커지고 복잡해져도 분리가 가능하고, 핵심을 이해하기 쉽기 때문에 유지보수가 쉽고 확장성이 좋아진다. 얼마나 나눌지에 대해서는 개인의 몫이다.

    728x90

    'ios > Architecture' 카테고리의 다른 글

    RIBs (미완성)  (0) 2022.08.22
    App Logic  (0) 2022.08.18

    댓글

oguuk Tistory.