[iOS][Weekly] 2021/11/26

4 분 소요

2021.12.06

[iOS][Weekly] 2021/11/26

본문

내용

⭐️ NEWS ⭐️

1. iOS 개발자를 위한 블랙프라이데이

⭐️ 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가 발생한다.
    • 즉, UserStoragethread-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
    }
}

참고

카테고리: ,

업데이트:

댓글남기기