개요
애플의 iOS 앱 개발에서는 delegate 패턴을 정말 많이 사용한다.
가장 처음에 보이는 파일인 AppDelegate.swift와 SceneDelegate.swift부터 각종 UI컴포넌트를 구현하기 위해 사용하는 Delegate들 까지, Delegate 패턴이 굉장히 많이 사용되는 것을 알 수 있다.▼
이렇게나 많이 사용되지만 이에 대해서 깊게 알아보려고 하지 않았다.
처음에는 내용물을 몰라도 사용할 수 있게 해주는 캡슐화 덕분인건지, 아니면 그냥 내가 아무렇게나 사용을 해서 그런건지는 몰라도 Delegate에 대해 많은 정보가 없이도 간단한 것들은 만들 수 있었다.
하지만 규모가 조금씩 커지고 기능들이 많아지게 되면서 이에 대한 이해도가 부족하면 개발을 제대로 할 수 없음을 깨달았고, 아래의 궁금증들이 생기게 되었다.
Delegate 패턴은 왜 이런 이름이 붙었을까?
Delegate 패턴은 어떤식으로 구현이 될까?
Delegate 패턴을 사용하면 어떤 이점이 있을까?
이 궁금증들을 하나씩 해결해보자.
Delegate 패턴이란?
큰 흐름
Delegate의 뜻은 위임, 대리자라는 뜻으로 조금 더 쉽게 풀면 대신해주는 것 정도로 볼 수 있다.
위의 뜻대로 Delegater, 즉 위임자는 다른 객체의 기능을 받아 대신 수행해준다.▼
예시
이렇게만 이야기하면 조금 와닿지 않을 것이라고 생각이 들기에 흐름을 쉽게 보여주는 예시를 가져와봤다.
1. 프로토콜
고라니가 청부살인 사무소를 차렸다고 하자. ▼
암살을 프로토콜로 표현하면 아래와 같이 된다.▼
protocol Assassination : AnyObject {
func killTarget()
func cleanTheScene()
func getOutSafely()
}
청부살인 사무소에서 암살자들은
killTarget() : 대상을 제거하고,
cleanTheScene() : 현장 청소를 하고,
getOutSafely() : 현장을 안전하게 빠져나오는 일을 한다.
2. 위임하는 객체
위의 일들을 대장 암살자가 다른 암살자들에게 시킨다고 해보자.▼
대장 암살자가 시키는 일들은 아래와 같다.
class BossAssassin {
weak var delegate : Assassination?
func assassinateTarget() {
self.delegate?.killTarget()
self.delegate?.cleanTheScene()
self.delegate?.getOutSafely()
}
}
weak var delegate에 Assassination을 배정함으로써 Assassination일을 맡을 수 있게 된다.
그리고 assassinateTarget() 함수를 통해 일을 수행한다.▼
3. 위임받는 객체
이제 암살 의뢰를 위임할 차례다.
아래 두 명에게 암살 의뢰를 위임했는데, 한 암살자는 AceAssassin이고, 다른 암살자는 DumbAssassin이다.▼
class AceAssassin : Assassination {
init (bossAssassin : BossAssassin) {
bossAssassin.delegate = self
}
func killTarget() {
print("killed target successfully")
}
func cleanTheScene() {
print("cleaned all the blood and flesh")
}
func getOutSafely() {
print("got out the scene safely")
}
}
class DumbAssassin : Assassination {
init (bossAssassin : BossAssassin) {
bossAssassin.delegate = self
}
func killTarget() {
print("killing target failed")
}
func cleanTheScene() {
print("there is nothing to clean")
}
func getOutSafely() {
print("got caught")
}
}
우선 생성자를 통해 `assassin` 객체의 delegate에 각자의 참조를 넣어준다.
`AceAssassin`과 `DumbAssassin` 모두 `Assassination` 프로토콜을 채택하고 있으므로 `assassin.delegate`에 알맞은 타입으로 들어갈 수 있다.
이렇게 되면 `AceAssassin`과 `DumbAssassin`에 `Assassin` 객체가 위임이 된다.
이후 두 암살자는 프로토콜로 제시된 `Assassination`을 받아 `killTarget`, `cleanTheScene`, `getOutSafely`를 정의하고, 정의된 함수들을 바탕으로 `Assassin`의 `assassinateTarget`을 수행하게 한다.
우선 위임하는 사람으로 `nog`를 만들고, `nog`의 일을 `AceAssassin`과 `DumbAssassin`에게 배정한 뒤 `assassinateTarget`을 수행하면 아래와 결과가 같다.▼
let nog = BossAssassin()
let gorani = AceAssassin(bossAssassin: nog)
nog.assassinateTarget()
let alpaca = DumbAssassin(bossAssassin: nog)
nog.assassinateTarget()
nog는 같은 함수인 `assassinateTarget()`을 실행시켰지만, 그 일을 수행하는 객체는 `AceAssassin`과 `DumbAssassin`이므로 각자의 결과가 달라진다.▼
위의 구조를 그림으로 보면 아래와 같은 식이 된다.▼
상속을 써도 되는거 아닌가?
위의 delegate 패턴의 흐름을 보면 상속의 오버라이딩과 굉장히 비슷한 형태인 것을 볼 수 있다.
심지어는 아래와 같이 만들어도 기능 구현이 똑같이 된다.▼
class Assassin {
func killTarget() {
print("")
}
func cleanTheScene() {
print("")
}
func getOutSafely() {
print("")
}
}
class AceAssassin : Assassin {
override func killTarget() {
print("killed target successfully")
}
override func cleanTheScene() {
print("cleaned all the blood and flesh")
}
override func getOutSafely() {
print("got out the scene safely")
}
}
그럼에도 Delegate 패턴을 사용하는 이유는 완전한 캡슐화를 위해서이다.
상속을 하게 된다면 부모 클래스의 구현이 자식 클래스에게 드러나게 된다.
특정 기능을 다른 객체에게 맡기기 위해서 부모 클래스에서는 해당 메서드를 private로 설정할 수 없기 때문이다.
위의 코드에서 Assassin의 활동은 private해야하기에 아래와 같이 고쳤다고 해보자.
class Assassin {
private func killTarget() {
print("")
}
private func cleanTheScene() {
print("")
}
private func getOutSafely() {
print("")
}
}
class AceAssassin : Assassin {
func killTarget() {
print("killed target successfully")
}
func cleanTheScene() {
print("cleaned all the blood and flesh")
}
func getOutSafely() {
print("got out the scene safely")
}
}
이렇게 되면 상속은 가능하나, 암살을 실제로 수행하는 AceAssassin이 일을 수행할 수 없게 된다.
Assassin의 메소드가 모두 private이기 때문이다.▼
그렇다고 public 메소드로 해놓게 되면 캡슐화를 파괴하게 된다.▼
이렇게 캡슐화가 파괴되고 의존성이 증가하게 되면, 부모 클래스(Assassin)의 구현 변화에 따라 자식 클래스(AceAssassin)도 영향을 받게 된다.
Delegate 패턴은 이런 의존성을 최대한 줄여주고, 자식 클래스가 부모 클래스에 대해서 알 수 없게 하여 캡슐화를 잘 지키게 해준다.
Delegate 패턴의 의의
Boilerplate code(보일러플레이트 코드) 제거
코드를 짜다보면 변화없이 여러 군데에서 반복되는 코드인 보일러플레이트 코드들이 생길 때가 많다.
반복되는 부분을 인터페이스로 바꾸고, 변화가 필요한 부분들을 위임하여 정의하면 보일러플레이트 코드들을 최대한 많이 제거할 수 있다.
Polymorphism(다형성)
위의 보일러플레이트 코드를 제거하는 방식은 다형성을 유지하는 좋은 방식이라고 생각이 든다.
다형성은 보통 하나의 부모 클래스를 통해 여러 타입의 객체를 만들 수 있는 것을 의미하며, 여러 객체들 중에서 공통적인 특성을 하나의 타입으로 추상화 시킨 후 상속시키는 것을 말한다.
위의 Delegate 패턴은 이를 잘 수행할 수 있는 방식이라고 생각이 드는게, 원래대로라면 객체별로 보일러플레이트 코드들이 생겼을 것이다.
하지만 Delegate 패턴을 이용하면 공통적인 특성을 인터페이스화 시킨 후, 이를 위임 시켜 각 객체에 맞게 새롭게 구현할 수 있게 된다.
이런 의미에서 하나의 인터페이스에 여러 개의 구현이라는 특성을 잘 지키는 방식이라고 생각된다.
One-Point 관리
다형성을 지킬 수 있게 되면서 관리할 지점을 한 곳으로 줄일 수 있게 된다.
하나의 인터페이스에 여러 개의 구현이라는 특성에서 여러 개의 구현은 그렇게 중요한 것이 아니다.
가장 중요한 것은 public하게 보여지는 인터페이스이기에 인터페이스에 one-point를 두어 관리하기 쉬워진다.
+) 일반적인 정석으로는 인터페이스는 바뀌면 안된다는 특성이 있지만, 여기서 말하는 관리는 인터페이스의 private한 메소드들을 말한다.
마치며
Delegate 패턴에 대해서 조금 아는게 생기고 나니, Swift 코드를 짤 때에 조금 더 많은 것이 보이게 된 거 같다.
애초에 iOS는 delegate 패턴을 많은 곳에서 사용하기 때문에 필수로 알아둬야하는 것 같다.
그동안 제대로 숙지하지 않고 한 게 참 신기하면서도 대책이 없었다는 생각이 든다.
Delegate 패턴에 대해서 정리를 하면서 다른 개념들에 대해서도 많이 모르는 것이 느껴져 부족한 부분들을 메꿔야겠다는 생각이 들었다.
'Develop > iOS' 카테고리의 다른 글
[iOS][Swift] 클로져 캡쳐에 대한 이해 (0) | 2024.02.11 |
---|---|
[iOS][Swift] Two Phase Initialization (0) | 2024.02.09 |
[iOS][Swift] Swift의 메모리 관리 : ARC 1 (0) | 2024.02.09 |
[iOS][UI] Auto Layout에 대해 (0) | 2024.01.23 |
[iOS][Swift] MetaType이란 (0) | 2024.01.23 |