[Combine] Subscribe(구독) 의 의미 (Pub-Sub 패턴)
목차)
1. Pub-Sub 패턴
2. Subscribe(구독)의 의미
3. 주로 쓰이는 구독자 생성 방식 - sink()
1. Pub-Sub 패턴
Combine은 Pub-Sub 패턴을 따른다.
구독의 의미를 이해하기 위해 먼저 Pub-Sub 패턴에 대해 간단하게 알아보고 시작하자.
· Publish(발행) - Subscribe(구독) 패턴
· 메시지기반 아키텍쳐 패턴 중 하나
· Publisher(발행자)와 Subscriber(구독자)간 메시지 수신 및 처리
· 구독자는 원하는 주제(Subject)에 대한 구독을 등록하여 메시지를 받음
· 주제(Subject)는 발행자(Publisher)가 발행하는 데이터의 종류 또는 주제를 나타낸다.
2. Subscribe(구독)의 의미
구독의 주요 목적은 데이터의 흐름을 확립하여 데이터를 처리하고 변환하는 작업을 하기 위함이다.
아래 예시 코드를 보자.
func PubSub(){
// 발행자(Publisher) - "숫자[Int]" 주제
let numbersPublisher = [1, 2, 3, 4, 5].publisher
// 구독자1(Subscriber)
let subscriber_1 = Subscribers.Sink<Int, Never>(
receiveCompletion: { completion in
print("subscriber_1 Received completion\n")
},
receiveValue: { value in
print("Received value:", value)
}
)
// 구독자2(Subscriber)
let subscriber_2 = Subscribers.Sink<Int, Never>(
receiveCompletion: { completion in
print("subscriber_2 Received completion:", completion)
},
receiveValue: { value in
print("Received value:", value + 5)
}
)
// 구독자가 발행자의 숫자(Int) 주제(Subject)를 구독
// 주제가 발행될 시 구독자에게 데이터 전달
numbersPublisher.subscribe(subscriber_1)
numbersPublisher.subscribe(subscriber_2)
}
Publisher(발행자) 와 Subscriber(구독자)를 나누어 코드를 작성해보았다.
numbersPublisher 는 [1,2,3,4,5]의 [Int] 타입을 주제로 하는 Publisher(발행자) 이다.
Int타입 의 주제를 구독하기 위한 구독자(Subscriber)가 subscriber_1 과 subscriber_2 로 정의되어 있다.
subscriber_2 는 발행받은 값을 +5 해주려고 한다.
numbersPublisher.subscribe(subscriber_1) 은 subscriber_1 이 발행자 numbersPublisher를 구독하였다.
numbersPublisher.subscribe(subscriber_2) 은 subscriber_2 가 발행자 numbersPublisher를 구독하였다.
아래는 실행 결과에 대한 영상이다.
위의 코드와 실행영상을 통해 구독의 의미를 정의:
관심있는 주제(Subject)를 가진 Publisher(발행자)를 구독(Subscribe)한다
위의 의미를 예시코드에 대입하여 설명:
subscriber_1 과 subscriber_2 는 관심있는 주제 Int 를 가진 numbersPublisher(발행자)를 구독했다.
numbersPublisher(발행자)가 주제에 대한 데이터를 발행시 subscriber_1 과 subscriber_2 는 데이터를 받아 처리하였다.
3. 주로 쓰이는 구독자 생성 방식 - sink()
두번째 목차에서는 Subscriber(구독자)를 변수에 할당하여 관심있는 주제를 가진 발행자를 구독하도록 하였다.
이번에는 대표적으로 자주 쓰이는 combine의 sink() 라는 메서드를 간단하게 소개하려고 한다.
Combine 을 사용한다면 보통 SwiftUI 를 적용하여 개발하고 있을것 이라고 생각한다.
그렇다면 현재 자주 쓰이고 있는 MVVM 디자인 패턴을 적용한다는 가정하에,
view 는 SwiftUI로 그리고 ViewModel은 ObservableObject Protocol을 준수하는 class로 만들것이다.
아래와 같은 ViewModel를 생성하였다.
final class ViewModel:ObservableObject{
private var cancellable: AnyCancellable?
@Published var userName:String? = nil
func getUser(){
cancellable = API.shared.getUser()
.sink{ [weak self] user in
self?.userName = user.userName
}
}
}
Struct User:Codable{
var id:Int
var userName:String
}
[코드 설명]
1. API.shared.getUser() 는 서버와의 통신을 통해 User 구조체를 발행해주는 Publisher 이다.
2. userName은 초기값으로 nil 이며, getUser() 를 통해 userName에 값을 할당해줄 것이다.
발행자와 주제는 정의되어 있고 해당 주제에 관심있는 구독자를 만들어 userName에 값을 할당해 주어야 하는데 예시 코드를 보면 구독자가 정의되어 있지 않다.
구독자를 생성한다는 것은 재사용할 수 있는 상황이라면 좋은 전략일 수 있지만,
그렇지 않은 상태에서 발행자와 구독자를 항상 1:1로 만들어 준다면 오히려 코드량이 늘어나 좋지 못할것으로 예상된다.
서버와의 통신을 통해 데이터를 발행받는 상황은 매우 빈번할 것이다.
API는 보통 하나의 목적을 가지며, DTO를 전달해 준다.
그렇다면 발행되는 DTO마다 구독자를 만들 확률이 높을 것이며 위에서 말한 것 처럼 비효율적일 것이다.
이럴때 Combine 의 sink() 메서드를 활용한다.
sink() 는 구독자(Subscriber)를 생성하고, 발행자(Publisher)에서 값을 받아 처리하는 역할을 하는 메서드 이다.
Subscribers.Sink 구조체를 초기화하여 구독자를 생성하고 두개의 클로저를 인자로 받는다.
발행자가 Publisher Protocol을 준수한다면 사용할 수 있다.
sink() 는 발행이 되면 구독자를 생성하여 발행된 값을 처리 할 수 있도록 해주는 메서드 이다.