델리게이션 패턴(Delegation Pattern)과 클로저의 차이점은 무엇인가요?

소혜 (Sohye)
xohxe (김소혜)· April 16, 2024

iOS에서 Delegate 패턴은 정말 많이 보긴했다. 대충 뭔지는 알지만 아직 잘 몰라서 한 번쯤 다시 정리해보고 싶어서 이번 주제로 갖고 왔다.

1. Delegation Pattern

먼저 델리게이션 패턴이란 무엇일까?

'Delegate'는 '위임하다, 대리자' 라는 사전적 의미 그대로

하나의 객체가 모든 일을 처리하는 것이 아니라, 처리해야할 일의 일부를 다른 객체에게 위임하는 디자인 패턴이다.

즉, 객체 지향 프로그래밍에서 많이 사용되는 디자인 패턴 중 하나로 객체와 객체 간 상호작용을 할 때 사용한다. (쌍방향 커뮤니케이션)

그리고 델리게이트 패턴을 크게 아래와 같이 3가지로 나눌수 있는데, 델리게이트 패턴을 사용하고 있는 UITableView를 예시로 하나하나 알아보자.

  • Delegate Protocol(프로토콜 정의)
  • Delegating Object(위임자 객체 생성)
  • Delegate Object(대리자 객체 생성)

1-1. Delegate Protocol

델리게이트(Delegate)를 구현한 프로토콜(Protocol)을 정의하자.

Delegate는 주로 프로토콜을 이용하여 구현되며, 이 프로토콜은 다른 객체에게 정의한 메서드를 구현하도록 요구한다.

COPY
// Delegate Protocol
protocol UITableViewDelegate: AnyObject {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
}

1-2. Delegating Object

Delegate 를 필요로 하는 객체로 위임하는 역할을 한다.

Delegating Object는 Delegate를 가지고 있는 객체이다.

그리고 앞서 앞서 정의한 프로토콜을 약한 참조 weak reference 로 선언되는데, 이는 기본적으로 델리게이트를 보유할 수 있게되고 델리게이트가 중복으로 보유하게되는 순환참조, 즉 retain cycle 을 피하기 위해서 weak reference 로 선언된다. (메모리 누수를 방지..)

COPY
// Delegating Object
class UITableView {

    weak var delegate: UITableViewDelegate?

    func didSelectedRowAt(indexPath: IndexPath) {
        delegate?.tableView(self, didSelectRowAt: indexPath)
    }
}

1-3. Delegate Object

델리게이트(Delegate) 대리자로 실제 해야할일 을 수행하는 객체

Delegate Protocol에서 구현된 메서드를 Delegate Object(델리게이트 객체)에서 호출함으로써, Delegate Protocol과 Delegate Object 서로 상호작용 할 수 있게 된다.

즉 델리게이트 패턴의 핵심은 두 객체를 연결하고, 두 개의 객체가 효율적으로 소통하도록 도와준다.

COPY
// Delegate Object
class ViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print(indexPath)
    }
}

2. Delegation 패턴의 메모리 누수와 해결방법

이처럼 델리게이트 패턴은 매우 유용하지만, 하나의 객체에 너무 많은 델리게이트를 생성한다면 문제가 생길 수 있다.

특히 Delegation 패턴에서 메모리 누수는 주로 delegate가 강한 참조를 가질 때 발생한다. 두 객체가 서로를 강하게 참조하게 되면 참조 카운트가 절대 0이 되지 않아 메모리가 해제되지 않아 메모리 누수가 발생하는 것이다.

1-2에서 언급했듯, Delegate를 weak 또는 unowned로 선언하면 retain cycle(순환 참조)를 방지할 수 있다.

3. Clousre Capture List의 역할

3-1. 클로저와 함수의 차이

클로저는 이름이 없는 함수, C언어에서의 Block으로 클로저는 함수랑 기능이 동일하지만 형태는 다른데 정리해보자면 다음과 같다.

  • 함수
    • 이름이 있는 코드 묶음
    • 다른 코드가 함수 이름으로 호출
  • 클로저
    • 이름이 없는 코드 묶음
    • 이름이 없어도 호출할 수 있는 형태

함수를 사용하면 되지, 왜 클로저를 사용하는지에 대한 의문이 들 수 있다. 그 이유는 아래 코드를 보면서 그 의문점을 풀어가보자.

3-2. 클로저를 사용하는 이유

먼저 클로저를 파라미터로 받는 함수를 정의해보자.

COPY
func closureCaseFunction(a: Int, b: Int, closure: (Int) -> Void) {
    let c = a + b
    closure(c)
}

