[TIL] Delegate 패턴

숑이·2023년 7월 28일
1

iOS

목록 보기
13/26
post-thumbnail

안녕하세요! :) 오늘은 iOS앱 개발에서 아주아주 많이 사용되고, 그만큼 중요한 Delegate 패턴에 대해서 알아보려고 합니다.
저는 이미 알고있고, 많이 사용해보긴 했지만 한번 더 정리해보면서 공부해보고자 합니다. 그리고 이전에 메모리 관리 포스팅에서 다뤘던 순환 참조(Retain Cycle)가 Delegate 패턴을 사용할 때에도 발생할 수 있기 때문에 그것과도 연관지어서 다뤄볼 것입니다!

Delegate

Delegate는 번역하면 "대리자" 라는 뜻을 가지고 있어요.

Delegate는 말 그대로 어떤 객체의 대리자 역할을 하도록 특정한 구조로 코드를 작성하는 디자인 패턴 입니다

Delegate 패턴을 사용하면, 서로 다른 객체간에 상호작용을 할 수 있고, 객체간 의존성을 낮추며 코드의 재사용성과 유지보수성을 향상시키는데 도움이 됩니다

이렇게 장점이 많은 Delegate를 사용하지 않을 이유가 없겠죠?

Delegate 패턴을 사용해보기 이전에 Delegate 패턴을 사용하지 않고 서로 다른 객체간에 상호작용하는 코드를 어떻게 작성할 수 있을까요?
간단한 예제 코드를 작성해보도록 하겠습니다.

Delegate 없이 객체간 상호작용

class MainViewController {
    weak var subVC: SubViewController?
    
    func callSubFunc() {
        subVC?.myFunc()
    }
    deinit {
        print("MainViewController deinit")
    }
}
class SubViewController {
    
    init(mainVC: MainViewController) {
        mainVC.subVC = self
    }
    
    func myFunc() {
        print("MyFunc")
    }
    
    deinit {
        print("SubViewController deinit")
    }
}

var mainVC: MainViewController? = MainViewController()
var subVC: SubViewController? = SubViewController(mainVC: mainVC!)
mainVC?.callSubFunc()

MainViewController의 CallSubFunc() 메서드에서 SubViewController의 myFunc() 메서드를 호출하고 싶어서 MainViewController에서는 SubViewController의 인스턴스를 참조합니다.

메모리 관리에서 배웠던 것 처럼 SubViewController 인스턴스는 클래스이기 때문에 힙 영역에 생성되겠죠?

그리고 SubViewController에서 MainViewController를 참조하진 않기 때문에 순환 참조가 발생하진 않지만, 예방 차원에서 weak(약한참조)로 선언했습니다.

자 다시 본론으로 돌아와서 MainViewController 내부에서 SubViewController의 메서드를 호출하고 있습니다. 다시 말해서 서로 다른 객체간 상호작용을 하고 있습니다.

그런데 위 코드의 문제점은 무엇일까요?

class MainViewController {
	weak var subVC: SubViewController?
    ...
}

MainViewController에서 SubViewController에 대한 강한 의존성을 가지고 있습니다.

의존성이 높다 는 말은 하나의 객체가 다른 객체에 강하게 의존하고 있는 상태를 의미합니다.

class SubViewController {
    
    init(mainVC: MainViewController) {
        mainVC.subVC = self
    }
}

SubViewController도 마찬가지 입니다. 생성자 파라미터로 MainViewController 를 받고 있죠?
이 경우에도 MainViewController에 대해 의존성이 강한 상태입니다.

그렇다면 의존성이 강한 경우에는 어떤 문제점이 발생할까요?

class HomeViewController {
    
    init(mainVC: MainViewController) {
        mainVC.subVC = self
    }
    
    func myFunc() {
        print("HomeViewController myFunc")
    }
}

만약 기능 추가를 위해서 HomeViewController라는 클래스를 새로 만들었고, HomeViewController에서도 마찬가지로 MainViewController와 상호작용을 하고 싶어서 MainViewController의 subVC를 self로 지정하려고 합니다.

