본문 바로가기
Dev/고민과 삽질의 기록들🤔

Self-deallocating Coordinator Pattern으로 변경 이유

by Mintta 2023. 12. 14.

기존 프로젝트에서 Coordinator Pattern을 적용해서 ViewController로부터 화면전환에 대한 책임을 분리시켜주고 있었습니다.

그리고 오늘 하나의 의문점을 바탕으로 기존의 Coordinator Pattern방식에서 조금 변형된 Self-deallocating방식으로 변경하기로 결정했습니다.

 

오늘은 그 결정을 내리기까지의 과정을 한번 정리해보고자 합니다. 꽤나 두서없는 글이 될 수도 있을 것 같지만 한번 가봅시다.


사건의 발달

protocol Coordinator : AnyObject {
    var parentCoordinator: Coordinator? { get set }
    var children: [Coordinator] { get set }
    var navigationController : UINavigationController { get set }
}

extension Coordinator {
    
    /// Removing a coordinator inside a children. This call is important to prevent memory leak.
    /// - Parameter coordinator: Coordinator that finished.
    func childDidFinish(_ coordinator : Coordinator){
        // Call this if a coordinator is done.
        for (index, child) in children.enumerated() {
            if child === coordinator {
                children.remove(at: index)
                break
            }
        }
    }
}

 

Coordinator Pattern관련해서 레퍼런스를 찾다보면 가장 많이 접할 수 있는 구조입니다. 처음 Coordinator Pattern을 제안한 Khanlou님의 방법을 그대로 따른 것 입니다.

 

(Coordinator Pattern에 대한 정리와 도입기는 아래 링크를 남겨두겠습니다!)

https://codingmon.tistory.com/64

 

Coordinator Pattern 도입 이유와 실제 도입기

https://github.com/Team-LionHeart/LionHeart-iOS GitHub - Team-LionHeart/LionHeart-iOS: 라이옹 🦁 라이옹 🦁. Contribute to Team-LionHeart/LionHeart-iOS development by creating an account on GitHub. github.com Coordinator Pattern을 공부를 시

codingmon.tistory.com

 

 

이 방법은 Coordinator는 Child Coordinator들을 통해서 자신의 하위 flow들을 관리하고, 이 때 본인이 생성한 하위 flow에 대한 Coordinator의 대한 parent를 self, 자기 자신으로 해두고 children배열에 하위 flow coordinator를 append함으로써 parent/child 관계를 명확히하여 결과적으로 Tree구조의 Coordinator들을 가질 수 있게 됩니다. 

 

스터디를 같이 하던 팀원 중 한분이 "children 배열의 의미"에 대해서 물었고 그것이 오늘 글의 계기가 되었습니다.

 

Khanlou는 이것에 대해 아래와 같이 말합니다.

We use an array of childCoordinators to prevent the child coordinators from getting deallocated.

View controllers exist in a tree, and each view controller has a view. Views exist in a tree of subviews, and each subview has a layer. Layers exist in a tree. Because of this childCoordinators array, you get a tree of coordinators.

https://khanlou.com/2015/10/coordinators-redux/

 

"child coordinator가 deallocated되는 것을 방지하기 위해서 children 배열을 사용한다."

 

그리고 이것이 의문의 시작이었습니다.

의문의 시작

childCoordinator배열..정말 필요한 건가?🤔

We use an array of childCoordinators to prevent the child coordinators from getting deallocated.

 

첫번째 문장을 보면 childCoordinator의 배열을 쓰는 이유는 child coordinator들이 deallocated되는 것을 막기 위함이라고 합니다.

한번 확인해보고자 children배열을 아예 사용하지 않고 append하는 과정을 모두 제외한채 앱을 돌려보았습니다.

 

그런데 결과는 '아무 문제가 없었습니다'

여기서부터 의심이 들기 시작했습니다. childCoordinator의 배열을 사용하는 것은 chlidCoordinator를 강한 참조로 붙들고 있음으로써 메모리에 할당해제가 되지 않게끔 하는 것이라고 생각했고, children(childCoordinator의 배열)을 사용하지 않는다면 분명 메모리에서 할당해제가 되어 작동이 되지 않을 것이라고 예상했기 때문입니다.

 

왜 잘되었냐? 

 

