A protocol representing the connection of a subscriber to a publisher.
구독자와 퍼블리셔의 연결을 나타내는 프로토콜입니다.
오~
Subscription이란?
Subscription은 Publisher와 Subscriber 사이의 연결을 관리하는 프로토콜입니다. 퍼블리셔가 데이터를 발행하고, 서브스크라이버가 이를 수신하는 과정에서 두 객체 간의 관계를 중재하고 제어하는 역할을 합니다.
Subscription은 단순히 데이터를 전달하는것을 넘어 데이터를 요청하고, 전달하고, 구독을 취소하는 메커니즘을 제공하여 메모리 관리와 리소스 효율성을 보장합니다.
Subscription은 Cancellable을 채택했네요.
Cancellable은 나중에 다시 살펴보고!
Subscription에는 특정 Subscriber가 Publisher를 subscribe 할 때 정의되는 ID가 있어서 Class로만 정의해야 한다. 또한 Subscription을 cancel 하는 작업은 스레드로부터 안전해야 한다고 하며 cancel은 한 번만 할 수 있다고 한다. Subscription을 cancel 하면 Subscriber를 연결해서 할당된 모든 리소스도 해제된다고 한다.
음~?
좀더 생각해봐요
Subscription 프로토콜 뜯어먹기
Subscription은 Combine 프레임워크에서 프로토콜로 정의되어 있습니다. 주요 메서드는 다음과 같습니다:
public protocol Subscription: Cancellable {
func request(_ demand: Subscribers.Demand)
}
주요 메서드 설명
- request(_:): 서브스크라이버가 퍼블리셔에게 데이터를 요청할 때 사용합니다. Subscribers.Demand는 서브스크라이버가 얼마나 많은 데이터를 요청할지를 결정합니다.
- cancel(): 구독을 취소하여 퍼블리셔와의 연결을 끊습니다. 이 메서드는 Cancellable 프로토콜에서 상속됩니다.
→ 이거는 cancellable 할때 자세히~
왜쓰는데ㅋㅋ
- 데이터 흐름 제어: Subscription을 통해 서브스크라이버가 퍼블리셔에게 얼마나 많은 데이터를 요청할지 결정할 수 있습니다. 이를 통해 과도한 데이터 스트림으로 인한 리소스 낭비를 방지할 수 있습니다.
- 구독 관리: 서브스크라이버는 필요에 따라 Subscription을 통해 언제든지 구독을 취소할 수 있습니다. 이를 통해 비동기 작업이 끝났거나 더 이상 필요하지 않은 경우 리소스를 해제할 수 있습니다.
- 수명 관리: 퍼블리셔와 서브스크라이버 간의 연결을 관리하여 메모리 누수를 방지하고, 시스템 성능을 최적화합니다.
Subscription의 작동 원리
Subscription은 퍼블리셔와 서브스크라이버 간에 다음과 같은 단계로 작동합니다:
- 퍼블리셔가 서브스크라이버와 연결합니다.
- 서브스크라이버는 데이터를 요청합니다(request(_:)).
- 퍼블리셔는 요청된 양만큼의 데이터를 발행하고, 이를 서브스크라이버에 전달합니다.
- 필요 시 서브스크라이버는 구독을 취소할 수 있습니다(cancel()).
이 흐름을 통해 Subscription은 퍼블리셔와 서브스크라이버 간의 데이터 스트림을 효율적으로 제어합니다.
조금 더 정확히는 publisher와 subscriber 사이에 subscription이 생성되고 나면 이것을 매개체로 해서 이후의 동작들이 일어난다고 보면
Subscription의 생명주기
Subscription의 생명주기는 크게 4단계로 나눌 수 있으며, 각 단계는 Publisher와 Subscriber 간의 상호작용을 정의합니다.
1. 구독 시작 (Subscription Establishment)
let publisher = ["Hello", "World"].publisher
let subscriber = CustomSubscriber()
publisher.subscribe(subscriber)
이 단계에서는:
- Publisher가 receive(subscriber:) 메서드를 통해 Subscriber를 받습니다.
- Publisher는 새로운 Subscription 인스턴스를 생성합니다.
- subscriber.receive(subscription:)을 호출하여 Subscription을 Subscriber에게 전달합니다.
2. 데이터 요청 (Demand Request)
func receive(subscription: Subscription) {
subscription.request(.max(2)) // 최대 2개의 값을 요청
}
이 단계에서는:
- Subscriber가 request(_:) 메서드를 통해 처리하고 싶은 데이터의 양을 지정합니다.
- Subscribers.Demand를 사용하여 요청할 항목 수를 지정합니다.
- .unlimited, .max(n), .none 등의 옵션을 사용할 수 있습니다.
3. 데이터 전송 (Data Transmission)
func receive(_ input: String) -> Subscribers.Demand {
print("받은 데이터: \\\\(input)")
return .none // 추가 데이터 요청 없음
// 또는 return .max(1) // 추가로 1개 더 요청
}
이 단계에서는:
- Publisher가 요청받은 데이터를 Subscriber에게 전송합니다.
- Subscriber는 각 값을 받을 때마다 추가 demand를 요청할 수 있습니다.
- 데이터 전송은 요청된 demand를 초과하지 않습니다.
4. 구독 종료 (Subscription Termination)
구독은 정상적인 완료 / 오류 발생 / 명시적인 취소 세가지로 나누어집니다!
a. 정상 완료
func receive(completion: Subscribers.Completion<Failure>) {
switch completion {
case .finished:
print("모든 데이터 수신 완료")
case .failure(let error):
print("에러 발생: \\\\(error)")
}
}
b. 오류 발생
publisher
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
// 에러 처리
}
},
receiveValue: { value in
// 값 처리
}
)
c. 명시적 취소
subscription.cancel()
// 또는
cancellables.removeAll()
생명주기 디버깅
Combine은 구독 생명주기의 각 단계를 추적하는 print() 도 있다네요!
let subscription = publisher
.print("Lifecycle")
.sink(
receiveCompletion: { print("Completed: \\\\($0)") },
receiveValue: { print("Received: \\\\($0)") }
)
출력 예시:
Lifecycle: receive subscription: (PublisherSequence)
Lifecycle: request unlimited
Lifecycle: receive value: (1)
Lifecycle: receive value: (2)
Lifecycle: receive finished
Subscription의 예제 코드
import Combine
// 1. Publisher 정의
final class NumberPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S: Subscriber>(subscriber: S) where S.Input == Int, S.Failure == Never {
let subscription = NumberSubscription(subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
// 2. Subscription 정의
final class NumberSubscription<S: Subscriber>: Subscription where S.Input == Int, S.Failure == Never {
private var subscriber: S?
private var currentNumber = 0
init(subscriber: S) {
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) {
// 요청받은 만큼 숫자 전달
currentNumber += 1
_ = subscriber?.receive(currentNumber)
}
func cancel() {
subscriber = nil
}
}
// 3. 사용
let publisher = NumberPublisher()
let cancellable = publisher.sink { number in
print("받은 숫자:", number)
}
코드 설명
- Publisher (발행자)
- Output과 Failure 타입 정의
- 구독자가 구독을 시작하면 해당 구독자를 위한 Subscription 생성
- Subscription (구독)
- Publisher와 Subscriber 사이의 연결 관리
- request 메서드에서 실제 데이터 전달
- cancel 메서드에서 구독 정리
- 사용
- sink를 통해 간단하게 구독 설정
- 숫자를 받을 때마다 출력
실행하면 "받은 숫자: 1" 이 출력됩니다.
결론
sink나 assign을 사용하자
오늘의 질문
왜 Subscription 의 구현은 calss에서만 될까요?
-
- Subscription은 Publisher와 Subscriber 간의 고유한 연결을 나타냅니다
- 이 연결은 참조로 유지되어야 합니다 (메모리상의 동일한 객체를 가리켜야 함)
- struct는 값 타입이라 복사되므로 동일한 연결을 유지할 수 없습니다
- Subscription의 cancel은 어떤 스레드에서든 호출될 수 있습니다
- 여러 스레드에서 동시에 cancel을 호출할 수 있습니다
- 데이터 경쟁 상태를 피하기 위해 동기화가 필요합니다
- 리소스 해제는 한 번만 일어나야 합니다
- 중복 해제는 크래시나 메모리 문제를 일으킬 수 있습니다
- 한 번 취소된 구독은 다시 활성화할 수 없습니다
- 메모리 누수 방지
- 불필요한 작업 중단
- 시스템 리소스 반환
'IOS - Swift > Combine' 카테고리의 다른 글
[iOS/Combine] Combine - Error Handling (0) | 2024.12.12 |
---|---|
[iOS/Combine] Transforming Operators (4) | 2024.12.05 |
Publisher - Just, Empty, Failure (1) | 2024.10.03 |
Combine - Publisher 기초 (2) | 2024.10.01 |
[iOS/Swift] Combine - 시작하기 (Publisher ,Operator, Subscriber) (1) | 2024.04.17 |