하지만, 이 코드는 위와 같은 에러 코드를 뿜뿜합니다.

weak var subVC: SubViewController?

당연한 에러죠? MainViewController의 subVC는 SubViewController 타입으로 명시되어 있는데, HomeViewController를 전달했으니까요!

그럼 이제 객체간 의존성이 강한 경우에 어떤 문제점이 있는지 짐작이 가시나요?

객체간 의존성이 높으면 코드를 수정하거나 확장하기 어렵게 만듭니다.

기능 확장이 전혀 필요없고, MainViewController에서는 오로지 SubViewController만 사용할거야!!!
라고 단정짓고 개발하는 경우는 거의 없겠죠...

대부분 기능 확장을 염두해두고, 확장성이 높은 코드를 작성하는 것이 좋습니다. 그럼 나중에 유지보수하기 편할테니까요 :)

그래서 어떻게 객체간 상호작용을 할 때 의존성을 낮추고 확장성을 높이는 코드를 작성하느냐????!!!

.
.
.
.
.
.
.
.
.
.

Delegate Pattern!

Delegate 패턴을 사용해 코드 리팩토링

Delegate 패턴의 핵심은 Protocol입니다.
서로 다른 객체간의 상호 작용을 위해 사용할 메서드를 protocol로 따로 분리해서 작성합니다.
코드를 보면서 알아볼게요!

protocol MainViewControllerDelegate {
    func customFunction()
}

MainViewControllerDelegate라는 Protocol을 만들고 그 안에는 customFunction()이라는 메서드를 선언만 해놨습니다. 이 메서드는 MainViewController와 다른 객체들 간에 상호작용을 위한 수단으로써 사용될 것 입니다.

class MainViewController {
	var delegate: MainViewControllerDelegate?

    func callSubFunc() {
        delegate?.customFunction()
    }
    deinit {
        print("MainViewController deinit")
    }
}

MainViewController에서는 MainViewControllerDelegate 프로토콜을 채택하는 녀석을 delegate라는 이름의 프로퍼티로써 갖습니다. 이름이 꼭 delegate일 필요는 없습니다. 하지만 보통 delegate라고 많이 씁니다.

그리고 callSubFunc() 메서드에서 delegate의 customFunction() 메서드를 호출하고 있죠?

MainViewControllerDelegate 프로토콜을 채택한 녀석을 MainViewController의 delegate에 전달해주면, 전달받은 delegate의 customFunction() 메서드를 호출하게 될 것 입니다.

class SubViewController {

    init(mainVC: MainViewController) {
        mainVC.delegate = self
    }

    deinit {
        print("SubViewController deinit")
    }
}

extension SubViewController: MainViewControllerDelegate {
    func customFunction() {
		print("SubViewController customFunction")
    }
}

이렇게 전달해주면 되겠죠?! 가독성을 위해 extension을 통해서 프로토콜을 채택했습니다.

var mainVC: MainViewController? = MainViewController()
var subVC: SubViewController? = SubViewController(mainVC: mainVC!)
mainVC?.callSubFunc()

각각 인스턴스를 생성해서 mainVC의 callSubFunc() 메서드를 호출하면 SubViewController에서 정의한 customFunction()을 호출하게 될거에요!

다시말해서 MainViewController의 일을 SubViewController가 대신해서 하는 것과 같은 말 입니다!
즉, SubViewController가 MainViewController의 대리자로써 역할을 수행하는 것이죠.

이 흐름이 이해가 되시나요??

이해가 안된다면 아까 작성했던 SubViewController와 비교하면서 다시 생각해보세요

class SubViewController {
    
    init(mainVC: MainViewController) {
        mainVC.subVC = self
    }
    
    func myFunc() {
        print("MyFunc")
    }
    
    deinit {
        print("SubViewController deinit")
    }
}