ViewController에서 Coordinator를 강한 참조로 붙들고 있기 때문에 할당해제가 되지 않고 있다가(이미 children배열 효과 달성), VC가 메모리에서 할당해제될 때, 즉유저가 화면에서 다른 화면으로 벗어날 때, ViewController와 함께 Coordinator도 같이 할당해제가 되었기 때문입니다.

 

그렇다면 childCoordinators 배열을 통해 childCoordinator가 deallocate되는 것을 방지하는 효과는 VC와 Coordinator가 서로 다른 생명주기를 가질 때 이점을 가지는 경우라고 생각했습니다.

 

ViewController가 해제되어서 Coordinator가 같이 사라지게 되는 것은 시스템에서 이뤄지는 행동인 것이고, children배열에 Child coordinator를 넣게 되는 순간 우리가 manual하게 관리해주는 것으로 바뀌게 되는 것 입니다. 그렇기에 우리가 직접 위의 프로토콜에서 childDidFinish같은 메서드를 통해서 children배열에서 coordinator를 지워주는 액션을 취했던 것 입니다. 만약 이 childDidFinsh메서드를 호출해주지 않는다면 ViewController는 메모리 할당해제가 되었지만, Coordinator는 메모리에 남아있게 될 것 입니다.

 

다른 생명주기를 가져야하는 경우라면 위의 방법을 통해서 해야할 것 입니다.

 

그런데 사실 아무리 생각해봐도 ViewController와 Coordinator가 다른 생명주기를 가져야만 하는 경우가 떠오르지 않았습니다.

(이 부분은 나중에 분명 사실이 아니였다라고 깨달을 수도 있을 것 같습니다. 하지만 지금의 저로서는 떠오르지가 않았습니다..하지만 아니였다라는 것을 깨닫는다면 그것도 성장했다는 증거겠지요)

 

그래서 만약 children을 안썼을 때 생길 수 있는 문제점 중 하나로 떠올랐던 것이..

 

🤔: 만약 Coordinator에 여러개의 ViewController가 있다면, 하나의 ViewController가 사라지게 되면 앞선 ViewController들의 Coordinator도 같이 없어지면 어떻게 해?

 

사실 그럴 걱정도 없습니다.

 

그림에서와 같이 navigation controller를 공유하고 있기에 navigationController의 stack에 VC들이 쌓이게 되고, 각 VC들은 Coordinator에 대한 참조를 가지고 있습니다. 따라서 pop을 하면서 마지막의 ViewController가 메모리에서 할당해제가 된다한들 Coordinator는 앞선 navigation stack에 있는 ViewController들이 여전히 강한 참조로 붙들고 있기 때문에 Coordinator가 사라질 걱정은 없습니다. 그리고 모든 flow가 끝나는, navigation stack이 empty가 되면 자연스레 Coordinator또한 할당 해제가 될 것입니다.

 

결국 VC와 Coordinator가 같은 생명주기를 유지시키겠다는 의도하에서라면 childCoordinators를 안써도 되겠다고 판단하기까지 이르게 된 것 입니다.

 

그렇다면 parent는 무사할 수 있을까요 ?

 

사실 parent의 의미는 children과 함께 해야만 의미가 있다고 생각합니다.

Because of this childCoordinators array, you get a tree of coordinators.


childCoordinator 배열을 통해서 Tree구조의 coordinator를 얻을 수 있다라고 말하고 있습니다. 그리고 이 Tree구조를 만들기 위해서 필요했던 것이 parent와 children, 각 노드(Coordinator)의 부모 자식에 대한 관계 규명이라고 생각했습니다.

 

children을 안쓴다면 parent또한 쓸 필요가 없겠다고 판단했습니다. 즉, Tree 구조를 사용할 필요가 없다고 판단한 것 입니다.

 

parent와 children이 필요없고 이로 인해 coordinator의 생명주기를 ViewController 혹은 ViewModel의 생명주기와 일치시켜서 self-deallocating coordinator를 구성할 수 있습니다.

 

 


결론

childCoordinators배열, children을 통해 ViewController혹은 ViewModel과 Coordinator가 다른 생명주기로 유지되어야함에 대해 그 필요성과 근거가 부족했습니다. 결국 ViewController 혹은 ViewModel과 Coordinator는 같은 생명주기를 가지는게 자연스럽고 적어도 지금의 우리의 판단으로써는 맞다고 생각이 되었습니다. 

 

