Delegate 패턴은 작은 프로젝트라도 만들어본 사람이라면 무조건! 썼을 디자인 패턴이다.
DI또한 의존성 주입이라는 이름으로 많이 들어봤을 터이다.
그리고 이 두 개념에 대해서 공부해보면 Protocol이 굉장히 많이 언급된다.
셋 다 개념이 추상적인 느낌이기도 하고 특히 DI는 개인적으로 크게 와닿기까지 시간이 오래 걸렸다…
Delegate 패턴과 DI, 그리고 Protocol과의 연관성을 뿌셔보쟈아아아
찾아봤던 내용과 개인적으로 헷갈렸던 내용들을 정리해보겠다.
(DI에서 자꾸 위임이라는 단어를 사용하길래 Delegate패턴과 뭔 연관성이 있나? 싶었던 사람인데 혹시나 나같은 사람이 있을까봐,,한번에 정리^^)
🍕 Delegate Pattern
디자인 패턴의 일종으로 그 중에서도 행위 디자인 패턴에 해당한다.
정처기 따본 사람은 아는 그 무조건 외워야 하는 디자인 패턴의 종류다.ㅋㅋㅋㅋ
Delegate는 ‘위임’이라는 뜻이고, 말 그대로 어떤 객체가 해야할 일을 다른 객체에게 맡기는 설계 패턴이다.
그렇다면 “위임을 해주는 객체” / “위임을 받는 객체”
이렇게 있을 것인데 솔직히 나는 이 용어에 초점을 두고 이해하려고 하는게 더 헷갈리는 것 같닼ㅋ쿠ㅜ
UICollectionView를 통해 좀 더 쉽게 이해해보자.
UICollectionView를 사용하는 뷰컨에서 UICollecitonViewDelegate 프로토콜을 따른다고 선언한 뒤
컬렉션 cell을 선택했을 때 자동으로 호출되는 메서드 내부에 코드를 작성한 경험이 있을 것이다.
그냥 프로토콜 따르면 자동으로 실행되나보다~~하는 사람 있나요?
Delegate 패턴을 정복해보겠어 라는 마음가짐으로 파헤쳐보자
UICollecitonView의 Definition으로 들어가보면 아래처럼 delegate라는 변수가 있다.

delegate변수 타입이 우리가 흔히 뷰컨의 프로토콜로 작성하던 UICollecitonViewDelegate이다.
어쨌든 프로토콜은 타입으로 동작하기도 하는데 UICollectionViewDelegate를 따르는 객체라면 delegate로 들어올 수 있다는 의미이다.
(이에 대한 부분은 혹시나 처음 들어본다면 다른 블로그에 잘 설명되어 있으니 참고하시긜)
그럼 이제 이해가 되는게 뷰컨에서
collectionView.delegate = self(뷰컨)
이런 코드를 작성해야하는데 delegate에 뷰컨을 할당할 수 있었던 이유가 바로 뷰컨이 UICollectionViewDelegate프로토콜을 따르도록 했기 때문이고, 이렇게 delegate를 self로 지정해주는 행위가 바로’ 위임’인 것이다.
UICollectionView객체에서 해줘야 할 일을 self,즉 뷰컨이 처리하도록 뷰컨에 책임을 위임해주었다! 라고 볼 수 있다.
그러니까 위임을 해준 객체는 UICollectionView / 위임을 받은 객체는 뷰컨이다.
쉽게 말해 delegate에 들어온 객체에 ‘아 이부분은 위임받으신 분이 실행해주세요.!’ 하고 책임을 떠맡긴 거라고 생각하면 된다.
그렇다면 collectionView의 셀을 선택했을 때 자동으로 실행되는 메서드는 어떻게 된 것일까!?
정확한 로직은 숨겨져 있어 보이지 않지만 내부적으로 자동으로 셀을 선택했을 때 동작하는 메서드가 UICollecitonView객체에 존재할 것이다. 메서드 내부에 아래와 같이 우리가 아는 그 메서드를 호출한다.
delegate?.collectionView(collectionView, didSelectItemAt: indexPath)
어? delegate 는 책임을 떠안은 뷰컨이라고 했는데? 그럼 이 뷰컨에 구현된 메서드가 실행되는거네?
그럼 뷰컨에 그 메서드가 있어야 하는데? 하고 보면
아, 이미 반사적으로 잘 작성해주던 메서드였네;; 하게 된다.
그럼 이제 Protocol과 연결지어서 Delegate Pattern에 대해서 더 깊이 알아보겠다.
delegate가 collectionView(collectionView, didSelectItemAt: indexPath) 메서드 호출이 가능햇던 이유가 무엇일까?
delegate의 타입을 엄밀히 말하면 프로토콜인데 이 프로토콜 안에 collectionView(collectionView, didSelectItemAt: indexPath)
가 있다는 건가?
-> 맞다. 정확히는 프로토콜은 메서드를 구현할 수 없고 시그니처만 선언할 수 있다. (Extension을 사용하면 구현도 할 수 있긴 하다.)

