호댕의 iOS 개발

[디자인 패턴] Delegation 패턴 본문

Software Engineering/iOS

[디자인 패턴] Delegation 패턴

호르댕댕댕 2022. 3. 19. 01:59

Delegation 패턴은 delgate란 단어에서 볼 수 있듯 특정 Action을 다른 객체가 하도록 위임 하는 것이다. 

즉, 특정 Action에 대한 위임을 받은 객체가 대신 일을 처리해주는 것이다. 

 

이는 iOS에서 TableView를 활용하다보면 UITableViewDelegate, UITableViewDataSource라는 프로토콜을 볼 수 있는데, 이 또한 Delegation 패턴을 사용하고 있는 것이다. 

 

애플이 구현해놓은 TableView를 사용할 때 이를 어떻게 구성할 지는 개발자가 정하게 된다. 

 

따라서, TableView가 UITableViewDelegate, UITableViewDataSource라는 프로토콜을 채택한 ViewController에게 테이블 뷰의 Section과 Row는 어떻게 구성이 되어 있는지, 만약 셀이 선택이 되었다면 어떻게 동작을 해야할지를 물어보고 특정 액션을 View 대신 프로토콜을 채택한 ViewController가 대신 할 수 있도록 하는 것이다.

 

🔨 그렇다면 Delegation 패턴을 어떻게 구현할 수 있나?

이미 애플이 구현해놓은 UITableViewDelegate, UITableViewDataSource의 경우 해당 프로토콜을 채택한 곳에서 다음과 같이 작성을 해주면 된다. 

override func viewDidLoad() {
    tableView.delegate = self
    tableView.dataSource = self
}

이런 식으로 tableView의 delegate는 나야!!, tableView의 dataSource는 나야! 라고 작성을 해주는 것이다.

 

그럼 직접 Delegate를 구현하려면 어떻게 해야할까?

 

Swift는 P.O.P 즉, 프로토콜 지향 프로그래밍을 따르고 있는데 Delegation 패턴에서도 Protocol을 사용한다. 

이를 통해 해당 프로토콜을 채택한 타입이 반드시 특정한 메서드를 구현하도록 하여, 특정 액션에 대한 위임을 수행할 수 있도록 하는 것이다. 

 

만약 Cell이 Long press로 눌렸을 때 특정 View를 Present하고 싶은 상황이라고 생각해보자. 

Cell에서는 View를 직접 present할 수 없기 때문에 view를 띄울 ViewController에 present를 위임해야 한다. 

 

일단 이를 위해선 셀이 longpressed로 눌렸을 때 delegate을 할 수 있도록 프로토콜을 구현해야 한다. 

protocol ProjectTableViewCellDelegate: AnyObject {
    
    func longpressed(at cell: ProjectTableViewCell)
    
}

여기서 ProjectTableViewCellDelegate에 AnyObject를 채택한 이유는 delegate 프로퍼티를 ViewController에서 호출하게 되는데 이렇게 되면 순환참조가 발생한다. 따라서 이를 해결해주기 위해 weak 키워드를 붙여 주어야 하는데 weak는 class가 아닌 곳에선 사용을 할 수 없기 때문이다.

 

weak 키워드를 사용하게 되면 해당 인스턴스를 직접 소유하지 않고 주소값만 가지고 있게 되어 retain count가 증가하지 않게 된다

 

final class ProjectTableViewCell: UITableViewCell {
    
    weak var delegate: ProjectTableViewCellDelegate?
    
    override func awakeFromNib() {
        addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(showPopupMenu)))
    }
    
    @objc private func showPopupMenu() {
        delegate?.longpressed(at: self)
    }
    
}

또한 weak 키워드를 붙일 경우 메모리에서 해제될 경우 자동으로 nil로 초기화되기 때문에 옵셔널(?)로 선언을 해줘야 한다.

 

그리고 long press가 되었을 때 long press가 특정 cell에서 됐다는 것을 알려주고 이 시점에 popup menu를 보여줄 수 있도록 선언을 해둬야 한다. 

 

 

이렇게 선언을 해준 뒤에는 해당 일을 할 ViewController로 가서 어떻게 위임받은 일을 할 지 정해줘야 한다. 

extension ProjectTableViewController: ProjectTableViewCellDelegate {
    
    func longpressed(at cell: ProjectTableViewCell) {
        // 어떤 뷰를 보여줄 지 present 메서드를 사용해 적어주면 됨!
    }
    
}

기존에 선언해놓았던 Protocol을 일을 위임받을 타입에 채택을 해주고 프로토콜에 정의해놓은 메서드를 어떻게 구현할 지 작성을 해주면 된다. 

 

그리고 tableView의 delegate를 본인이 한다고 말해줬던 것처럼 여기서도 cell의 delegate가 본인이라고 적어주면 된다! 

(예제에 RxCocoa 관련 코드가 있어 난해할 수도 있지만 cell.delegate = self로 선언해주는 부분만 확인해주시면 됩니다!)

private func configureTableView() {
    tableView.delegate = self

    viewModel.list.observe(on: MainScheduler.instance)
        .bind(to: tableView.rx.items(
            cellIdentifier: String(describing: ProjectTableViewCell.self),
            cellType: ProjectTableViewCell.self
        )) { [weak self] _, item, cell in
            cell.configureCellContent(for: item)
            cell.setupData(work: item)

            cell.delegate = self // 요렇게 선언해주면 끝!
        }
        .disposed(by: disposeBag)
}

간단하게 그림으로 표현을 해보자면 이런 식으로 Delegation 패턴이 작동하게 된다.

 

 

👍 Delegation 패턴의 장점

그렇다면 이런 Delegation 패턴은 어떤 장점을 가지는 것일까?

 

일단 이를 사용할 경우 객체간 의존성을 줄이면서 서로 상호작용을 할 수 있도록 할 수 있다. 

만약 Cell에서 View를 띄우려고 했으면 ViewController를 직접 가지고 있으면서 view를 띄워야 했을 것이다. 이렇게 되면 Cell이 부모 ViewController를 직접 가지고 있는 이상한 상황이 발생하며 서로 의존도도 높아지게 된다.

 

또한 만약 Long press가 있을 때 다른 View나 ViewController에서도 처리할 일이 있다면 또 Delegate 프로토콜을 채택하여 해야할 일을 정해주면 되기 때문에 코드의 재사용성이 높아진다

Comments