이런 결론을 내리고 나니, children배열을 써가면서 직접 childDidFinish를 통해서 매번 Flow가 끝날 때마다 명시적으로 지워주는 과정이 불필요하게 다가왔고, parent와 children을 완전히 없앤 Self deallocating 방식으로 Coordinator Pattern방식을 전환하기로 결정했습니다. 

 

여전히 Coordinator Pattern을 찾아보면 parent/child방식으로 구현되어있는 레퍼런스 수가 압도적으로 많기에, 우리의 이 방식과 근거들이 틀릴 가능성도 많다고 생각합니다. 만약 나중에 우리가 내렸던 판단과 그 근거들이 틀린 것임을 깨닫고 다시 parent/child 방식의 Coordinator Pattern으로 다시 리팩토링을 해야하는 상황이 온다면, 그건 그거대로 우리의 근거를 더 탄탄히한 더욱 더 좋은 설계로 가는 길이 아닐까 하고 생각합니다.

 

아직 적용은 되기 전 상황이라 수정이 완료되면 해당 PR과 프로젝트링크도 아래 첨부하겠습니다 !

https://github.com/Team-Plu/Plu-iOS/pull/37

 

[REFACTOR] 기존 Coordinator의 parent/child구조를 self-deallocation로 리팩터링(#35) by kimscastle · Pull Request #37

[#35] REFACTOR : 기존 Coordinator의 parent/child구조를 self-deallocation로 리팩터링 🌱 작업한 내용 참여인원 @ffalswo2 @cchanmi @kimscastle @LentoAssai Plu 코어타임에 Coordinator를 적용하는 와중에 기존 child와 parent구

github.com

++ 추가적으로 나눴던 토론들

🤔: Coordinator를 설계할 때 우리는 직관에 따라 논리적 흐름으로 ViewController들을 분류하여 묶었는데, 이런 논리적 흐름만으로 분류할 것이 아니라 코드 레벨로 기능을 생각해서 Coordinator를 설계한 후에 parent와 child를 정의했다면 VC 혹은 VM과 Coordinator간의 생명주기가 달라지게 되는 경우도 있지 않을까 ? 우리가 Coordinator를 설계할 때 놓친게 있지 않을까 ?
✅ Coordinator Pattern이 본래 제안된 이유, 본질을 생각해봅시다. Coordinator가 없는 기존의 방법을 사용한다면 하위 요소인 ViewController가 상위 요소인 NavigationController에게 user flow를 명령하는 명령의 흐름이 역전된 모습일 뿐만 아니라 UI를 관할하는 ViewController책임에 벗어나기에 화면전환에 대한 책임을 전담하는 Coordinator객체를 만들자! 가 본질입니다.
따라서 코드 단위로 기능적인 것 까지 염두에 두고 Coordinator를 분류하는 것은 Coordinator Pattern이 해결하고자 하는 문제의 본질에서 벗어난 것으로 판단했습니다.

결국 논리적 흐름에 따라 Coordinator를 설계한 것은 잘못되지 않았고, 결국 이는 하나의 User flow를 관리하는 NavigationController의 생명주기와 Coordinator는 대응되는 개념이고 이들의 생명주기는 운명을 같이할 수 밖에 없습니다.

 

 

🤔: parent와 child를 통해 부모와 자식 관계를 규명하는 Tree구조(기존방식) vs Self deallocating 방식의 non -Tree구조
✅ Tree구조와 non-Tree구조를 비교했을 때 Tree구조를 유지했을 때 non-tree구조에 비해 기술적으로 얻을 수 있는 이점이 있다라고 느끼지 못했습니다.

처음에는 parent/child방식은 Tree구조를 사용해서 부모와 자식을 규명하기 때문에 자연스러운 인간의 사고 흐름에 더 가깝게 표현되기에 더 익숙하고 이해하기 쉽고, Self deallocating방식은 non-Tree방식이기에 새로 해당 방식을 접하는 사람에게 매우 불친절하고, 사용하기에 앞서 모든 user flow를 완벽히 숙지해야하는 단점이 존재할 것이라고 생각했습니다.
하지만, Self-deallocating방식으로 한다 했을 때에도, 우리가 자료구조에서 정의하는 트리의 정의에는 맞지 않을 수 있지만 user flow상으로 충분히 트리로 표현될 수 있고 이 정도만으로도 개발자간에 소통하는데에 어떠한 지장도 없다고 판단했습니다.

 

 

댓글