Delegate 패턴 도입의 적합성
네트워크 상태에 따라 오디오 재생 이벤트를 처리하려고 한다.
처음엔 KVO와 NotificationCenter를 조합하여 코드를 구성했더니 NotificationCenter에 너무 많은 관찰자들의 등록을 요구했고, KVO로 인해 코드가 길어지며 가독성도 굉장히 떨어졌다. 정말 마음에 안들어서 다시 나만의 코드로 재구성하기 위해 어떤 패턴을 사용할지 어떤 프레임워크를 사용할지 생각해봤다.
NotificationCenter를 쓸까? 아니면 1:1 통신에 적합한 Delegate패턴을 사용하여 네트워크 실시간 모니터링을 구현할까?
최초엔 Delegate패턴이 더 깔끔하고 관리하기도 편한데다 어차피 AudioManager와 1:1통신이니 이보다 좋을 수 없다고 생각했다.
하지만 네트워크 실시간 모니터링을 AudioManager에서만 쓰는게 아니라 VideoManager에서도 사용하려는 경우에는 AudioManager 또는 VideoManager가 delegate를 등록할 수 없어, 둘 중 하나만 네트워크 실시간 모니터링 이벤트를 받게된다.
만약 어거지로 delegate를 AudioManager, VideoManager 두 곳 모두 등록하려고 한다면 delegate 배열을 사용하여 1:N 통신이 가능하지만 이러면 delegate를 사용하는 본질적인 의미가 없어진다. 오히려 notificationCenter를 사용하는게 더 의미있다.(전역이벤트와 n:m통신에 강하기때문)
따라서 delegate패턴이 아닌 notification center 프레임워크를 한번 더 사용하며 네트워크 실시간 모니터링 이벤트를 받기로 했다.
또다른 방법도 있다. AudioManager와 VideoManager 커스텀 클래스를 하나로 합하여 MediaManager 클래스를 만들어 delegate패턴으로 코드를 정리한 다음 extension을 활용하여 로직을 좀 더 정리해보는 것이다.(하지만 AudioManager와 VideoManager를 분리하여 두는 것이 나을 것 같아서 이 방법은 채택하지 않겠다)
좀 정리 좀 해볼까?
- 목적 : 네트워크 상태에 따라 오디오 재생 이벤트를 처리하려고함.
- 문제점
- NotificationCenter는 너무 많은 관찰자들의 등록 요구.
- Delegate패턴 사용시 1:N를 대상(AudioManager, VideoManager)으로 해야하니 적합하지 않다고 생각
- KVO 사용시 메모리 누수위험, 길어지는 코드, 떨어지는 가독성으로 인해 '내 입맛'에 굉장히 안맞음.
- 고려해야할 것
- 네트워크 실시간 모니터링을 단순히 AudioManager에서만 관찰하는게 아닌 ViewManager등 확장성도 고려해야함.
- 결론
- 1:N 소통과 전역이벤트에 강점을 보이는 NotificationCenter를 채택하기로함.
채택되지 않았지만 굉장히 자주 쓰이는 패턴이니, 아예 이 기회에 Delegate패턴에 대해 자세히 배워보자.
Delegate 패턴
- 객체 간의 통신이 주 목적이며 1:1 통신에 적합하다.
- 객체 : Class, Struct, instance를 의미하며 데이터(속성)과 동작(메서드)도 포함한다.
- 통신: 객체와 객체가 데이터나 이벤트를 주고받는 과정. (Delegate패턴에서는 Delegator가 Delegate에게 작업을 위임하는 방식으로 통신이 이루어짐)
- 한 객체가 다른 객체에게 특정 작업을 위임하는 방식이며, 위임받은 객체(delegate)는 특정 이벤트나 동작을 처리한다.
- Protocol을 사용하여 구현된다.
동작원리와 역할
- Delegator :
- 작업을 위임하는 객체 ( UITableView는 셀을 구성하는 작업을 UITableViewDelegate에게 위임한다 )
- Delegator는 Delegate가 구현해야하는 메서드를 프로토콜로 정의한다.
- Delegator는 delegate 속성을 통해 Delegate를 등록한다.
- Delegator는 특정 이벤트가 발생하면 Delegate의 메서드를 호출하여 작업을 위임한다.
- Delegate : 위임받은 작업을 처리하는 객체 ( VC는 UITableViewDelegate를 채택하여 셀을 구성한다 )
- Protocol : Delegate가 구현해야 하는 메서드를 정의한다.
장점
- Delegator와 Delegate의 역할이 명확히 분리되어 코드의 가독성과 유지보수성이 향상된다.
- 프로토콜을 사용하므로, 컴파일 타임에 타입 검사를 수행할 수 있다.
- Delegate를 동적으로 변경할 수 있다.
- 1:1통신으로 두 객체 간의 직접적인 통신이 가능하다.
단점
- 1:1통신의 한계로 인해 하나의 Delegator는 하나의 Delegate만 가질 수 있어 여러 객체에게 이벤트를 전달하려면 추가적인 작업이 필요하다.
- Protocol을 정의하고, Delegate에 등록하며, 메소드를 또 구현하는 과정으로 인해 코드가 복잡해질 수 있다.
- Delegate는 weak참조로 선언해야 메모리 누수를 방지할 수 있다.
Why? weak은 무엇인가?
- 약한 참조를 의미하며 참조하는 객체를 강하게 유지하지 않는다. 참조하는 객체가 메모리에서 해제되면 자동으로 nil으로 설정된다.
- weak은 optional 타입으로 변수선언 해야한다. ex) weak var delegate : MyDelegate?
- Delegate패턴이나 Clouser에서 self를 캡처할때 weak를 사용하면 메모리를 안전하게 관리할 수 있다.
Why? 왜? 어느 상황에서? 사용해야하는거지?
1. 순환 참조(circular Reference) 문제
- 두 객체가 서로 강한 참조(strong reference)로 가지고 있으면 Circular Reference가 발생하여 메모리에서 해제되지 않아 Memory Leak이 발생한다.
Why? 가끔가다 보이는 [weak self]는 뭐야?
- 그 가끔가다 보이는 [weak self]의 예시 코드르 한번 보자.
class MyVC : UIViewController {
var onButtonTapped : (() -> Void)?
func test(){
onButtonTapped = { [weak self] in
guard let self= self else { return }
self.handleButtonTap()
}
}
}
[weak self]란 클로저에서 self를 weak으로 캡처한 것인데, 이러면 ViewController가 해제될때 순환참조를 방지하여 메모리 누수를 방지할 수 있다.
Why? [weak self]는 Closure에서 구체적으로 어떻게 메모리 누수를 방지하는거지?
- 순환참조를 방지하여 메모리 누수를 방지한다는 것이다.
여기서는 순환참조가 왜 발생하는건가?
- Closure가 자신이 캡쳐한 객체를 strong reference로 유지한다. 만약 Closure가 self를 강하게 참조하고, self도 Closure를 강하게 참조하면 Circular reference가 발생한다. 이로인해 self와 closure는 서로 참조하게 되어 메모리에서 해제되지 않는다.
- 코드로 봐볼까?
class ViewController: UIViewController {
var onButtonTapped: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// 클로저가 self를 강하게 참조
onButtonTapped = {
self.handleButtonTap() // self를 강하게 참조
}
}
private func handleButtonTap() {
print("버튼이 탭됨")
}
deinit {
print("ViewController가 해제됨")
}
}
// 사용 예제
var viewController: ViewController? = ViewController()
viewController?.onButtonTapped?() // 출력: 버튼이 탭됨
viewController = nil // ViewController가 해제되지 않음 (메모리 누수 발생)
- onButtonTapped Closure는 self를 Strong reference하고 있음.
- ViewController가 onButtonTapped를 Strong reference하므로 Circular reference가 발생한다.
- viewController = nil로 설정해도 ViewController는 메모리에서 해제되지 않는다.
Why? 비슷한거로는 'unowned'라는게 있는데 차이가 뭐지?
- 참조하는 객체가 Optional타입이 아니어도 되며 해제되어도 nil이 되지 않는데, 해제된 후 접근하면 런타임 오류가 발생한다.
Why? weak대신 그냥 옵셔널 타입으로해서 nil을 사용하면 안되나?
- weak과 nil or optional타입은 서로 다른 개념이다.
- optional타입은 단순히 nil을 허용하는 타입일뿐, 참조 카운트를 관리하지 않기 때문에 optional타입만으로는 순환참조를 방지할 수 없다.
- optional타입의 사용목적은 '값의 존재 여부를 나타냄'이지만, weak의 사용목적은 '메모리 관리와 순환참조 방지'이다.
Why? 참조 카운트(Reference Count)가 뭐지?
- ReferenceCount는 swift(objective-c)에서 메모리를 관리하기 위해 사용되는 개념이다.
- 객체가 메모리에 유지되거나 해제되는 것을 결정하는데 Reference Count가 중요한 역할을 한다.
- 객체가 생성되면 ReferenceCount는 1로 설정되고 객체를 참조하는 것이 추가될떄마다 Reference Count가 증가하며 참조가 해제 될 때마다 Reference Count가 감소한다. 만약 Reference Count가 0이 되면 객체는 메모리에서 해제된다.
- optional타입이 Reference Count를 관리한다고 볼수없는 이유는 단순히 nil을 허용하는 타입일뿐 직접관리 하지 않으며 ReferenceCount 관리는 ARC가 담당한다. 따라서 optional타입은 ReferenceCount에 '영향을 줄 뿐', 관리하지는 않는다..
'iOS' 카테고리의 다른 글
[iOS] AVPlayer, AVPlayerItem, AVPlayer.currentItem (Video) (0) | 2025.02.11 |
---|---|
[iOS] AVPlayer + NotificationCenter (ep 01) (0) | 2025.01.17 |
[iOS] TabLayout (0) | 2023.08.10 |
[iOS] info.plist없이 권한 추가 설정하는법. (0) | 2022.12.14 |