[iOS] Delegate 패턴을 이해해보자

룰루날라·2022년 7월 3일
11
post-thumbnail

Delegate 패턴은 iOS에서 정말 많이 쓰이지만, 뭔지 모르고 그냥 코드를 치고 있기 제일 쉬운 것들 중 하나인 것 같다.

예를 들어, UITableView를 구현하기 위해서 UITableViewDataSource, UITableViewDelegate를 당연하게 ViewController에 채택하곤 하지만 왜 꼭 테이블뷰를 구현하기 위해 UITableViewDataSource를 채택해야 하는지, UITableViewDataSourceUITableViewDelegate는 왜 프로토콜로 구현되어 있는지 등등을 설명하는 건 특히나 초심자에겐 쉽지 않은 일이다.

애플은 왜 그렇게나 많은 UI요소들에 Delegate 패턴을 사용하고 있는 걸까?

대체 Delegate패턴이 뭐길래?!
항상 이해한 듯 안 한듯, 아는 듯 모르는 듯한 Delegate 패턴을 이해해보자!

🔁 Delegate Pattern이란?

델리게이트 패턴은 보통 “객체가 자신의 책임을 다른 객체에게 위임(delegate)하는 디자인 패턴”이라고 설명된다.

예를 들어, 테이블뷰는 셀을 탭했을 때 어떤 행동을 할지에 대한 책임을 뷰컨트롤러에게 UITableViewDelegate를 사용해 위임한다.

테이블뷰 외에도 콜렉션뷰, 텍스트필드 등 많은 UI요소들이 델리게이트 패턴을 사용해 다른 객체에게 책임을 위임하고 있다.

왜 굳이 “위임”을 하는 것일까?

UI요소에서의 Delegate Pattern

사용자가 테이블뷰의 셀을 탭하는 상황을 예로 들어보자.

셀을 탭하면 테이블뷰는 탭 이벤트를 받는다. 테이블뷰가 탭 이벤트를 받으면 delegate의 didSelectRowAt 메소드를 실행시킨다. 이벤트를 받았을 때 어떤 행동을 할 것인지를 delegate에게 위임한 것이다.

보통은 ViewController에 tableView.delegate = self와 같은 코드를 작성해 ViewController self, 즉 ViewController의 인스턴스 자신을 위임자(delegate)로 설정하고, didSelectRowAt 메소드에 셀을 탭했을 때 어떤 행동을 할지를 정의한다.

테이블뷰가 알아서 다 하면 될 것 같은데 왜 굳이 다른 객체에게 위임을 해주는 걸까?

바로 우리가 UITableView의 내부 코드를 수정할 수 없기 때문이다.

셀이 탭되었을 때 어떤 행동을 할지는 상황에 따라 다르기 때문에 개발자가 코드를 작성해야 한다. 하지만 우리는 애플이 숨겨놓은 테이블뷰 안의 코드를 수정할 수 없다. 따라서 다른 객체에서 해당 코드를 작성해준 뒤, 테이블뷰가 그 객체의 코드를 호출해줘야 한다.

이 때 테이블뷰와 객체를 연결해주는 방식이 Delegate Pattern인 것이다.

Delegate Pattern을 통해 구현한 방식을 간략히 상상해본다면 아래와 같을 것이다.

// Delegate Protocol
protocol UITableViewDelegate: AnyObject {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
}

// Delegating Object
class UITableView {

    weak var delegate: UITableViewDelegate?

    func didSelectedRowAt(indexPath: IndexPath) {
        delegate?.tableView(self, didSelectRowAt: indexPath)
    }
}

// Delegate Object
class ViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print(indexPath)
    }
}

즉, 델리게이트 패턴의 핵심은 두 객체를 연결하는 것, 두개의 객체가 효율적으로 소통하도록 도와주는 이다.

UI요소에서는 위의 사례에서 볼 수 있는 것처럼
이벤트를 받는 객체(ex. UITableView)가 그 이벤트를 받아 어떤 행동을 할지를 delegate(ex. ViewController)에게 위임한다.

