[iOS] Coordinator Pattern에서 굳이 Delegate Pattern을 사용하는이유가 뭘까?(feat. Strategy Pattern)

Youth·2024년 2월 21일
4

고찰 및 분석

목록 보기
14/21

오랜만에 블로그글을 작성하고있는 킴스캐슬입니다
추석연휴때 쉬다가 다시 원래의 생활패턴으로 돌아오는게 쉽지않았던것같네요...프로젝트를 새로하고있고 이런저런 일들이 지나가다보니 약 2주간의 블로그 휴식기를 가지게 되었습니다

오늘의 주제는 iOS에 관련된 주제라기보다는 Design Pattern에 관련된 이야기에 조금더 가깝지 않을까싶긴합니다만 결국은 iOS에서 쓰는 방식으로 이야기를 풀어나가고자 iOS카테고리로 설정을 해두게되었습니다

그럼 전략패턴(Strategy Pattern)과 델리게이트패턴(Delegate Pattern)에 대한 이야기를 시작해보겠습니다

문제상황

제가 리팩터링을 진행중인 프로젝트에서는 모든 객체에 추상화를 적용하는 작업이 되어있습니다. 그중에서도 coordinator패턴을 사용하기 위한 coordinator객체 또한 추상화가 되어있습니다

추상화가 되어있는 객체의 메서드를 사용하기위해서 객체자체를 protocol을 활용해 약한결합으로 생성자에서 주입받는방식을 사용하고있습니다

예를들어서 아래와 같은 뷰가 있다고 가정해보겠습니다

간단한 뷰입니다. 빨간색 터치영역을 터치하면 해당 카드의 상세내용을 볼수있는 새로운 ViewController로 navigation을 통해 push되게됩니다

팀내에서는 화면전환에 대한 역할을 하는 객체를 만들어서 역할을 분리하기로했고(coordinator의 사용의 정당성을 설명드리는 내용은 아니기에 coordinator라는 객체에게 화면전환의 역할을 맡겼구나 정도로 생각해주시면 좋을것같습니다)

우선 팀내에서는 coordinator패턴은 coordinator라는 화면전환을 담당하는 객체가 화면전환을 담당한다라는 패턴자체의설명을 듣고 구현을 시작했습니다. 당연히 coordinator도 하나의 객체이기에 해당 객체가 필요한 곳은 user의 input이 들어오는 viewModel이라고 생각했습니다 결국 coordinator패턴을 viewmodel이 소유하게되는 구조가 됩니다

예를들어서 위의 View가 MainViewController라고 하면 해당 화면에서의 오늘의 아티클이라는 카드영역을 터치해서 화면전환을 하는 메서드를 아래와같이 추상화할 수 있습니다

protocol MainViewControllerNavigation {
    func todayArticleCardTapped()
}

그러면 해당 프로토콜을 채택한 객체를 받아서 그 객체에 필수로구현되어있을 todayArticleCarTapped()라는 메서드를 실행시켜주면 된다고 생각했습니다

final class MainCoordinator: MainViewControllerNavigation {
    ...생략...
    func todayArticleCardTapped() {
    	let viewModel = MainViewModel(coordinator: self)
        let nextViewController = ArticleDetailViewController(viewModel: viewModel)
        self.navigationController.pushViewController(nextViewController, animated: true)
    }
}

그리고 이 coordinator객체를 viewModel의 생성자에서 주입받는 구조로 코드를 짰습니다

final class MainViewModel {
    let coordinator: MainViewControllerNavigation
    init(coordinator: MainViewControllerNavigation) {
        self.coordinator = coordinator
    }
    ...생략...
    // 만약에 touch event가 발생했을때 화면전환역할을 담당하는 객체의 메서드 실행
    self.coordinator.todayArticleCardTapped()
}

실제 프로젝트의 코드를 보면 캡슐화를 위해 추상화된 adaptor객체가 중간에 껴있긴한데 해당 구조는 이번에 이야기하고자하는 주제에서 벗어나는거같아 해당 내용을 제외한 코드를 적어봤습니다

여기까지 coordinator pattern을 구현했을때 큰 문제가 없다고 생각했었습니다. 그런데 해당 코드를 보고 지인분께서 의미상으로는 coordinator가 viewcontroller나 viewModel을 소유하고(편하게 viewcontroller를 소유한다고 하겠습니다) viewcontroller가 coordinator를 소유하고있는 관계가 되어버린것같다라는 이야기를 해주셨습니다

Coordinator Pattern의 Delegate Pattern

