Delegate Pattern

안녕하세요, yaza입니다. 🌴

iOS 개발을 하게 된다면 Delegate 패턴에 대해 들어보지 않을 수 없을 것입니다.

그만큼 Delegate 패턴은 iOS 개발을 하는 데에 있어서 핵심적인 개념이며, 실제로 Apple의 각종 프레임워크 내부에서 Delegate 패턴을 사용하는 경우를 빈번하게 찾아볼 수 있습니다.

The Swift Programming Language에서는 Delegation을 다음과 같이 정의하고 있습니다.

Delegation클래스 혹은 구조체가 다른 타입의 인스턴스에게 자신의 책임 중 일부를 건네는(위임하는) 디자인 패턴입니다. 이 디자인 패턴은 위임할 책임들을 캡슐화하는 protocol을 정의함으로써 구현할 수 있습니다. 기능을 위임받은 타입(대리자)는 위임받은 기능들에 대한 실제 동작을 제공해야 합니다.
위임을 통해 특정 액션에 대해 반응하거나, 해당 소스의 근간이 되는 타입에 대해 알 필요 없이 외부 소스로부터 데이터를 가져올 수 있습니다.

아직까지는 이게 무슨 소리인지 이해하기 어렵습니다.

오늘은 Delegate 패턴이 무엇이고, 클래스 혹은 구조체가 자신의 책임을 왜 위임해야하며, 어떤 경우에 책임을 위임하는지에 대해 자세히 알아보겠습니다.

Delegate 패턴이 무엇인가?

개념 자체는 간단합니다. 간단한 비유를 해보겠습니다.

어느 편의점에 점장이 있고, 편의점 점장에게는 발주 넣기, 매대 채우기, 손님 응대하기, 매장 청소하기 등과 같은 업무가 기본적으로 주어질 것입니다. 하지만 이 사람이 전부 이 업무를 수행하기 어려운 상황이 온다면, 아르바이트생을 뽑아서 업무를 수행하도록 할 수 있겠죠. 아르바이트생이 점장의 업무를 위임받게 된 것입니다.

점장이 아르바이트생에게 업무를 알려주기 위해서, 어떤 업무를 수행할지를 알려주기 위해 알바생이 해야할 일을 정리하여 알려줄 것입니다. Delegate 패턴에서 이러한 업무 리스트를 Protocol이라고 부릅니다. Protocol이란 단어의 뜻 그대로, 점장과 아르바이트생 간의 약속인 것이죠.

따라서 점장이 아르바이트생에게 손님 응대하기, 매장 청소하기 업무를 시키고 싶다고 한다면, 다음과 같이 표현할 수 있습니다.

(아르바이트생이 매대 채우기도 하겠지만 예시와 맞지 않는 것 같아서 뺐습니다.
선입선출과 같은 분명한 가이드가 존재하므로.. 이 말이 무슨 말인지 모르겠다면, 좀 더 읽어보시면 이해가 될 거에요 😀)

protocol 알바생에게맡길업무들 {
	func 손님응대하기()
	func 매장청소하기()
}

알바생은 한 명일수도 있겠지만, 시간대별로 여러 명일수도 있을 것입니다.
Delegate 패턴 세계에서 점장님은 매우 자유로운 분이라서 단순히 업무를 위임할 뿐 세부적인 내용에 대해서는 관여하지 않습니다.
위임받은 업무는 오로지 아르바이트생의 몫입니다.

손님응대하기 업무에 대해서 A 알바생은 친절히 환하게 웃으며 손님을 응대할 수도 있고, B 알바생은 적당히 계산만 하고 손님을 보낼 수도 있습니다. 어떻게 하는지는 모두 알바생의 몫입니다. 매장청소하기 업무 역시 A 알바생은 대걸레와 빗자루 등의 도구를 동원해서 아주 꼼꼼하게 처리할 수도 있고, B 알바생은 적당히 보이는 쓰레기만 청소할 수도 있겠죠. 일을 위임한 이후는 모두 알바생의 몫입니다.

이 과정을 코드로 표현해보겠습니다.

protocol 알바생에게위임할내용들: AnyObject {
    func 손님응대하기()
    func 매장청소하기()
}

class 점장 {
    weak var 알바생들: 알바생에게위임할내용들?
    
    func 알바생한테일시키기() {
		알바생들.매장청소하기()
		알바생들.손님응대하기()
	}
}

class 알바생A: 알바생에게위임할내용들 {
	let 점장님: 점장 = 점장()

