[Design Pattern] 내가 보려고 정리하는 Swift 디자인 패턴 (2) - Delegate Pattern

2024. 2. 20. 13:54Architecture, Design Pattern

1️⃣ Delegate가 등장하게 된 이유


Delegate는 "대리자" "위임하다" 같은 뜻으로 번역되는 단어다.
"위임하다"의 의미를 구체적으로 들어가보면, "당사자의 일방이 상대방에 대하여 '사무의 처리'를 위탁하고, 상대방이 이를 승낙함으로써 성립하는 것"이라고 한다.

즉, 쉽게 말해 Delegate는 "너가 나 대신 일 좀 해줘!" 라는 의미를 가지고 있는 단어다. (전문용어로 짬 때린다)
"본인 일을 본인이 하면 되지 왜 남한테 떠넘겨?" 라고 할 수도 있지만, 사실 이 Delegate가 필요하게 된 상황을 보게되면 공감이 될 거다.

프랜차이즈 햄버거 가게 사장이 되었다고 예시를 들어보겠다.
불고기 버거와 치킨 버거 두 가지 메뉴만 판매하는 가게에서

햄버거 레시피는 우리 가게 프랜차이즈의 특성을 지키기 위해 <우리가게 햄버거 레시피>라는 큰 틀을 준수하며 만들어진다.

그래서 불고기 버거도, 치킨 버거도 모두 <우리가게 햄버거 레시피>에서 비롯된 레시피를 사용중이었다.
*<우리가게 햄버거 레시피>는 햄버거를 만들 때 공통으로 구현되는 사항을 정해준다. (빵은 어떻게, 패티는 어떻게, 야채는 어떻게와 같은 공통적인 사항을 정의한 곳)

그런데, 장사가 너무 잘된 나머지 새로운 햄버거 메뉴를 많이 추가하게 된 상황이다.
메뉴가 너무 많아진 나머지, 매번 레시피의 세부 내용까지 만들어주던 <우리가게 햄버거 레시피>가 이제는 너무 힘이 붙이기 시작했다.
새로운 레시피가 필요할 때마다 매번 해야 할 일이 너무 많아지게 되고, 이전 레시피까지 계속 축적되어 몸집이 너무 커지게 되는 상황.

Delegate는 해당 문제를 해결하기 위해 등장한 개념이다.
*해당 예시의 <우리가게 햄버거 레시피>를 <UIScrollView>로, <여러개의 햄버거 메뉴>를 <스크롤뷰가 사용되는 여러 화면들(네이버, 토스, 당근.. )>로 바꾸면 직관적인 비교 이해가 가능하다.

 

2️⃣ Protocol, 반드시 있어야만 하는 규칙


Delegate Pattern의 시작은 Protocol (프로토콜)의 개념을 이해하는 것부터이다.
프로토콜은 직관적으로 설명했을 때, "가이드라인 혹은 규칙"이라고 말할 수 있겠다.

"가이드라인"이라는 말이 잘 와닿지 않는다고? 예시를 이어서 들어보자.

우리 가게의 햄버거 레시피가 빵은 이렇고... 패티는 이래야 하고... 야채는 이렇고... 등을 매번 세부적으로 정해주는 대신,
반드시 필요한 내용, 하지만 세부 내용이 들어있지는 않은 큰 틀을 <햄버거 레시피 가이드라인>이라는 별도의 규칙으로 만들어줄거다.

해당 가이드라인 안에는 "햄버거 빵 함수" "패티 함수" "야채 함수"와 같이 말 그대로 햄버거를 만들 때 필요한 가이드라인만을 담을 것이고, 세부적인 내용은 각 메뉴마다 차이가 있을것이기 때문에 해당 가이드라인에는 구현하지 않을 것이다.

이 가이드라인이자 큰 틀의 역할이 바로 프로토콜(Protocol)이라 생각하면 된다.

해당 예시를 Swift 코드로 쓰자면, 아래처럼 나타낼 수 있겠다.

protocol 햄버거레시피가이드라인 {
    func 빵세팅()
    func 패티조리()
    func 야채추가()
}

결국 프로토콜의 핵심 내용을 요약해보면, 아래와 같이 정리할 수 있다. (엄청 단순한 개념)

  1. 프로토콜은 "필수로 들어가야하는 내용의 큰 틀만을 적어둔 설명서"이다.
  2. 해당 설명서는 모든 객체 (예시 기준, 각 메뉴마다)에 전달될 것이다. -> 왜? 햄버거를 만들 때 해당 레시피를 지켜야하니까.
  3. 단, 레시피의 세부적인 내용은 각 메뉴마다 차이가 있을 수 있으니 설명서(프로토콜)에는 자세하게 적어두지 않는다.

