[ swift ] Combine 비동기 프로그래밍 정리해보자

sonny·2024년 12월 28일
0

TIL

목록 보기
85/133

전 글에서도 설명 했듯이, Combine은 Apple이 Swift 생태계에 도입한 강력한 프레임워크로 비동기 프로그래밍 및 데이터 스트림 처리를 손쉽게 할 수 있도록 도와준다고 했다.

자세히 알아보자.


Combine이란?

Combine은 Swift 언어의 비동기 작업 처리와 이벤트 기반 프로그래밍을 위해 설계된 프레임워크다.

Combine의 주요 목표는 데이터 스트림을 선언적으로 표현하고, 이를 처리하거나 변환하는 작업을 간단하게 수행할 수 있도록 지원하는 것이다.

Combine을 사용하면 다음과 같은 작업이 편해진다.

  1. 비동기 작업 처리
  2. 데이터 스트림의 변환
  3. 데이터 흐름의 연결
  4. 에러 처리

Combine은 PublisherSubscriber라는 두 가지 주요 구성 요소로 이루어지는데,

  • Publisher: 데이터를 방출하며, 이벤트를 스트림 형태로 전달한다.
  • Subscriber: Publisher로부터 데이터를 받아 처리한다.

이 두 구성 요소 사이에 다양한 연산자를 추가해서 데이터 변환, 필터링, 병합 등의 작업을 수행할 수 있다.


Combine의 주요 개념

1. Publisher

Publisher는 데이터를 방출하는 역할을 하는데, 네트워크 요청, 알림(Notification), 사용자 입력 등 다양한 데이터 소스가 Publisher가 될 수 있다.

Publisher는 아래 두 가지 이벤트를 방출한다.

  • 데이터 이벤트: 실제 데이터 값을 전달함
  • 완료 또는 실패 이벤트: 작업이 성공적으로 완료되었거나 에러가 발생했음을 나타냄

2. Subscriber

Subscriber는 Publisher로부터 전달된 데이터를 구독해서 처리 해준다. Combine에서는 sink 연산자를 사용해 Subscriber를 생성한다.

3. Operators

Combine은 데이터 스트림을 조작하기 위한 다양한 연산자를 제공한다.

주요 연산자는,

  • map: 데이터를 변환함.
  • filter: 특정 조건에 따라 데이터를 필터링 함.
  • combineLatest: 여러 Publisher의 최신 값을 결합 함.
  • decode: 데이터를 디코딩하여 Swift의 모델 객체로 변환 함.
  • receive(on:): 데이터를 특정 스레드에서 처리하도록 함.

Combine을 활용한 네트워크 요청 예제

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()

(1) .map(\.data)

  • Publisher로부터 응답((data, response)) 중에서 데이터(data) 부분만 추출한다.
  • KeyPath(\.data)를 사용하여 간결하게 접근한다. ( 이 부분은 지피티가 알려줬는데 \를 사용한 걸 처음봐서 생소했다. )

(2) .decode(type: [Post].self, decoder: JSONDecoder())

  • JSON 데이터를 디코딩해서 [Post] 타입의 객체 배열로 변환한다.
  • 그리고 JSONDecoder를 사용해 JSON 데이터를 Swift 모델에 맞게 디코딩해주는데 디코딩 실패 시에는 에러를 방출하게 된다.

(3) .receive(on: DispatchQueue.main)

  • 데이터를 받을 때 사용하는 스레드를 지정하는 것인데, UI와 관련된 작업은 반드시 메인 스레드에서 실행되어야 하기 때문에 이 연산자를 사용한다.

(4) .eraseToAnyPublisher()

  • 연산의 결과를 AnyPublisher로 변환한다.
    • 그렇게 구체적인 Publisher 타입을 숨기고, 추상화된 AnyPublisher로 반환하여 유연성을 높인다고 한다.

전체 동작 과정 요약하자면,

  1. URL을 기반으로 네트워크 요청을 시작한다.

  2. 요청 결과에서 데이터(data)를 추출한다.

  3. JSON 데이터를 [Post] 모델로 디코딩한다.

  4. 변환된 데이터를 메인 스레드에서 받을 수 있도록 설정한다.

  5. 결과를 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 객체를 생성하는데, 이 객체는 네트워크 요청과 관련된 기능을 제공하고,
    Combine의 구독을 관리할 cancellables 프로퍼티를 포함하고 있다.

