델리게이션(Delegation)

ellyheetov·2021년 2월 22일
0
post-thumbnail

Delegation Pattern이란?

간단히 말하자면, 특정 기능을 다른 객체에게 위임하고, 그에 따라 필요한 시점에서 메소드의 호출만 받는 패턴이다. 무슨말인지 전혀 모르겠다.

현실에서의 델리게이션 패턴

델리게이션은 프로토콜 타입의 특성을 이용하여 구현한다. 델리게이션 설명을 하기 앞서 프로토콜에 대해 잠깐 짚고 넘어가자.

Protocol

프로토콜은 마치 자동차 부품과 같다. 자동차를 만들 때를 생각해보자. 엔진, 모터, 배터리 등등 여러가지 부품들을 각각 만들어 놓고 한 데 모아 자동차를 생산한다.

각 부품들은 기능을 가진다. 예를들면, 엔진은 연료를 사용하거나 충전할 수 있다. 어떤 연료를 사용하냐에 따라서 충전 방식과 사용방식이 다를 수는 있다. 하지만, 연료를 사용하고 충전할 수 만 있으면 된다. 구체적인 방법은 해당 엔진을 사용하는 자동차가 결정한다.

여기서 부품들은 프로토콜이고, 자동차는 프로토콜을 채택한 객체에 해당한다. 프로토콜(엔진)은 동일한 기능을 가진다(연료를 소비/충전하는 일). 그러나 프토토콜을 채택한 객체(차)마다 프로토콜(엔진)의 내부 메소드 구현 방식(엔진을 소비/충전하는 방식)은 다를 수 있다. 프로토콜(엔진, 더 나아가 부품)을 채택한 객체는 자신만의 방법으로 구현하면 된다.

Delegation Pattern

자, 그럼 이제 델리게이션은 무엇이냐🤔

자동차의 연료는 엔진이 관리한다. 연료가 부족한 경우(연료가 가득 찬 경우)에만 자동차에게 알려주면 된다. 자동차가 엔진에게 연료관리를 위임했다고 볼 수 있다.

자동차는 엔진을 잘 사용하기만 하면 되고, 엔진은 연료가 부족할 경우에만 자동차에게 알리면 된다.

이것을 델리게이션 패턴이라고 한다. 연료를 관리하는 기능을 엔진에게 위임하고, 연료 보충이 필요한 시점에서 호출을 받는다.

예시

이제 코드로 예시를 들어보려고 한다.

스피커, 카메라, 스마트폰 등에서 공통으로 사용되는 배터리가 있다고 해보자. 이 배터리는 특별히 신경쓰지 않아도 잘 사용되다가 부족해지면 알림을 해주는 기능이 있다고 해보자. 이 알림을 받은 기기는 배터리를 충전하게 되고, 완충이 되면 이를 알려주는 기능이 있다.

여기서 중요한 점은 내가 직접 배터리를 매번 확인하지 않고, 배터리가 부족하면 알아서 알림을 해주는데에 있다. 알아서 알림을 해주는 것이 델리게이션의 역할이다. (조금 더 고급스럽게 말하자면 델리게이션은 필요한 시점에 메소드를 호출해주는 역할이다.)

배터리의 양에 따라 필요한 알림을 전달하게 될 BatteryDelegate 프로토콜 이다.

protocol BatteryDelegate {
    func lackBattery()
    func fullBattery()
}

이 프로토콜은 두 개의 메소드로 이루어져 있다. 하나는 배터리가 부족할 때 호출되는 메소드, 또다른 하나는 배터리가 가득 찼을 때 호출되는 메소드 이다. 이 메소드들은 기기(스피커, 카메라, 스마트폰)에 따라 나름대로 구현하게 된다. 배터리는 이 객체들의 메소드만 호출하여 배터리를 충전하거나 충전을 중단한다.

class Battery {
    var maxBattery : Double = 100.0
    var delegate : BatteryDelegate? = nil
    
    var battery : Double {
        didSet {
            if oldValue < 10 {         //배터리가 부족한 경우
                self.delegate?.lackBattery()
            } else if oldValue == self.maxBattery { // 배터리가 완충된 경우
                self.delegate?.fullBattery()
            }
        }
    }
    // 생략
}

배터리 클래스는 BetteryDelegate 프로토콜을 구현한 객체의 정보를 delegate 프로퍼티에 저장해 두었다가, 필요한 시점에 프로토콜의 메소드를 호출하는 대상으로 사용한다. 남은 배터리를 표현하는 battery 프로퍼티에 대한 프로퍼티 옵저버를 작성하 배터리의 양이 변화할 때마다 적정 수치를 검사하고, 10 미만으로 떨어지면 델리게이트 프로퍼티에 저장된 객체에 lackBattery() 메소드를, 배터리가 가득 차면 fullBattery()메소드를 각각 호출한다.

delegate 프로퍼티는 선언된 타입으로 인해, 실제 그 객체가 어떤 타입이든지 관계없이 BetteryDelegate 프로토콜에 정의된 lackBattery(),fullBattery() 메소드 만을 사용할 수 있다. BetteryDelegate을 채택한 객체가 다른 메소드를 구현하고 있겠지만, 여기서는 그 정보를 알 필요가 없다. 단지, 필요한 시점에서 lackBattery(),fullBattery() 메소드들을 호출 할 수 있으면 충분한 것이다.

마지막으로 배터리 클래스를 전자기기에 장착해보자. 배터리를 사용하는 클래스는 반드시 BetteryDelegate 프로토콜을 구현해야 한다.

class Phone : BatteryDelegate {
    var battery = Battery(battery: 100)
    
    init(){
        self.battery.delegate = self
    }
    
    func lackBattery(){
        print("lack of battery")
    }
    func fullBattery(){
        print("full of battery")
    }
    func start(){
        phone.startUse()
    }
}

