안녕하세요 !
벌써 combine(3) 편입니다.
곧 있으면 제 프로젝트에도 combine을 야무지게 적용할 수 있겠죠?
원래는 3편부터 바로 예제 프로젝트로 살펴보려고 했는데 공부하다보니 실전에서 꼭 알아야 할 메서드나 개념들이 자꾸 생겨서 해당 부분만 빠르게 정리하고 넘어가려고 합니다.
미래의 제가 기억이 안나면 다시 돌아와서 보기 위함도 있기 때문에, 조금 자세할 수 있는 내용들도 이해한 만큼 적어보겠습니다.
Cancellables
저는 이 녀석을 공부할 때 많이 해맸어서 정리를 해두고 가려고 합니다.
관련 코드들을 살펴보다보니 계속 드는 의문이 있었습니다. (제가 동작 구조를 잘 몰랐어서 그랬던 것 같아요)
private var cancellables = Set<AnyCancellable>()
바로 "도대체 binding 해제는 어떻게 해주어야 하는것인가?(ViewController, ViewModel 에서!)" 였습니다
apple docs 나 다른 블로그들의 설명을 보면
메모리 누수 방지를 위해 존재하는 객체이며,
binding 정보가 남아있으면 binding된 두 객체는 메모리에서 지워지지 않기 때문에 꼭 cancel 을 해주어야 한다고 합니다.
그래서 저는!
일단 해당 cancel 이 일어나는 동작 원리부터 이해를 해보기로 했습니다.
(개념을 정확히 모르고 사용하면 결국 탈이 난다는 사실을 너무 뼈저리게 알았답니다..)
1. cancellables 객체를 담는 변수 생성
private var cancellables = Set<AnyCancellable>()
2. subscribe를 설정할 때, store(in: )을 통해 해당 binding 정보를 cancellables 변수에 저장
output.receive(on: DispatchQueue.main)
.sink { [weak self] event in
switch event {
case .fetchQuoteDidSucceed(let quote):
self?.authorLabel.text = quote.author
self?.quoteLabel.text = quote.content
case .fetchQuoteDidFail(let error):
self?.quoteLabel.text = error.localizedDescription
case .toggleButton(let isEnabled):
self?.refreshButton.isEnabled = isEnabled
}
}.store(in: &cancellables)
이러면 끝 입니다 !
Cancellables 객체는 해당 객체가 포함된 상위 객체가 메모리에서 해제되는 순간 binding 정보를 자동으로 전부 삭제하도록 설계되어있다.
때문에 저희가 굳이 ViewController나 ViewModel에서 객체가 deinit 되는 시점을 일일이 찾아서 cancel 해 줄 필요가 없다는 것입니다.
그렇기에, Cancellables를 활용하여 binding 정보를 관리하고자 한다면 Subscribe 을 구현할 때, 해당 binding 정보를 store만 잘해주면 된다는 말이 됩니다.
(저는 실제로 이게 잘 이해가 안되서 클래스 여러 개를 만들고, cancellables를 deinit 구간마다 직접 cancel 시켜 봤는데 하지 않은 것과 동일하더라구요 !)
아무튼 사용법과 그에 대한 합당한 이유까지 찾았으니, 맘편히 사용하면 되겠네요 ! (뒤의 참조 관계 관련 내용만 잘 숙지한다면...ㅎ)
Strong, Weak Reference
저는 MVVM DI를 기준으로 글을 작성하고 있기 때문에 ViewController와 VIewModel을 기준으로 설명 해보겠습니다.
(혹시 아래의 설명이 잘 이해되지 않으신다면, ARC + iOS의 참조 관계에 대해서 간략하게 서칭 및 공부해보시는걸 추천드립니다 !!)
<기본 조건>
a. ViewController에서 ViewModel 객체를 연결할 때는 Strong Reference 방식을 체택해야합니다.
b. ViewController와 ViewModel에서 서로의 in/output 정보들을 Subscribe 할 때는 Weak Reference 방식을 체택해야합니다.
저렇게만 말하면 뭔말인지 감이 잘 안오실 것 같아 예시를 준비해봤습니다.
예를 들어, ViewController가 Pop 되는 상황이 되었다고 가정해보겠습니다.
[ViewController]
1. ViewController가 Pop, 즉 매모리에서 해제되어 deinit 됨
2. ViewController의 Cancellables에 있는 바인딩 정보가 전부 소멸
[ViewModel]
3. ViewController에 강한 참조가 되어있던 ViewModel 객체가 메모리에서 해제
4. ViewModel의 Cacnellables에 있는 바인딩 정보가 전부 소멸
이런 플로우가 저희가 가정하는 이상적인 플로우 입니다.
그런데 만약 a, b 둘 중 하나라도 만족하지 못하면 어떤 일이 발생할까요?
(a를 만족하지 못하는 경우)
3번의 과정에서 약한 방식으로 ViewModel 객체가 연결되어있었다면, ViewModel 객체는 메모리에서 해제되지 않고 남아있었을 것이기 때문에 ViewModel 자체는 물론이고, 4번 과정도 이뤄지지 않을 것입니다.
(b를 만족하지 못하는 경우)
ViewController는 input을 수신 -> ViewModel은 input을 송신 (binding)
ViewModel은 output을 수신 -> ViewController는 output을 송신 (binding)
이러한 binding 관계가 연결되어 있을 것 입니다.
이렇게 참조하고 있는 관계에서 강한 참조를 걸어버리게 되면,
2번에서 바인딩 정보를 소멸하고 있는데, ViewModel 쪽은 아직 메모리에서 해제된게 아니기 때문에 완전히 binding 정보가 삭제되지 않습니다.
반대로 4번에서도 이전에 일어난 불완전 소멸 때문에 ViewController가 메모리에서 해제되지 못하여 제대로 된 binding 해제가 일어나지 않습니다.
그렇기 때문에 ViewController와 ViewModel은 동적 메모리 공간 어딘가에서 한참을 해매다가 앱이 종료되면 그제서야 해제가 되겠죠.
즉, 크나큰 Memory leak을 유발할 수 있는 부분입니다.
때문에 해당 부분은 정말 유의해서 작성해야 할 것 같습니다.
마무리
일단 combine에 대한 기초 공부는 이 정도로 마무리 해보기로 하고, 다음 포스팅에서는 실제 MVVM + Combine 구조의 코드를 가져와 어떤 식으로 프로젝트가 구성되고 코드가 짜여지는지 정리해보겠습니다.
Flutter를 공부할 당시 Provider라는 비동기 이벤트 처리 + 상태 관리 프레임워크를 사용했었는데, 아무래도 선언형 프로그래밍을 지향하는 Dart와 Swift는 매우 다르지만
DI 관점에서 봤을 때, 비동기 이벤트를 어떤 방식으로 처리(State update & notify, Publisher & Subscriber)했느냐 정도의 차이인 것 같아서 조금 더 빠르게 이해가 된 것 같습니다.
또한 코드의 책임 분리 및 의존성 주입을 통해 각 코드의 유지 보수성과 테스트 용이성을 늘린다는 부분에서 MVVM 구조와 위의 개념들을 합쳐서 프로젝트를 구성하는건 매우 좋은 선택인 것 같습니다.
저는 iOS 기반의 프로젝트를 구현할 때 마다 늘 ViewController에게 정말 수많은 책임을 전가시키고, MVC(Massive View Controller)를 만들어낸 적이 많아서 유지보수가 쉽지 않았기에 이번 기회에 싹 다 리팩토링 해보려 합니다.
그럼 오늘은 여기까지 작성하고 다음 포스팅에서 뵙도록 하겠습니다.
mins 주인장 올림
'iOS > iOS' 카테고리의 다른 글
[iOS] Combine (4) (6) | 2024.11.11 |
---|---|
[iOS] Combine (2) (9) | 2024.11.07 |
[iOS] Combine (1) (6) | 2024.11.05 |
[iOS] Frame & Bounds (2) - Bounds (0) | 2023.02.13 |
[iOS] Frame & Bounds (1) - Frame (4) | 2023.02.13 |