상황 : 15명의 iOS 개발자, 새 모듈을 만들고 싶음. 각자의 바운더리를 디자인 하는 방법, monolith pack 과 통신하는 방법을 모름. 또 8분의 긴 빌드시간이 존재함.
-> 모듈성을 염두에 두고 구축되지 않은 모놀리식 앱에 모듈을 추가하려고 함
실제 문제가 무엇인지? 내가 얻는 것은 ? 이런 점들을 염두에 두고 질문을 해야한다. 솔루션이 무엇인지에 대한 명확한 요구사항 목록이 없다면 더 안 좋아질 수도 있다.
현재 위 이미지와 같이 의존하고 있는데 여기서 새롭게 광고(AD) 모듈과 관련된 셀을 추가하려고 한다.
이런 식으로 추가할 때 하나의 방법으로, 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)를 추출하면 테스트가 가능하다. 이는 모놀리스 외부에서 클래스를 추출하여 가능하다. 이것이 모놀리스 앱에서 모듈을 사용할 경우, 가능한 일.
탈출 클로저내에서, 객체 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초) 유지하고 있기는 한다. 따라서 참조 사이클이 일어나지 않을 수는 있지만, 특정 개체의 수명을 연장된다면 예기치 않은 동작이 발생할 수도 있다.
self
를 캡처하는 경우와 같이 강한 참조 사이클이 일어나는 상황이 경우에 변환할 수 있는 방법 중 인상 깊은 방법을 소개해보려고 한다.
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/