service.fetchPosts()

  • fetchPosts() 메서드를 호출하여 네트워크 요청을 시작한다.
  • 이 메서드는 AnyPublisher<[Post], Error>를 반환하고 데이터를 방출하거나 에러를 방출하게 된다.

sink 구독 연산자

    .sink(
        receiveCompletion: { completion in ... },
        receiveValue: { posts in ... }
    )
  • sink이란 : Combine의 Subscriber를 생성하는 연산자다.
    Publisher가 방출하는 데이터와 완료 이벤트를 처리할 수 있다.
  • 또한 이 코드는 Publisher를 구독하고, 데이터 또는 완료/실패 이벤트를 처리하는 클로저를 제공해준다.

receiveCompletion 클로저

  • Publisher가 완료 또는 실패 이벤트를 방출했을 때 실행된다.
  • completionSubscribers.Completion 타입이며, 두 가지 케이스를 갖는다:
    • .finished: 네트워크 요청이 성공적으로 완료되었음을 나타낸다.
    • .failure(let error): 요청 중 에러가 발생했음을 나타낸다.
receiveCompletion: { completion in
    switch completion {
    case .finished:
        print("네트워크 요청 성공")
    case .failure(let error):
        print("에러 발생: \(error.localizedDescription)")
    }
}
  • 성공 시 "네트워크 요청 성공"을 출력하고, 실패 시 에러 메시지를 출력한다.

receiveValue 클로저

  • Publisher가 데이터를 방출했을 때 실행되는데, 여기서는 네트워크 요청 결과로 받아온 [Post] 데이터를 처리한다.
receiveValue: { posts in
    print("받은 게시글:", posts)
}
  • 방출된 게시글 배열 posts를 출력한다.

store(in:) 메서드

.store(in: &service.cancellables)
  • Combine의 구독을 Set<AnyCancellable>에 저장한다.
  • service.cancellablesNetworkService 객체가 메모리에서 해제될 때 구독도 자동으로 취소되도록 보장한다.
  • 이 메서드를 사용하면 명시적으로 구독을 취소하지 않아도 안전하게 메모리를 관리할 수 있다.

&의 역할

&는 Swift에서 in-out parameter를 나타낸다고 한다. 처음본다.

  • 일반적인 in-out parameter: 함수가 전달받은 값을 직접 수정할 수 있도록 허용한다.
  • Combine에서의 활용: store(in:) 메서드에서 Combine의 구독(Cancellable) 객체를 지정된 컬렉션에 추가하기 위해 사용된다.

데이터 구독 및 처리코드의 전체 동작 과정

  1. fetchPosts()를 호출하여 게시글 데이터를 가져오는 네트워크 요청을 시작한다.

  2. sink를 통해 Publisher의 데이터를 구독하고, 방출된 데이터를 receiveValue 클로저에서 처리한다.

  3. 네트워크 요청이 성공적으로 완료되면 receiveCompletion에서 .finished를 처리하고, 에러가 발생하면 .failure를 처리한다.

  4. 구독이 service.cancellables에 저장되어 네트워크 요청이 종료된 후에도 메모리 누수를 방지한다.

여기서 주요 개념은

  1. Publisher와 Subscriber: fetchPosts()는 Publisher를 반환하고, sink는 Subscriber를 생성하여 이걸 구독한다.
  2. 비동기 처리: 네트워크 요청이 완료되면 데이터가 방출되고 이를 sink에서 처리한다.
  3. 메모리 관리: store(in:)를 통해 구독을 안전하게 관리한다.

Combine 사용 시 주의점

  1. 메모리 관리: Combine의 cancellable 객체를 통해 구독을 관리하지 않으면 메모리 누수가 발생할 수 있다. 이걸 방지하기 위해 Set<AnyCancellable>을 활용한다.
  2. 스레드 관리: UI 업데이트는 반드시 메인 스레드에서 이루어져야 한다. 그러기 위해 receive(on:) 연산자를 사용한다.
  3. 에러 처리: 에러 처리를 위해 sinkreceiveCompletion 블록에서 적절한 로직을 작성해야 하는 점이 필요하다.

음...

Combine은 비동기 작업을 선언적이고 효율적으로 처리할 수 있다.

특히 네트워크 요청과 같은 비동기 작업을 간단하게 구현할 수 있었다보니 코드 가독성과 유지보수성을 크게 향상시켜준 듯 한데

그래도 위 코드를 통해 Combine의 기본적인 사용법과 주요 연산자를 공부해볼 수 있었던 것 같다.

profile
iOS 좋아. swift 좋아.

0개의 댓글

관련 채용 정보