[RxSwift] DelegateProxy

정유진·2022년 8월 26일
1

concurrency

목록 보기
1/1
post-thumbnail

👏 들어가며

RxSwift를 언제 사용하면 좋을까? 나는 Data를 수신하거나 발신하는 (또는 모두 해내는) 객체를 곳곳에 심어두고 데이터를 수신한 순간을 포착해 특정 액션이 일어나도록 프로그래밍하고 싶을 때 사용한다. 웹에서 비슷한 표현을 찾자면 event를 emit하여 데이터를 전달하고 그 데이터를 props으로 받는 행위라고 생각한다. (vue의 용어를 빌려왔다.)

다시 Swift로 돌아와서 생각해보자. 이와 같은 동작을 First party로만 구현한다면 delegate pattern과 notification을 사용할 것 같은데 (Combine을 쓰는 것을 개인적으로 좋아하지만) 이미 delegate 패턴을 사용하여 구현이 되어 있는 앱을 RxSwift로 전환해야 한다면 어떻게 하면 좋을까? 이와같은 고민을 떠안은 사람들을 위해 delegate pattern을 재사용할 수 있는 방법을 RxSwift가 제안하고 있다.

🌉 DelegateProxy

original code

delegate pattern이 적용되어 있는 sample code에 RxSwift를 적용해보자.
먼저 Rx를 적용하기 전의 original code를 아래와 같이 적어보았다. 기능은 중요하지 않아 생략하였다.

@objc 함수를 사용한 이유?

  • optional로 작성하기 위해
  • rx에서 delegate의 메서드를 호출할 때 selector를 사용하기 때문에

🧐 성능 저하의 우려?

https://stackoverflow.com/questions/48246124/would-it-have-any-bad-influence-to-add-objc-to-a-swift-method-or-variable

우리 회사 코드는 objective-c runtime에서 클래스나 메서드를 생성, 호출하기 때문에 이미 objc 함수로 작성이 되어있어서 rx를 연결할 때에 따로 어노테이션을 붙일 필요가 없었지만 보통 delegate의 메서드가 objc 함수로 작성되어 있지는 않기에 'delegate를 작성할 때부터 objc 어노테이션을 붙여서 설계해야하는지?' '그렇게 되면 성능이 저하되지는 않는지?' 에 대한 궁금함이 있었는데 선임님께 물어본 결과, 처음에는 이를 고려하지 않고 delegate를 작성하더라도 후에 어노테이션을 붙이는게 어려운 일은 아니니까 상관없고 (내가 바보같은 질문을 한 것 같다.) 애초에 RxSwift를 쓸 것이면 delegate 패턴을 채택 안 하는게 맞지 않느냐 라는 답변을 들었다. 맞는 말씀입니다.


@objc protocol EngineProtocol {
    @objc optional func create()
    @objc optional func start()
    @objc optional func stop()
}

public class Engine {
    var delegate: EngineProtocol?
    
    func setDelegate (_delegate: EngineProtocol) {
        delegate = _delegate
    }
    
    func trigger() {
        delegate?.create!()
        delegate?.start!()
    }
    
    func pullup() {
        delegate?.stop!()
    }
}

EngineProtocol을 따르는 delegate가 존재하고 Engine 클래스는 구현을 viewController에 위임한다.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let myEngine = Engine()
        myEngine.delegate = self
    }
}

extension ViewController: EngineProtocol {
    func create() {
        print("Engine is created")
    }
    
    func start() {
        print("Engine is started")
    }
    
    func stop() {
        print("Engine is stopped")
    }
    
}

1) Delegate Proxy 클래스 생성하기 🍒

proxy 클래스의 구현 내용은 이미 정해져 있기 때문에 크게 까다롭지 않다.

  • 상속 받아야 할 객체 3가지
    • DelegateProxy<delegate를 가진 클래스 이름, delegate 이름>
    • DelegateProxyType
    • Delegate 이름
  • 구현해야 하는 메서드 3가지
    • registerknownImplementations : 델리게이트를 가진 클래스를 RxDelegateProxy 클래스로 등록한다.
    • currentDelegate
    • setCurrentDelegate
import Foundation
import RxSwift
import RxCocoa
import UIKit

class RxEngineDelegateProxy: DelegateProxy<Engine, EngineProtocol>, DelegateProxyType, EngineProtocol{
    static func registerKnownImplementations() {
        self.register { engine -> RxEngineDelegateProxy in
            RxEngineDelegateProxy(parentObject: engine, delegateProxy: self)
        }
    }
    
