'델리게이트'에 해당하는 글 2건


Swift는 Objective-C와 같은 벤더를 가지고 있기 때문에 Objective-C의 후계자이다. 이것은 비슷하게 생기지도 않고 똑같이 동작하지도 않으며 비슷한 느낌이 들지 않는다. Objective-C에서 (메소드 이름처럼) 잘 동작했던 패러다임은 새로운 Swift 세계로 넘어오기 위해 천천히 바뀌고 있다. 예를들어 아래 메소드는 Objective-C에서 이런식의 긴 형태로 호출되었다.
[string dataUsingEncoding:NSUTF8StringEncoding];

Swift2.2에서는 다소 어색하게 바뀌었고

string.dataUsingEncoding(NSUTF8StringEncoding)

Swift3에서 이 메소드는 비로소 간결해졌다.

string.data(using: .utf8)

Swift3 버전의 메소드가 Swift에게 알맞는 것이고, 같은 의미에서 Objective-C 버전은 Objective-C 버전에 알맞는 것이다. 여러분의 메소드를 어떻게 Swift에 맞춰 사용할 수 있는지 다루는데 이 글이 도움을 줄 것이다.


우리 앱을 만들기 위해 사용하는 것들 중에 Swift하게 바꿔야하는 프레임워크나 언어적인 부분들이 있다. 오늘 나는 델리게이트 네이밍에대해 이야기하려 한다.

Swift에서 델리게이트는 Obejctive-C에서처럼 부드럽게 잘 바뀌지 않는다. Objective-C는 "sender"와 "receiver"에대해 굉장히 친숙하다. 애플의 Objective-C 문서에서는 이 용어를 자주 사용한다. 예를들어 UIResponer에있는 isFirstResponder 메소드 문서를 확인해 보아라.

receiver가 첫 응답자(first responder)인지 나타내는 불리언 값을 반환한다.
- (void)buttonTapped:(UIButton *)sender { }
델리게이트는 비슷한 방식으로 동작한다:: Objective-C에서 델리게이트 메소드의 첫 인자도 항상 sender였다. 이것이 왜 유용할까? 만약 receiver가 같은 타임의 여러 오브젝트 델리게이트라면 그것들을 구분해야할 것이다. 델리게이트는 첫번째 인자를 제공하면서 그것을 구분할 수 있게 해준다.

나는 Backchannel SDK 예제로 몇 클래스 이름을 간단히 해볼 것이다.

여기엔 두가지 타입의 델리게이트 메소드가 있는데, 첫번째는 이벤트가 일어났는지 나타낸다.
- (void)messageFormDidTapCancel:(BAKMessageForm *)messageForm;
Swift로 번역하면 다음과 같다.
func messageFormDidTapCancel(_ messageForm: BAKMessageForm)
이것은 더이상 Swift3에 알맞지 않다. Swift3에서는 불필요한 것(두번 나타난 messageForm)을 제거하고, 언더바를 사용해서 없에는게 아니라 자동으로 첫번째 인자 이름을 따른다.

두번째 타입의 델리게이트 메소드는 이벤트가 일어나고 거기에 몇 데이터를 가지고 있는지 나타낸다. 아래 예제를 보자.
- (void)messageForm:(BAKMessageForm *)messageForm didTapPostWithDraft:(BAKDraft *)draft;
- (void)messageForm:(BAKMessageForm *)messageForm didTapAttachment:(BAKAttachment *)attachment;
Swift3으로 번역하면 다음과 같다.
func messageForm(_ messageForm: BAKMessageForm, didTapPostWithDraft draft: BAKDraft)
func messageForm(_ messageForm: BAKMessageForm, didTapAttachment attachment: BAKAttachment)
흠, 좋지 않아 보인다. 왜 이 메소드를 둘다 messageForm이라 부르나? 또한 명사로 시작하는 메소드는 별로 좋지 않다. 보통 그 타입의 오브젝트를 반환할때 사용한다(NSStringdata(using:)를 생각해보면 Data를 반환한다). 여기서는 아무 메시지 형식 오브젝트도 반환하지 않을 것이다. 그 "메시지 형식"은 사실 첫번째 파라미터의 이름이다. 매우 혼란스러운 메소드 이름이 아닐 수 없다!