물론 추상화를 통해 약하게 결합되긴했지만 의미상 강하게 결합되어있는것같다라는 말을 듣고 어떤의미일까를 고민해보게되었습니다

우선 처음에 이이야기를 듣고 다른 분들이 coordinator pattern을 어떻게 사용할까를 구글링해봤습니다. 근데 거의 100이면 100 delegate pattern을 통한 위임방식을 사용하고 있었습니다

실제로 유명한 개발자분의 coordinator 튜토리얼에서도 viewController의 action을 delegate pattern을 통해 위임자인 coordinator객체에 알리는 방식으로 구조를 시각화한걸 볼 수 있었습니다

이걸보고나서 기존보다 조금 명확한 coordinator pattern의 개념이 정립되었습니다

coordinator pattern은 화면전환의 역할을 coordiantor 객체에 위임하는거구나

결국 위임을 하기위해서는 위임패턴이라고 불리는 delegate pattern을 사용하는게 맞습니다
여기까지는 고개를 끄덕이며 그렇지 그렇지 했습니다

그러다 문득 이런생각이 들었습니다.만약에 위의 코드들을 delegate pattern으로 고친다면 아래와같은 코드가 될겁니다. 헷갈리지않게 네이밍을 조금 변경해보면 우선 delegate protocol을 만들어주고

protocol MainViewControllerDelegate {
    func todayArticleCardTapped()
}

viewModel에서 해당 delegate를 weak으로 가지고 있게됩니다 그럼 당연히 생성자에서 객체를 주입받을 필요가 없어집니다

final class MainViewModel {
    weak var delegate: MainViewControllerNavigation?

    ...생략...
    // 만약에 touch event가 발생했을때 화면전환역할을 담당하는 객체의 메서드 실행
    self.delegate?.todayArticleCardTapped()
}

만약에 터치이벤트가 발생하면 위임자의 delegate메서드를 실행시키게됩니다 당연히 여기서 위임자는 coordiantor객체가 되겠죠

final class MainCoordinator: MainViewControllerNavigation {
    ...생략...
    func todayArticleCardTapped() {
    	let viewModel = MainViewModel()
        /// 위임자 설정 해주는 코드
        viewModel.deleate = self
        let nextViewController = ArticleDetailViewController(viewModel: viewModel)
        self.navigationController.pushViewController(nextViewController, animated: true)
    }
}

위임자설정이 완료되면 viewModel에서의 todayArticleCardTapped라는 메서드는 실제 coordinator객체의 todayArticleCardTapped메서드가 실행되고 다음viewcontroller로 화면전환을 할 수 있게됩니다

아마 위의 코드가 coordinator를 사용해보신 거의 대부분의 iOS개발자분들이 사용하신 코드구조일것같습니다

그러다 문득 이런 생각이들었습니다

결국 delegate를 통해서 해당 변수에 coordinator를 넣어주는 코드가 viewModel.deleate = self인데 애초에원래쓰던방식도 생성자를 통해서 coordinator를 넣어주는거니까(맨위의 코드의 let viewModel = MainViewModel(coordinator: self))둘이 넣어주는 시점이 생성할때 혹은 생성직후가 다른것 빼고는 똑같은거아닌가?

delegate를 사용하더라도 어떤 타이밍에는 해당변수에 해당프로토콜(MainViewControllerNavigation)을 채택한 객체가 들어올거고 그 객체가 들어왔다면 변수에는 프로토콜을 채택한 객체가 들어오게되고 그 객체의 메서드를 실행하는거니까 맨 위에서의 코드와 다를게 없어보였습니다

조금더 확장해보면 팀에서 사용하던 의존성주입방식을 어떻게보면 전략패턴이라고도 할수있을것같습니다 생성자의 타입을 프로토콜타입으로 두면 해당 프로토콜을 채택한 어떤 객체더라도 변수에들어올수있어 런타임에 다양한 객체의 행동을 사용할수있게되는거니까요

그래서 생각의 흐름이 delegate pattern과 전략패턴이라고 불리는 strategy pattern은 뭐가다른거지?라는 의문으로 변하게 되었습니다

Delegate Pattern과 Strategy Pattern의 선택기준

전략패턴에 대해서 공부하다보면 런타임에 객체를 갈아끼울수있다라는 뉘앙스의 설명이 있습니다

예시코드를 하나 보겠습니다. 요즘은 자동차에 시동거는 방식이 여러가지죠 key를 이용해서 걸거나 버튼을 눌러서 걸수도있습니다.시동거는 방식을 추상화를 해보겠습니다

