[42CM] Observable 대신 Single 사용해보기

·2024년 4월 17일

프로젝트-42CM

목록 보기
3/9
post-thumbnail

저는 RxSwift를 사용하여 네트워크 통신을 구현할 때, 여태까지 다음과 같이 Observable을 사용했습니다. 예를 들어, 로그인 기능을 구현하는 경우 아래와 같이요!

import Alamofire
import RxSwift

struct NetworkManager {
    
    static func createLogin(query: LoginQuery) -> Observable<LoginModel> {
        return Observable.create { observer in
            do {
                let urlRequest = try Router.login(query: query).asURLRequest()
                                
                AF.request(urlRequest)
                    .validate(statusCode: 200..<300)
                    .responseDecodable(of: LoginModel.self) { response in
                        switch response.result {
                        case .success(let loginModel):
                            observer.onNext(loginModel)
                            observer.onCompleted()
                        case .failure(let error):
                            observer.onError(error)
                        }
                    }
            } catch {
                observer.onError(error)
            }
            
            return Disposables.create()
        }
    }
}

이 방식에는 몇 가지 문제가 있습니다 ..

  1. 여러 값 방출 가능성
    Observable은 원칙적으로 여러 개의 값을 방출할 수 있습니다. 로그인 요청은 하나의 성공 또는 실패 결과만을 기대하는데, Observable을 사용하면 로직 상에서 실수로 여러 번의 onNext()를 호출할 가능성이 있으며, 이는 예상치 못한 버그를 유발할 수 있습니다.

  2. 스트림 완료 관리
    Observable을 사용하면 구독자는 명시적으로 onCompleted를 체크하여 스트림의 완료 여부를 판단해야 합니다. 이는 코드의 복잡성을 증가시킬 수 있습니다.

이러한 문제를 해결하기 위해 Observable 대신 Single 타입을 사용해보았습니다!
Single은 RxSwift에서 제공하는 타입으로, 하나의 값 또는 에러를 방출하고 종료되는 특성을 가져서 로그인과 같은 네트워크 요청에 적합합니다. 사용은 아래와 같이 할 수 있습니다.

static func createLogin(query: SignInQuery) -> Single<SignInModel> {
        return Single<SignInModel>.create { single in
            do {
                let urlRequest = try Router.login(query: query).asURLRequest()
                                
                AF.request(urlRequest)
                    .validate(statusCode: 200..<300)
                    .responseDecodable(of: SignInModel.self) { response in
                        switch response.result {
                        case .success(let signInModel):
                            do {
                                try Keychain.shared.saveToken(kind: .accessToken, token: signInModel.accessToken)
                                print("AccessToken 저장 성공: \(signInModel.accessToken)")
                                try Keychain.shared.saveToken(kind: .refreshToken, token: signInModel.refreshToken)
                                print("RefreshToken 저장 성공: \(signInModel.refreshToken)")
                                
                                single(.success(signInModel))
                            } catch {
                                single(.failure(error))
                            }
                        case .failure(let error):
                            print("키체인 저장 오류: \(error)")
                            if response.response?.statusCode == 401 {
                                single(.failure(NetworkError.unauthorized))
                                return
                            }
                            single(.failure(error))
                        }
                    }
            } catch {
                single(.failure(error))
            }
            
            return Disposables.create()
        }

Single 사용의 이점은 다음과 같습니다.

  1. 명확성
    Single은 정확히 한 번의 success 이벤트 또는 한 번의 error 이벤트만을 방출한다. 이는 로직을 명확하게 하고, 코드를 이해하고 관리하기 쉽게 만든다.

  2. 에러 처리의 단순화
    Single을 사용하면 성공 또는 실패의 두 가지 경우만을 처리하면 된다. 이는 에러 핸들링 로직을 간소화하고, 예외 처리를 더욱 명확하게 할 수 있다.


이렇게 Single을 사용하는 것은 특히 하나의 결과만을 기대하는 네트워크 요청에서 코드의 안정성과 관리 용이성을 크게 향상시킬 수 있었습니다.

이번 글은 여기서 마무리 하겠습니다. 읽어주셔서 감사합니다!
추가 내용이나 수정이 필요한 내용은 댓글 남겨주세요 :)

0개의 댓글