samsung-ga / woody-iOS-tip

🐶 iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포
19 stars 0 forks source link

Composition Pattern #31

Open samsung-ga opened 2 years ago

samsung-ga commented 2 years ago

패스트캠퍼스 노수진님의 슈퍼앱 운영을 위한 확장성 높은 앱 아키텍처 구축 강의를 듣고 정리한 내용입니다.

Composition Pattern

유래

정의

상속보다 컴포지셔널을 더 활용하자




iOS, Swift에서 컴포지션이 활용된 예

iOS 프레임워크에서도 컴포지션이 다양하게 활용된다.

1. UIViewController

✨ 상속은 화이트박스, 컴포지셔널은 블랙박스 ✨

B가 A를 상속받게 되면, A의 모든 것을 알게 된다. 프로그래밍을 할 때 하나의 객체가 너무 많은 걸 알고 있고, 많은 로직과 데이터를 가지고 있는 경우가 좋을 때는 거의 없다. 더 작고 더 적은 정보를 아는 객체를 만드는게 유지보수를 할 때 더 좋기 때문이다. 그래서 C를 이용하여 조합하게 되면 A와 B는 서로 모르게 된다.

class A: UIViewController { }
class B: UIViewController { }

class C: UIViewController {
  private let a: UIViewController
  private let b: UIViewController
}

2. UIView

3. UINavigationController, UITabBarController

함수: map, filter, reduce

Sequence, Optional, Result, Publisher 타입에 map이 선언되어 있다.

let result = [1, 2, 3].map { $0 + 1 }.map { "만 \($0) 살"}
print(result) // ["만 2 살", "만 3 살", "만 4 살"]

let num: Int? = 1
let reulst2 = num.map { $0 + 1 }
print(result2) // 2

let myResult = Result<Int, Error> = .success(2)
let result3 = myResult.map { $0 + 1 }
print(result3) // success(3)

// Publisher 생략

4가지 경우의 공통점은 아래와 같다.

  1. Generic 타입
  2. transform 함수를 인자로 받음

map 같은 경우 A -> B로 변환하는 함수가 있다면, F -(map)-> F와 같이 동작한다. F는 4가지 타입에 공통으로 활용될 수 있다. 그렇다면, flatmap은 무엇일까? A -> B로 타입 변환할 때, Failurable한 메소드에 의해서 Optional이 반환이 되는 경우에 사용된다. 즉 A -> Optiona(B)와 같이 반환이 되는 것이다. 아래의 예시를 보자.

let ageString: String? = "10"
let result = ageString.map { Int($0) }

// Optional<A> -(map)-> Optional<Optional<B>>

중첩된 옵셔널을 풀어줄 수는 있지만, 알아보기 어려운 코드를 작성하게 된다. 이 때 flatmap은 중첩된 옵셔널을 제거한다. transform 함수의 optional를 반환하더라도 최종결과물은 optional라는 것을 알 수 있다.

let result = ageString.flatmap { Int($0) }

// Optional<A> -(flatmap)-> Optional<B>


앱에서 일어나는 작업들은 타입의 변환이 일어나는 작업이라고 생각하면 타입의 변환은 아래와 같다.

tableView의 아이템 하나를 선택해서 그에 해당하는 데이터를 서버에서 가져와서 보여주는 작업

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

그렇다면 위의 작은 고차함수들을 활용하여 더 복잡한 작업들을 하나의 흐름으로 조합해볼 수 있을 것이다. mapflatmap을 활용한 간단한 조립은 아래와 같다.

// 이전 
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)"
    myLabel.text = welcomeMessage
  }
}

// 이후 ✅
UserDefaults.standard.data(forKey: "my_data_key")
    .flatMap { try? JSONDecoder().decode(MyModel.self, from: $0) }
    .map(\.name)
    .map { "Hello \($0)" }

myLabel.text = welcomeMessage

모듈에서의 컴포지션

  • 복잡한 기능들이 단 하나의 모듈 안에 있게 된다면, 내부 객체들끼리 참조가 자유롭기 때문에 혼잡한 참조 관계에 매우 취약하게 된다.
  • 예시로, 택시 호출 모듈의 경우 결제, 위치, 쿠폰, 택시호출, UI 등 여러가지 기능들이 존재하고 이들이 하나의 모듈 안에 있게 된다면 복잡한 참조관계가 이루어진다.
  • 이를 독립적인 모듈들로 분리하게 된다면 복잡한 참조관계가 많이 해소된다.

장점

  • public 으로 공개하지 않은 부분은 다른 모듈에서 접근이 불가능하므로 사이드이펙트도 덜 일어난다.
  • 모듈의 public 인터페이스만 보아도 모듈의 내부를 들여다보지않아도 무슨 역할을 하는 지 쉽게 파악된다.
  • UI도 자신이 맡은 역할의 모듈로 분리하여 그 역할을 담당하게 바꾼다면, 택시 호출 모듈에서도 각 UI들의 위치만 잡아주면 된다.
image

아키텍처 패턴에서의 컴포지션

The Composable Architecture, ReSwift에서도 앱이 점점 커질수록 로직이 많아지면서, 상태와 상태를 업데이트하는 Reducer도 점점 커질 수 밖에 없다. 이 때, 각 역할의 State와 Reducer로 쪼개서 관리할 수 있다. 컴포지션을 염두에 두고 만든 아키텍처이므로 sub reducer, sub state로 쪼개서 조합시킬 수 있는 구조가 가능하다.

image

그래서 트리 구조로 시각화할 수가 있다. 화면이 아무리 복잡해져도 그리고 앱이 아무리 복잡해져도, 쪼개서 조합할 수 있기 때문에 3000줄의 코드를 300줄로 줄여 코드를 나눠 작성할 수 있다. 이는 개발자가 이해하기 쉬워지며 유지보수에도 용이해지게 된다.

Ref