protocol StartEngineStrategy {
    func startEngine()
}

그리고 해당 전략을 채택한 두가지 객체를 만들수있습니다

class AutoStartEngine: StartEngineStrategy {
    func startEngine() {
        print("버튼을 눌러 시동을 겁니다")
    }
}

class MenualStartEngine: StartEngineStrategy {
    func startEngine() {
        print("차키를 꽂아 시동을 겁니다")
    }
}

그리고 두객체를 통해서 제가 가지고있는 차에 시동을 건다고 해보겠습니다. 제 차는 우선은 차키를 꽂아서 시동을 거는 방식이라고 해보겠습니다

class MyCar {
   let startEngine: StartEngineStrategy
   init(startEngine: StartEngineStrategy) {
       self.startEngine = startEngine
   }
   func ready() {
       self.startEngine.startEngine()
   }
   
   func chageStrategy(_ input: StartEngineStrategy) {
       self.startEngine = input
   }
}

let myCar = MyCar(startEngine: MenualStartEngine())
myCar.ready() //"차키를 꽂아 시동을 겁니다"

그런데 갑자기 중간에 차를 튜닝해서 자동으로 시동을 걸수있게 바꿔본다고 하면 아래와같이 런타임에 전략을 교체할수있습니다

let myCar = MyCar(startEngine: MenualStartEngine())
myCar.ready() //"차키를 꽂아 시동을 겁니다"

myCar.chageStrategy(AutoStartEngine())
myCar.ready() //"버튼을 눌러 시동을 겁니다"

그런데 말이죠

이게 전략패턴에만 적용되는 장점일까를 생각해보면 아닌거같다는 생각이들었습니다. 위 코드를 delegate pattern으로 바꿔도 똑같을거같다는 생각이들었습니다. 실제로도 그렇기도 하구요

class MyCar {
   weak var delegate: StartEngineStrategy?

   func ready() {
       self.delegate?.startEngine()
   }
}

let myCar = MyCar()
myCar.delegate = MenualStartEngine()
myCar.ready() //"차키를 꽂아 시동을 겁니다"

myCar.delegate = AutoStartEngine()
myCar.ready() //"버튼을 눌러 시동을 겁니다"

교체방식이 생성자혹은 메서드를 활용하는지 혹은 변수자체의 값을 바꾸는 방식인지만 다르고 결국 런타임에 교체가 가능하다는 점에서 두 방식은 같은 효과를 낼수있다고 생각했습니다

여기서 내릴수있는 하나의 결론은 이렇습니다

Strategy Pattern, Delegate Pattern 둘다 프로토콜에 의존하기 때문에 런타임에 교체할수있는 장점을 가진다. 즉, 어떤 객체가 프로토콜을 따른다면 런타임에 Strategy역할을 할 수 있게 된다.

여기서 더 혼란이 가중되기 시작했습니다. 정말 주입시점과 방식만 다른건가?, 코드단에서 보면 똑같은데 명목상으로 나눠놓은 디자인패턴인건가?같은 생각이 들기 시작했습니다

그러다가 제 고민을 해결해줄 한 문단을 보게되었습니다

Use the strategy pattern when you have two or more different behaviors that are
interchangeable.
This pattern is similar to the delegation pattern: both patterns rely on a protocol
instead of concrete objects for increased flexibility. Consequently, any object that
implements the strategy protocol can be used as a strategy at runtime.
Unlike delegation, the strategy pattern uses a family of objects.
Delegates are often fixed at runtime. For example, the dataSource and delegate for
a UITableView can be set from Interface Builder, and it’s rare for these to change
during runtime.
Strategies, however, are intended to be easily interchangeable at runtime. 

저는 영어가 약점이기에 파파고의 힘을 빌려(도와줘요 파파고!) 핵심문장을 뽑아보면
제가 고민한대로 유연성(런타임에 교체 및 변경가능한점)은 프로토콜을 바라보는 모든 패턴에서 전략패턴처럼 쓰일수있기에 해당 특징이 전략패턴과 위임패턴의 선택기준이 될수는 없다고 합니다

하지만 밑에보면 이런말이있습니다 Unlike delegation, the strategy pattern uses a family of objects. delegate와 다르게 strategy는 객체의 famaily를 사용한다.

여기서 약간의 힌트를 얻게되었습니다. 위임자 패턴의 경우엔 위임자가 (거의)하나로 결정됩니다. 위의 설명에도 나와있듯이 우리가 UItableview의 delegate메서드를 사용할때 해당 tableview의 위임자인 viewcontroller를 바꿔주지 않습니다. 처음설정한 위임자그대로 변하지 않고 고정되죠

