Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- 리펙토링
- HIG
- 애니메이션
- swiftUI
- RxCocoa
- ribs
- 리팩토링
- Observable
- Xcode
- 리펙터링
- 클린 코드
- ios
- Human interface guide
- Protocol
- uiscrollview
- rxswift
- MVVM
- combine
- SWIFT
- map
- UITextView
- collectionview
- Refactoring
- 스위프트
- tableView
- Clean Code
- uitableview
- clean architecture
- UICollectionView
- swift documentation
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] MVVM 구조에서 안전하게 dataSource 관리하는 방법 (DispatchQueue, 스레드, Background, Memory Safety, 크래시) 본문
iOS 응용 (swift)
[iOS - swift] MVVM 구조에서 안전하게 dataSource 관리하는 방법 (DispatchQueue, 스레드, Background, Memory Safety, 크래시)
jake-kim 2023. 7. 29. 20:48dataSource 관리
- MVVM 구조에서 보통 dataSource를 사용할 때 아무런 큐 없이 구현하지만, 데이터 처리 최적화를 위해 background 시키고 싶은 경우 중간중간 DispatchQueue.global()를 사용하거나 커스텀 큐를 사용하는 경우가 존재
- 개발자가 실수로 DispatchQueue.global()를 사용하게 되면, 이 큐는 serial이 아닌 concurrent이므로 value type인 dataSource 배열을 수정과 동시에 읽기를 하다가 크래시가 발생하는 경우가 존재
- 크래시 - 이전 Memory Access Conflict 글 참고
- 이럴때는 커스텀 큐를 선언하여 관리하는것이 가장 좋은데, 먼저 queue의 종류를 이해가 필요
DispatchQueue의 종류 3가지
* 이전 포스팅 글에서 알아본 개념인 3가지의 DispatchQueue 개념
1) main (serial)
- main thread에서 처리되는 serial queue (모든 UI관련 작업은 해당 큐에서 main queue에서 실행)
2) global (concurrent)
- 전체 시스템에서 공유되는 concurrent queue이고, concurrent이기 queue끼리의 우선순위를 위해서 queue를 사용할 때 QoS 설정 필요
- userInteractive: 유저가 누르면 즉각 반응 (main queue)
- userInitiated: 유저가 실행시킨 작업들을 즉각적이지는 않지만, async하도록 처리
- default
- utility: I/O, n/w API 호출
- background: 유저가 인지하지 못할 정도의 뒷단에서 수행하는 작업
3) 커스텀
- 개발자가 임의로 정의한 queue이고 serial / concurrent 모두 정의 가능
- 기본적으로 주어진 것은 main은 serial, global은 concurrent이므로 main이 아닌 뒷단에서 serial로 수행하고 싶은 경우 사용
커스텀 큐를 사용해 DataSource 처리
- Memory Safety를 위해서 serial queue에서 처리하여 value type가 업데이트 동시에 읽기가 안되도록 구현이 필요
ex) MVVM 구조 준비
(RxSwift, RxCocoa 의존성 사용)
- ViewController
import UIKit
import RxCocoa
import RxSwift
enum Action {
case viewDidLoad
}
protocol DataSourceable {
var dataSource: [String] { get }
}
protocol ViewModelable: DataSourceable {
var output: Observable<State> { get }
func input(_ action: Action)
}
class ViewController: UIViewController {
// MARK: UI
private let tableView: UITableView = {
let view = UITableView()
view.allowsSelection = false
view.backgroundColor = .clear
view.separatorStyle = .none
view.bounces = true
view.showsVerticalScrollIndicator = true
view.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
view.estimatedRowHeight = 34
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
// MARK: Properties
let viewModel: ViewModelable = ViewModel()
private var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
bind()
viewModel.input(.viewDidLoad)
}
private func configureUI() {
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
])
tableView.dataSource = self
}
private func bind() {
viewModel.output
.observe(on: MainScheduler.instance)
.bind { [weak self] state in
guard let self else { return }
switch state {
case .updateUI:
tableView.reloadData()
}
}
.disposed(by: disposeBag)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = viewModel.dataSource[indexPath.row]
return cell!
}
}
- ViewModel
import RxSwift
import RxCocoa
enum State {
case updateUI
}
final class ViewModel: ViewModelable {
// MARK: State
var dataSource = [String]()
// MARK: Output
var output: RxSwift.Observable<State> {
outputSubject
}
private var outputSubject = PublishSubject<State>()
// MARK: Input
func input(_ action: Action) {
switch action {
case .viewDidLoad:
dataSource = (1...10).map(String.init)
outputSubject.onNext(.updateUI)
}
}
}
- 여기에는 dataSource가 동시에 변경될 일이 없겠지만, serial queue와 background thread에서 API의 응답에 맞추어서 dataSource를 변경해준다던지 async하게 업데이트가 발생하는 동시에 읽기가 발생한다면 crash 위험이 크게 존재
- 크래시 케이스) background thread에서 업데이트하지만, 스레드 관리의 실수로 main thread에서 업데이트 되는 경우
- 이런 위험을 막기 위해서 전역에 serial 커스텀 큐를 선언해놓고 이 큐 안에서만 dataSource를 업데이트 하도록 구현이 필요
// ViewModel.swift
let dataSourceUpdateQueue = DispatchQueue(label: "data_source_update_queue")
...
func input(_ action: Action) {
switch action {
case .viewDidLoad:
dataSourceUpdateQueue.async { // <-
self.dataSource = (1...10).map(String.init)
self.outputSubject.onNext(.updateUI)
}
}
}
- 뷰쪽에서의 updateUI 케이스에서 background thread로 동작할 것이므로 main으로 다시 스레드 전환하여 UI 업데이트
private func bind() {
viewModel.output
.observe(on: MainScheduler.instance)
.bind { [weak self] state in
guard let self else { return }
switch state {
case .updateUI:
DispatchQueue.main.async { // <-
self.tableView.reloadData()
}
}
}
.disposed(by: disposeBag)
}
정리
- 데이터 처리나 오래 걸리는 작업은 background에서 수행해야함
- global()을 사용하면 concurrent하게 동작하므로 memory access conflict가 발생하므로 custom queue를 생성
- custom queue를 사용한다고 하더라도 실수로 main queue에서 dataSource를 업데이트 치는 경우, 다른 스레드에서 동시에 값을 업데이트 할 수 있으므로 위험
- custom queue를 사용하면서 dataSource에 대한 갱신 부분은 무조건 custom queue로 감싸서 memory safety하게 사용할 것
'iOS 응용 (swift)' 카테고리의 다른 글
Comments