찹모찌의 기록일지
RxSwift로 코드 개선하기(with. Clean Architecture) 본문
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
'iOS' 카테고리의 다른 글
AutoLayout 애니메이션 적용하기 (0) | 2024.04.30 |
---|---|
Clean Swift로 Unit Test 작성하기 (0) | 2024.04.28 |
MVVM + RxSwift 직접 써보기 // 주어진 API에서 최선을 다히기 (0) | 2024.04.13 |
Mock 활용해서 환경 의존도 없애기(with. URLSession, UserDefaults) (0) | 2024.04.11 |
내 앨범이 너에게 닿기를: 서버 없이 앨범 전달받기 (0) | 2024.03.15 |