관리 메뉴

찹모찌의 기록일지

RxSwift로 코드 개선하기(with. Clean Architecture) 본문

iOS

RxSwift로 코드 개선하기(with. Clean Architecture)

찹모찌 2024. 4. 13. 13:31

RxSwift는 반응형 프로그래밍으로서의 장점뿐만 아니라 Completion Handler로 발생하는 Clousre의 코드 depth 또한 해결할 수 있다.

기존코드와 개선한 코드를 비교해가며 이를 알아보자.

1. 기존 코드

기존 코드의 ViewModel, UseCase, Repository다. 사용하는 부분만 간단히 모아보면

//
//  InterestStocksViewModel.swift
//  Or-rock-Nari-lock

import Foundation
import RxSwift

final class InterestStocksViewModel {
    private let loadInterestStocksUseCase: LoadInterestStocksUseCase
    private let checkTodayPriceUseCase: CheckTodayPriceUseCase

    private var checkStockTasks: [String:Cancellable?] = [:]
    private let mainQueue: DispatchQueueType

    private var stockInformationArray : [StockInformation] = []
    private let stockInformationsSubject = PublishSubject<[StockInformation]>()

    struct Input {
        let viewDidLoadEvent: Observable<Void>
    }

    struct Output {
        let stockInformations: Observable<[StockInformation]>
    }

    init(loadInterestStocksUseCase: LoadInterestStocksUseCase = DefaultLoadInterestStocksUseCase(),
         checkTodayPriceUseCase: CheckTodayPriceUseCase = DefaultCheckTodayPriceUseCase(),
         mainQueue: DispatchQueueType = DispatchQueue.main) {
        self.loadInterestStocksUseCase = loadInterestStocksUseCase
        self.checkTodayPriceUseCase = checkTodayPriceUseCase
        self.mainQueue = mainQueue
    }

    func transform(from input: Input, disposeBag: DisposeBag) -> Output {
        let output = createViewModelOutput()
        input.viewDidLoadEvent.subscribe(
            onNext: { [weak self] _ in
                guard let self = self else { return }
                self.load()
            }
        )
        .disposed(by: disposeBag)
        return output
    }
}

private extension InterestStocksViewModel {
    func createViewModelOutput() -> Output {
        Output(stockInformations: stockInformationsSubject.asObservable())
    }

    func load() {
        let names: [String] = loadInterestStocksUseCase.execute()
        names.forEach { name in
            let checkStockTask = checkTodayPriceUseCase.execute(stockName: name) { [weak self] result in
                self?.mainQueue.async {
                    switch result {
                    case .success(let stockInformation):
                        self?.stockInformationArray.append(stockInformation)
                        guard let stockInformationArray = self?.stockInformationArray else { return }
                        self?.stockInformationsSubject.onNext(stockInformationArray)
                    case .failure(let error):
                        print(error)
                    }
                }
            }
            checkStockTasks[name] = checkStockTask
        }
    }
}
//
//  CheckTodayPriceUseCase.swift
//  Or-rock-Nari-lock

import Foundation

protocol CheckTodayPriceUseCase {
    func execute(
        stockName: String,
        completion: @escaping (Result<StockInformation, Error>) -> Void
    ) -> Cancellable?
}

final class DefaultCheckTodayPriceUseCase: CheckTodayPriceUseCase {
    private let checkStockRepository: CheckStockRepository

    init(checkStockRepository: CheckStockRepository = DefaultCheckStockRepository()) {
        self.checkStockRepository = checkStockRepository
    }

    func execute(
        stockName: String,
        completion: @escaping (Result<StockInformation, Error>) -> Void)
    -> Cancellable? {
        checkStockRepository.fetchStockTodayPrices(stockNames: stockName) { result in
            completion(result)
        }
    }
}
//
//  CheckStockRepository.swift
//  Or-rock-Nari-lock

import Foundation

final class DefaultCheckStockRepository: CheckStockRepository {
    private let dataTransferService: DataTransfer
    private let backgroundQueue: DataTransferDispatchQueue = DispatchQueue.global(qos: .userInitiated)

    init(dataTransferService: DataTransfer = APIDataTransfer(apiProvider: APIProvider(sessionManager: MockNetworkSessionManager(response: nil, data: nil, error: nil)))) {
        self.dataTransferService = dataTransferService
    }
}

extension DefaultCheckStockRepository {
    func fetchStockTodayPrices(
        stockNames: String,
        completion: @escaping (Result<StockInformation, Error>) -> Void)
    -> Cancellable? {
        let task = RepositoryTask()
        let endPoint: EndPoint<StockInformationDTO> = EndPoint<StockInformationDTO>(path: "/uapi/domestic-stock/v1/quotations/inquire-price", method: .GET)
        task.networkTask = self.dataTransferService.request(
            with: endPoint,
            on: backgroundQueue,
            completion: { result in
                switch result {
                case .success(let responseDTO):
                    completion(.success(responseDTO.toDomain()))
                case .failure(let error):
                    completion(.failure(error))
                }
            })
        return task
    }
}

위와 같이 Completion Handler를 사용하면서 각 코드 부분을 clousre로 처리하다 보니 코드의 depth가 깊어지는 것을 확인할 수 있다.

이를 RxSwift를 통해 개선해보면?

2. 개선코드

