Delegate Pattern (Protocol)

YeongHyeon Jo·2024년 2월 15일
0

Swift

목록 보기
3/6

디자인 패턴(Design Pattern)

소프트웨어 엔지니어링에서 자주 발생하는 문제를 해결하기 위해 반복적으로 사용되는 해결책의 모음.

  1. 생성 패턴(Creational Patterns): 객체의 생성 메커니즘에 관련된 패턴. 객체 생성을 유연하게 만들고 객체 간의 의존성을 줄이는 방법을 제공.
  2. 구조 패턴(Structural Patterns): 객체와 클래스를 조합하여 더 큰 구조를 형성하는 패턴. 객체의 구성을 통해 새로운 기능을 제공하거나 객체 간의 인터페이스를 변경.
  3. 행위 패턴(Behavioral Patterns): 객체나 클래스 간의 알고리즘 및 역할 분배에 관련된 패턴. 객체 간의 상호작용을 개선하고 조직화하는 방법을 제공. 앞으로 알아볼 Delegate Pattern은 여기에 속한다.

Delegate Patterns

객체가 자신의 기능을 다른 객체에게 위임하여 기능을 실행하는 디자인 패턴. (주로 UIKit 프레임워크에 사용)
객체 간의 결합도를 낮춰서 유지보수성과 확장성을 높이는데 사용됨.

객체 간의 결합도를 낮춘다

한 객체가 다른 객체에 너무 의존하지 않고, 변경이 발생할 때 다른 객체에게 영향을 덜 주도록 만드는 것.

  • 유연한 구현 교체: 객체 A가 객체 B에 직접 의존한다면, 객체 B의 구현이 변경되면 객체 A도 변경될 수 있다. 하지만 Delegate Pattern을 사용하여 객체 A는 특정 기능을 수행하기 위해 객체 B를 호출하는 대신 인터페이스를 통해 Delegate로 연결하고 필요에 따라 다른 객체를 Delegate로 사용할 수 있다. 이렇게 하여 객체 A는 객체 B의 구현 변경과 무관하게 유지가 가능하다.
  • 모듈성 향상: Delegate Pattern을 사용하면 애플리케이션의 다양한 부분을 더 작고 독립적인 모듈로 나눌 수 있다. 이는 코드를 이해하고 유지보수하는 데 도움이 되며, 특정 모듈을 수정할 때 다른 모듈에 영향을 덜 줄 수 있습니다.
  • 테스트 용이성: Delegate Pattern을 사용하면 의존성을 주입할 수 있으므로, 단위 테스트 작성이 더 쉬워진다. 특정 기능을 테스트할 때 해당 기능과 직접적으로 관련된 Delegate를 모의(Mock) 객체로 대체하여 테스트를 진행할 수 있다.

내용을 검색하다보니, 실제로 내가 사용해보았던 것으로는 UITableView를 구현할 때 사용을 했다는 것이다. UIViewController가 직접적으로 UITableView를 생성하고 데이터를 관리하게 된다면 각 객체의 구현이 변경된다면 서로 간에 많은 영향을 주게된다. 하지만 UITableViewDelegate와 UITableViewDataSource 프로토콜을 사용하여 각각의 객체를 만들고, UIViewController에 할당하기 때문에 나는 UITableView에 대해 구현하였던 세부 사항은 모르지만 내부의 메서드를 쉽게 사용할 수 있었다. 결과적으로 UITableView를 구현할 때,UIViewController에 영향을 덜 주며 유지보수와 확장성이 향상되는 것이 된다!

사용 예시

예시를 참조한 블로그
delegatet Pattern의 사용법을 참조한 블로그

우선, delegate pattern에서 각각의 역할로 나뉘게 된다.

  1. 프로토콜(Protocol): 대리자가 수신자에게 전달할 내용의 약속
  2. 수신자(Receiver): 대리자가 특정 기능을 수행 후 전달 받을 대상
  3. 대리자(Delegate): 수신자를 대신하여 처리할 대리자

해당 역할을 참조하여 순서대로 코드를 작성한다!

1. 프로토콜 정의

  • Delegate가 준수해야 하는 프로토콜을 정의.
  • 대리자에게 필요한 메서드나 속성을 선언.
