Swift/스위프트 정리

[Swift] Combine 연산자와 CheckedContinuation

asong 2023. 1. 29. 20:33

combinelatest

func combineLatest<P>(_ other: P) -> Publishers.CombineLatest<Self, P> where P : Publisher, Self.Failure == P.Failure

추가적인 publisher를 구독한다. 그리고 다른 publisher로부터 받는 output을 publish한다.

 

Return 값은 자기 자신과 다른 publisher의 요소들을 받고 결합하는 publisher이다.

 

언제 사용하는가?

다양한 publisher들이 값을 방출할 때 가장 최근 값의 튜플을 받고자할 때 사용한다.

다만, publisher들이 값 하나만을 방출했다면 combined publisher는 값을 방출하지 않는다.

 

다양한 publisher들로부터 요소들을 짝짓고 싶다면 zip(_:)을 사용하면 된다.

튜플보다 최근 값들을 그냥 받고자 한다면 merge(with:)를 사용한다.

 

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()

cancellable = pub1
    .combineLatest(pub2)
    .sink { print("Result: \($0).") }

pub1.send(1)
pub1.send(2)
pub2.send(2)
pub1.send(3)
pub1.send(45)
pub2.send(22)

// Prints:
//    Result: (2, 2).    // pub1 latest = 2, pub2 latest = 2
//    Result: (3, 2).    // pub1 latest = 3, pub2 latest = 2
//    Result: (45, 2).   // pub1 latest = 45, pub2 latest = 2
//    Result: (45, 22).  // pub1 latest = 45, pub2 latest = 22

 

class LoginViewModel: ObservableObject {

    @Published var id: String = ""
    @Published var password: String = ""
    
    var validInfo: AnyPublisher<Bool, Never> {
        return self.$password.combineLatest(self.$id) {
            return $0 == $1
        }.eraseToAnyPublisher()
    }
}

다음과 같이 아이디와 패스워드 검증에 쓰일 수 있다.

combineLatest와 함께 클로저를 사용할 수 있다. 이를 통해 검증 과정을 구현하여 활용할 수 있다.

 

combineLatest가 종료되기 위해서는 모든 publisher가 완료되어야 한다.

결합된 publisher들 중 하나라도 실패로 종료되면 publisher가 실패하게 되고,

계속 값을 publish하지 않으면 publisher는 종료되지 않는다.

 

 

compactMap

func compactMap<T>(_ transform: @escaping (Self.Output) -> T?) -> Publishers.CompactMap<Self, T>

받은 값의 클로저를 호출하고, 값을 가진 옵셔널을 publish한다.

 

Return 값은 공급된 클로저를 호출함에 의한 non-nil 옵셔널 결과이다.

Parameter인 transform은 값을 받고, 옵셔널 값을 리턴하는 클로저이다.

 

Combine의 compactMap(:)은 스위프트의 compactMap(_:)과 유사하다. publisher stream의 nil을 제거하고 non-nil값을 republish한다.

 

let numbers = (0...5)
let romanNumeralDict: [Int : String] =
    [1: "I", 2: "II", 3: "III", 5: "V"]

cancellable = numbers.publisher
    .compactMap { romanNumeralDict[$0] }
    .sink { print("\($0)", terminator: " ") }

// Prints: "I II III V"

딕셔너리에서 nil값을 리턴할 경우, compactMap(_:)은 nil 요소를 필터링한다.

따라서 nil값을 마주할 경우 걸러져 프린트되지 않는다.

 

tryCompactMap은 해당 클로저에서 에러를 던질 수 있는 클로저를 넣을 수 있는 연산자이다.

 

 

Empty, Never

struct Empty<Output, Failure> where Failure : Error

어떤 값도 publish하지 않는 publisher. 그리고 선택적으로 즉시 끝낸다.

 

Never publisher를 만들 수 있다. 이것은 결코 값을 보내지 않고 끝내거나, 실패하지 않는다.

Empty(completeImmediately: false)로 생성할 수 있다.

 

 

CheckedContinuation

struct CheckedContinuation<T, E> where E : Error

 

동기와 비동기 코드 사이의 인터페이스 메카니즘.

 

continuation 은 프로그램 상태의 불투명한 표현이다.

