전 글에서도 설명 했듯이, Combine은 Apple이 Swift 생태계에 도입한 강력한 프레임워크로 비동기 프로그래밍 및 데이터 스트림 처리를 손쉽게 할 수 있도록 도와준다고 했다.
자세히 알아보자.
Combine은 Swift 언어의 비동기 작업 처리와 이벤트 기반 프로그래밍을 위해 설계된 프레임워크다.
Combine의 주요 목표는 데이터 스트림을 선언적으로 표현하고, 이를 처리하거나 변환하는 작업을 간단하게 수행할 수 있도록 지원하는 것이다.
Combine을 사용하면 다음과 같은 작업이 편해진다.
Combine은 Publisher와 Subscriber라는 두 가지 주요 구성 요소로 이루어지는데,
이 두 구성 요소 사이에 다양한 연산자를 추가해서 데이터 변환, 필터링, 병합 등의 작업을 수행할 수 있다.
Publisher는 데이터를 방출하는 역할을 하는데, 네트워크 요청, 알림(Notification), 사용자 입력 등 다양한 데이터 소스가 Publisher가 될 수 있다.
Publisher는 아래 두 가지 이벤트를 방출한다.
Subscriber는 Publisher로부터 전달된 데이터를 구독해서 처리 해준다. Combine에서는 sink 연산자를 사용해 Subscriber를 생성한다.
Combine은 데이터 스트림을 조작하기 위한 다양한 연산자를 제공한다.
주요 연산자는,
map: 데이터를 변환함.filter: 특정 조건에 따라 데이터를 필터링 함.combineLatest: 여러 Publisher의 최신 값을 결합 함.decode: 데이터를 디코딩하여 Swift의 모델 객체로 변환 함.receive(on:): 데이터를 특정 스레드에서 처리하도록 함.Combine을 통해 실제 네트워크 요청을 처리하는 간단한 예제를 한번 보자.
아래 예제에서는 https://jsonplaceholder.typicode.com/posts에서 게시글 데이터를 가져오는 과정을 구현해봤다.
먼저 JSON 데이터를 Swift 모델로 변환하기 위해 Codable 프로토콜을 준수하는 구조체를 정의해준다.
struct Post: Codable {
let id: Int
let title: String
let body: String
}
네트워크 요청을 처리하는 클래스를 작성하는데, 이 클래스는 Combine의 dataTaskPublisher를 활용하해 네트워크 데이터를 가져온다.
import Foundation
import Combine
class NetworkService {
private var cancellables = Set<AnyCancellable>()
func fetchPosts() -> AnyPublisher<[Post], Error> {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\ .data)
.decode(type: [Post].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
이 코드는 Combine을 활용하여 네트워크 요청을 수행하고 JSON 데이터를 처리하는 NetworkService 클래스를 정의한 것인데,
class NetworkService {
private var cancellables = Set<AnyCancellable>()
}
NetworkService: 네트워크 작업을 수행하는 역할을 하는 클래스다.cancellables: Combine의 구독(Subscription)을 저장하는 컬렉션인데,Set<AnyCancellable>에 저장되면 객체가 메모리에서 해제될 때 구독도 자동으로 취소된다.fetchPosts 메서드func fetchPosts() -> AnyPublisher<[Post], Error> {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
}
fetchPosts: 게시글 데이터를 가져오는 비동기 작업을 수행한다.
반환값: AnyPublisher<[Post], Error>
[Post]: 게시글 배열을 방출하고,Error: 네트워크 요청 중 발생할 수 있는 에러를 방출한다.URL(string:): 네트워크 요청을 보낼 URL을 생성하게 된다.
https://jsonplaceholder.typicode.com/posts라는 임시 API를 사용했다.dataTaskPublisherURLSession.shared.dataTaskPublisher(for: url)
Combine에서 URLSession의 dataTaskPublisher를 사용해 네트워크 요청을 수행한다.
이 연산자는 Publisher를 반환하고 네트워크 요청의 결과(데이터 및 응답)를 방출한다.
성공 시에 데이터와 응답((data: Data, response: URLResponse) 형태)을 방출하고, 실패 시 에러를 방출한다.
.map(\ .data)
.decode(type: [Post].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
.map(\.data)Publisher로부터 응답((data, response)) 중에서 데이터(data) 부분만 추출한다.KeyPath(\.data)를 사용하여 간결하게 접근한다. ( 이 부분은 지피티가 알려줬는데 \를 사용한 걸 처음봐서 생소했다. ).decode(type: [Post].self, decoder: JSONDecoder())[Post] 타입의 객체 배열로 변환한다.JSONDecoder를 사용해 JSON 데이터를 Swift 모델에 맞게 디코딩해주는데 디코딩 실패 시에는 에러를 방출하게 된다..receive(on: DispatchQueue.main).eraseToAnyPublisher()AnyPublisher로 변환한다.AnyPublisher로 반환하여 유연성을 높인다고 한다.URL을 기반으로 네트워크 요청을 시작한다.
요청 결과에서 데이터(data)를 추출한다.
JSON 데이터를 [Post] 모델로 디코딩한다.
변환된 데이터를 메인 스레드에서 받을 수 있도록 설정한다.
결과를 AnyPublisher로 반환하여 호출자가 구독할 수 있도록 한다.
cancellables: 이 컬렉션은 Set<AnyCancellable>로, Combine의 구독을 저장하고 구독이 cancellables에 저장됨으로써 NetworkService 객체가 해제되면 모든 구독도 함께 취소된다. 이렇게 하면 메모리 누수 방지와 안전한 리소스 관리를 가능하게 한다고 한다.작성한 네트워크 서비스를 활용하여 데이터를 구독하고 처리한다
let service = NetworkService()
service.fetchPosts()
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("네트워크 요청 성공")
case .failure(let error):
print("에러 발생: \(error.localizedDescription)")
}
},
receiveValue: { posts in
print("받은 게시글:", posts)
}
)
.store(in: &service.cancellables)
이 코드는 NetworkService 클래스의 fetchPosts 메서드를 호출하여 네트워크 요청을 수행하고,
Combine을 활용해 데이터를 처리하고 구독(Subscription)을 관리하는 부분이다.
let service = NetworkService()NetworkService 객체를 생성하는데, 이 객체는 네트워크 요청과 관련된 기능을 제공하고,cancellables 프로퍼티를 포함하고 있다.service.fetchPosts()fetchPosts() 메서드를 호출하여 네트워크 요청을 시작한다.AnyPublisher<[Post], Error>를 반환하고 데이터를 방출하거나 에러를 방출하게 된다.sink 구독 연산자 .sink(
receiveCompletion: { completion in ... },
receiveValue: { posts in ... }
)
sink이란 : Combine의 Subscriber를 생성하는 연산자다.receiveCompletion 클로저completion은 Subscribers.Completion 타입이며, 두 가지 케이스를 갖는다:.finished: 네트워크 요청이 성공적으로 완료되었음을 나타낸다..failure(let error): 요청 중 에러가 발생했음을 나타낸다.receiveCompletion: { completion in
switch completion {
case .finished:
print("네트워크 요청 성공")
case .failure(let error):
print("에러 발생: \(error.localizedDescription)")
}
}
receiveValue 클로저[Post] 데이터를 처리한다.receiveValue: { posts in
print("받은 게시글:", posts)
}
posts를 출력한다.store(in:) 메서드.store(in: &service.cancellables)
Set<AnyCancellable>에 저장한다.service.cancellables는 NetworkService 객체가 메모리에서 해제될 때 구독도 자동으로 취소되도록 보장한다.&는 Swift에서 in-out parameter를 나타낸다고 한다. 처음본다.
fetchPosts()를 호출하여 게시글 데이터를 가져오는 네트워크 요청을 시작한다.
sink를 통해 Publisher의 데이터를 구독하고, 방출된 데이터를 receiveValue 클로저에서 처리한다.
네트워크 요청이 성공적으로 완료되면 receiveCompletion에서 .finished를 처리하고, 에러가 발생하면 .failure를 처리한다.
구독이 service.cancellables에 저장되어 네트워크 요청이 종료된 후에도 메모리 누수를 방지한다.
fetchPosts()는 Publisher를 반환하고, sink는 Subscriber를 생성하여 이걸 구독한다.sink에서 처리한다.store(in:)를 통해 구독을 안전하게 관리한다.cancellable 객체를 통해 구독을 관리하지 않으면 메모리 누수가 발생할 수 있다. 이걸 방지하기 위해 Set<AnyCancellable>을 활용한다.receive(on:) 연산자를 사용한다.sink의 receiveCompletion 블록에서 적절한 로직을 작성해야 하는 점이 필요하다.Combine은 비동기 작업을 선언적이고 효율적으로 처리할 수 있다.
특히 네트워크 요청과 같은 비동기 작업을 간단하게 구현할 수 있었다보니 코드 가독성과 유지보수성을 크게 향상시켜준 듯 한데
그래도 위 코드를 통해 Combine의 기본적인 사용법과 주요 연산자를 공부해볼 수 있었던 것 같다.