delegate는 어떤 객체가 이벤트를 만났을 때 그 객체를 대신해서 행동하는 객체인 것!
그래서 주로 delegating 객체는 이벤트를 받고 처리하는 responder 객체(UIResponder를 상속하는 객체)다.

두 객체의 소통을 위한 Delegate Pattern

UI요소에서 델리게이트 패턴을 이용하는 이유는 우리가 UI요소의 내부 코드를 수정할 수 없기 때문이라고 이야기했다.

그렇다면 애플이 이미 구현해놓은 델리게이트 패턴이 아닌, 우리가 델리게이트 패턴을 활용해 코드를 작성하는 건 어떤 경우일까?

위에서 말했던 것처럼 델리게이트 패턴의 핵심은 두 객체의 소통이므로 두 개의 객체를 연결해주고 싶을 때 델리게이트 패턴을 활용해 볼 수 있다.

보통은 이벤트를 받은 객체와 그 이벤트를 처리할 객체가 다른 경우 두 객체를 소통하게 하기 위해 델리게이트 패턴을 쓰는 경우가 많다.

가장 간단한 예제부터 만들어보자

누워있는데 갑자기 아이스크림이 먹고싶어졌다. 그런데 나가기가 너무 귀찮아서 동생에게 아이스크림을 사오라고 시키려과 한다.

class Me {
    weak var delegate: Brother?

    func 올때메로나() {
        delegate?.buyIcecream()
    }
}

class Brother {
    let me = Me()

		init() {
        me.delegate = self
    }

    func buyIcecream() {
        print("메로나를 샀다")
    }
}

델리게이트 패턴에 대한 많은 예제 코드들을 보면 프로토콜을 꼭 구현해놓지만, 위의 코드처럼 프로토콜 없이도 델리게이트 패턴을 구현할 수 있다.
위임하는 객체와 위임받는 객체, 위임할 행동만 정의하면 된다.

Me는 아이스크림을 먹고 싶다는 이벤트를 받았지만 내가 사러 가기 싫기 때문에 다른 객체, Brother에게 아이스크림을 사는 행위를 위임했다. Me가 이벤트를 받아 올때메로나를 실행시켜 이벤트를 처리하면, 실질적으로는 델리게이트인 Brother가 대신 일을 처리해준다.

그렇다면 왜 델리게이트 패턴의 많은 예제에서 프로토콜을 사용하는 것일까?

프로토콜을 사용하면, 코드의 유연성과 재사용성을 높일 수 있다. 예를 들어보자.

protocol MeDelegate: AnyObject {
    func buyIcecream()
}

class Me {
    weak var delegate: Brother?

    func 올때메로나() {
        delegate?.buyIcecream()
    }
}

class Brother: MeDelegate {
    let me = Me()

		init() {
        me.delegate = self
    }

    func buyIcecream() {
        print("메로나를 샀다")
    }
}

class Father: MeDelegate {
    let me = Me()

		init() {
        me.delegate = self
    }

    func buyIcecream() {
        print("메로나를 샀다")
    }
}

나는 이제 남동생뿐만 아니라 아빠에게도 아이스크림을 사오라고 시킬 수 있다. MeProtocol을 만들어놓으면 MeMeProtocol을 채택한 누구에게든 아이스크림을 사는 행위를 위임할 수 있다.

이렇게 프로토콜을 사용해 델리게이트 패턴을 구현하면, 훨씬 더 유연하고 재사용 가능한 코드를 만들 수 있다.

실제로 앱에서는 어떤 방식으로 사용할까?

좀 더 실제 프로젝트에서 사용할만한 예를 살펴보자.

델리게이트 패턴을 사용하는 가장 흔한 경우는 두 뷰컨트롤러 간에 데이터를 전달할 때다. 사용자 입력 등의 이벤트를 받은 뷰컨트롤러와 그 결과를 처리해줘야하는 뷰컨트롤러가 서로 다른 경우, 예를 들어 사용자가 프로필 수정창에서 이름, 전화번호, 주소 등을 입력하고 확인 버튼을 눌러 이전 화면으로 돌아갔을 때 입력받은 정보를 이전 화면으로 전달해 보여줘야 하는 경우들을 말한다.