작성된 클래스는 Phone이라는 이름의 클래스인데, 앞에서 작성한 Battery클래스의 인스턴스를 할당한다. 초기화 구문을 통해서 클래스가 만들어질 때 배터리를 100으로 채우고, 배터리의 델리게이트 프로퍼티를 자신으로 설정한다. 이제 Phone 클래스를 누군가 인스턴스로 생성하여 start 메소드를 호출하면 배터리 역시 작동되면서 배터리가 부족한 시점이 오면 delegate 객체를 대상으로 lackBattery() 메소드를 호출한다.

delegate 프로퍼티에는 Phone의 인스턴스가 할당되어 있으므로 Phone 클래스에서 작성한 lackBattery() 메소드가 실행된다.

델리게이션 참조를 통해 메소드를 호출할 인스턴스 객체를 전달받고, 이 인스턴스 객체가 구현하고 있는 프로토콜에 선언된 메소드를 호출하는 것이 델리게이션이라고 할 수 있다.

직접 적용해 보기

UIKit에서 제공하는 UISegmentControl을 관리하는 델리게이션을 만들어 보자.
먼저 프로토콜을 생성한다.

protocol UISegmentControlDelegate {
    func didChangeSegment(index item : Int)
}

프로토콜을 호출할 대상을 생성한다. 이 클래스는 UISegmentControlDelegate 프로토콜을 구현한 객체의 정보를 delegate 프로퍼티에 저장해 두었다가, 필요한 시점에 프로토콜의 메서드를 호출하는 대상으로 사용한다.

class OptionSegmentControl: UISegmentedControl {

    var delegate : UISegmentControlDelegate?
    
    init(items : [String]){
        super.init(items: items) // segmentControl의 item을 초기화 한다.
        self.addTarget(self, action: #selector(segmentValueChanged), for: .valueChanged) 
    }
    @objc func segmentValueChanged(){ // segment의 value가 바뀌었을때 호출 할 메소드이다.
        delegate?.didChangeSegment(index: self.selectedSegmentIndex)
    }
}

segmentControl의 값이 변하면 segmentValueChanged() 메소드가 호출된다. 그런데 segmentValueChanged() 메소드 안에서는 델리게이션의 메소드를 호출한다. 위임받은 객체가 구현한 didChangeSegment 메소드를 호출 할 수 있게 된 것이다.


class ViewController: UIViewController {
    let mysegmentControl = OptionSegmentControl(items: items)
    
    override func viewDidLoad() {
        mysegmentControl.delegate = self // segmentCotrol에 대해서는 너가 알아서 처리해. 위임!
    }
}
extension ViewController : UISegmentControlDelegate {
    func didChangeSegment(index item: Int) {
        //do something
    }
}

UISegmentControlDelegate 프로토콜을 채택한 CardViewController는 자신이 원하는 대로 didChangeSegment(:index)를 구현하여 사용하면 된다.

Delegation in Practical iOS Developement

델리게이션은 iOS 개발에서 가장 많이 사용하고 있는 디자인 패턴이다. 델리게이션을 사용하지 않고 앱을 만드는 것은 불가능하다.

Delegation을 사용하고 있는 클래스

  • UITableView : UITableViewDelegate와 UITableViewDataSource 프로토콜을 사용한다. 뷰 간 상호작용을 관리하고, 셀을 보여주거나 테이블 뷰를 바꾸는 작업을 한다.
  • CLLocationManager : CLLocationManagerDelegate 프로토콜을 사용한다. 사용자의 앱의 위치와 관련된 정보를 보고하는 작업을 한다.
  • UITextView : UITextViewDelegate 프로토콜을 사용한다. text view에 변화에 대해 보고하는 작업을 한다.

이 세가지 클래스의 공통 패턴이 있다. 위임된 모든 이벤트들은 사용자나 하드웨어, 운영체제에 의하여 초기화된다는 것이다.

  • UITableView : 스크린에 셀을 보여주는 메커니즘을 가지고 있는데, 이것은 사용자가 요구하는 셀을 제공한다.
  • CLLocationManager : 사용자의 위치가 수정될 때 마다 알려준다.
  • UITextView : 사용자의 입력에 반응한다.

위에 예시에서 배터리 객체는 자체적으로 배터리를 관리한다. 기기의 통제를 벗어났으므로 배터리 이벤트에 응답할 델리게이트가 필요하다. 제어 할 수 없는 이벤트 및 작업을 수행해야 하는 경우 델리게이션(위임)이 필요하다.

왜 Delegation을 사용할까?

  • 클래스간 상호작용을 전달하는 간단한 접근 방식이다.
  • 클래스간 요구 사항을 전달하기 위해서 프로토콜만 필요하다. 이는 클래스간의 결합을 크게 줄일 수 있다.
  • 클래스의 책임을 분리 할 수 있다.

왜 프로토콜을 사용할까?

클래스를 이용할 경우 상속을 통해 부모 클래스에서 정의된 변수나 상수를 활용할 수 있다. 하지만, 클래스의 다운캐스팅 또는 업캐스팅이 발생하여 매번 타입을 체크해야하며, 상속으로 인한 결합도가 증가할 수 있다. 또한, 클래스는 단일 상속만을 지원하기 때문에 하나의 부모 클래스를 상속받고 나면 더는 다른 클래스를 상속 받을 수 없으므로 기능을 덧붙이기에는 제한적이다. 이를 극복하기 위해 구현 개수에 제한이 없는 프로토콜을 이용하여 필요한 기능 단위별 객체를 작성하는 것이다.

참고
https://learnappmaking.com/delegation-swift-how-to/

profile
 iOS Developer 좋아하는 것만 해도 부족한 시간

0개의 댓글