하지만 전략패턴은 위의 예시처럼 객체가 런타임에 매번바뀔수있기에 해당 프로토콜을 채택한 비슷한 action을 하는 객체가 여러개존재하고(이걸 family라고 표현한것같네요) 그 객체를 메서드를 통해 런타임에 갈아끼워가면서 사용할수있게하는 디자인패턴인거죠

그럼 처음으로 돌아가 코드를 보면 저희팀은 coordinator를 delegate이아니라 strategy pattern으로 사용하고 있었던거죠. coordinator pattern자체가 화면전환의 책임을 coordinator라는 객체에 위임한다에서 위임이라는 단어가있으니까 delegate을 쓰자가 아니라 해당 viewcontroller의 화면전환을 담당하는 객체는 변하지않고 앱이 종료될때까지 하나로 유지되기때문에 delegate pattern을 사용해서 구현하는게 맞다고 결론을 내리는게 맞지안을까라는 생각이 들었습니다

+ Coordinator에서 Delegate를 선택해야하는 이유

해당 주제로 공부를 하다보니 한가지 개인적으로 내린 결론이 있습니다. 물론 제 개인적인 생각이기에 틀릴수도있겠지만 어느정도는 일리가 있다는 생각이들어 추가로 기록하게되었습니다

유명한 zeddios님의 coordinator글을 보면 이런 내용이 있습니다

Coorinator가 훌륭한 이유

  1. 각 ViewController의 고립.

→ ViewController는 데이터를 표시하는 방법 이외에는 아무것도 모르며, 어떤일이 발생할 때마다 대리자(Delegate)에게 알리지만 Delegate가 누군지는 알 수 없습니다.
[출처] https://zeddios.medium.com/coordinator-pattern-bf4a1bc46930

아래보시면 어떤일이 발생할 때마다 대리자(Delegate)에게 알리지만 Delegate가 누군지는 알 수 없습니다라는 문장이있는데 해당 내용을 보면서 드는생각이 delegate를 사용하면 대리자가 누군지 혹은 어떤상태인지에 관계없다라는 말과 일맥상통하다고 생각이들었습니다

그말은 조금 다르게 말하면 의미상 결합도가 약하다라고도 표현할수있다고 생각합니다
계속 의미상 약하다 강하다라고 표현을 하는데

만약에 strategy pattern을 사용했다면 생성시점에 프로토콜을 채택한 객체를 받아와서 변수에 할당해줬을겁니다

final class MainViewModel {
    let coordinator: MainViewControllerNavigation
    init(coordinator: MainViewControllerNavigation) {
        self.coordinator = coordinator
    }
    ...생략...
    // 만약에 touch event가 발생했을때 화면전환역할을 담당하는 객체의 메서드 실행
    self.coordinator.todayArticleCardTapped()
}

위 코드처럼 말이죠
근데 그러면 장점은 생성을 이미 한 시점에서 startEngine이라는 변수에는 무조건 객체가 들어와있게되고 해당 메서드를 실행할때는 무조건 객체가 들어왔다라는 보장이됩니다

생성자를 통해 주입되는 객체가 있어야만 객체가 생성이되고 그 이후에 메서드를실행할수있는 이 상황자체가 두 객체의 관계가 서로가 서로에 관계없는 코드가 아니라는 생각이 들었습니다

프로토콜을 바라보기에 약한결합이지만 서로가 있어야만 생성및 실행이 가능한 의미상 강한결합도를 가진다고 생각했습니다

반대로 delegate를 사용하면 어떤가요

final class MainViewModel {
    weak var delegate: MainViewControllerNavigation?

    ...생략...
    // 만약에 touch event가 발생했을때 화면전환역할을 담당하는 객체의 메서드 실행
    self.delegate?.todayArticleCardTapped()
}

viewmodel를 생성할때 delegate라는 변수에 어떤 객체가 들어오나 안들어오나 생성하는데 아무런 문제가없습니다(optional이니까요 없으면 nil인거죠) 그리고 실행할때는 어떤가요 객체가 들어와야만 실행이되는게 아닙니다 없으면 optional chaining에 의해서 실행이 안될거고 있으면 실행됩니다

mainViewModel입장에선 delegate이라는 변수에 실제로 어떤 타입의 객체가 들어올지 심지어는 들어왔는지 안들어왔는지에 관심이 없습니다. 이런 관계가 저는 의미상으로 결합도가 약한게 아닐까라는 생각을 하게되었습니다