이 두가지 타입의 델리게이트 메소드는 그 줄 마지막에 "sender"를 보내고 동사를 앞으로 옮겨서 고칠 수 있다. 먼저 sender가 델리게이트를 알려주는 이벤트는 messageFormDidCancel대신 didTapCancel이다.
func didTapCancel(messageForm: BAKMessageForm)
굉장히 좋아졌다. 액션이 앞으로 오면서 메소드 이름이 되었다. 그리하여 메소드가 무슨 일을 하는지 더 명확해졌다. 내 생각엔 읽을때 좀 더 좋게 하기 위해서 파라미터 이름 대신에 전치사를 써도 괜찮을 것 같다.
func didTapCancel(on messageForm: BAKMessageForm)
이렇게 써보면서 아직까지는 전치사를 사용하기에 어색해보이는 상황을 발견하지 못했다. 그리고 다른 여러 상황에서도 "on", "for", "with", "in" 모두 유용하게 쓰인다는 것도 알아냈다. 사용자가 "on"형식을 탭 하면서 나는 "on"이 이곳에 적합하다고 생각한다.

이제 뒤에 데이터를 전달하는 델리게이트 메소드도 한번 보자. 동사를 앞으로 보내는게 도움이 될 것이고, 델리게이트 이름에 전치사로 바꾸는 것도 이런 메소드 타입을 깔끔하게 해준다. 아래 예제 대신
func messageForm(_ messageForm: BAKMessageForm, didTapPostWithDraft draft: BAKDraft)
좀 더 Swift하게 만들고
func didTapPost(with draft: BAKDraft, on messageForm: BAKMessageForm)
좀 더 바꾼다.
func didTap(attachment: BAKAttachment, on messageForm: BAKMessageForm)

이러한 규칙은 나를 빼고 누구도 보증하진 않지만 우리가 그런식으로 쓰면 현재 규칙보다 더 이해하기 쉬워질거라 생각된다. 더 나아가 아마 내 Swift 델리게이트 메소드를 이런 구조로 짜기 시작할 것이다.

UITableView의 델리게이트와 데이터소스 메소드를 보면서 이것이 나중엔 어떻게 생겼을지 생각해보자.
func numberOfSections(in tableView: UITableView) -> Int
numberOfSections는 이 스킴을 따르고 이미 꽤 좋아보인다.

그러나 아래 메소드는 그렇게 좋아 보이진 않는다.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
여기 좀 더 사랑스러운 방법이다.
func numberOfRows(inSection section: Int, in tableView: UITableView) -> Int
func cellForRow(at indexPath: IndexPath, in tableView: UITableView) -> UITableViewCell
func didSelectRow(at indexPath: IndexPath, in tableView: UITableView)

 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

델리게이트도 괜찮지만, 더 나은 방법이 있다.

당신은 iOS앱을 만들고 있는 개발자라 가정하자. 당신은 당신이 할 수 있는 최선의 견고한(SOLID) 아키텍처로 앱의 구조를 만들어 놓았다. 앱은 모델, 네트워크 레이어, UI 레이어 또는 그것을 돕는 것들로서 구성된다. 이 레이어들 사이에 데이터를 주고 받을때 책에서는 델리게이션을 이용해라고 알려준다. 실제로 iOS 개발에서 일반적으로 사용되는 유용한 패턴이기도 하다.

델리게이션은 간단히 말하자면, 어떤것의 변화로부터 알림받기 원할때 그 어떤것에 알림받기 원하는 대상을 등록해놓는 방식의 패턴이다. 이렇게하면 그 어떤것으로부터 반응(react)할 수 있다. 예를들면 ViewController가 네트워크 서비스에게 말을 걸어서 (ViewController를 네트워크 서비스 델리게이트로 등록해서) 어떤 요청이 완료될때 자신에게 알려달라고 한다. 이때 ViewController를 네트워크 서비스의 델리게이트로 만들어 가능하게 된다. 네트워크 서비스는 요청이 완료되었을 때 델리게이션 메소드를 호출 할 것이다.
델리게이트 참조를 보내는 것은 괜찮은 방법이고 기능적으로도 아무 문제가 없다. 그러나 Swift에서는 더 나은 방법이 있고, 왜 이 방법이 더 나은지 설명해 보겠다.


델리게이션을 위해 콜백을 사용
콜백은 델리게이트 패턴과 비슷한 기능을 가진다. 어떤 일이 발생할때 다른 오브젝트가 알게 해주고, 데이터를 전달하는 기능이다.