    static func currentDelegate(for object: Engine) -> EngineProtocol? {
        return object.delegate
    }
    
    static func setCurrentDelegate(_ delegate: EngineProtocol?, to object: Engine) {
        object.delegate = delegate
    }
}

2) Extension Reactive에서 Observable 생성 🌽

delegate 연결에서 가장 핵심이라고 생각하는 구현 부분이다. Rxswift를 사용하다보면 extension Reactive where Base: SomeClass 를 자주 사용하게 되는데 SomeClass.rx 와 같은 네임스페이스를 생성하기 위해서 작성한다.

나는 주로 프로퍼티의 타입으로 Binder를 사용해왔다. 그러면 SomeObservable.bind(to:UIViewController.rx.내가만든Binder프로퍼티) 와 같이 데이터를 전송하고 closure를 실행할 수 있어서 viewModel과 UI단을 연결하는 용도로 유용하게 써왔다.

아래의 예시 샘플은 Observable을 만들고 UIViewController에서 이 Observable을 subscribe 하는 구조로 만들고자 Observable type을 사용했다. Binder가 데이터 수신용 통로를 만든 것이라면 이 Observable들은 데이터 발신용 통로이다. 그렇다면 데이터 sequence는 누가 만들까? 바로 delegate.sentMessge 또는 delegate.methodInvoked가 담당한다.

  • sentMessage: delegate method의 invocation 직전에 observable sequence를 return 한다.
  • methodInvoked: delegate method의 invocation 직후에 observable sequence를 return 한다.
extension Reactive where Base: Engine {
    var delegate: RxEngineDelegateProxy {
        return RxEngineDelegateProxy.proxy(for: self.base)
    }
    
    var onCreate: Observable<Bool> {
        return delegate.sentMessage(#selector(EngineProtocol.create))
            .debug("engineDelegate-create Engine")
            .map {_ in return true }
    }
    
    var onStart: Observable<String> {
        return delegate.sentMessage(#selector(EngineProtocol.start))
            .debug("engineDelegate-will start Engine")
            .map {_ in return "engine will starts"}
    }
    
    var onStop: Observable<Int> {
        return delegate.methodInvoked(#selector(EngineProtocol.stop))
            .debug("engineDelegate-did stop Engine")
            .map {_ in return 1}
    }
}

Engine 클래스의 delegate를 이곳에서 DelegateProxy로 initialize 하고 있어 original code에서 setDelegate(:EngineProtocol) 하던 과정이 필요없게 되었다. 그리고 기존의 viewController가 protocol을 채택하여 콜백 내용을 구현했던 것과 달리 DelegateProxy의 property를 bind(onNext:) 해서 이벤트 결과로서 데이터 처리 내용을 클로저 안에 작성하면 된다. 따라서 수정된 ViewController는 아래와 같다.

class ViewController: UIViewController {
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        let myEngine = Engine()
        myEngine.rx.onCreate.asObservable().bind(onNext: { message in
            print("Engine is created")
        })
        .disposed(by: disposeBag)
        
        myEngine.rx.onStart.bind(onNext: {message in
            print(message)
        })
        .disposed(by: disposeBag)
        
        myEngine.rx.onStop.bind(onNext: { data in
            print(data)
        })
        .disposed(by: disposeBag)
    }
}

Observable을 구독하는 방법으로는 크게 세가지를 꼽을 수 있다.

  • subscribe(onNext:onError:onCompleted:): error 컨트롤이 가능하다.
  • bind(onNext:)
  • drive(onNext:): MainThread에서 동작하고 Driver 타입을 구독한다.

🐳 정리하며

역시 RxSwift의 장점을 살리기 위해선 delegate pattern과 혼재하여 사용하기 보다는 MVVM 패턴과 함께 Rx로 시작부터 갈아엎는게 훨씬 낫겠다는 생각이 든다. 물론 Rx 자체를 대체할 수 있는 수단이 지금은 아주 많지만 무시할 수 없는 것이, 잊고 있다가도 어김없이 다시 만나게 되기 때문이다.이미 Rx를 쓰고 있는 회사들도 많고 유지 보수를 한다치면 앞으로도 한동안은 만나게 되겠지. 이번 기회에 다시 짚어볼 수 있어서 좋았다. 틈틈이 다른 개념들도 정리해볼 생각이다.

profile
느려도 한 걸음 씩 끝까지

0개의 댓글