//
//  InterestStocksViewModel.swift
//  Or-rock-Nari-lock

import Foundation
import RxSwift

final class InterestStocksViewModel {
    private let loadInterestStocksUseCase: LoadInterestStocksUseCase
    private let checkTodayPriceUseCase: CheckTodayPriceUseCase

    private var checkStockTasks: [String:Cancellable?] = [:]
    private let mainQueue: DispatchQueueType

    private var stockInformationArray : [StockInformation] = []
    private let stockInformationsSubject = PublishSubject<[StockInformation]>()

    struct Input {
        let viewDidLoadEvent: Observable<Void>
    }

    struct Output {
        let stockInformations: Observable<[StockInformation]>
    }

    init(loadInterestStocksUseCase: LoadInterestStocksUseCase = DefaultLoadInterestStocksUseCase(),
         checkTodayPriceUseCase: CheckTodayPriceUseCase = DefaultCheckTodayPriceUseCase(),
         mainQueue: DispatchQueueType = DispatchQueue.main) {
        self.loadInterestStocksUseCase = loadInterestStocksUseCase
        self.checkTodayPriceUseCase = checkTodayPriceUseCase
        self.mainQueue = mainQueue
    }

    func transform(from input: Input, disposeBag: DisposeBag) -> Output {
        let output = createViewModelOutput()
        input.viewDidLoadEvent.subscribe(
            onNext: { [weak self] _ in
                guard let self = self else { return }
                fetchStockInformation(for: self.load(), disposeBag: disposeBag)
            }
        )
        .disposed(by: disposeBag)
        return output
    }
}

private extension InterestStocksViewModel {
    func createViewModelOutput() -> Output {
        Output(stockInformations: stockInformationsSubject.asObservable())
    }

    func load() -> [String] {
        loadInterestStocksUseCase.execute()
    }

    func fetchStockInformation(for names: [String], disposeBag: DisposeBag) {
        Observable.from(names)
            .flatMap { name in
                return self.checkTodayPriceUseCase.execute(stockName: name)
            }
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] stockInformation in
                guard let stockInformation else { return }
                self?.stockInformationArray.append(stockInformation)
                guard let stockInformationArray = self?.stockInformationArray else { return }
                self?.stockInformationsSubject.onNext(stockInformationArray)
            }, onError: { error in
                print(error)
            })
            .disposed(by: disposeBag)
    }
}
//
//  CheckTodayPriceUseCase.swift
//  Or-rock-Nari-lock


import Foundation
import RxSwift

protocol CheckTodayPriceUseCase {
    func execute(stockName: String) -> Observable<StockInformation?>
}

final class DefaultCheckTodayPriceUseCase: CheckTodayPriceUseCase {
    private let checkStockRepository: CheckStockRepository

    init(checkStockRepository: CheckStockRepository = MockCheckStockRepository()) {
        self.checkStockRepository = checkStockRepository
    }

    func execute(stockName: String) -> Observable<StockInformation?> {
        checkStockRepository.fetchStockTodayPrices(stockName: stockName)
    }
}
//
//  CheckStockRepository.swift
//  Or-rock-Nari-lock

import Foundation
import RxSwift

final class DefaultCheckStockRepository {
    private let dataTransferService: DataTransfer
    private let backgroundQueue: DataTransferDispatchQueue = DispatchQueue.global(qos: .userInitiated)

    init(dataTransferService: DataTransfer = APIDataTransfer(apiProvider: APIProvider(sessionManager: MockNetworkSessionManager(response: nil, data: nil, error: nil)))) {
        self.dataTransferService = dataTransferService
    }
}

extension DefaultCheckStockRepository: CheckStockRepository {
    func fetchStockTodayPrices(stockName: String) -> Observable<StockInformation?> {
        return Observable.create { observer in
            let task = RepositoryTask()
            let endPoint: EndPoint<StockDayPriceDTO> = EndPoint<StockDayPriceDTO>(path: "/uapi/domestic-stock/v1/quotations/inquire-price", method: .GET)
            task.networkTask = self.dataTransferService.request(
                with: endPoint,
                on: self.backgroundQueue,
                completion: { result in
                    switch result {
                    case .success(let responseDTO):
                        observer.onNext(responseDTO.toDomain(korName: stockName, engName: stockName))
                        observer.onCompleted()
                    case .failure(let error):
                        observer.onError(error)
                    }
                })
            return Disposables.create {
                task.cancel()
            }
        }
    }
}

훨씬 코드가 깔끔해진 것을 볼 수 있다. 기존 구성해 놓았던 Network Layer까지 변경하는 건 코드를 변경할 부분이 너무 많아진다고 생각했기 때문에 Repository에서부터 RxSwift를 사용하여 주식 시세 조회 결과를 Observable로 전달한다.

RxSwift가 반응형 프로그래밍으로서 역할도 하지만, 비동기 동적을 관리함에 있어서 코드의 가독성을 높여주는 역할도 할 수 있다는 것을 경험했다.

https://github.com/App-in-App-le/Or-rock-Nari-lock/tree/iOS/dev

 

GitHub - App-in-App-le/Or-rock-Nari-lock: 주식 정보 iOS 애플리케이션

주식 정보 iOS 애플리케이션. Contribute to App-in-App-le/Or-rock-Nari-lock development by creating an account on GitHub.

github.com