프로토콜의 핵심은, 규칙의 큰 틀만을 정해둘 뿐, 규칙의 세부적인 내용을 써두지 않는다는 것이다.

 

3️⃣ 나 대신 네가 일 좀 해줘, Delegate


프로토콜을 만든 이상, 이제는 처음 예시처럼 모든 내용들을 일일이 만들어서 넘겨줄 필요가 없어졌다.
이제 메뉴마다 차이나는 레시피의 세부 내용은 각각 햄버거별로 각자 구현해주기만 하면 된다.

그럼 이제 우리에게 필요한 작업은,
1) 각 메뉴마다 햄버거 레시피 가이드라인을 지키면서 레시피를 만들라고 알려주는 것과 2) 각 메뉴마다 세부적인 레시피를 구현해주는 것만 남게 된 셈이다. 순서대로 가보자.

햄버거 레시피 객체를 만들고, 객체 안에 프로토콜 값을 저장할 수 있는 변수를 만들어줬다. 
우리는 특정 상황이 발생했을 때, delegate라는 변수를 통해 프로토콜에 있는 함수의 세부 내용(함수)를 작동하라고 알려주게 될 것이다.

class 햄버거레시피 {
    var delegate: 햄버거레시피가이드라인
    func 특정상황() {
        delegate.빵세팅()
        delegate.패티조리()
        delegate.야채추가()
    }
}

이제 치즈버거 레시피를 만들어주자.

치즈버거 레시피를 햄버거 레시피에 받아와서 구현을 할 건데, 
이때 이 레시피의 세부내용 (delegate)는 여기서 구현한다 (self)는 것을 명시함으로써 알려주면, 레시피 가이드라인을 지키면서 햄버거를 만들 수 있게 된다.

class 치즈버거: 햄버거레시피가이드라인 {  // 햄버거는 가이드라인을 꼭 따라야 하니깐!
    var 레시피: 햄버거레시피
    레시피.delegate = self

    func 빵세팅() { 치즈버거 빵은 어쩌고 저쩌고... }    
    func 패티조리() { 치즈버거 패티는 어쩌고 저쩌고... }
    func 야채추가() { 치즈버거 야채는 어쩌고 저쩌고... }  
}

즉 다시 정리하면,
햄버거를 만들 때 그 세부 내용을 원래 담당하던 레시피가 아니라, 각 메뉴별로 각자 구현하라고 "위임"하기 때문에 Delegate라 부르는 것이었다.

 

4️⃣ Delegate를 이용한 데이터 전달 예시


예시를 들어봤으니 실제 Swift 코드에서 Delegate를 활용해서 데이터를 전달하는 예시도 살펴보자.
CollectionView의 Cell 안에 있는 버튼 클릭 액션을 ViewController로 받아와야 하는 상황이다.

1. Protocol에서는 버튼 클릭 이후의 세부적인 구현 내용은 작성하지 않은 채 "버튼 클릭된 상황"에 대한 가이드라인만 제공한다.

protocol ExampleCellDelegate: AnyObject {
    func buttonTapped()
}

2. Cell에서 버튼 클릭 이벤트가 발생했을 때 Protocol 내의 메서드를 작동하라고 연결해준다.

class ExampleCollectionViewCell: UICollectionViewCell {    
    private let button = UIButton()

    // ExampleCellDelegate 프로토콜을 채택한 다른 객체(VC)가 해당 Cell을 참조할 때 강한 참조가 걸리지 않도록 하기 위해 weak를 붙인다. 
    // VC와 Cell 간의 순환 참조가 발생하는 메모리 누수를 예방한다.
    weak var delegate: ExampleCellDelegate?

    // 버튼과 objc func를 연결
    button.addTarget(self, action: #selector(buutonTapped), for: .touchUpInside)
	
    // 버튼 클릭시 Delegate 함수 작동
    @objc func buutonTapped() {
        delegate?.buttonTapped()
    }
}

3. ViewController에서 Cell에 구현된 대리자는 self라고 지정하고, 세부 내용을 구현해준다.

class ExampleVC: UIViewController, YourCellDelegate {
	...
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ExampleCollectionViewCell", for: indexPath) as! ExampleCollectionViewCell
        
        // Cell의 Delegate는 여기서 구현한다고 알림!
        cell.delegate = self
        
        return cell
    }

    func buttonTapped() {
        // 버튼을 눌렀을 때 작동하는 실제 구현부
    }
}

마지막으로 Delegate Pattern은 위의 예시 같이 데이터를 전달하는 부분에서 매우 자주 쓰이는 패턴인 만큼,
꼭 스스로 예시를 들어보고 많은 연습을 통해 직접 익히길 추천하며 이 글을 마무리하겠다!