단순하게 생각해보면, 위에서 작성한 SubViewController의 메서드를 Protocol로 분리한 것 뿐이에요!
그 프로토콜을 SubViewController가 채택한 것이고요. 전혀 어렵지 않죠? :)

근데 아직 Delegate 패턴을 왜 사용하는지 모르시겠나요? 코드의 확장성을 높이고 유지보수를 용이하게 해준다... 라는 장점이 아직까지는 크게 와닿지 않죠?

개발자가 기능확장을 위해 HomeViewController를 생성했고, 이 곳에서도 MainViewController와 상호작용을 하고 싶어요. 그럼 어떻게 하면 될까요?!!

다음 코드를 보면서 따라와보세요!

class HomeViewController {

    init(mainVC: MainViewController) {
        mainVC.delegate = self
    }
    
    deinit {
        print("HomeViewController deinit")
    }

}

extension HomeViewController: MainViewControllerDelegate {
    func customFunction() {
        print("HomeViewController customFunction")
    }
}

SubViewController와 마찬가지로 MainViewControllerDelegate 프로토콜을 채택하고 customFunction()을 정의하면 되겠죠???!!

mainVC.delegate = self를 통해 대리자를 명시해주는 것을 잊지 말구요 :)

var mainVC: MainViewController? = MainViewController()
var subVC: SubViewController? = SubViewController(mainVC: mainVC!)
mainVC?.callSubFunc()
var homeVC: HomeViewController? = HomeViewController(mainVC: mainVC!)
mainVC?.callSubFunc()

위의 코드를 실행했을 때 실행결과는

SubViewController customFunction
HomeViewController customFunction

위와 같습니다.
이제 왜 이렇게 하는지 감이 잡히시나요???!!!

Delegate 패턴을 사용하면 서로 다른 객체간 상호작용.
즉, 어떤 객체의 일을 대리자가 대신해서 수행하도록 할 수 있습니다.
그리고, 코드의 재사용성, 확장성을 높이고 코드의 유지보수를 용이하게 합니다.

이 의미를 이제는 이해하실 수 있겠죠?
SubViewController와 HomeViewController는 모두 MainViewController와 상호작용하는 동일한 작업을 하고 싶지만, 각각은 서로 다른 동작을 하도록 커스텀 하고 싶을 때 protocol을 사용하면 아주아주 좋습니다.

코드를 실행해보면 아시겠지만 MainViewController의 delegate(대리자)로 SubViewController가 오느냐 아니면 HomeViewController가 오느냐에 따라 다른 실행 결과를 출력하고 있습니다!!!!

지금까지 Delegate 패턴에 대해서 알아봤고, 추가적으로 한가지 더 공부하고 마무리 하도록 하겠습니다.

Delegate는 Retain되는가?

물음에 답하기 전에 용어 정리부터 하겠습니다.

  • Retain : 강한 참조에 의해서 인스턴스의 Reference Count를 증가 시켜 객체의 수명을 유지하는 것을 의미
  • Retain Cycle : 서로 다른 객체가 서로를 강하게 참조해서 Reference Count 가 0에 도달하지 못해 ARC가 메모리 해제를 하지 못하고, memory leak이 발생하는 것

따라서 Delegate는 Retain되는가? 라는 물음은 delegate가 참조하는 인스턴스의 Reference Count를 증가시키는가? 와 같은 말입니다. 즉, 쉽게 말해 delegate가 참조 타입이냐 아니냐를 묻는 것이겠죠

class MainViewController {
	var delegate: MainViewControllerDelegate?
}

위에서 봤던 코드를 보면, delegate는 MainViewControllerDelegate를 채택하는 녀석을 받습니다.
그리고, MainViewControllerDelegate를 채택하는 녀석들은 모두 Class 였죠?

따라서 강하게 참조(strong)를 하는 경우 Retain됩니다.

var mainVC: MainViewController? = MainViewController()
var subVC: SubViewController? = SubViewController(mainVC: mainVC!)

위 코드를 실행 시켜서 각각의 인스턴스를 생성했을 때 메모리에 어떻게 할당이 되는지 그림으로 확인해볼게요!