	func 계약서에싸인하기() {
		점장님.알바생들 = self
	}

	func 손님응대하기() {
		웃으며친절하게응대()
	}

	func 매장청소하기() {
		안보이는곳구석구석까지닦기()
	}
}

class 알바생B: 알바생에게위임할내용들 {
	let 점장님: 점장 = 점장()

	func 계약서에싸인하기() {
		점장님.알바생들 = self
	}

	func 손님응대하기() {
		적당히대충응대()
	}

	func 매장청소하기() {
		보이는곳만쓸기()
	}
}

여기서 제가 은근슬쩍 추가한 것이 있는데요. 뭘까요? 계약서에싸인하기() 부분과, 알바생 클래스 뒤에 있는 : 알바생에게위임할내용들 부분입니다. 알바생이 태어날 때부터 알바생으로 태어나진 않았죠. 점장님이 계약서를 보여주면 알바생이 네~ 그 일 할게요. 하고, 계약서에 싸인을 한 뒤 업무를 시작할 것입니다. 그 과정을 Swift Delegate 패턴에서는 다음과 같이 표현한 것입니다.

class 알바생B: 알바생에게위임할내용들 {
	...

	func 계약서에싸인하기() {
		점장님.알바생들 = self
	}

	...
}

알바생으로서 업무를 수행(채택)할 것임을 클래스 뒤에 위임할 프로토콜을 붙임으로써 나타낸 것이고 점장님이 맡길 일들을 self, 즉 내가 처리하겠다! 이거죠. 😀

여기까지 Delegate 패턴이 무엇인지에 대해 알아봤습니다. 그렇다면 여기서 드는 궁금증이 있습니다.

왜 Delegate 패턴을 쓸까?

Delegate 패턴이 무엇인지는 알겠지만, 그래서 Delegate 패턴을 왜 쓰는데? 하는 궁금증이 듭니다. 점장님은 혼자 일을 할 수 없을까요?

사실 현실 세계에서는 많은 이유를 찾을 수 있을 것입니다. 시간이 부족하다거나, 체력적으로 한계가 있다거나, 단지 그냥 하기 싫어서! 와 같이 여러 이유를 찾을 수 있습니다. 다만 이를 코드의 세계로 옮겨와서 이야기를 하려고 하면 현실 세계와 상충되는 부분이 생깁니다. 객체 간에 체력적으로 한계가 있다거나, 단지 하기 싫어서 안한다. 와 같은 이유는 납득하기 어렵죠.

따라서 현실 세계의 비유에서 잠시 벗어나 프로그래밍의 관점에서 Delegate 패턴을 사용하는 이유를 말하자면, 결국은 코드를 작성할 때 중요하게 강조되는 관심사의 분리를 이유로 들 수 있습니다.

점장 클래스는 매출 등을 확인하며 어떤 상품을 들여오고 어떤 상품을 들여오지 않을지와 같이 중요한 업무는 당연히 직접 해야겠지만, ‘알바생이 존재하는 한’ 응대, 청소하기와 같이 알바생도 쉽게 할 수 있는 업무에는 크게 신경 쓸 필요는 없겠죠.

프로그래밍 세계에서 체력적으로 힘들어서, 시간이 없어서와 같은 이유는 없습니다. 애초에 프로그래밍적 사고에 의해 점장 객체, 알바생 객체가 존재하도록 설계됩니다.
이렇게 분리된 각 개체의 관심사는 명확하며, 명확히 분리된 관심사는 로직을 수정해야 할 때나 버그가 발생할 때 찾아가야할 곳을 분명히 하도록 해줍니다.

결과적으로 관심사를 완전히 분리함으로써 코드 간 결합도를 줄일 수 있습니다.
이는 굳이 상속을 쓰지 않고 Delegate 패턴을 사용하는 이유이기도 합니다. 상속은 결합도를 높이고, 필요하지 않은 속성까지 상속을 받아야한다는 단점이 있습니다.

결국 Delegate 패턴을 사용하게 되면 각각 클래스 간 서로에 대해 직접적으로 알 필요가 없게 됩니다. 사실 현실 세계의 점장-알바생의 예시에서 점장이 알바생을 모른다! 라는 건 말도 안되는 소리이긴 하지만, 코드만 봤을 때는 이야기가 달라집니다.

위에서 봤던 코드를 조금만 수정해볼게요. 😀

protocol 알바생에게위임할내용들 {
    func 손님응대하기()
    func 매장청소하기()
}

