-
Notifications
You must be signed in to change notification settings - Fork 1
swift6 도입기 ‐ @unchecked Sendable을 사용해야만 했던 이유
우선적으로 Swift6를 경험해보자가 저희의 목표였습니다. Swift6에서는 Data-race safety
를 중요시하기 때문에 데이터 레이스가 생길 가능성이 있는 부분에 대해서 컴파일러가 오류를 내고, 저희가 그것을 해결해보는 경험을 가지고자 도입하게 되었습니다.
Swift6에서는 클래스 내부의 프로퍼티가 외부에서 변경, 접근할 가능성이 있으면 바로 컴파일러가 오류를 내뿜거나 경고를 발생시킵니다.
하지만 어디까지나 가능성입니다. 동시에 수정이 이루어지거나 접근할 가능성을 없애주면 됩니다.
기본적으로 Swift6
는 Sendable
하면 오류를 내뿜지 않습니다. Sendable
하다는 것은 Data race
에 대해 방지가 잘되어 있음을 의미하는 것이니까요.
하지만 Sendable
을 준수하기 위해서는 꽤나 번거로운 작업이 동반됩니다. 데이터에 접근을 위해서는 비동기적으로 프로퍼티에 접근해야 하고 함수 작성에도 여러 사항을 준수하면서 작성되어야 합니다.
하지만 위에서 말씀드렸듯이 Data-race
가 일어날 가능성이 있다! 가능성이 있다는 것이지 안 일어날 수도 있음을 의미합니다.
그럼 개발자가 Sendable
을 준수하진 않지만 data-race
가 일어나지 않음을 명시해주어야 컴파일러가 오류를 내지 않습니다.
이때 사용되는 것이 @unchecked Sendable
입니다.
final class HummingViewModel: @unchecked Sendable {
...
}
이런 식으로 해당 키워드를 입력해주면 컴파일러는 해당 클래스에 대해 Sendable 한지 확인하지 않습니다.
data race
는 꽤나 중요한 문제입니다. 수정을 진행하는 2개의 작업이 순서가 중요한데 마지막에 되었어야 할 작업이 먼저 되었다면 그 값을 읽는 부분에서 잘못된 정보를 읽은 것이 되어버리기 때문입니다.
대부분의 ViewModel은 하나의 VC 혹은 하나의 View에 1:1의 관계를 가지게 됩니다.
RehummingViewModel과 RehummingVC를 예로 들어보겠습니다.
final class RehummingViewController: UIViewController {
private var progressBar = ProgressBar()
private var musicPanel = MusicPanel()
private var hummingPanel = RecordingPanel(.asMint)
private var recordButton = ASButton()
private var submitButton = ASButton()
private var submissionStatus = SubmissionStatusView()
private var buttonStack = UIStackView()
private let viewModel: RehummingViewModel
init(viewModel: RehummingViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
setupUI()
setupLayout()
setAction()
bindToComponents()
}
override func viewDidDisappear(_ animated: Bool) {
viewModel.cancelSubscriptions()
}
private func bindToComponents() {
submissionStatus.bind(to: viewModel.$submissionStatus)
progressBar.bind(to: viewModel.$dueTime)
musicPanel.bind(to: viewModel.$music)
hummingPanel.bind(to: viewModel.$isRecording)
hummingPanel.onRecordingFinished = { [weak self] recordedData in
self?.recordButton.updateButton(.reRecord)
self?.viewModel.updateRecordedData(with: recordedData)
}
submitButton.bind(to: viewModel.$recordedData)
}
private func setupUI() {
recordButton.updateButton(.idle("녹음하기", .systemRed))
submitButton.updateButton(.submit)
submitButton.updateButton(.disabled)
buttonStack.axis = .horizontal
buttonStack.spacing = 16
buttonStack.addArrangedSubview(recordButton)
buttonStack.addArrangedSubview(submitButton)
view.backgroundColor = .asLightGray
view.addSubview(progressBar)
view.addSubview(musicPanel)
view.addSubview(hummingPanel)
view.addSubview(buttonStack)
view.addSubview(submissionStatus)
}
...
}
VC 그 어디에도 VM의 데이터에 수정을 일으키는 부분은 존재하지 않습니다. 오직 bind를 통해서 값의 변화만 지켜보고 있어 data-race
를 일으키지 않습니다.
final class RehummingViewModel: @unchecked Sendable {
@Published public private(set) var dueTime: Date?
@Published public private(set) var recordOrder: UInt8?
@Published public private(set) var status: Status?
@Published public private(set) var submissionStatus: (submits: String, total: String) = ("0", "0")
@Published public private(set) var music: Music?
@Published public private(set) var recordedData: Data?
@Published public private(set) var isRecording: Bool = false
private let gameStatusRepository: GameStatusRepositoryProtocol
private let playersRepository: PlayersRepositoryProtocol
private let recordsRepository: RecordsRepositoryProtocol
private var cancellables: Set<AnyCancellable> = []
public init(
gameStatusRepository: GameStatusRepositoryProtocol,
playersRepository: PlayersRepositoryProtocol,
recordsRepository: RecordsRepositoryProtocol
) {
self.gameStatusRepository = gameStatusRepository
self.playersRepository = playersRepository
self.recordsRepository = recordsRepository
bindGameStatus()
}
func submitHumming() async throws {
guard let recordedData else { return }
do {
let result = try await recordsRepository.uploadRecording(recordedData)
if result {
// 전송됨
} else {
// 전송 안됨, 오류 alert
}
} catch {
throw error
}
}
func startRecording() {
if !isRecording {
isRecording = true
}
}
func updateRecordedData(with data: Data) {
// TODO: - data가 empty일 때(녹음이 제대로 되지 않았을 때 사용자 오류처리 필요
guard !data.isEmpty else { return }
recordedData = data
isRecording = false
}
private func bindRecord(on recordOrder: UInt8) {
recordsRepository.getHumming(on: recordOrder)
.sink { [weak self] record in
guard let record else { return }
self?.music = Music(record)
}
.store(in: &cancellables)
}
private func bindGameStatus() {
gameStatusRepository.getDueTime()
.sink { [weak self] newDueTime in
self?.dueTime = newDueTime
}
.store(in: &cancellables)
gameStatusRepository.getRecordOrder()
.sink { [weak self] newRecordOrder in
self?.recordOrder = newRecordOrder
self?.bindRecord(on: newRecordOrder)
self?.bindSubmissionStatus(with: newRecordOrder)
}
.store(in: &cancellables)
gameStatusRepository.getStatus()
.sink { [weak self] newStatus in
self?.status = newStatus
}
.store(in: &cancellables)
}
private func bindSubmissionStatus(with recordOrder: UInt8) {
let playerPublisher = playersRepository.getPlayersCount()
let recordsPublisher = recordsRepository.getRecordsCount(on: recordOrder)
playerPublisher.combineLatest(recordsPublisher)
.sink { [weak self] playersCount, recordsCount in
let submitStatus = (submits: String(recordsCount), total: String(playersCount))
self?.submissionStatus = submitStatus
}
.store(in: &cancellables)
}
...
VM에서만 VM 내부의 프로퍼티를 변경할 수 있는 함수가 존재합니다. 이렇게 되면 VM 내부에서만 데이터의 변경이 일어납니다.
이렇게 되면 오로지 데이터를 변경하는 곳은 VM이므로, 동시에 프로퍼티가 변경될 일이 사라지기 때문에 암시적으로 Sendable하게 만들 수 있습니다.
이 부분도 동일합니다. 각 CacheManager들은 ASCacheManager라는 하나의 큰 CacheManager와 1:1 연관관계로 연결되어 있습니다.
다른 곳에서는 각 CacheManager를 참조하지 않기에 오로지 ASCacheManager에서만 변화를 일으키고, 각 CacheManager 내부에서 Manager 프로퍼티에 변화를 시키기에 data-race
가 일어나지 않음을 보장할 수 있습니다.
- 📒 기획의 과정과 의도
- 📒 swift6 도입기 ‐ @unchecked Sendable을 사용해야만 했던 이유
- 📒 WaveForm(파형) 제작기
- 📒 프로젝트 구조와 이유
- 📒 화면 전환(Game NavigationController)
- 📒 DIContainer를 사용한 계기
- 📒 AudioHelper 제작기
- 📒 음악 플레이어의 compact 버전 제작기
- 📒 Combine을 이용한 데이터 전달
- 📒 파이어베이스를 쓰며 있었던 일
- 📒 캐싱 모듈 구현과 문제점
- 📒 로그 시스템 제작기
- ❗ Data 끼리의 비교
- ❗ 프레임워크 Reference 안잡히는 문제
- ❗ actor 안에서 timer가 실행되지 않는 문제
- ❗ NSLayoutConstraint 옵셔널 문제
- ❗ 테이블 뷰가 보고 있는 배열과 bind하고 있는 배열 간의 race condition 문제
- ❗ 테스트끼리의 독립성
- ❗ 네트워크 테스팅 시 Error 핸들링
- ❗ 여러 클라이언트가 서버에 동시 요청시, 데이터가 반영이 안되는 이슈 해결
- ❗ 의존성 framework 추가시 불러오지 못하는 문제
- ❗ Timer를 6초 설정해도 더 실행되는 문제
- ❗ Music Kit Data Request 에러
- ❗ DI Container 에서 생성한 인스턴스가 동시에 존재 하는 이슈