지역 변수인 mainVC와 subVC는 스택 영역에 할당 되어 각각의 인스턴스를 강하게 참조하고 있습니다.
강한 참조이기 때문에 Retain돼서 RC값을 1 증가시킵니다.

또한, MainViewController는 delegate로 전달받은 SubViewController를 강하게 참조하고 있죠?
따라서 Retain되고, SubViewController는 RC 값이 2입니다.

사실, 위 예제에서는 delegate가 retain 돼서 SubViewController의 RC를 1 증가시키기는 하지만, Retain Cycle(순환 참조)는 발생하지 않아 메모리에서 정상적으로 해제될 수 있습니다.
단, MainViewController가 언젠가는 메모리 해제된다는 가정하에 말이죠...!

그러나 앱이 실행되는 동안 MainViewController의 수명이 계속 유지 된다면....??
SubViewController의 메모리는 앱이 종료될 때까지 계속 남아 있을 겁니다.

이렇게요! :) MainViewController가 SubViewController를 강하게 참조하고 있기 때문에 MainViewController가 살아있는 한 SubViewController도 역시 계속 메모리를 차지하고 있겠죠?

이것 또한 memory leak이라고 할 수 있는지는 잘 모르겠네요... 아직 이 부분에 대해서는 확실하게 말할 수 없을 것 같습니다만... 제가 생각했을 때는 이것 또한 메모리 낭비고, memory leak이라고 생각합니다..! 확실하게 아시는 분은 알려주시면 감사하겠습니다.

class SubViewController {
    let mainVC: MainViewController
    
    init(mainVC: MainViewController) {
        self.mainVC = mainVC
        mainVC.delegate = self
    }
}

그리고, 만약 위 코드처럼 SubViewController에서도 강한 참조로 MainViewController를 가리키고 있다면....?
Retain Cycle(순환 참조)가 발생하게 됩니다.

이렇게요! 지역 변수가 모두 메모리 해제 됐음에도 불구하고, 힙 영역의 인스턴스들은 RC가 1로 유지되기 때문에 메모리 해제가 되지 않고, memory leak이 발생합니다.

해결 방법은 간단하죠. delegate를 weak(약한 참조)로 선언하면 됩니다.

protocol MainViewControllerDelegate: AnyObject {
    func customFunction()
}

class MainViewController {
	weak var delegate: MainViewControllerDelegate?
}

weak로 선언하기 위해서 MainViewControllerDelegate를 Class만 채택할 수 있도록 제한해야합니다.
그래서 AnyObject 프로토콜을 상속받았습니다.

delegate를 weak로 선언하면 subVC가 메모리에서 해제됐을 때, RC가 0이 돼서 메모리에서 해제되게 되고,

delegate에는 자동으로 nil이 할당되겠죠? SubViewController에서 MainViewController를 강하게 참조하고 있었기 때문에 메모리 해제되면서 RC값도 1 감소하게 됩니다.

정리하자면,

Delegate는 클래스 인스턴스를 참조하기 때문에 Retain 됩니다.
서로 다른 객체가 서로를 강하게 참조하는 경우 Retain Cycle이 발생하므로, delegate를 weak로 선언해서 Retain Cycle을 예방할 수 있습니다.

최종 정리

  • Delegate 패턴이란 서로 다른 객체간의 상호작용을 위해서 iOS에서 사용하는 디자인 패턴 중 하나입니다.
  • Delegate 패턴을 사용해서 한 객체(대리자)가 다른 객체를 대신해서 작업을 처리할 수 있습니다.
  • Delegate 패턴을 사용하면 코드의 재사용성과 확장성을 높이고, 유지보수를 용이하게 합니다.
  • Delegate는 클래스 인스턴스를 참조하기 때문에 Retain 됩니다.
  • delegate를 weak로 선언해서 Retain Cycle을 예방합니다.
profile
iOS앱 개발자가 될테야

0개의 댓글