Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combine 프레임워크에서 에러 처리는 어떻게 하나요? #24

Open
hamfan524 opened this issue Apr 22, 2024 · 5 comments
Open
Assignees

Comments

@hamfan524
Copy link
Member

  • 에러 이벤트를 처리하기 위한 Operator에는 어떤 것들이 있나요?
  • 에러 이벤트 발생 시 Subscription을 자동으로 취소하는 방법은 무엇인가요?
  • Combine과 Result 타입을 함께 사용하여 에러 처리를 하는 방법을 설명해주세요.
@hamfan524 hamfan524 self-assigned this Apr 22, 2024
@hamfan524
Copy link
Member Author

hamfan524 commented Apr 23, 2024

Combine 프레임워크에서 에러 처리는 어떻게 하나요?

에러를 처리하는 Publisher와 Operator들을 알아보겠습니다.

Handling Errors에 분류된 Publisher들은 아래와 같습니다.

  • AssertNoFailure

    • Publisher가 실패하지 않는 것을 보장하는데 사용됩니다.
    • 만약 Publisher가 오류를 내보내면, 런타임에 fatalError가 발생하여 앱이 중단됩니다.
import SwiftUI
import Combine

struct ContentView: View {
    @State private var data: String?
    private var cancellables = Set<AnyCancellable>()

    var body: some View {
        VStack {
            if let data = data {
                Text("Data: \(data)")
            } else {
                ProgressView()
            }
        }
        .onAppear {
            fetchData()
                .assertNoFailure() // Publisher가 실패하지 않음을 보장
                .sink(receiveCompletion: { completion in
                    switch completion {
                    case .finished:
                        print("데이터 패치 완료")
                    }
                }, receiveValue: { receivedData in
                    self.data = receivedData
                })
                .store(in: &self.cancellables)
        }
    }

    private func fetchData() -> AnyPublisher<String, Error> {
        // 비동기로 데이터 패치
        return Just("데이터 패치 성공")
            .delay(for: .seconds(2), scheduler: DispatchQueue.main) 
            .setFailureType(to: Error.self)  // 오류 유형을 Error로 설정
            .eraseToAnyPublisher()
    }
}
  1. fetchData 함수를 통해 데이터를 가져오고, assertNoFailure 연산자를 사용하여 데이터를 성공적으로 가져오는 것을 보장합니다.
  2. 만약 fetchData 함수에서 오류가 발생하면, fetalError가 발생합니다.
  3. 데이터가 정상적으로 가져와진 경우에는 receiveValue 블록이 호출됩니다.
  • Catch

    • Publisher가 실패할 때 대체 Publisher를 제공하거나 특정 값을 반환할 수 있어,
      앱이 예기치 않게 중단되는 것을 방지할 수 있습니다.
import SwiftUI
import Combine

struct ContentView: View {
    @State private var data: String?
    private var cancellables = Set<AnyCancellable>()

    var body: some View {
        VStack {
            if let data = data {
                Text("Data: \(data)")
            } else {
                ProgressView()
            }
        }
        .onAppear {
            fetchData()
                .catch { error in
                    Just("데이터 패치 실패") // 대체 값을 반환
                }
                .sink(receiveValue: { receivedData in
                    self.data = receivedData
                })
                .store(in: &self.cancellables)
        }
    }

    private func fetchData() -> AnyPublisher<String, Error> {
        // 실패하는 비동기 데이터 가져오기 시뮬레이션
        return Future<String, Error> { promise in
            DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
                promise(.failure(MyError.dataFetchingError)) // 오류 반환
            }
        }
        .eraseToAnyPublisher()
    }
}

enum MyError: Error {
    case dataFetchingError
}
  1. 위 코드에서는 데이터를 가져오는 도중 실패하는 코드입니다.
  2. Catch 연산자를 통해 실패한 경우 대체 값을 반환합니다.
  • TryCatch

    • 비동기 작업 중에 발생한 오류를 catch하고 대체 Publisher를 제공할 수 있습니다.
import SwiftUI
import Combine

struct ContentView: View {
    @State private var data: String?
    private var cancellables = Set<AnyCancellable>()

