[iOS] Delegate Pattern / 델리게이트(위임) 패턴

2024. 12. 30. 07:42🍏/DesignPattern

1. iOS에서 Delegate 패턴이란?

Delegate 패턴은 객체 간의 통신을 구현하는 데 사용되는 디자인 패턴으로, 한 객체가 다른 객체의 행동을 위임할 때 사용됩니다.

  • 역할:
    • A 객체가 특정 이벤트나 작업을 처리할 권한을 B 객체에게 위임(delegate)합니다.
    • 위임받은 객체는 정의된 프로토콜을 준수하여 해당 작업을 수행합니다.
  • 주요 특징:
    • 1:1 관계: 한 객체는 하나의 delegate만 가질 수 있음.
    • 타이트한 결합: 위임받은 객체를 명시적으로 지정해야 함.
    • 유연성: 이벤트나 작업 처리 로직을 외부 객체에 위임하여 재사용성과 코드 분리를 향상.

2. Delegate 패턴 사용 사례

Delegate 패턴은 1:1 이벤트 처리가 필요한 상황에서 사용됩니다.

사용 사례

  1. UITableView, UICollectionView:
    • 데이터 소스와 사용자 인터랙션 처리를 UITableViewDelegate, UITableViewDataSource로 위임.
  2. 커스텀 뷰의 이벤트 처리:
    • 사용자 정의 UI 요소가 특정 동작을 다른 객체에 위임할 때.
  3. 비동기 작업 결과 전달:
    • 네트워크 요청의 결과를 호출한 객체로 전달.

 

3. Delegate 패턴, Notification, KVO의 차이점

1. Delegate 패턴

  • 특징:
    • 1:1 관계에서 사용.
    • 이벤트를 명확히 정의된 프로토콜을 통해 전달.
  • 장점:
    • 명확한 구조와 강한 타입 안정성.
  • 단점:
    • 위임 객체를 명시적으로 설정해야 함.
    • 객체 간 결합도가 높아질 수 있음.

2. Notification (NSNotificationCenter)

  • 특징:
    • 1:N 관계에서 사용.
    • 앱 전체에 이벤트를 방송(Broadcast)하여 여러 객체가 이를 수신.
  • 장점:
    • 느슨한 결합(loose coupling).
    • 이벤트를 전역적으로 관리 가능.
  • 단점:
    • 이벤트 관리가 복잡해질 수 있음.
    • 수신 대상이 명시적이지 않음.

3. KVO (Key-Value Observing)

  • 특징:
    • 특정 객체의 속성 값이 변경되었을 때 이를 감지.
    • 객체 간 결합 없이 상태 변화를 관찰.
  • 장점:
    • 상태 변경 감지에 적합.
  • 단점:
    • Objective-C 기반의 구현.
    • Swift에서 잘 사용되지 않으며, SwiftUI의 Combine이나 Property Observer로 대체됨.

비교 표

패턴 관계 용도 결합도
Delegate 1:1 이벤트를 특정 객체에 위임 높음
Notification 1:N 이벤트를 전역적으로 브로드캐스트 낮음
KVO 1:N 속성 값의 변경 감지 낮음

4. Delegate 패턴 구현 방법

구현 단계

  1. 프로토콜 정의:
    • Delegate가 수행해야 할 동작을 정의.
  2. Delegate 속성 선언:
    • 작업을 위임할 객체를 참조하는 속성.
  3. Delegate 호출:
    • 특정 이벤트가 발생했을 때 delegate 메서드를 호출.
// 1. 프로토콜 정의
protocol MyDelegate: AnyObject {
    func didReceiveData(_ data: String)
}

// 2. 작업 위임 객체 (Sender)
class Sender {
    weak var delegate: MyDelegate? // Delegate 속성 선언
    
    func performTask() {
        let data = "Hello, Delegate!"
        delegate?.didReceiveData(data) // Delegate 호출
    }
}

// 3. Delegate 구현 객체 (Receiver)
class Receiver: MyDelegate {
    func didReceiveData(_ data: String) {
        print("Data received: \(data)")
    }
}

// 4. 사용 예
let sender = Sender()
let receiver = Receiver()
sender.delegate = receiver // Delegate 설정

sender.performTask() // "Data received: Hello, Delegate!"

5. Delegate 패턴에서 주의할 점

Delegate 패턴을 사용할 때는 Strong Reference Cycle(강한 순환 참조)을 주의해야 합니다. 두 객체가 서로를 강하게 참조하면, 둘 중 하나가 더 이상 필요하지 않더라도 메모리에서 해제되지 않아 메모리 누수(Memory Leak)가 발생할 수 있습니다.

왜 Strong Reference Cycle이 발생하는가?

  • Delegate 패턴을 구현할 때, 위임자(Delegator)가 delegate 프로퍼티를 통해 Delegate 객체를 참조합니다.
  • 반대로 Delegate 객체도 위임자를 강한 참조(예: 프로퍼티)를 통해 보유하고 있는 경우, 강한 참조 순환이 발생합니다.

예를 들어, 두 개의 뷰 컨트롤러가 Delegate 패턴으로 연결된 상황에서:

  • A 뷰 컨트롤러가 B 뷰 컨트롤러를 Delegate로 설정.
  • B 뷰 컨트롤러가 A 뷰 컨트롤러를 강하게 참조.

이 경우, A와 B는 서로를 참조하면서 메모리에서 해제되지 않습니다.

해결 방법

1. Delegate 프로퍼티를 weak로 선언

  • Delegate 프로퍼티를 weak로 선언하면, 참조 카운트가 증가하지 않아 순환 참조를 방지할 수 있습니다.
protocol MyDelegate: AnyObject {
    func didPerformAction()
}

class AViewController: UIViewController {
    weak var delegate: MyDelegate? // weak 선언
}

2. 중재 객체를 사용하여 간접적으로 연결

 

  • Delegate 역할을 담당하는 별도의 객체를 생성하여 순환 참조를 방지합니다.

3. unowned 사용

  • 특정 상황에서 Delegate 객체가 항상 위임자보다 더 오래 살아있을 것이 확실한 경우, unowned를 사용할 수 있습니다.
unowned var delegate: MyDelegate

 

하지만 Delegate 객체가 먼저 해제될 가능성이 있다면, 런타임 에러가 발생할 수 있으니 신중하게 사용해야 합니다.