dataTaskPublisher() 는 URLSession 에 속해있는 컴바인 연산자로 네트워크 응답을 Publisher 를 통해 비동기적으로 처리할 수 있다. dataTaskPublisher(for:) 메소드는 URL 요청을 받아서 실행하고, 그 결과를 Publisher 형태로 반환합니다. 반환된 Publisher는 다음과 같은 튜플 형태의 출력을 내보낸다 -> (data, response)
- data: 서버로부터 받은 데이터를 담고 있는 Data 객체
- response: 요청에 대한 응답 메타데이터를 포함하는 URLResponse 객체
컴바인에서 Publishers는 구조체로 정의되어 값으로 전달되며, 각각의 복사본이 독립적으로 작업을 수행한다. 여기서 share() 연산자를 사용하면 이 행동이 변경되어 여러 구독자가 동일한 작업 결과를 공유할 수 있게 된다. share()는 Publisher를 참조 형태로 관리하게 하여 여러 구독자가 동일한 Publisher 인스턴스를 공유하도록 한다. 이는 네트워크 요청 같은 비용이 많이 드는 작업을 중복해서 수행하지 않도록 도와준다.
이때 주의할 점은 share() 를 사용할 경우 Publisher를 Lazy로 선언해야 한다! lazy로 선언된 Publisher와 함께 사용될 때, share()는 첫 구독 시점에만 Publisher를 활성화하고, 이후 모든 구독자에게 동일한 데이터 스트림을 제공한다! lazy로 선언하지않고 그냥 Publisher를 선언할 경우 각 인스턴스가 서로다른 Publisher를 참조하게된다!!
백문이불여일견이다. 개념설명이 끝났으니 예제코드를 통해 살펴보자
import Combine
import Foundation
protocol BoxOfficeRepository {
var boxOfficeDataPublisher: AnyPublisher<BoxOfficeDTO, NetworkError> { get }
}
class BoxOfficeRepositoryImpl: BoxOfficeRepository {
private let urlString = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=f5eef3421c602c6cb7ea224104795888&targetDt=20240101"
lazy var boxOfficeDataPublisher: AnyPublisher<BoxOfficeDTO, NetworkError> = {
guard let url = URL(string: urlString) else {
fatalError("Invalid URL")
}
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: BoxOfficeDTO.self, decoder: JSONDecoder())
.mapError { error -> NetworkError in
switch error {
case is URLError:
return .urlError
case is DecodingError:
return .decodingError
default:
return .sessionFailed(error: error)
}
}
.share()
.eraseToAnyPublisher()
}()
}
class BoxOfficeViewModel {
private let repository: BoxOfficeRepository
private var cancellables: Set<AnyCancellable> = []
init(repository: BoxOfficeRepository) {
self.repository = repository
}
func loadData() {
repository.boxOfficeDataPublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed successfully")
case .failure(let error):
print("Error occurred: \(error.localizedDescription)")
}
}, receiveValue: { boxOfficeDTO in
print("Box Office Type: \(boxOfficeDTO.boxOfficeResult.boxofficeType)")
print("Show Range: \(boxOfficeDTO.boxOfficeResult.showRange)")
for movie in boxOfficeDTO.boxOfficeResult.dailyBoxOfficeList {
print("Rank: \(movie.rank) | Movie Name: \(movie.movieName) | Sales Amount: \(movie.salesAmount)")
}
})
.store(in: &cancellables)
}
}
class BoxOfficeViewModel2 {
private let repository: BoxOfficeRepository
private var cancellables: Set<AnyCancellable> = []
init(repository: BoxOfficeRepository) {
self.repository = repository
}
func loadData() {
repository.boxOfficeDataPublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("ViewModel2: Completed successfully")
case .failure(let error):
print("ViewModel2: Error occurred: \(error.localizedDescription)")
}
}, receiveValue: { boxOfficeDTO in
print("ViewModel2: Box Office Type: \(boxOfficeDTO.boxOfficeResult.boxofficeType)")
print("ViewModel2: Show Range: \(boxOfficeDTO.boxOfficeResult.showRange)")
for movie in boxOfficeDTO.boxOfficeResult.dailyBoxOfficeList {
print("ViewModel2: Rank: \(movie.rank) | Movie Name: \(movie.movieName) | Sales Amount: \(movie.salesAmount)")
}
})
.store(in: &cancellables)
}
}
// Usage
let repository = BoxOfficeRepositoryImpl()
let viewModel1 = BoxOfficeViewModel(repository: repository)
let viewModel2 = BoxOfficeViewModel(repository: repository)
viewModel1.loadData()
viewModel2.loadData()
(예제 코드에서 DTO, Custom Error 객체는 생략했다)
전체적인 구조는 프레젠테이션 레이어의 뷰모델 객체가 데이터 레이어의 레포지토리 객체를 가져다쓴다. 이때 느슨한 관계를 위해 Protocol 인터페이스를 두었다. 이때 메서드로 두지 않고 읽기 전용으로 둔 것은 참조를 유지해야 publisher 공유가 되기 때문이다.
share 연산자와 같이 subscriber들이 publisher를 공유하는 연산자로 multicast() 연산자가 있다. 두 연산자 모두 동일한 데이터 스트림을 여러 구독자에게 전달할 수 있지만 차이점은 multicast는 Subject를 받아서 이벤트 트리거와 방출을 모두 할 수 있기 때문에 소비자 측에서 공유 시작 시점을 정해줄 수 있다.
share 와 multicast의 차이를 3가지로 정리할 수 있다.
- 제어의 수준: multicast는 connect()를 통해 발행을 명시적으로 시작해야 하므로 더 많은 제어가 가능하다. 반면, share는 자동으로 구독과 발행이 시작되므로 제어가 덜 필요하다.
- 사용의 용이성: share는 구현이 간단하며, 직접 Subject를 관리할 필요가 없다. multicast는 사용자가 Subject를 설정하고 관리해야 하므로 복잡도가 높아진다.
- 성능과 자원 관리: multicast는 connect() 호출 전까지는 아무런 자원도 사용하지 않으며, 사용자가 자원 사용을 최적화할 수 있다. share는 자동으로 관리되기 때문에, 특정 경우에는 비효율적일 수 있다.
import Combine
// 이벤트를 발행할 subject 생성
let subject = PassthroughSubject<Int, Never>()
// multicast로 새로운 PassthroughSubject를 만듦
let multicasted = subject
.multicast(subject: PassthroughSubject<Int, Never>())
// 구독자들을 추가
let subscriber1 = multicasted.sink(
receiveCompletion: { print("Subscriber 1 completed: \($0)") },
receiveValue: { print("Subscriber 1 received: \($0)") }
)
let subscriber2 = multicasted.sink(
receiveCompletion: { print("Subscriber 2 completed: \($0)") },
receiveValue: { print("Subscriber 2 received: \($0)") }
)
// multicast 연결 시작
let connection = multicasted.connect()
// 이벤트 발행
subject.send(1)
subject.send(2)
subject.send(3)
// 이벤트 스트림 완료
subject.send(completion: .finished)
// 구독 취소
connection.cancel()
subscriber1.cancel()
subscriber2.cancel()
위 코드처럼 connect() 호출 을 통해 이벤트 스트림을 실제로 구독자에게 전달하도록 이벤트 구독 활성화 시점을 정해줄 수 있다.
https://www.kodeco.com/21773708-intermediate-combine/lessons/1
'Flutter' 카테고리의 다른 글
| Combine | 11. Mapping Errors (0) | 2024.05.14 |
---|---|
| Combine | 10. Managing Backpressure (0) | 2024.05.14 |
| Combine | 8. Sequencing Operators (0) | 2024.05.13 |
| Combine | 7. Scheduling Operators (0) | 2024.05.13 |
Swift에서 CustomStringConvertible 프로토콜 활용하기 (0) | 2024.05.13 |