class 점장 {
    var 알바생들: 알바생에게위임할내용들?
    
    init(알바생들: 알바생에게위임할내용들) {
        self.알바생들 = 알바생들
    }
    
    func 알바생한테일시키기() {
        알바생들?.매장청소하기()
        알바생들?.손님응대하기()
    }
}

class 알바생A: 알바생에게위임할내용들 {
    
    func 손님응대하기() {
        print("어서오세요")
    }
    
    func 매장청소하기() {
        print("빗자루로 쓸기 🧹")
    }
    
}

var 윤점장 = 점장(맡길일들: 알바생A())
윤점장.알바생한테일시키기()

위의 코드를 보면 점장 클래스와 알바생A는 서로에 대해 전혀 알지 못합니다.

결국 Delegate 패턴을 사용함으로써 결합도를 낮추고, 유지 보수가 쉬운 코드를 작성할 수 있게 됩니다.
알바생B, 알바생C, 알바생D 클래스를 추가적으로 작성한다고 하더라도, 점장 클래스에 전혀 영향을 미치지 않습니다.
즉, 위임자와 대리자는 서로의 동작에 대해 충돌 없이 작업을 수행합니다. 이러한 관계를 느슨하게 결합(loosely coupled) 되어있다고 말 할 수 있습니다.
이를 통해 객체 지향 원칙 중 하나인 개방-폐쇄의 원칙도 지킬 수 있게 됩니다.

실제 세계의 예시로 비유해보면 요 정도로 표현할 수 있겠네요.

  • loose coupling 🙆‍♀️ (점장) : 손님 응대할 줄 알고, 청소할 줄 아는 알바생 아무나 괜찮아.
  • tight coupling 🙆‍♀️ (점장) : 나는 알바생으로 A만 채용할거야. 다른 사람은 안돼.

우리는 이러한 느슨한 결합을 추구함으로써, 결국에는 확장성과 재사용성을 높일 수 있게 됩니다.
점장의 구현에 영향을 주지 않고 알바생을 계속 추가할 수 있다는 점에서 확장성이 보장된 사실은 이미 알 수 있었습니다.
그렇다면 재사용성이랑은 무슨 상관이 있을까요? 먼저 강한결합점장 클래스를 구현해보겠습니다.

class 강한결합점장 {
	var 알바생A = 알바생A()
	
	func 알바생에게일시키기() {
		알바생A.매장청소하기()
		알바생A.손님응대하기()
	}
}

강한결합점장은 본래 대기업 본사의 직원이고, 대기업 계열사인 어떤 편의점의 점장으로 발령이 난 상태라고 비유해보겠습니다.
그런데 이 직원의 편의점 발령 기간이 끝나, 이번에는 영화관으로 발령이 났다고 합니다.

그때 이 강한결합점장 클래스를 그대로 재사용할 수 있을까요? 그렇지 못합니다. 왜일까요?

알바생A는 편의점 일만 할 수 있는 사람이기 때문입니다. 영화관으로 발령이 난 점장님은 영화관에서 일 할 수 있는 알바생을 구해서, 클래스를 새롭게 수정해야만 합니다.
결과적으로 느슨하게 결합되어있지 않은 클래스 간의 관계는 확장과 재사용을 보장하지 못하게 된다는 사실을 알 수 있습니다.

어떤 경우에 사용될까?

앞선 간단한 코드를 통해 Swift로 어떻게 Delegate 패턴을 구현하고, 이를 통해 어떠한 장점을 취할 수 있는 지에 대해 알아보았습니다.

이번에는 좀 더 Apple의 생태계에 맞춰 Delegate 패턴이 어떤 경우에 사용되는 지 알아봅시다.

  1. UI Framework

Apple에서는 화면에 리스트를 렌더링할 수 있는 UICollectionView를 제공합니다.
화면이 스크롤되는 리스트를 표현하는 데에는 여러 공통된 논리가 있습니다. 수천 개의 셀이 렌더링 될 때마다 새로운 뷰를 사용하지 않고 재사용하면 좋을 것이고, 화면에 표시되는 셀들을 관리할 수 있어야 합니다. 따라서 이를 쉽게 할 수 있는 클래스가 있으면 좋을 것이고, 이 클래스가 바로 UICollectionView가 될 것 입니다.