// UI 변경을 위한 프로토콜 정의
protocol ChangeUIDelegate: class {
    func changeUI()
}

UI를 변경하기 위한 메서드를 정의한 프로토콜로 changUI()라는 메서드를 선언했다.

2. 수신자(Delegate Receiver) 클래스 구현

  • 프로토콜을 준수하는 클래스
  • 프로토콜의 요구사항에 따라 메서드를 구현한다.
  • Delegate로 동작할 클래스에서는 해당 프로토콜을 채택해야한다!!!
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)
    }
}

3. 대리자(Delegate) 클래스 구현

  • 대리자로 동작할 클래스
  • 대리자 역할을 하는 클래스에서는 대리자 프로토콜에 선언된 메서드를 호출해야한다.
  • 필요한 경우 대리자 프로토콜을 채택하고, 수신자 객체를 참조할 수 있는 프로퍼티를 정의한다.
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, 약한 참조로 선언하게 되면 대리자 객체가 메모리에서 해제될 때 감한 참조 순환을 방지하고, 메모리 누수를 예방하는데 도움이 된다.

4. 수신자 객체에 대리자 설정

  • 대리자 객체를 생성하고, 수신자 객체에 대리자를 설정한다.
  • 대부분의 경우에는 대리자 객체를 생성하는 것이 먼저이며, 수신자 객체에 대리자를 설정하는 과정은 보통 사용자 인터페이스 객체에 해당한다.
// FirstViewController에서 다음 버튼 액션에서 SecondViewController의 delegate 설정
let secondViewController = SecondViewController()
secondViewController.delegate = self

Delegate 역할을 수행하는 클래스로 FirstViewController을 선언했다.
위에서 선언한 프로토콜을 채택해야하는데, 아래의 코드를 작성하여 적용한다.
이 코드가 빠질 경우 원하는 동작을 하지 않는다.

5. Delegate 패턴 사용

  • 대리자 객체에서 수신자 객체로 메시지를 보내어 작업을 수행한다.
  • 이때 대리자 객체는 프로토콜을 준수하는지 확인하고, 프로토콜에 선언된 메서드를 호출한다.
// SecondViewController에서 UI 변경 요청 시 delegate를 통해 FirstViewController의 changeUI() 호출
self.delegate?.changeUI()

6. 필요한 경우에 대리자 패턴 확장

  • 코드의 유지보수성과 확장성을 높이기 위해 필요한 경우에 대리자 패턴을 확장하거나 수정할 수 있다.

👍 사용 예시의 결과

FirstViewControllerChangeUIDelegate 프로토콜을 준수하고 있으며, SecondViewController 는 이 프로토콜을 통해 FirstViewController 에게 UI 변경을 요청하고 있는 코드.

  1. ChangeUIDelegate 프로토콜: UI 변경을 위한 메서드 changeUI()를 선언하고 있다.

  2. FirstViewController 에서 nextButtonTapped() 메서드 내에서 SecondViewController 를 생성한 후, SecondViewControllerdelegate 프로퍼티에 self 를 할당하고 있다. 이렇게 함으로써 SecondViewControllerFirstViewController 의 Delegate 역할을 수행하게 된다.

  3. SecondViewController 에서는 UI 변경을 위해 changeUIButtonTapped() 메서드 내에서 delegate?.changeUI() 를 호출하고 있다. 이는 Delegate를 통해 FirstViewControllerchangeUI() 메서드를 호출하고 있음을 의미한다.

  4. FirstViewController 에서는 ChangeUIDelegate 프로토콜을 채택하여, changeUI() 메서드를 구현한다. 이 메서드 내에서 UI를 변경하고 있다.

전체 코드

수신자: FirstViewController

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
    }
} 

대리자: SecondViewController

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
    }
}

결론

  • 한 객체가 다른 객체의 대리자(delegate)가 되는 디자인 패턴
  • 다른 객체의 이벤트나 데이터를 처리하는 방식으로 구현
  • 객체 간의 결합도를 낮추고, 유연하고 확장 가능한 코드 작성 가능
profile
my name is hyeon

0개의 댓글