결국은 coordinator라는 design pattern을 만들었을때 화면전환객체와 화면전환의 주체간의 관계가 고정되어있고 viewcontroller입장에선 화면전환의 주체가 누군지 알필요가없어 ViewController가 고립되어 독립적으로 관리하고 유지할수있게 하고 싶었고 이러한 효과를 위해서는 strategy pattern가 아닌 delegate pattern을 통해서 coordinator pattern을 구현해야 의도에 맞다는 결론을 내리게되었습니다

24.02.24추가

현재 활동하고있는 iOS글쓰기모임에서 함께활동하시는 크루원분께서 두가지 피드백을 해주셨습니다
1. 제목이 조금더 아티클의 내용과 fit했으면 좋겠다
2. strategy pattern이 유용한 예시가 있었으면 좋겠다

두가지 피드백 모두 반영할만한 이유가 충분하다고 생각되어서
[iOS] Strategy Pattern과 Delegate Pattern는 무슨차이가 있을까? 에서 [iOS] Coordinator Pattern에서 굳이 Delegate Pattern을 사용하는이유가 뭘까?(feat. Strategy Pattern)로 변경했습니다

-> 기존에는 결국 해당 아티클을 쓰게된 이유가 coordinator에서 delegate말고 strategy를 썼었는데 다른사람들이 delegate로 쓰는 이유가 궁금해서 공부를 하다보니 궁극적으로는 delegate와 strategy는 비슷한거같은데 다른 디자인패턴이라고 불리는 분기점이뭘까에대한 고민을 담으려고 했는데 막상 써보니 coordinator에서는 delegate pattern이 어울린다는 말을 하고있는것 같아서 기존 제목으로도 고민에대한 대변은 가능하지만 전체적인 아티클의 내용과 fit한 제목은 coordinator에서의 delegate의 사용정도가 적당할것같습니다

두번째피드백의 경우엔 제가 전략패턴인줄모르고 썼던 전략패턴에대한 아티클이 있어서 그 아티클을 첨부해보려합니다:)
전략패턴은 언제쓰는게 좋을지가 궁금하다면?


마무리

기존에 사용하던 coordinator pattern구조에 관한 피드백을 받고 수정하는 과정에서 strategy pattern과 delegate pattern의 차이에 대한 의문이 들었고 의문을 해결함으로 인해서 protocol을 통해 런타임에 유연성을 가질수있는 두 디자인 패턴의 선택 기준을 스스로 정립할수있었습니다

물론 제 개인적인 결론이 추가되기도했고 지금 당장은 제가 내린결론이 맞다고생각하지만 디자인패턴에 대한 이해도가 높아지고 다양한 경험이 쌓이다보면 생각이바뀔수도있고 혹은 추가적인 근거가 생길수도있겠네요

그럴때마다 해당 포스팅을 업데이트해보겠습니다:-)

결국 디자인패턴은 개념이고 코드로구현하는 방법은 자유도가 높다고 생각하기에 개발자마다 의견이 다를수있다고 생각합니다! 그렇기에 피드백이나 반대의견은 언제나 환영입니다! 그런 의견이있다면 언제든지 댓글로 남겨주세요

그럼 20000!

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

3개의 댓글

comment-user-thumbnail
2024년 2월 22일

코드로만 봤을 때는 optional인지 아닌지 정도의 차이만 있지만 그 안에 담긴 의미는 생각보다 크네요!
확실히 디자인 패턴은 상황마다 적합성이 달라지는 것 같아요

제목을 봤을 때는 두 디자인 패턴에 대한 비교가 메인 주제일 줄 알았는데
내용은 Coordinator와 ViewModel 간의 연결을 구현할 때 Delegate와 Strategy 중 Delegate가 더 적합하다는 이야기로 보입니다

내용과 더 fit 할 수 있도록 제목을 바꿔보는 건 어떤가요?
Strategy 패턴이 효과적인 예시를 들어보는 것도 좋을 것 같습니다
Strategy 패턴이 빛을 발하는 순간도 있으니까요!

흥미로운 글 감사합니다

1개의 답글
comment-user-thumbnail
2024년 2월 24일

아직 (실력이 부족하여..) 디자인 패턴에 대해 고민해본 적이 없지만
디자인 패턴에 대해서도 공부의 필요성을 느끼게 해주는 글입니다.
(공부하고 싶은 건 산더미지만.. 시간은 없네요 ㅜㅜ)

스스로 고민하고 결론까지 내리는 모습 멋있습니다. 👍👍

답글 달기