그런데 여기서 부딪히는 이슈는 각 테이블에 표시하려는 콘텐츠 요소가 다르다는 점입니다.
Apple에서 이러한 문제를 해결하기 위한 솔루션으로 Delegate 패턴을 사용했습니다. 내부적으로 셀을 효율적으로 재사용하는 논리는 UICollectionView 자체에서 제공하되, ‘어떤 셀을 표시하고, 몇 개나 표시할 지는 너희가 알아서 정하라’ 라는 것입니다. 다음과 같은 프로토콜을 이용해서 말입니다.

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
  1. 데이터 / 이벤트 전달

화면 동선이 A에서 B로 이동할 때 데이터를 전달해야 하는 경우도 있지만, B에서의 작업이 마무리 되고 A로 다시 이동할 때 A가 B의 작업이 끝났음을 알 필요가 있거나, B에서 작업이 끝나면서 만들어진 어떠한 데이터를 A가 활용하고 싶은 경우가 있을 수 있습니다.

예를 들어 커머스 앱의 경우는 로그인 없이도 해당 앱에서 어떤 상품이 판매되고 있는 지를 확인할 수 있습니다.
그러나 로그인 없이 상품을 구경하는 상태에서 이 사용자가 구매 버튼을 누르게 되면 사용자는 로그인 화면으로 이동하게 될 것입니다.
사용자가 로그인을 마치면, 현재 로그인 화면이 네비게이션 스택에서 사라짐과 동시에 상품 화면은 상품 구매 화면으로 넘어가야 할 것입니다.

상품 설명 화면은 로그인 화면의 동작이 끝났음을 어떻게 알고 상품 구매 화면으로 이동할 수 있을까요?

여러 방법이 있겠지만, 이 경우에도 Delegate 패턴을 활용할 수 있습니다.

로그인 화면은 로그인만 하면 되기 때문입니다. 로그인 이후의 동작은 정의할 필요가 없다는 것이죠.
이 말은 곧, 로그인 이후의 동작은 필요한 화면에 위임해버리면 된다는 뜻이기도 합니다.

protocol LoginViewControllerDelegate: AnyObject {
    func didFinishLogin()
}

먼저 상품 설명 화면에서 로그인이 끝났을 때의 동작을 구현할 수 있도록 didFinishLogin() 함수를 프로토콜 내 작성해줍니다.

class LoginViewController: UIViewController {
    weak var delegate: LoginViewDelegate?
    private let loginButton = UIButton()

    loginButton.addTarget(self, action: #selector(didTapLoginButton), for: .touchUpInside)

    @objc private func didTapLoginButton(_ sender: UIButton) {
				// 로그인 수행 코드 생략
        delegate?.didFinishLogin()
    }
}

로그인 버튼을 누르면, 로그인이 끝나고 대리자들에게 끝났을 때의 동작을 위임해버립니다.

class ProductDescriptionViewController: UIViewController {
	private let buyButton = UIButton()
		
    buyButton.addTarget(self, action: #selector(didTapBuyButton), for: .touchUpInside)

    @objc private func didTapBuyButton(_ sender: UIButton) {
		if !isLoggedIn {
			let loginViewController = LoginViewController()
			loginViewController.delegate = self
			self.navigationController?.pushViewController(loginViewController, animated: true)
		}
    }
}

extension ProductDescriptionViewController: LoginViewControllerDelegate {
	func didFinishLogin() {
		let buyViewController = BuyViewController()
		self.navigationController?.pushViewController(buyViewController, animated: true)
	}
}

상품 설명 화면에서는 로그인이 끝났을 때의 동작으로 구매 화면으로 넘어가는 것으로 정의해두었습니다.
따라서 LoginViewcontroller에서 delegate?.didFinishLogin() 코드를 실행하게 되면, 대리자인 ProductDescriptionViewController에서 정의한 didFinishLogin() 액션이 수행되는 것입니다. 데이터 전달이 필요하면 didFinishLogin 함수에 파라미터를 추가하면 됩니다.

지금까지 Delegate 패턴이 무엇이고, 이를 통해 얻을 수 있는 장점은 어떤 것이 있으며, 어떤 경우에 사용되는 지에 대해 알아보았습니다.

읽어주셔서 감사합니다 😃

profile
a tidal wave of question mark

2개의 댓글

comment-user-thumbnail
2023년 8월 13일

이렇게 유용한 정보를 공유해주셔서 감사합니다.

답글 달기
comment-user-thumbnail
2023년 8월 13일

야자수님 이해하기 쉬운 코드와 설명 감사합니다🏝️

답글 달기
Powered by GraphCDN, the GraphQL CDN