UICollectionViewDelegate 프로토콜 안에 저 메서드가 정의되어있다.
어떤 객체가 프로토콜을 따르면, 해당 프로토콜이 가진 메서드를 가지고 있다고 보장할 수 있으므로 \
이 collectionView(collectionView, didSelectItemAt: indexPath) 메서드를 호출할 수 있었던 것이다.
(혹시 이해가 안된다면 Protocol에 대해서 간단하게 공부하고 오는 것을 추천한다!)
☝️ 그러니까 Delegate 패턴과 Protocol은 뗄 수 없는 관계이며, 프로토콜을 따르기만 한다면 어떤 객체던간에 그 객체가(여기서는 뷰컨이) 위임을 받아 액션을 대신 처리할 수 있다.
결국 delegate에 할당될 객체가 꼭 뷰컨이라는 법도 없고 흔히 사용하는 매니저객체든, 뭐든 상관이 없다는 의미이다.
어떤 동작 처리를 다른 객체에 위임해서 분리하고 싶다면 사용하면 된다!
UICollectionView도 셀을 눌렀을 때 어떤 일을 할지 delegate(ViewController)에 분리했다고 생각하면 된다.
(처음엔 시야가 좁아 뷰컨만 Delegate패턴을 사용하는 줄 알았다..)
그런데, 그냥 자기가 다 처리하면 되는거 아닌가? 왜 위임을 하는거지? 라는 의문도 들 수 있다.
아래 코드로 설명하겠다.
로그인이 성공하면 할 행위들을 LoginViewController내부에 모두 작성했다.
이는 MVVM패턴이라면 뷰모델에서 1, 3번을 분리해 작성했을 수도 있겠다.
class LoginViewController: UIViewController {
func loginDidSucceed(user: User) {
// 1. 사용자 데이터 저장
UserDefaults.standard.set(user.name, forKey: "username")
// 2. 홈 화면으로 전환
let homeVC = HomeViewController()
navigationController?.pushViewController(homeVC, animated: true)
// 3. Analytics 전송
Analytics.track(event: "LoginSuccess", userID: user.id)
}
}
하지만 다른 곳에서도 사용하는 함수이기도 하고, 코드를 분리하고 싶다면 아래와 같이 작성할 수 있겠다.
protocol LoginDelegate: AnyObject {
func loginDidSucceed()
}
class LoginFlowHandler {
var delegate: LoginDelegate?
func loginHandler(user: User) {
// 여기서 실제로 저장, 추적 등을 처리
// 1. 사용자 데이터 저장
UserDefaults.standard.set(user.name, forKey: "username")
// 2. Analytics 전송
Analytics.track(event: "LoginSuccess", userID: user.id)
delegate?.loginDidSucced(user:User)
}
}
class LoginViewController: UIViewController, LoginDelegate {
var handler = LoginFlowHandler()
override func viewDidLoad() {
super.viewDidLoad()
handler.delegate = self
}
func loginDidSucceed() {
// 로그인 성공 시 화면 전환 등
let homeVC = HomeViewController()
navigationController?.pushViewController(homeVC, animated: true)
}
}
그럼 위임을 누가 받는게 좋다는 기준이 있는지?
위임을 받는, 즉 delegate가 될 객체는 결과를 받고 후속 처리를 해야 하는 객체이다.
결과를 알고 싶은 쪽이 위임을 받는 쪽인 것임
지피티가 쌈뽕하게 비유해줬다.
• “부하 직원이 사장에게 결과 보고”
• 부하직원: LoginManager (일을 함)
• 사장: ViewController (결과 보고받고 판단함) • → 사장은 “위임을 받은 사람”
그러니까 보통ViewController가 위임을 받아 일부 책임을 Manager등으로 넘기고 결과만 delegate로 받으면 깔끔한 것!
후속 처리를 해야 할 객체가 꼭 ViewController가 아니어도 어떤 객체든 사장이 되는 건 가능하다!(자신의 판단에 맞게 구현하면 될 것 같다)
현실에서는 보통 사장이 일을 시키는 쪽(=위임자)이고 부하 직원이 지시받는 쪽이지만, iOS Delegate 패턴에서는 반대로 사장이 결과를 “받는 쪽(=Delegate)”이다. 즉, 일을 하는 쪽(LoginManager)이 결과를 “위임”하고, 사장(ViewController)이 그 결과를 받아 처리하는 구조인 것이다. 그래서 실제 코드에서는 “보고를 받는 사람”이 Delegate가 된다. 현실과 약간 다르니 처음엔 헷갈릴 수 있지만, “결과를 처리하는 사람이 Delegate다” 라고 기억하면 된다!
🍕 DI(의존성 주입)
의존성 주입은 진짜 너무 추상적이라고 생각하고, 솔직히 이름만 어려운 것이지, 알고보면 별 거 아닌 것 같다..
DI는 객체를 내부에서 생성하여 결합도를 높이는 대신 외부에서 생성해 주입받는 구조 설계 패턴이며
아래 특징이 있다.
- 테스트에 용이하고,
- 결합도를 최소화 할 수 있으며
- 클래스 간 의존성을 느슨하게 해준다는 장점이 있다.
🤯 후 뭔소린지 모르겠죠,,,
외부 주입이라는 것은 ViewModel 클래스 내부에서 NetworkManager라는 객체를 생성하지 않고 아래 코드처럼
ViewModel객체를 생성할 때 init을 통해 NetworkManager객체를 주입해준다는 것을 의미한다.
class NetworkManager {
func fetchData() -> String {
print("데이터를 네트워크에서 받아옴")
}
}
class ViewModel {
let networkManager: NetworkManager
init(networkManager: NetworkManager) {
self.networkManager = networkManager
}
}
Let neworkManager = NetworkManager() // 외부에서 객체 생성
Let viewModel = ViewModel(networkManager: neworkManager) // 주입
DI에서도 Delegate패턴과 마찬가지로 Protocol을 사용하여 의존성을 더 느슨하게 만들어줄 수 있다.
주입할 객체가 어떤 기능을 제공할지만 Protocol 을 통해 알면 되지 구체 타입을 알 필요는 없다.
protocol NetworkingProtocol {
func fetchData()
}
class NetworkManager: NetworkingProtocol {
func fetchData() {
print("데이터를 네트워크에서 받아옴")
}
}
class ViewModel {
private let networkManager: NetworkingProtocol
// ✅ 외부에서 의존성을 주입받음
init(networkManager: NetworkingProtocol) {
self.networkManager = networkManager
}
func loadData() {
networkManager.fetchData()
}
}
Let neworkManager: NetworkingProtocol = NetworkManager() // 외부에서 객체 생성(타입 프로토콜)
Let viewModel = ViewModel(networkManager: neworkManager) // 주입
그렇다면 아래의 장점들은 도대체 어떤 의미일까?
- ViewModel은 NetworkManager에 직접적으로 의존하지 않는다
- ViewModel와 NetworkManager의 결합도가 낮다
- 테스트에 용이하다
아래의 테스트과정을 생각해보면 확 와닿을 수 있다.
ViewModel을 테스트 코드를 통해서 실행하고 있는데 내부의 NetworkManager객체를 테스트용 NetworkManager로 바꿔주고 싶을 수 있다. (테스트용에서는 좀 다른 로직으로 실행하고 싶을 수도 있지 않나? 예를 들어 아래처럼 실제 네트워크를 호출하지 않는 것처럼…)
만약 내부에서 객체를 생성했다면 ViewModel내부의 코드를 원하는 대로 직접 수정해야 한다…
하지만 DI를 잘 준수했다면
class MockNetworkManager: NetworkingProtocol { // 테스트용 NetworkManager
func fetchData() {
print("Mock으로 테스트 중")
}
}
let mock = MockNetworkManager()
let viewModel = MyViewModel(networkManager: mock)
viewModel.loadData() // 👉 테스트 시 실제 네트워크 호출 X
이렇게 외부에서 테스트용 객체를 생성하여 주입시 테스트용으로 교체만 해주면 된다.
프로토콜을 사용했기 때문에 프로토콜을 따르는 객체라면 얼마든지 networkManager자리에 들어갈 수 있다.
또한, NetworkingProtocol이 가진 fetchData메서드는 networkManager를 다른 객체로 교체했더라도 호출할 수 있다.
ViewModel은 networkManager자리에 어떤 것이 올 지 모르며, NetworkingProtocol이 온다는 것만 알려줄 뿐이다.
즉, 결합도가 낮고, ViewModel이 NewtorkManager자체에 의존하지 않으며, 내부 코드를 수정하지 않아도 된다.
테스트가 용이하다는 건 알겠는데 결합도가 낮고 의존성이 낮다는 거는 아직도 이해가 안되는데? 한다면
한 가지 예를 더 들어보겠다.
내가 실제로 소셜 로그인 기능을 구현하면서 DI와 Protocol을 사용하여 구현한 코드들을 간단하게 적었다.
Protocol LoginManagerProtocol {
func login()
}
Class LoginViewModel {
private let loginManager: LoginManagerPtotocol
init(loginManager: LoginManagerProtocol) {
self.loginManager = loginManager
}
func doLogin() {
loginManager.login()
}
}
Class LoginManager: LoginManagerProtocol {
func login() {
//
}
}
Class NaverLoginManager: LoginManagerProtocol {
func login() {
//
}
}
Class KakaoLoginManager: LoginManagerProtocol {
func login() {
//
}
}
만약 의존성 주입을 하지 않았고 Protocol도 없었다면?
Class LoginManager {
func login() {
}
}
Class LoginViewModel {
private let loginManager = LoginManager() // 직접 생성
func doLogin() {
loginManager.login()
}
}
LoginManager가 변경되면 LoginViewModel도 같이 수정이 되어야 한다.
왜냐면 LoginViewModel은 항상 LoginManager객체를 가지고 있다고 생각하고 그에 맞춰서 구현이 되어있기 때문이다.
LoginManager의 login메서드의 반환값이 String이 된다고 가정했을 때 LoginViewModel에서는 로직이 변경될 것이다.
바로 이게 결합도가 높으며 의존성이 높다고 할 수 있다.
그러니 LoginViewModel에서는 loginManager이 뭐든 영향을 받지 않도록 DI를 사용하는 것이고, 의존성과 결합도가 확실히 낮아질 수 밖에 없다.
⭐️ DI와 Delegate 같이 사용하기
DI와 Delegate 패턴을 같이 사용하면 더 좋은 코드가 될 수 있다.
위의 DI에서 설명한 코드에 Delegate pattern을 적용해 확장해보겠다.
// 1. Delegate 정의
protocol LoginManagerDelegate: AnyObject {
func loginDidSucceed() // 로그인이 성공했을 때 실행될 메서드
func loginDidFail(error: Error) // 로그인이 실패했을 때 실행될 메서드
}
// 2. Protocol 정의
protocol LoginManagerProtocol {
var delegate: LoginManagerDelegate? { get set }
func login()
}
책임을 위임해줄 객체가 delegate에 들어올 수 있도록 LoginManagerProtocol에 delegate변수를 추가했다.
그리고 LoginManager들의 막중했던 책임을 delegate로 들어올 LoginManagerDelegate에 위임하여 분리해주겠다.
class KakaoLoginManager: LoginManagerProtocol {
var delegate: LoginManagerDelegate?
func login() {
// 로그인 성공 시
delegate?.loginDidSucceed()
// 로그인 실패
// delegate?.loginDidFail(error: MyError.kakaoFailed)
}
}
class NaverLoginManager: LoginManagerProtocol {
var delegate: LoginManagerDelegate?
func login() {
// 네이버 로그인 처리 후 delegate 호출
delegate?.loginDidSucceed()
}
}
그럼 이제 이거를 어케 사용해볼까
MVVM패턴으로 작성했다면 아래와 같을 것이다.
class LoginViewModel {
private let loginManager: LoginManagerProtocol
// DI적용!
init(loginManager: LoginManagerProtocol) {
self.loginManager = loginManager
}
func setDelegate(_ delegate: LoginManagerDelegate) {
loginManager.delegate = delegate
}
func doLogin() {
loginManager.login()
}
}
class LoginViewController: UIViewController, LoginManagerDelegate {
private var viewModel: LoginViewModel!
init(viewModel: LoginViewModel) {
self.viewModel = viewModel
}
override func viewDidLoad() {
super.viewDidLoad()
// Delegate 위임
viewModel.setDelegate(self)
}
func loginDidSucceed() {
print("로그인 성공! 홈 화면으로 이동")
}
func loginDidFail(error: Error) {
print("로그인 실패: \(error.localizedDescription)")
}
// 로그인 버튼을 눌렀을 때
func didTapLoginButton() {
viewModel.doLogin()
}
}
// 의존성 주입 (어떤 로그인 매니저를 쓸지 결정)
let loginManager = KakaoLoginManager()
let viewModel = LoginViewModel(loginManager: loginManager)
LoginViewController(viewModel: viewModel)
디자인 패턴의 종류
디자인 패턴이란 소프트웨어 설계에서 자주 등장하는 문제를 해결하기 위한 재사용 가능한 해법이다.
3가지 범주가 있는데
- 생성 : 객체 생성 방식에 관련된 패턴
- 구조 : 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴
- 행위 : 객체 사이의 상호작용 및 책임 분배에 대한 패턴 ex. Observer, Strategy, Delegate
여기서 Delegate 가 Delegate Pattern이다.
참고로 DI는 그 유우명한 SOLID원칙 중 DIP(의존성 역전 원칙)에 기반한 디자인 패턴 중 하나이다.
비동기 처리
추가로 Delegate패턴은 iOS에서 사용하는 비동기 이벤트 처리 방식이기도 하다.
비동기 이벤트 처리 방식에는 Delegate와 클로저가 있는데
Delegate는 구조적으로 책임을 분리하기 쉽고, 하나의 객체에 여러 콜백을 모아둘 수 있어서 정리가 잘 되는 장점이 있다.
반면 클로저는 호출 위치가 더 명확하고 간결하게 표현할 수 있어, 둘 중 상황에 따라 적절히 선택하는 것이 좋다.
대신 클로저를 사용하면 콜백 함수를 직접 넘겨야 해서 코드가 복잡해 보일 수 있는데 Delegate는 그냥 메서드 내부에 작성하면 된다.
비동기 이벤트는 예측할 수 없는 시점에서 발생하는 이벤트라고 하는데
조금이라도 개발을 해봤던 사람이라면 예시들이 많이 떠오를 것이다.
- 컬렉션뷰 셀 선택했을 때
- 키보드 나타났을 때
- 사용자가 버튼을 눌렀을 때
등등의 동작들이 비동기 이벤트라고 생각하면 된다.
Delegate는 그 이벤트가 발생했을 때 ✌️나 대신 그걸 처리해주세요✌️라고 위임하는 것이고(like 부하직원), 위임받은 쪽(like 사장)에 그 결과를 전달한다.
즉, “언제 발생할지 모르는 비동기 이벤트”에 대한 처리를 준비하는 것이다.