- RxSwift와 Combine은 어떻게 대응되나요? 그리고 대응되지 않는 것은 무엇인가요?
- Combine의
Failure가 타입에 박혀 있어서 생기는 실제 불편은? - demand(백프레셔)는 언제 개발자 눈에 보입니까?
assign(to:on:)이 메모리 누수를 만드는 이유는?
먼저 대응표 — 여기까지는 쉽다
Rx를 써봤다면 Combine의 8할은 이름만 바꾸면 됩니다. 두 프레임워크는 같은 아이디어의 다른 구현입니다.
| RxSwift | Combine | 비고 |
|---|---|---|
Observable<E> | Publisher | Combine은 Output·Failure 두 연관타입 |
Observer | Subscriber | |
Disposable / DisposeBag | AnyCancellable / Set<AnyCancellable> | |
PublishSubject | PassthroughSubject | |
BehaviorSubject | CurrentValueSubject | |
observe(on:) | receive(on:) | 다운스트림 실행 위치 |
subscribeOn | subscribe(on:) | 업스트림 구독 위치 |
flatMapLatest | map { }.switchToLatest() | 단일 오퍼레이터 없음 |
Single | Future | |
Driver / Signal | 대응 없음 | 직접 구성해야 함 |
shareReplay | 기본 제공 없음 | multicast로 직접 구성 |
면접에서 대응표만 읊으면 "문서 봤구나"로 끝납니다. 변별력은 대응되지 않는 칸과 아래 세 가지 차이에서 나옵니다.
진짜 차이 ① — Failure가 타입에 박혀 있다 🔥 핵심
Rx에서 에러는 전부 Error입니다. Combine의 Publisher는 Output과 함께
Failure라는 연관타입을 가집니다. 그래서 실패 타입이 다른 퍼블리셔는 그냥 합칠 수 없습니다.
enum AppError: Error { case network(URLError), decoding }
let a = urlPublisher.mapError { AppError.network($0) } // URLError → AppError
let b = localPublisher.setFailureType(to: AppError.self) // Never → AppError
Publishers.CombineLatest(a, b) // 이제 결합 가능
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
.store(in: &cancellables)
Failure == Never는 단순한 표기가 아니라 계약입니다.
"이 스트림은 실패로 끝나지 않는다"를 컴파일러가 보증하므로,
UI 바인딩처럼 실패를 처리할 곳이 없는 자리에서 안전하게 쓸 수 있습니다.
진짜 차이 ② — demand(백프레셔) 🔥 핵심
Rx에는 없는 개념입니다. Combine에서 구독은 3자 관계이고, Subscriber가 "몇 개까지 받겠다"고 요청합니다. 값은 밀려오는 게 아니라 당겨집니다.
.sink가 무제한을 요청하기 때문입니다. 커스텀 Publisher나 Subscriber를 직접 만들면 반드시 마주칩니다."왜 이런 게 있나요?"에 대한 답 — 생산자가 소비자보다 빠를 때 소비자가 감당할 수 있는 만큼만 당겨가게 하기 위해서입니다. 센서 데이터나 대용량 파일 스트림처럼 값이 무한히 쏟아지는 상황에서 메모리를 지켜줍니다.
진짜 차이 ③ — share()에 리플레이가 없다
Rx의 share(replay:)에 익숙하면 반드시 걸리는 함정입니다.
Combine의 share()는 멀티캐스트만 하고 값을 저장하지 않습니다.
// 최신 1개를 늦은 구독자에게도 주려면 직접 구성한다
let shared = upstream
.multicast(subject: CurrentValueSubject<Output, Failure>(initial))
.autoconnect()
메모리 누수 함정 🔥 핵심
// ❌ self → cancellables → 클로저 → self 로 고리가 닫힌다
publisher.sink { self.update($0) }.store(in: &cancellables)
// ✅ 고리를 끊는다
publisher.sink { [weak self] in self?.update($0) }.store(in: &cancellables)
// ❌ assign(to:on:)은 대상(on:)을 강하게 잡는다
publisher.assign(to: \.title, on: self).store(in: &cancellables)
// ✅ @Published에 직접 연결 — 내부적으로 순환을 만들지 않는다
publisher.assign(to: &$title)
assign(to:on:)이 특히 위험한 이유는 클로저가 없어서 캡처 리스트를 쓸 자리도 없기 때문입니다.
[weak self]를 붙일 곳이 없으니, assign(to: &$prop) 형태를 쓰거나
sink로 바꿔야 합니다.
Concurrency와 섞어 쓰기 🧩 롱테일
"Combine과 Concurrency 중 뭘 쓰시겠어요?"에 대한 좋은 답은 "용도가 다릅니다"로 시작하는 것입니다. Concurrency 심화는 Swift Concurrency 면접 꼬리질문 10선에서 이어집니다.