전 글에서도 설명 했듯이, 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를 사용했다.dataTaskPublisher
URLSession.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의 기본적인 사용법과 주요 연산자를 공부해볼 수 있었던 것 같다.