[iOS][Weekly] 2021/11/26
2021.12.06
[iOS][Weekly] 2021/11/26
본문
내용
⭐️ NEWS ⭐️
1. iOS 개발자를 위한 블랙프라이데이
- https://github.com/mRs-/Black-Friday-Deals
- 소프트웨어, 교육, 툴, 책 등 여러가지 할인 정보를 얻을 수 있다.
⭐️ CODE ⭐️
1. (Xcode) Xcode Cloud에 대해 알아보자
- Xcode Cloud란?
- WWDC 2021에 처음 소개됨.
- 빌드, 테스트, 배포를 쉽게 할 수 있는 클라우드 기반 Xcode 유료 기능 (자체 CI/CD 툴)
- Xcode Cloud를 쓰고 싶다면?
- Xcode 13 이상부터 가능
- Automatic code signing을 사용해야 한다.
- Xcode Cloud 베타로 가서 권한을 신청해야 한다.
- Xcode Cloud가 지원하는 SCM은?
- Git, Git Enterprise
- Bitbucket Cloud, Bitbucket Server
- Gitlab
- 프로젝트에 Xcode Cloud 설정하기
Product > Xcode Cloud
또는왼쪽 내비게이션의 Report Navigator
로 접근 가능Xcode Cloud > Create Workflow
를 선택 후 product를 선택한다.Review Workflow box
가 나오고Next
버튼을 누르면 소스코드에 대한 권한 요청 화면이 뜨고 권한 요청을 승인해준다.- 권한을 승인하면, Xcode가 App Store Connect와 Github 인증을 진행한다.
- 모든 것이 완료되면 아래와 같은 화면이 나온다.
- Xcode Cloud workflow 설정
Product > Xcode Cloud > Manage Workflow
를 클릭- Workflow 설정 화면
- 브랜치가 변경될 때 자동으로 빌드할 수도 있다.
Actions
에서는 Build, Test, Analyze, Archive 설정을 넣을 수 있다.Post-Actions
메뉴에서는 Notify, Testflight External Testing, Testflight Intenral Testing 등 여러 기능을 제공한다.- 브랜치 변경되어서 자동으로 workflow가 실행되거나, 수동으로 workflow를 실행시킬 수 있다.
직접 써보고 확인해보고 싶지만, 현재 Xcode Cloud 권한이 없는 상태라 포스트 글로 내용을 대체한다.
2. (Swift) multi-thread 환경에서 Actor를 사용해보자
- actor를 사용하면 data races를 해결할 수 있다.
- data races란, 여러 thread에서 동일한 데이터를 수정, 읽기를 할 때 충돌이 발생하는 것을 발한다.
- 아래처럼 구현을 했을 경우,
struct User: Identifiable {
var id = UUID()
}
class UserStorage {
private var users = [User.ID: User]()
func store(_ user: User) {
users[user.id] = user
}
func user(withID id: User.ID) -> User? {
users[id]
}
}
- 단일 thread에서는 문제가 없지만, 멀티 thread 환경에서는
store(_:)
,user(withID:)
method에서 data races가 발생한다.- 즉,
UserStorage
는 thread-safe하지 않는다는 말과 동일하다
- 즉,
actor
를 사용하지 않고 해결하는 방법 중 하나는 특정 DispatchQueue에게 읽기, 쓰기 동작을 전달하고 DispatchQueue는 순차적으로 동작을 수행시키면 된다.
class UserStorage {
private var users = [User.ID: User]()
private let queue = DispatchQueue(label: "UserStorage.sync") // <- DispatchQueue 생성
func store(_ user: User) {
queue.sync { // <- 생성한 DispatchQueue에 동작 전달
self.users[user.id] = user
}
}
func user(withID id: User.ID) -> User? {
queue.sync { // <- 생성한 DispatchQueue에 동작 전달
self.users[id]
}
}
}
- 위처럼 구현할 경우 data races는 성공적으로 해결되지만, 다른 문제가 발생한다.
- queue에서 전달받은 동작을 sync(순차적)으로 처리하기 때문에, queue가 처리해주기 전까지 현재 실행이 멈추게 된다.
- data contention 발생
- 이로 인해 성능이 저하된다.
- 위 문제를 해결하기 위해선, method를 async하게 구현하면 된다.
class UserStorage {
private var users = [User.ID: User]()
private let queue = DispatchQueue(label: "UserStorage.sync")
func store(_ user: User) {
queue.async { // <- async하게 처리
self.users[user.id] = user
}
}
func loadUser(withID id: User.ID, handler: @escaping (User?) -> Void) {
queue.async { // <- async하게 처리 + 비동기 callback 처리
handler(self.users[id])
}
}
}
- 위 방법으로 data races, data contention을 해결할 수 있지만, handler로 인해 가독성이 떨어지며 코드가 복잡해질수록 이 문제는 더욱 심각해진다.
- Swift 5.5에서 소개된
actor
를 사용하면 더욱 쉽게 문제를 해결할 수 있다.
actor UserStorage { // <- actor 키워드 사용
private var users = [User.ID: User]()
func store(_ user: User) {
users[user.id] = user
}
func user(withID id: User.ID) -> User? {
users[id]
}
}
- actor를 사용하기 위해선
await
키워드도 함께 사용해야 하기 때문에 DispatchQueue.async 구현을 할 필요가 없다. - 예를 들어 아래처럼 actor의 method를 사용하려면
await
키워드도 함께 사용해야 한다.
class UserLoader {
private let storage: UserStorage
private let urlSession: URLSession
private let decoder = JSONDecoder()
init(storage: UserStorage, urlSession: URLSession = .shared) {
self.storage = storage
self.urlSession = urlSession
}
func loadUser(withID id: User.ID) async throws -> User {
if let storedUser = await storage.user(withID: id) { // <- await
return storedUser
}
let url = URL.forLoadingUser(withID: id)
let (data, _) = try await urlSession.data(from: url)
let user = try decoder.decode(User.self, from: data)
await storage.store(user) // <- await
return user
}
}
- 하지만, 위처럼 actor를 사용했다고 해서 race condition이 해결되는 것은 아니다.
- data races != race condition
- 위 예제로 설명을 하자면, 여러 thread에서 동일한 ID의 User를 load할 경우, 동시에 많은 네트워크 통신 후 storage에 저장하기 때문에 race condition이 발생하게 된다.
- 이를 해결하기 위해 UserLoader도 actor로 선언하면 될거라 생각했지만, 그렇지는 않다.
- store, load의 동작 순서에 따라 값이 달라지는 것이기 때문에 actor만으로 해결이 되는 것은 아니다.
actor UserLoader {
...
}
- 이를 해결하기 위해선, 특정 ID에 대한 수행동작을 따로 저장하고, 동일 ID 요청이 들어왔을 경우 저장하고 있던 동작의 결과를 던져주면 된다.
- 동일 ID로 인한 중복 네트워크 호출을 방지할 수 있다.
actor UserLoader {
private let storage: UserStorage
private let urlSession: URLSession
private let decoder = JSONDecoder()
private var activeTasks = [User.ID: Task<User, Error>]() // <- Task 저장
func loadUser(withID id: User.ID) async throws -> User {
if let existingTask = activeTasks[id] {
return try await existingTask.value
}
let task = Task<User, Error> {
if let storedUser = await storage.user(withID: id) {
activeTasks[id] = nil
return storedUser
}
let url = URL.forLoadingUser(withID: id)
let (data, _) = try await urlSession.data(from: url)
let user = try decoder.decode(User.self, from: data)
await storage.store(user)
activeTasks[id] = nil
return user
}
activeTasks[id] = task
return try await task.value
}
}
댓글남기기