소프트웨어 엔지니어링에서 자주 발생하는 문제를 해결하기 위해 반복적으로 사용되는 해결책의 모음.
객체가 자신의 기능을 다른 객체에게 위임하여 기능을 실행하는 디자인 패턴. (주로 UIKit 프레임워크에 사용)
객체 간의 결합도를 낮춰서 유지보수성과 확장성을 높이는데 사용됨.
한 객체가 다른 객체에 너무 의존하지 않고, 변경이 발생할 때 다른 객체에게 영향을 덜 주도록 만드는 것.
내용을 검색하다보니, 실제로 내가 사용해보았던 것으로는 UITableView를 구현할 때 사용을 했다는 것이다. UIViewController가 직접적으로 UITableView를 생성하고 데이터를 관리하게 된다면 각 객체의 구현이 변경된다면 서로 간에 많은 영향을 주게된다. 하지만 UITableViewDelegate와 UITableViewDataSource 프로토콜을 사용하여 각각의 객체를 만들고, UIViewController에 할당하기 때문에 나는 UITableView에 대해 구현하였던 세부 사항은 모르지만 내부의 메서드를 쉽게 사용할 수 있었다. 결과적으로 UITableView를 구현할 때,UIViewController에 영향을 덜 주며 유지보수와 확장성이 향상되는 것이 된다!
예시를 참조한 블로그
delegatet Pattern의 사용법을 참조한 블로그
우선, delegate pattern에서 각각의 역할로 나뉘게 된다.
해당 역할을 참조하여 순서대로 코드를 작성한다!
// UI 변경을 위한 프로토콜 정의
protocol ChangeUIDelegate: class {
func changeUI()
}
UI를 변경하기 위한 메서드를 정의한 프로토콜로 changUI()라는 메서드를 선언했다.
class FirstViewController: UIViewController, ChangeUIDelegate {
// 프로토콜 메서드 구현
func changeUI() {
self.pageTitleLabel.text = "UI가 변경된 상태!"
self.view.backgroundColor = .blue
}
// 다음 화면으로 이동하는 메서드
@objc func nextButtonTapped() {
let secondViewController = SecondViewController()
secondViewController.delegate = self
present(secondViewController, animated: true, completion: nil)
}
}
class SecondViewController: UIViewController {
weak var delegate: ChangeUIDelegate?
// UI 변경 버튼 동작
@objc func changeUIButtonTapped() {
self.delegate?.changeUI()
self.dismiss(animated: true)
}
}
delegate 프로퍼티가 ChangeUIDelegate 프로토콜을 채택한 부분에 weak 로 선언하였다. 이는 강한 참조 순환을 방지하기 위해서이다.
Delegate Pattern에서 대리자는 일반적으로 수신자 객체의 소유주가 된다.
위의 코드에서, SVC(SecondViewController)가 FVC(FirstViewController)의 대리자이므로 FVC가 SVC를 생성하고 SVC의 delegate 프로퍼티를 설정한다. 이때 만약에 delegate가 강한 참조로 선언되면 수신자 객체와 대리자 객체가 서로를 강한 참조로 가지게 되어 메모리 누수가 발생할 수 있다.
결과적으로 weak, 약한 참조로 선언하게 되면 대리자 객체가 메모리에서 해제될 때 감한 참조 순환을 방지하고, 메모리 누수를 예방하는데 도움이 된다.
// FirstViewController에서 다음 버튼 액션에서 SecondViewController의 delegate 설정
let secondViewController = SecondViewController()
secondViewController.delegate = self
Delegate 역할을 수행하는 클래스로 FirstViewController을 선언했다.
위에서 선언한 프로토콜을 채택해야하는데, 아래의 코드를 작성하여 적용한다.
이 코드가 빠질 경우 원하는 동작을 하지 않는다.
// SecondViewController에서 UI 변경 요청 시 delegate를 통해 FirstViewController의 changeUI() 호출
self.delegate?.changeUI()
FirstViewController 가 ChangeUIDelegate 프로토콜을 준수하고 있으며, SecondViewController 는 이 프로토콜을 통해 FirstViewController 에게 UI 변경을 요청하고 있는 코드.
ChangeUIDelegate 프로토콜: UI 변경을 위한 메서드 changeUI()를 선언하고 있다.
FirstViewController 에서 nextButtonTapped() 메서드 내에서 SecondViewController 를 생성한 후, SecondViewController 의 delegate 프로퍼티에 self 를 할당하고 있다. 이렇게 함으로써 SecondViewController 는 FirstViewController 의 Delegate 역할을 수행하게 된다.
SecondViewController 에서는 UI 변경을 위해 changeUIButtonTapped() 메서드 내에서 delegate?.changeUI() 를 호출하고 있다. 이는 Delegate를 통해 FirstViewController 의 changeUI() 메서드를 호출하고 있음을 의미한다.
FirstViewController 에서는 ChangeUIDelegate 프로토콜을 채택하여, changeUI() 메서드를 구현한다. 이 메서드 내에서 UI를 변경하고 있다.