closureCaseFunction 함수를 실행할 때, 파라미터를 클로저 형태로 전달한다.

다시 말해 기존에 정의한 함수를 쓰는게 아니라, 파라미터 안에서 클로저 형태로 하나 정의해서 바로 전달할 수 있다. 그래서 함수를 만들고도, 사후적으로 클로저를 정의할 수 있는데 이는 엄청난 장점이다. 또 클로저를 사용할 때, 인라인으로 로직을 작성할 수 있어 편리하며 비동기 작업이나 콜백 처리에 유용합니다.

COPY
// 사후적으로 클로저를 정의한 경우
closureCaseFunction(a: 5, b: 2, closure: { (n) in    
    print("이제 출력할게요: \(n)")
})
// 소괄호 땡기는 문법
closureCaseFunction(a: 5, b: 2) {(number) in         
    print("출력할까요? \(number)")
}

closureCaseFunction(a: 5, b: 3) { (number) in        
    print("출력")
    print("출력")
    print("출력")
    print("값: \(number)")
}

3-3. Closure Capture List란?

클로저에 대해 알아보았다면, Swift를 접해봤다면 한번쯤 봤을 법한 Capture List에 대해 알아보자.

Clousre Capture 클로저 캡쳐는 클로저 내부에서 외부에 있는 값에 접근하면 값에 대한 참조를 획득하는 것을 말한다. 이해가 안되면 코드를 먼저 살펴보자.

바로 [weak self] 부분이 바로 Capture List이다.

COPY
var printTitleClosure = { [weak self] in
  print(self?.title)
}

이처럼 Closure Capture List는 자신의 외부 환경을 캡처할 때 참조 방식을 정의하며, [weak self], [unowned self], [self] 등을 사용하여 강한 참조, 약한 참조, 소유되지 않은 참조를 구분할 수 있다.

클로저의 캡처 리스트(Capture List)는 무슨 역할을 하는지 정리해보자면...

  • 클로저 내부에서 참조타입을 획득하는 규칙을 정하는 기능이다.
  • 이 기능을 통해서 메모리 누수의 주 원인인 "강한 참조 순환" 을 막을 수 있다.
  • 참조하는 값이 변경되어 클로저 내부의 값이 변경되는 것을 막을 수 있다.

4. Delegation 패턴과 Closure의 함께 사용시 장단점

델리게이션 패턴과 클로저를 함께 사용하는 예시 코드를 살펴보자.

COPY
protocol RequesterDelegateSwift {
    func onRequestSuccess()
    func onRequestFailure()
}

// 델리게이트 대리
class RequesterDelegateSwiftImplementation:  RequesterDelegateSwift {
    var requestSuccess: ((Void) -> Void)?
    var requestFailure: ((Void) -> Void)?

    func onRequestSuccess() {
        if let successClosure = self.requestSuccess {
            successClosure()
        }
    }

    func onRequestFailure() {
        if let failureClosure = self.requestFailure {
            failureClosure()
        }
    }
}
// 델리게이트 위임
class RequestManagerSwift {

    var delegate: RequesterDelegateSwift?

    func get(url: String) {
        //Do the call
        let requestSucceed: Bool = self.isSuccess()

        //After the call
        if requestSucceed {
            self.delegate?.onRequestSuccess()
        } else {
            self.delegate?.onRequestFailure()
        }
    }

    private func isSuccess() -> Bool {
        return true
    }
}

func callWebService() {
	let manager: RequestManagerSwift = RequestManagerSwift()
    let requesterDelegate: RequesterDelegateSwiftImplementation = RequesterDelegateSwiftImplementation()
    
    requesterDelegate.requestSuccess = {}
    requesterDelegate.requestFailure = {}
    
    manager.delegate = requesterDelegate
    manager.get(url: "http://just-doit.me")
 }

4-1. 장점

  • 보다 유연한 델리게이트 패턴을 작성할 수 있다.
  • 클린 코드
  • 메서드를 더 적게 작성할 수 있다.
  • Delegate 의 Callback에 대해 핸들링을 감소시킬 수 있다

4-2. 단점

  • 프로토콜에 많은 메소드가 포함되어 있으면, 많은 것을 다시 구현해야 한다.
  • 또한 관련된 모든 클로저를 정의해야한다.
  • 모든 호출에 대해 클로저를 재정의해야 한다.

참고

  1. HOW TO COMBINE DELEGATES AND CLOSURES
다음 게시물
Combine 프레임워크와 RxSwift
이전 게시물
Swift 제네릭(Generic)에 대해 설명해주세요.

© 2023 - 2024 xohxe. All Rights Reserved.