[TIL]10.11

rbw·2022년 10월 11일
0

TIL

목록 보기
48/97

Architecting legacy monoliths for modular iOS app featuers

상황 : 15명의 iOS 개발자, 새 모듈을 만들고 싶음. 각자의 바운더리를 디자인 하는 방법, monolith pack 과 통신하는 방법을 모름. 또 8분의 긴 빌드시간이 존재함.

-> 모듈성을 염두에 두고 구축되지 않은 모놀리식 앱에 모듈을 추가하려고 함

실제 문제가 무엇인지? 내가 얻는 것은 ? 이런 점들을 염두에 두고 질문을 해야한다. 솔루션이 무엇인지에 대한 명확한 요구사항 목록이 없다면 더 안 좋아질 수도 있다.

현재 위 이미지와 같이 의존하고 있는데 여기서 새롭게 광고(AD) 모듈과 관련된 셀을 추가하려고 한다.

if else를 사용하여 추가하는 경우

이런 식으로 추가할 때 하나의 방법으로, if, else 같은 조건문으로 나누어서 어떤 때는 AdTableViewCell, 어떤 때는 NewAdTableViewCell을 추가해주면 된다.

하지만 이 방법은 OCP 원칙을 위배한다. 만약 새로운 셀이 또 추가된다면 기존 코드를 변경하여야 하는 일이 일어난다. 이는 유지보수에 있어서 좋지 않다고 볼 수 있다. (더 많은 다형성을 원하게 된다.)

서브클래싱을 사용하여 추가하는 경우

이런 그림으로 추가하는 방법은 셀을 서브클래스화 하는 것입니다. 하지만 예를 들어, 이미지를 제거해야 하는 셀이 있는 경우에 상위 클래스가 하위 클래스에서 제거하려는 항목을 정의하기 때문에 어려울 것입니다. 보통 서브클래싱을 사용하여서 동작을 제거하지 않고 추가합니다. 서브클래싱은 다형성을 어느 정도 얻을 수는 있지만, 상당히 제한적입니다.

따라서 다음의 방법들로 해보려고 합니다.

식별자를 통한 셀 분류

보통 뷰 컨트롤러가 셀을 로드할 때 스토리보드의 식별자를 사용합니다. 이를 이용하여, 스토리보드에서 셀을 하나 더 만들어서, 다른 클래스명을 사용한다면, 셀의 식별자를 구별해서 사용이 가능합니다.

일반적으로, 코드를 재사용 하는 것은, 동일한 것은 유지하고 다른 것을 주입하는 것입니다.

이 방법을 사용한다면 루트 컨트롤러(scenedelegate 같은 곳?)에서 분기를 나누어 A/B 테스트 등을 할 수 있습니다.

두 셀이 공유하는 인터페이스인 다형성 인터페이스를 만들자

결국 다형성이 중요하다고 생각합니다. 그런 의미에서는 아무래도 스위프트에서 자주 사용하는 프로토콜을 만들어야 합니다.

CellProtocol을 만들고, UITableViewCell을 채택합니다. 이는 내부 구현은 다르지만, 동일한 인터페이스를 공유합니다.

그리고 이를 사용하는 곳은 바로 타입캐스팅을 하는 곳입니다. as를 통한 타입캐스팅에서 해당 셀에 맞는 프로토콜을 사용할 수 있습니다.

그리고, 모듈의 기능을 다 프로토콜로 옮긴다면, 다음과 같은 다이어그램이 나옵니다.

해당 코드에서, AdVC가 AdFeature 프로토콜을 구현하고 있기 때문에 두 사이의 의존성이 있다는 것을 나타내었다.


추가로 알아두면 좋은 내용들

모놀리식 아키텍처에서 모듈의 종속성을 제거하려면, 모듈들의 커뮤니케이션을 조정하는 중앙 집중화된 장소를 마련하면 된다고 한다. -> 어댑터, 라우터 ?

종속성은 App -> Module의 방향이어야만 한다. 반대는 x.

만약, 스토리보드를 불러오는 코드에서, 다른 모듈(다른 앱)에 있는 스토리보드를 찾아오려면 bundle을 설정해줘야 한다. 동일한 앱이라면 nil로 설정해도 찾아 준다.

UIStoryboard(name: "detail", bundle: nil or Bundle(...))

셀만 추출한다면, 테스트가 어렵지만, 더 많은 구성요소(예를 들어, VC)를 추출하면 테스트가 가능하다. 이는 모놀리스 외부에서 클래스를 추출하여 가능하다. 이것이 모놀리스 앱에서 모듈을 사용할 경우, 가능한 일.


Closure를 사용 시, 항상 weak self를 사용해야 하는가 ?

탈출 클로저내에서, 객체 or 값이 참조가 되면, 해당 클로저가 실행될 때, 사용되도록 캡처가 된다. 이느 강한 참조이므로 강한 참조 사이클이 발생할 수 있다.

아래 코드는 강한 참조 사이클이 일어남.

// 버튼이 클로저를 유지(retain)하고, vc는 이 버튼을 유지하기 때문에 참조 사이클이 일어난다.
buyButton.handler = {
    self.showShoppingCart()
    self.purchaseController.startPurchasingProcess()
}

이를 해결하기 위해, 캡처 리스트를 사용합니다.

buyButton.handler = { [weak self] in
    guard let self = self else { return }
    self.showShoppingCart()
    self.purchaseController.startPurchasingProcess()
}

하지만 DispatchQueue와 같은 경우 참조 사이클이 발생하지 않는 경우도 존재한다.

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    self.showPromotionView()
}

하지만, 클로저 자체가 메모리에 남아있는 경우(위 코드는 2초) 유지하고 있기는 한다. 따라서 참조 사이클이 일어나지 않을 수는 있지만, 특정 개체의 수명을 연장된다면 예기치 않은 동작이 발생할 수도 있다.

weak self를 사용해야 하는 경우

  • 같은 객체에 의해 유지되는 클로저 내에서 self를 캡처하는 경우와 같이 강한 참조 사이클이 일어나는 상황
  • 장기간 저장될 클로저로 작업시 좋은 아이디어가 될 수 있다. 동일한 시간 동안은 메모리에 남아있기 때문에
  • 다른 모든 상황은 추가해도 해는 없지만 optional!

추가로, 약한 참조로 캡처 후, 강한 참조로 변환하는 경우

이 경우에 변환할 수 있는 방법 중 인상 깊은 방법을 소개해보려고 한다.

dataLoader.loadData(from: url) { [weak self] data in
    guard let strongSelf = self else {
        return
    }

    let model = try strongSelf.parser.parse(data, using: strongSelf.schema)
    strongSelf.titleLabel.text = model.title
    strongSelf.textLabel.text = model.text
}

아래 코드는 self를 강한 참조하지 않고 위 코드와 같은 동작을 수행함.

// We define a context tuple that contains all of our closure's dependencies
let context = (
    parser: parser,
    schema: schema,
    titleLabel: titleLabel,
    textLabel: textLabel
)

dataLoader.loadData(from: url) { data in
    // We can now use the context instead of having to capture 'self'
    let model = try context.parser.parse(data, using: context.schema)
    context.titleLabel.text = model.title
    context.textLabel.text = model.text
}

참조

https://www.swiftbysundell.com/questions/is-weak-self-always-required/

https://www.swiftbysundell.com/articles/capturing-objects-in-swift-closures/

profile
hi there 👋

0개의 댓글