    var body: some View {
        VStack {
            if let data = data {
                Text("Data: \(data)")
            } else {
                ProgressView()
            }
        }
        .onAppear {
            fetchData()
                .tryCatch { error -> AnyPublisher<String, Error> in
                    // 오류가 발생한 경우 대체 값을 반환
                    return Just("데이터 패치 실패").setFailureType(to: Error.self).eraseToAnyPublisher()
                }
                .sink(receiveValue: { receivedData in
                    self.data = receivedData
                })
                .store(in: &self.cancellables)
        }
    }

    private func fetchData() -> AnyPublisher<String, Error> {
        // 실패하는 비동기 데이터 가져오기 시뮬레이션
        return Future<String, Error> { promise in
            DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
                promise(.failure(MyError.dataFetchingError)) // 오류 반환
            }
        }
        .eraseToAnyPublisher()
    }
}

enum MyError: Error {
    case dataFetchingError
}
  1. 위 Catch 예시와 같은 데이터 패치에 실패하는 코드입니다.
  2. TryCatch 연산자를 사용하여 fetchData 함수에서 발생한 오류를 catch하여 대체값을 반환합니다.
  • Retry

    • Publisher가 실패할 때 지정된 횟수만큼 다시 시도할 수 있습니다.
    • 네트워크 요청이나 기타 비동기 작업에서 발생하는 임시적인 문제를 처리할 수 있습니다.
import SwiftUI
import Combine

struct ContentView: View {
    @State private var data: String?
    private var cancellables = Set<AnyCancellable>()

    var body: some View {
        VStack {
            if let data = data {
                Text("Data: \(data)")
            } else {
                ProgressView()
            }
        }
        .onAppear {
            fetchData()
                .retry(3) // 최대 3번 재시도
                .sink(receiveCompletion: { completion in
                    switch completion {
                    case .finished:
                        print("데이터 패치 완료")
                    case .failure(let error):
                        print("데이터 패치 실패: \(error.localizedDescription)")
                    }
                }, receiveValue: { receivedData in
                    self.data = receivedData
                })
                .store(in: &self.cancellables)
        }
    }

    private func fetchData() -> AnyPublisher<String, Error> {
        // 실패하는 비동기 데이터 가져오기 시뮬레이션
        return Future<String, Error> { promise in
            DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
                if Bool.random() {
                    promise(.success("데이터 패치 성공"))
                } else {
                    promise(.failure(MyError.dataFetchingError)) // 오류 반환
                }
            }
        }
        .eraseToAnyPublisher()
    }
}

enum MyError: Error {
    case dataFetchingError
}
  1. 랜덤으로 데이터 패치를 성공하는 코드입니다.
  2. Retry 연산자를 사용하여 fetchData 함수에서 발생한 오류를 처리하고, 최대 3번까지 다시 시도합니다.

에러 이벤트를 처리하기 위한 Operator에는 어떤 것들이 있나요?

  • assertNoFailure(_:file:line:)

    • 특정 파일과 라인에서 Publisher가 실패하지 않는 것을 확인하는 데 사용됩니다.
    • 만약 실패한다면, 런타임에 fatalError가 발생하여 앱이 중단됩니다.
import Combine

let publisher = Just("Hello, World!")

publisher
    .assertNoFailure("Publisher 실패", file: "Example.swift", line: 20) // 실패를 확인하는 메시지 및 파일, 라인 지정
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Publisher 처리 완료")
        case .failure(let error):
            print("Publisher 실패: \(error.localizedDescription)")
        }
    }, receiveValue: { value in
        print("Received value: \(value)")
    })
  1. assertNoFailure(_:file:line:) 메서드를 사용하여 실패 여부를 확인합니다.
  2. 위 코드에서 file: "Example.swift", line: 20은 실패 메시지가 발생할 때 해당 메시지가 "Example.swift" 파일의 20번째 라인에서 발생했음을 나타냅니다.
  3. Publisher가 성공적으로 완료되지 않거나 실패할 경우, 지정된 메시지와 함께 fatalError가 발생합니다.
  • catch(_:)

    • Publisher가 실패했을 때 발생하는 오류를 catch하고 처리할 수 있습니다.
    • 대체 값을 발행하거나 다른 Publisher로 대체할 수 있습니다.
import Combine

let publisher = PassthroughSubject<String, Error>()