델리게이트 패턴과 다른점은, 응답받고 싶은 객체 자체를 넘겨주는 것 대신에 함수만을 넘겨준다. 함수는 Swift에서 클래스의 첫번째 요소이다. 따라서 함수를 프로퍼티로 가지고 있을수도 있다.
MyClass는 이제 myFunction 이라는 프로퍼티를 가지는데, 어딘가에서 호출할 수도 있고, 누구나 값을 바꿀 수도 있다(Swift에서 정의된 규칙에의해 프로퍼티는 디폴트로 internal이 된다). 이것이 델리게이션 대신 콜백을 사용하는 기본 아이디어이다. 아래 예제는 위 예제에서 델리게이트 대신 콜백으로 대체한 것이다:
콜백을 사용하는 다른 상황은 데이터가 바뀔때 알림을 받고 싶을 경우이다. 프로퍼티 옵저버에서 콜백을 호출함으로서 가능하다:
콜백에대한 간단한 노트 : 델리게이트에서는 리테인(retain) 사이클을 멈추기위해 weak 프로퍼티로 만들어야하는 것처럼, 여러분도 클로저 안에서는 self를 weak 변수로 해두어야한다.

그래서 왜 콜백이 더 나은가?
1. 분리됨(Decoupling)
델리게이트는 코드를 분리하는 경향이 있다. 프로토콜로 구현하는 한 NetworkService에게 누가 그것의 델리게이트인지 알 필요가 없다. 그러나 델리게이트가 프로토콜 구현을 가지고 있고, @objc 프로토콜 대신 Swift를 사용한다면, 델리게이트는 프로톸콜에서 모든 메소드 구현을 가진다.(따라서 옵셔널 프로토콜 일치도 필요없다)

다르게 말해보면 콜백을 사용할 때, NetworkService는 메소드를 호출하기위한 델리게이트를 가지고 있을 필요가 없고 누군가가 이 메소드를 구현할 것이라는 것을 알고 있으면 된다. 메소드가 호출되는 시점만 관리하면 되고, 이 메소드가 어떻게 구현되있을지는 알 필요가 없는 것이다.

2. 다중 델리게이션
요청이 끝나고 ViewController에 알림을 주고 싶은데, 그때 로그를 남기는 클래스나 통계를 남기는 클래스를 넣고 싶을 수 있다.

델리게이트로 구현하면, 델리게이트의 배열을 가지고 있어야 할 것이다. 아니면 세개의 서로 다른 프로토콜을 가지는 델리게이트 프로퍼티를 가질 것이다!

그러나 콜백으로 구현한다면 함수의 배열을 선언하고(Swift의 이런 점이 좋다!) 뭔가 처리가 끝날때 각각 호출하면 된다. 따라서 리테인 사이클의 위험을 감수하거나 어마어마한 양의 코드로 작성되거나 하는 수많은 오브젝트와 프로토콜이 필요없어진다.

3. 일을 분리하기에 더 명확하다.
내가 생각하는 델리게이트와 콜백의 차이는 이렇다. 델리게이트는 NetworkService가 델리케이트에게 "이봐, 나 갱신됐어!"라고 말하는 반면 콜백은 델리게이트가 NetworkService를 응시하고 바라보고 있는 느낌이다.

실제로 작은 차이 같지만, 후자의 방법으로 생각하면 NetworkService가 자신의 기능을 하지 못하고 화면 표시의 기능으로 변질되는 그런 것을 방지해주는 패턴으로서는 크게 도움이 될 것이다!

4. 테스트하기 쉬움!
유닛테스트와 함께 구현하면 항상 코드베이스가 두배로 늘어난다고 느낄것이다. 그 이유는 앱의 모든 델리게이트를 포함하여 매 프로토콜마다 목(mock)을 만들어야하기 때문이다.

콜백으로 구현하면 어떠한 델리게이트에 목을 할 필요 없을 뿐만 아니라 각 테스트에서 원하는 어떠한 콜백이든 사용할 수 있게 해준다.

한 테스트에서 콜백이 제대로 호출되는지 호출는지 테스트해보고, 다른 테스트에서 그것이 호출될 때 옳바른 결과를 내는지 테스트 할 수 있다. 그리고 어떠한것도 someFuncDidGetCalled 불리언(boolean)이나 비슷한 프로퍼티를 가진 복잡하게 목(mocked)된 델리게이트를 필요로 하지 않는다.

나는 개인적으로 콜백이 코드와 테스트를 명확하게 만들어주고, 더 Swift스럽게 데이터를 주고 받는 방법이라 생각한다. 여러분이 오늘 뭔가 새로운 것을 배웠기를 바란다! 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,