import Foundation
import UIKit
class FirstViewController: UIViewController {
lazy var pageTitleLabel: UILabel = {
let label = UILabel()
label.textColor = AppTheme.Color.text
label.font = AppTheme.Font.Cell.title
label.textAlignment = .center
label.text = "아직 UI가 변경되지 않았습니다."
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var nextButton: UIButton = {
let button = UIButton()
button.setTitleColor(AppTheme.Color.text, for: .normal)
button.titleLabel?.font = AppTheme.Font.Cell.body
button.setTitle("2번째 ViewController로 이동", for: .normal)
button.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
autoLayout()
}
func autoLayout() {
view.addSubview(pageTitleLabel)
view.addSubview(nextButton)
pageTitleLabel.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(10)
make.leading.trailing.equalToSuperview().inset(16)
}
nextButton.snp.makeConstraints { make in
make.top.equalTo(pageTitleLabel.snp.bottom).offset(10)
make.leading.trailing.equalToSuperview().inset(16)
}
}
// MARK: Action
@objc func nextButtonTapped() {
print("nextButtonTapped")
let secondViewController = SecondViewController()
secondViewController.modalPresentationStyle = .fullScreen
secondViewController.delegate = self
present(secondViewController, animated: true, completion: nil)
}
}
extension FirstViewController: ChangeUIDelegate {
func changeUI() {
self.pageTitleLabel.text = "UI가 변경된 상태!"
self.view.backgroundColor = .blue
}
}
import Foundation
import UIKit
class FirstViewController: UIViewController {
lazy var pageTitleLabel: UILabel = {
let label = UILabel()
label.textColor = AppTheme.Color.text
label.font = AppTheme.Font.Cell.title
label.textAlignment = .center
label.text = "아직 UI가 변경되지 않았습니다."
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var nextButton: UIButton = {
let button = UIButton()
button.setTitleColor(AppTheme.Color.text, for: .normal)
button.titleLabel?.font = AppTheme.Font.Cell.body
button.setTitle("2번째 ViewController로 이동", for: .normal)
button.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
autoLayout()
}
func autoLayout() {
view.addSubview(pageTitleLabel)
view.addSubview(nextButton)
pageTitleLabel.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(10)
make.leading.trailing.equalToSuperview().inset(16)
}
nextButton.snp.makeConstraints { make in
make.top.equalTo(pageTitleLabel.snp.bottom).offset(10)
make.leading.trailing.equalToSuperview().inset(16)
}
}
// MARK: Action
@objc func nextButtonTapped() {
print("nextButtonTapped")
let secondViewController = SecondViewController()
secondViewController.modalPresentationStyle = .fullScreen
secondViewController.delegate = self
present(secondViewController, animated: true, completion: nil)
}
}
extension FirstViewController: ChangeUIDelegate {
func changeUI() {
self.pageTitleLabel.text = "UI가 변경된 상태!"
self.view.backgroundColor = .blue
}
}