새싹 커뮤니티 앱을 개발하면서 발생한 이슈를 정리한 페이지입니다.
이슈페이지는 평어체로 쓰도록 하겠습니다.
SLP란 현재 싹에서 진행하고 있는 Service Level Project의 약자입니다.
이번 SLP프로젝트에서는 Rxswift를이용한 MVVM-C를 기반으로한 클린 아키텍쳐(아키텍쳐 이름 참 길다)를 구성하고 있기 때문에 ,클래스끼리의 강한참조 순환이나 클로저에서 강한 참조순환이 많이 발생할 수 있는 구조를 가지고 있었음..! 그렇기 때문에 수시로 클래스가 Deinit이 제대로 되는지 프린트로 확인을 하고 있었다. 약한 참조나 미소유 참조를 사용하여 참조순환이 발생하지 않았을 것으로 예상하였지만, 몇몇 상황에서 Deinit이 되지 않는 상황이 발생하여 이슈를 정리하게 되었다
우선 강한 참조 순환을 방지하기위해, 클로저에서 [weak self] or [unowned self] 를 사용(주로 Rx 바인딩)하였고, 클래스 간의 강한참조를 방지하기 위해서 weak 선언(프로토콜 델리게이트 패턴이나, 코디네이터패턴에서 ViewModel에 선언 될때 서로 참조 하고 있는 구조로 weak를 해주었음)을 잊지 않고 해주었다.
의문1 . Rxswift에서는 왜 Unowned self가 아닌 Weak self를 주로 사용할까?
Rxswift에서 바인딩 할 때나 여러가지 operator를 사용하면서 클로저가 많이 등장하게 되므로 우리는 <약한참조>나 <미소유 참조>를 사용하게 된다. 이 부분에서 'Rxswift에서는 주로 왜 약함참조를 사용하는지'와 같은 의문을 새싹팀원 SM이 제기하였음.. 나도 팀원과 같이 덩달아 제목에 대한 의문점이 생기게 되었다. 나는 우선, 프로젝트를 시작하기 전에 강한 참조순환에 대해 Swift 문서를 정독하고 빡세게 공부를 마친 상태였음. 그래서 이 질문을 듣고 Rxswift 구독에 있는 모든 클로저에는 처음에 미소유 참조를 모두 써도 될 것같다고 생각가지게 되었다. 이유는 이러함.
Class안에서 이루어진 구독들은 Deinit되는 시점전에 모두 Disposed가 될텐데, 그럼 자동 구독이 해제되고, 절대로 클로저 안에 있는 구문들이 실행될 일이 없을 것이라 생각하게 되었다.
스위프트 문서로 공부한 결과 Unowned를 쓰는 것에 기준은 클로져보다 클로저 안에서 캡쳐할 인스턴스(self) 가 먼저 nil이 될 가능성이 없을때 (동시에 해제도 가능) 쓸 수 있다고 알게 됨! ARC가 클래스 Deinit전에 dispose시키고 deinit될텐데 그럼 클로저에서 캡쳐할 인스턴스가 먼저 nil이 될 가능성은 없다고 판단하게 된 것임..! 그런데 유명 Rxswift를 쓴 라이브러리나 깃허브를 찾아보면 전부 weak self로 꼭 쓰고 있었고, unowned를 쓰는 예제는 거의 찾기 어려울정도로 없었다.
그래서 든 생각이 그러면 Swift는 다른 언어들처럼 Deinit이 되기전에 모든것을 정리 되는게 아닌건가? 라는 생각을 갖게 되었듬. 그래서 프린트를 해보았는데 결과는 두둥!!
스위프트는 Deinit이 먼저 이루어지고 그다음 Disposed가 되더라 ! 그러니까 엄밀히 말하면 Deinit후에 구독이 취소된단 말. 결론적으로 미묘한 차이이지만 먼저 캡쳐할 인스턴스가 nil이 될 가능성이 있으니 weak self를 기본적으로 쓰되, 판단하에 nil이 되고서 클로저가 실행될 일이 정말 없다면 unowned을 써야 함
의문2. Rxswift에서 map을 사용할 때, 함수를 인자로써 넘기면 강한참조 순환이 발생할까?
결론부터 말하자면 발생할 수도 있고 발생하지 않을 수도 있음. Rx에대한 숙련도가 조금 있다면, 보통 map을 사용하여 Validation검사 같은 것들을 함수로 따로 빼서 넣으면 깔끔하게 사용할 수 있음을 공감할 거라 생각함..!
강한 참조 발생하지 않는 예제
예를 들면 아래와 같음.
input.didLimitText
.map(validationCertificationCode)
.distinctUntilChanged()
.drive(onNext: { [weak self] in
self?.isValidState.accept($0)
})
.disposed(by: disposeBag)
private func validationCertificationCode(text: String) -> Bool {
return text.count == 6 && text.isValidCertificationNumber()
}
map으로 validationCertificationCode 메소드 함수 이름만으로 넣으면 함수를 인자로 전달할 수 있다. (First Class Object) 이부분에서는 이렇게 사용하여도 순환참조가 발생하지 않는다.
번외로 얘기하건데, 함수 이름을 인자로 바로 전달하지 않으면 이러한 구성이 될 것이다.
input.didLimitText
.map{ [weak self] in
self?.validationCertificationCode ?? false
}
.distinctUntilChanged()
.drive(onNext: { [weak self] in
self?.isValidState.accept($0)
})
.disposed(by: disposeBag)
위아래 차이점은 [weak self] 이코드가 들어가냐 안들어가냐의 차이점.
[weak self]
guard let self = self else { return }
나만 그런지 모르겠지만 , 순환참조를 방지하기 위해 들어가는 위의 코드 구조는 불필요하게 코드를 차지하게 되어 방법이 있다면 안쓰고 싶은 마음이 들었음. 그래서 함수를 인자로 보내면 self를 쓸 필요도 없어서 자주 이용을 하게 되었는데 Deinit이 안되는 상황이 발생함.
강한 참조 발생하는 예제
아래 예제에서는 강한 참조 순환이 발생함.
private lazy var input = MyPageEditViewModel.Input(
viewDidLoad: Observable.just(()).asSignal(onErrorJustReturn: ()),
didWithdrawButtonTap: footerView.withdrawButtonTap,
requestWithdrawSignal: requestWithdrawSignal.asSignal(),
requestUpdateSignal: saveBarButton.rx.tap.map(makeUpdateInfo).asSignal(onErrorJustReturn: (false, 0, 0, .total, ""))
)
private func makeUpdateInfo() -> UpdateUserInfo {
return self.footerView.getUserInfo()
}
내용은 팀원 JD와 발견한 이슈라 같이 고민해보게 되었음. 똑같이 map으로 makeUpdateInfo라는 함수를 인자로 전달하고 있는데 왜 발생하는 것일까? 차이는 함수안에서 self.footerView~~로 self를 사용하고 있기 때문이다. 즉 클로저로 함수를 캡쳐할때 클래스의 인스턴스를 캡쳐하게 되면서 클로저와 인스턴스가 강한 참조 순환을 발생시킨다.
결론적으로 강한참조 순환을 피하기 위해, map에 함수를 바로 인자로 넣는 것도 좋지만, 함수안에 self로 클래스의 인스턴스를 사용하고 있는지 아닌지도 확인을 꼭 하고 사용하여야 한다.
회고
예상치 못한 곳에서 참조순환이 이루어 질때가 몇번씩 있다. 그래서 꼭 클래스가 Deinit이 잘되는지 확인하는 습관을 가져야겠다. 그러다보면 강한 참조 순환이 이루어지는 많은 상황에 익숙해지고 해결법도 다양해지지 않을까 싶다!
'🍒 iOS 개발 > 이슈' 카테고리의 다른 글
[SLP] 테이블뷰 Self-Sizing셀 안에 컬렉션뷰 Left-Align Self-Sizing와 Drop-Down 이슈 (0) | 2022.02.14 |
---|---|
[SLP] 커스텀 뷰 UI 이슈 (0) | 2022.01.31 |
[새싹커뮤니티 앱] Moya Access Token Plugin 이슈 (0) | 2022.01.11 |