Delegate는 한 객체가 작업 실행을 다른 객체에 위임하는 디자인 패턴입니다. iOS에서는 Swift 프로토콜과 @protocol Objective-C를 통해 패턴이 구현됩니다. 위임은 Cocoa Touch의 기본 패턴 중 하나이며 UITableViewDelegate, UITextFieldDelegate 및 수백 개의 다른 Apple API에서 사용됩니다. Apple Developer Documentation(2025)에 따르면 UIKit 시스템 클래스의 약 70%가 상속 없이 동작 사용자 정의를 위해 델리게이트를 사용합니다.
핵심 사항
Delegate는 객체가 자신의 책임 일부를 다른 객체에 위임할 수 있게 하는 행동 디자인 패턴입니다. 자식 클래스가 부모 메서드를 재정의하는 상속과 달리, 델리게이트는 컴포지션을 사용합니다. 소유자 객체는 델리게이트에 대한 참조를 유지하고 특정 지점에서 해당 메서드를 호출합니다.
델리게이트는 프로토콜을 정의합니다 — 델리게이트가 구현할 수 있는 메서드 집합입니다. 메서드는 필수(required)와 선택적(optional)으로 나뉩니다. Swift에서는 선택적 프로토콜 메서드가 @objc optional 키워드로 표시됩니다.
객체 A(소유자)는 delegate 속성 — 객체 B(델리게이트)에 대한 약한 참조(weak reference)를 포함합니다. 이벤트가 발생하면 A는 B가 해당 프로토콜 메서드를 구현하는지 확인하고 호출합니다. 약한 참조는 필수입니다. 이것이 없으면 소유자가 강한 참조로 유지하기 때문에 델리게이트가 메모리에서 해제될 수 없습니다.
protocol LoaderDelegate: AnyObject {
func loaderDidStart(_ loader: DataLoader)
func loader(_ loader: DataLoader, didLoad data: Data)
func loader(_ loader: DataLoader, didFailWith error: Error)
}
class DataLoader {
weak var delegate: LoaderDelegate?
func start() {
delegate?.loaderDidStart(self)
// 비동기 로딩
}
}DataLoader 클래스는 LoaderDelegate 프로토콜을 정의하고 로딩 라이프사이클의 주요 지점에서 델리게이트 메서드를 호출합니다. AnyObject는 프로토콜이 클래스에 의해서만 구현될 수 있음을 보장합니다 — 이는 약한 참조에 필요합니다.
Swift의 델리게이트 구현은 세 단계로 구성됩니다: 프로토콜 선언, 소유자에 약한 delegate 속성 생성, 델리게이트 클래스에서 프로토콜 구현입니다. 유효성 검사가 있는 사용자 정의 UITextField 예제를 살펴보겠습니다.
protocol ValidatorDelegate: AnyObject {
func validate(_ input: String) -> Bool
func validatorDidFail(_ input: String)
}
class ValidatedTextField: UITextField {
weak var validator: ValidatorDelegate?
override func textDidChange() {
guard let text = self.text else { return }
if validator?.validate(text) == false {
validator?.validatorDidFail(text)
self.layer.borderColor = UIColor.red.cgColor
}
}
}
class LoginViewController: UIViewController, ValidatorDelegate {
let textField = ValidatedTextField()
override func viewDidLoad() {
super.viewDidLoad()
textField.validator = self
}
func validate(_ input: String) -> Bool {
return input.count >= 6
}
func validatorDidFail(_ input: String) {
print("유효성 검사 실패: 입력이 너무 짧습니다")
}
}LoginViewController는 ValidatorDelegate 프로토콜을 구현하고 자신을 textField의 델리게이트로 설정합니다. 텍스트가 변경될 때마다 ValidatedTextField는 validate(_:)를 호출하고, 유효성 검사가 실패하면 validatorDidFail(_:)을 호출합니다. 컨트롤러는 View와 유효성 검사 로직 사이의 중개자 역할을 합니다.
델리게이트, 알림, 클로저 중 선택은 수신자 수와 컴포넌트 결합도에 따라 달라집니다. 각 메커니즘은 객체 간 통신 문제를 해결하지만, 서로 다른 트레이드오프가 있습니다.
| 특성 | Delegate | NotificationCenter | Closure |
|---|---|---|---|
| 통신 유형 | 1:1 | 1:N | 1:1 |
| 결합도 | 약함 (프로토콜을 통해) | 매우 약함 (문자열 키) | 중간 (컨텍스트 캡처) |
| 타입 안전성 | 완전 | 없음 (Any?) | 완전 |
| Retain cycle 위험 | 없음 (weak) | 없음 | 있음 (self 캡처) |
| 사용 시기 | 여러 메서드가 있는 복잡한 콜백 | 많은 사람이 관심 있는 이벤트 | 1-2 콜백이 있는 간단한 클로저 |
Delegate는 일련의 관련 이벤트를 단일 수신자에게 전달해야 할 때 최적입니다. NotificationCenter는 브로드캐스트 알림에 더 좋습니다. Closure는 URLSession의 완료 핸들러와 같은 간단한 비동기 작업에 사용합니다.
Objective-C는 델리게이트 선언을 위해 @protocol과 @optional을 사용합니다. Swift와 달리 모든 프로토콜 메서드는 기본적으로 선택적입니다. 주요 차이점은 메서드가 구현되지 않았을 수 있으므로 델리게이트에 메시지를 보내기 전에 respondsToSelector:를 호출한다는 점입니다.
@protocol ImageCacheDelegate
@optional
- (void)cacheDidStartDownload: (ImageCache *)cache;
- (void)cache: (ImageCache *)cache didCacheImage: (UIImage *)image;
@required
- (void)cache: (ImageCache *)cache didFailWithError: (NSError *)error;
@end
@interface ImageCache : NSObject
@property (nonatomic, weak) id<ImageCacheDelegate> delegate;
- (void)downloadImageAtURL: (NSURL *)url;
@end
@implementation ImageCache
- (void)downloadImageAtURL: (NSURL *)url {
if ([self.delegate respondsToSelector:@selector(cacheDidStartDownload:)]) {
[self.delegate cacheDidStartDownload:self];
}
// 비동기 이미지 로딩
}
@endObjective-C의 주요 차이점: 선택적 메서드를 호출하기 전에 respondsToSelector: 확인이 필요합니다. Swift에서는 선택적 프로토콜 메서드가 이 확인을 불필요하게 만듭니다 — 옵셔널 체이닝(?.)이 구현 부재를 자동으로 처리합니다.
델리게이트 사용 실수는 메모리 누수, 앱 충돌 및 명확하지 않은 버그로 이어집니다. 다섯 가지 가장 일반적인 문제를 살펴보겠습니다.
Retain cycle은 가장 일반적인 실수입니다. delegate 속성이 strong으로 선언되고 델리게이트가 소유자 객체를 소유하는 경우, 보유 주기가 형성됩니다. 두 객체 모두 메모리에서 해제되지 않습니다. 해결책: Swift에서는 항상 weak var로, Objective-C에서는 @property (weak)로 델리게이트를 선언하세요.
소유자 객체가 델리게이트보다 오래 살고 참조가 남아 있으면, 델리게이트 메서드를 호출할 때 EXC_BAD_ACCESS가 발생합니다. 약한 참조는 이 문제를 자동으로 해결합니다: 델리게이트가 해제된 후 속성은 nil이 됩니다. 그러나 멀티스레드 시나리오에서는 메인 스레드에서 델리게이트를 추가로 확인해야 합니다.
20개 이상의 메서드가 있는 프로토콜은 인터페이스 분리 원칙(ISP)을 위반합니다. UITableViewDelegate에는 약 30개의 선택적 메서드가 있습니다 — 이것은 역사적 예외입니다. 자신의 프로토콜에서는 책임을 여러 개의 작은 프로토콜로 나누는 것이 좋으며, 각각 고유한 역할을 가집니다.
Apple의 시스템 API는 Delegate 패턴을 적극적으로 사용합니다. 모든 iOS 애플리케이션에 등장하는 UIKit의 세 가지 주요 예제를 살펴보겠습니다.
| API | 프로토콜 | 주요 메서드 |
|---|---|---|
| UITableView | UITableViewDelegate | didSelectRowAt, heightForRowAt, willDisplay |
| UITextField | UITextFieldDelegate | shouldChangeCharactersIn, didBeginEditing, shouldReturn |
| URLSession | URLSessionDelegate | didReceiveChallenge, didCompleteWithError, didBecomeInvalidWithError |
이 프로토콜들은 각각 다른 동작 측면을 구현합니다: UITableViewDelegate는 모양과 터치 응답을 관리하고, UITextFieldDelegate는 텍스트 입력을 제어하며, URLSessionDelegate는 네트워크 이벤트를 처리합니다. 이는 패턴의 유연성을 보여줍니다: 델리게이트는 모든 책임 영역에 적응할 수 있습니다.
자주 묻는 질문
Delegate는 동작과 모양을 관리합니다(셀 높이, 터치 응답). DataSource는 데이터를 제공합니다(행 수, 셀 내용). UITableViewDelegate와 UITableViewDataSource에서는 — 프레젠테이션과 데이터에 대한 책임을 나누는 두 개의 별도 프로토콜입니다.
약한 참조는 retain cycle을 방지합니다. 소유자(예: UITableView)는 델리게이트에 대한 참조를 weak로만 유지합니다. 델리게이트(UIViewController)가 테이블을 소유하는 경우, 델리게이트에 대한 강한 참조는 순환을 생성합니다: ViewController → UITableView → Delegate(ViewController). Weak가 이 순환을 끊습니다.
SwiftUI에서는 Delegate 패턴이 덜 자주 사용됩니다 — @Binding, @State 및 클로저로 대체됩니다. 그러나 델리게이트는 UIViewRepresentable을 통한 UIKit 통합을 위해 여전히 사용됩니다. 예를 들어, MKMapViewDelegate와 WKUIDelegate는 UIKit 컴포넌트를 SwiftUI로 래핑할 때 여전히 관련이 있습니다.
@objc optional은 Swift 프로토콜에서 선택적 메서드를 선언할 수 있게 합니다. 이는 Objective-C 런타임과의 호환성 메커니즘입니다. @objc가 없으면 모든 Swift 프로토콜 메서드는 기본적으로 필수입니다. Optional은 델리게이트가 필요한 메서드만 구현할 수 있는 UIKit 프로토콜에서 사용됩니다.
하나의 객체는 각 delegate 속성에 대해 하나의 델리게이트만 가질 수 있습니다. 여러 객체에 알려야 하는 경우, 멀티캐스트 델리게이트, 델리게이트 배열 또는 NotificationCenter를 사용하세요. Delegate 패턴은 원래 1:1 관계로 설계되었습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.