publisher
    .catch { error in
        // 오류가 발생한 경우 대체 값을 발행
        return Just("데이터 발행 실패").setFailureType(to: Error.self)
    }
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Publisher 처리 완료")
        case .failure(let error):
            print("Publisher 실패: \(error.localizedDescription)")
        }
    }, receiveValue: { receivedData in
        print("Received value: \(receivedData)")
    })

// Publisher가 오류를 내보냄
publisher.send(completion: .failure(MyError.publishingError))

enum MyError: Error {
    case publishingError
}
  1. catch(_:) 연산자를 사용하여 Publisher가 발생한 오류를 catch하고, 대체 값을 발행하도록 처리합니다.
  • tryCatch(_:)

    • 비동기 작업을 수행하는 동안 발생한 오류를 catch하고, 대체 Publisher를 제공할 수 있습니다.
import Combine

func fetchData() throws -> Future<String, Error> {
    return Future<String, Error> { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            if Bool.random() {
                promise(.success("데이터 패치 성공"))
            } else {
                promise(.failure(MyError.dataFetchingError)) // 오류 반환
            }
        }
    }
}

let publisher = tryCatch(fetchData)

publisher
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Publisher 처리 완료")
        case .failure(let error):
            print("Publisher 실패: \(error.localizedDescription)")
        }
    }, receiveValue: { receivedData in
        print("Received value: \(receivedData)")
    })

enum MyError: Error {
    case dataFetchingError
}
  1. 랜덤으로 데이터 패치를 성공하는 함수입니다.

  2. tryCatch(_:) 연산자를 사용하여 fetchData 함수에서 오류가 발생할 시 catch하고, 대체 값을 반환합니다.

  • retry(_:)

    • Publisher가 실패할 경우 지정된 횟수만큼 다시 시도할 수 있습니다.
    • 네트워크 요청이나 기타 비동기 작업에서 발생하는 임시적인 문제를 처리할 수 있습니다.
import Combine

let publisher = PassthroughSubject<String, Error>()

publisher
    .retry(3) // 최대 3번 재시도
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Publisher 처리 완료")
        case .failure(let error):
            print("Publisher 실패: \(error.localizedDescription)")
        }
    }, receiveValue: { receivedData in
        print("Received value: \(receivedData)")
    })

// 실패하고 재시도되는 Publisher
publisher.send(completion: .failure(MyError.publishingError))
publisher.send(completion: .failure(MyError.publishingError))
publisher.send("데이터 보냄")

enum MyError: Error {
    case publishingError
}
  1. retry(_:) 연산자를 사용하여 Publisher가 실패할 경우 3번(지정된 횟수) 더 다시 시도하도록 처리하였습니다.
  2. 이를 통해 임시적인 오류를 처리하고, 데이터를 정상적으로 가져올 수 있습니다.

에러 이벤트 발생 시 Subscription을 자동으로 취소하는 방법은 무엇인가요?

sink 연산자의 cancelOnFailure 옵션을 사용하면, Publisher가 실패할 때 자동으로 Subscription이 취소됩니다.

import Combine

let publisher = Just("Hello, World!")

let cancellable = publisher
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Publisher 처리 완료")
            case .failure(let error):
                print("Publisher 실패: \(error.localizedDescription)")
            }
        },
        receiveValue: { value in
            print("Received value: \(value)")
        }
    ) {
        print("Subscription 취소")
    }
    .store(in: &cancellables, cancelOnFailure: true) // 에러 발생 시 Subscription 취소 

// 실패하는 Publisher
publisher.send(completion: .failure(MyError.exampleError))

enum MyError: Error {
    case exampleError
}
  • 위 코드에서 sink 연산자를 사용하여 Subscription을 생성합니다.
  • cancelOnFailure 옵션을 true로 설정하여, Publisher가 실패할 때 Subscription이 자동으로 취소됩니다.

Combine과 Result 타입을 함께 사용하여 에러 처리를 하는 방법을 설명해주세요.

Result 타입

Combine에서 Result 타입을 발행하는 Publisher를 만들어 에러를 처리합니다.
map 연산자를 사용하여 Result 타입을 반환하는 클로저를 구현합니다.
클로저에서 성공적인 값과 실패한 경우의 에러를 Result 타입으로 반환합니다.

import Combine