좀 더 간단하게 버튼을 눌렀을 때 뜬 모달에서 어떤 버튼을 누르느냐에 따라 강아지나 고양이 이미지 그림이 나오도록 하는 앱을 생각해보자.

위 앱을 구현한 코드는 아래와 같다.

class FirstViewController: UIViewController, SecondViewControllerDelegate {
    @IBOutlet weak var presentButton: UIButton!
    @IBOutlet weak var animalImageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func presentButtonTapped(_ sender: Any) {
        let secondViewController = storyboard?.instantiateViewController(
            withIdentifier: "SecondViewController"
        ) as! SecondViewController
        secondViewController.delegate = self
        self.present(secondViewController, animated: true)
    }
    
    func showImage(of animal: String) {
        presentButton.isHidden = true
        animalImageView.image = UIImage(named: animal)
    }
}

protocol SecondViewControllerDelegate: AnyObject {
    func showImage(of animal: String)
}

class SecondViewController: UIViewController {
    weak var delegate: SecondViewControllerDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func dogButtonTapped(_ sender: Any) {
        delegate?.showImage(of: "dog")
        self.dismiss(animated: true)
    }
    @IBAction func catButtonTapped(_ sender: Any) {
        delegate?.showImage(of: "cat")
        self.dismiss(animated: true)
    }
}

FirstViewController는 SecondViewDelegate 프로토콜을 채택해 SecondViewController의 위임자가 되었다.

SecondViewController가 강아지 또는 고양이 버튼을 누르는 이벤트를 받으면 delegate에게 눌러진 버튼에 해당하는 이미지를 보여달라고 delegate의 showImage(of:)메소드를 호출한다.
delegate인 FirstViewController는 델리게이트 패턴을 통해 어떤 버튼이 눌렸는지에 대한 데이터를 전달받아 해당하는 이미지를 보여주고 있다.

⚠️ 주의할 점: Strong Reference Cycle

예제 코드를 보면 delegate 프로퍼티를 정의할 때 weak로 선언한 것을 확인할 수 있다.

두 개의 클래스를 델리게이트 패턴을 사용해 연결할 경우, Strong Reference Cycle이 생길 수 있으므로 주의해야 한다.

delegating 객체는 delegate 프로퍼티를 통해 상대 객체를 강하게 참조하고 있고, delegate 객체 역시 delegating 객체를 강하게 참조하고 있기 때문에 Strong Reference Cycle이 생기게 되는 것이다.

delegate 프로퍼티를 선언할 때는 weak로 선언하거나 delegate를 담당하는 별도의 객체를 생성하는 등의 방식으로 Strong Reference Cycle을 피할 수 있다.

예를 들어 두개의 뷰컨트롤러 사이에 데이터 전달을 위해 델리게이트 패턴을 사용한 경우, delegating 객체는 delegate프로퍼티를 통해 상대 객체를 참조하고 있고, delegate 객체도 프로퍼티 등을 통해 delegating 객체를 참조하고 있어 서로를 강하게 참조하고 있게 된다.
이렇게 되면 하나의 뷰컨트롤러가 팝돼서 사라진다고 해도 메모리에서 해제되지 않아 메모리 누수가 생긴다!!!!

profile
즐거운 인생 (~-_-)~ ~(-_-~)

2개의 댓글

comment-user-thumbnail
2023년 8월 16일

간만에 delegate 이해를 다시 상기하고자 읽었다가 제대로 이해하고 갑니다.. 최고

답글 달기
comment-user-thumbnail
2024년 5월 1일

안녕하세요! 대리자 지정에 관해 궁금하여 댓글드립니다!
위에 적어주신 코드에서 secondViewController.delegate = self 대리자 지정 코드를 왜 버튼 액션에 넣어야 작동할까요? 테이블뷰나 키보드 대리자 지정할 때는 viewDidLoad에 적어 줬는데, 위 코드는 viewDidLoad에 적으면 작동하지 않더라구요!

답글 달기