비동기 코드에서 continuation을 만들기 위해서는 withUnsfateFoncinuation(function:_:_) 또는 withUnsafeThrowingContinuation(function:_:_ )함수를 호출해야 한다. 비동기적 테스크를 재개하기 위해서는 resume(returning:), resume(throwing:), resume(with:), resume() 메서드를 호출하면 된다. resume 메서드는 정확히 단 한 번 매 실행에서 호출해야 한다.

 

continuation으로부터 한 번 이상으로 재개하는 것은 정의되지 않은 행동이다. 결코 재개하지 않는 것은 suspended 상태에서 무기한으로 테스크를 남긴다. 그래서 연관된 자원들을 누출시킨다.

CheckedContinuation은 다양한 재개 작동을 위해 런타임 체크를 수행한다.

 

- withCheckedContinuation

현재 테스크를 중지한다. 현재 테스크를 위한 checked continuation과 함께 주어진 클로저를 호출한다.

func withCheckedContinuation<T>(
    function: String = #function,
    _ body: (CheckedContinuation<T, Never>) -> Void
) async -> T

 

- withCheckedThrowingContinuation

현재 테스크를 중지한다. 그리고 현재 테스크를 위한 checked throwing continuation과 함께 주어진 클로저를 호출한다.

func withCheckedThrowingContinuation<T>(
    function: String = #function,
    _ body: (CheckedContinuation<T, Error>) -> Void
) async throws -> T

 

이 친구를 어떤 상황에서 어떻게 쓰게 될지 감이 오지 않을 것이다.

지금부터 더 알아보자!

 

fetchResults(name: "foo") { results in
    if let results = results {
        doSomeWork(with: results)
    } else {
        print("Upda")
    }
}

다음의 평범한 비동기 함수가 있다.

이를 async await을 활용하여 사용하고자 할 때

let results = await fetchResults(name: "foo")

이렇게 쓰고자 할 텐데 이렇게 호출할 수가 없다. 아직 인터페이스가 맞지 않기 때문이다. (async함수가 아니다.)

 

이를 해결하기 위해서 checkedContinuation을 활용하면 되는 것이다.

= 일반적인 completionHandler 메서드를 async 메서드로 활용하고자 할 때 사용한다.

 

func fetchResults(name: String) async -> [String]? {
    await withCheckedContinuation { continuation in
        // 레거시 코드 호출
        fetchResults(name: name) { results in
            continuation.resume(returning: results)
        }
    }
}

// 최종 활용
if let results = await fetchResults(name: "foo") {
    doSomeWork(with: results)
}

withCheckedContinuation 안에서 평범한 비동기 함수를 continuation.resume(returning:) 메서드를 활용하여 리턴값을 만들어내어 이 평범한 비동기 함수를 async 함수로 래핑할 수 있게 되는 것이다.

 

withCheckedThrowingContinuation 함수의 경우에는 예외처리까지 가능하게 하는 함수이다.

 

func fetchResults(name: String) async throws -> [String] {
    try await withCheckedThrowingContinuation {
        continuation in
        // 레거시 코드 호출
        fetchResults(name: name) { results in
            if let res = results {
                continuation.resume(returning: res)
            } else {
                // 예외가 발생한 경우
                continuation.resume(throwing:
                    FetchedResultsError.noResults
                )
            }
        }
    }
}

// 활용
do {
    let results = try await fetchResults(name: "foo")
    doSomeWork(with: results)
} catch FetchedResultsError.noResults {
    print("error handle")
}

async throws 함수를 만들어냈다. 에러를 던질 수 있게끔 구성하였다. 이 때 resume(throwing:)을 활용하는 것을 볼 수 있다.

 

Checked와 Unsafe 함수가 있는데 둘의 차이는 스레드 안정성 보장 여부이다.

Checked는 동시 실행 시에 단일 실행을 보장하는 함수지만,

Unsafe의 경우 동시에 호출하면 병렬로 동시 실행될 가능성이 있다.

스레드 안정성이 생기면 성능 저하가 생길 수 있으니 잘 결정하여 사용하는 것이 좋다.

 

 

참고

https://developer.apple.com/documentation/combine/just/combinelatest(_:)

https://zeddios.tistory.com/1085

https://developer.apple.com/documentation/combine/fail/compactmap(_:)

https://developer.apple.com/documentation/combine/empty

https://developer.apple.com/documentation/swift/checkedcontinuation

https://seorenn.tistory.com/197