func fetchData() -> AnyPublisher<Result<String, Error>, Never> {
    return URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
        .map { data, _ in
            // 네트워크 요청 성공
            return .success(String(data: data, encoding: .utf8)!)
        }
        .mapError { error in
            // 네트워크 요청 실패
            return error
        }
        .eraseToAnyPublisher()
}
  1. fetchData 함수는 Result 타입을 발행하는 Publisher를 생성합니다.
  2. 네트워크 요청이 성공하면 .success 케이스에 성공적인 데이터를 담아 반환하고, 실패하면 .failure 케이스에 발생한 에러를 담아 반환합니다.

이 Publisher를 구독하여 결과를 처리하는 코드를 작성해보겠습니다.

var cancellables = Set<AnyCancellable>()

fetchData()
    .sink { result in
        switch result {
        case .success(let data):
            print("데이터 패치 성공: \(data)")
        case .failure(let error):
            print("데이터 패치 실패: \(error.localizedDescription)")
        }
    }
    .store(in: &cancellables)
  1. sink 연산자를 사용하여 Result 타입을 처리합니다.
  2. 성공적인 경우 .success 케이스에 대한 처리와 실패한 경우 .failure 케이스에 대한 처리를 구현합니다.

@Phangg
Copy link
Member

Phangg commented Apr 24, 2024

.assertNoFailure() 예시 코드를 보았을때, 성공 보장을 말하는 코드지만, 혹시나 에러가 발생하면 fatal error 를 보내기때문에 case .failure() 안쓰게 되는거구나 라고 생각랬는데,
이후에 설명해주신 assertNoFailure(_:file:line:) 예시코드를 보니 .failure 를 쓰더라구요!
.failure 의 역할이 따로 있나요..?

@Phangg
Copy link
Member

Phangg commented Apr 24, 2024

에러 이벤트 발생 시 Subscription을 자동으로 취소하는 방법 파트에서 Subscription 이 정확하게 뭔지 잘 모르겠어요..

@hamfan524
Copy link
Member Author

.assertNoFailure() 예시 코드를 보았을때, 성공 보장을 말하는 코드지만, 혹시나 에러가 발생하면 fatal error 를 보내기때문에 case .failure() 안쓰게 되는거구나 라고 생각랬는데, 이후에 설명해주신 assertNoFailure(_:file:line:) 예시코드를 보니 .failure 를 쓰더라구요! .failure 의 역할이 따로 있나요..?

오 맞아요. .assertNoFailure() 연산자는 성공을 보장할 때 사용되며, 에러가 발생하면 해당 위치에서 fatalError를 발생시킵니다. 따라서 에러가 발생하는 경우에는 코드 실행이 중지됩니다.

정확히, 위의 예시에서 사용된 assertNoFailure(_:file:line:)는 실패를 강제로 확인하고, 실패하는 경우에는 메시지와 함께 해당 파일과 라인을 기록하며, 런타임에 fatalError를 발생시켜요.

위 코드에서, assertNoFailure(_:file:line:) 에서 이미 에러가 발생하면 이후의 코드가 실행되지 않기에, 이후의 .failure 케이스는 실행되지 않습니다.

위 코드에선 다른 예시 코드들과 같은 코드를 사용하면서 .failure 케이스가 남아있게 된건데, 아래에 제거한 코드를 따로 작성해둘게요!

import Combine

let publisher = Just("Hello, World!")

publisher
    .assertNoFailure("Publisher 실패", file: "Example.swift", line: 20)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Publisher 처리 완료")
        }
    }, receiveValue: { value in
        print("Received value: \(value)")
    })

@hamfan524
Copy link
Member Author

에러 이벤트 발생 시 Subscription을 자동으로 취소하는 방법 파트에서 Subscription 이 정확하게 뭔지 잘 모르겠어요..

간단하게 설명하면 Publisher를 구독하는 과정이에요.

Publisher를 구독하면 Subscriber가 Publisher로부터 이벤트를 수신하는데, 여기서의 Subscriber와 Publisher 사이의 연결을 나타내는게 Subscription이에요!

그러니 Subscription을 자동으로 취소한다면 구독을 취소해서 이벤트 전달을 더 이상 받지 않는다는 것이니, 메모리 누수나 불필요한 작업을 방지할 수 있습니다. ㅎ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants