๋๊ธฐ & ๋น๋๊ธฐ
synchronous๋ ์์ ์ ๋๊ธฐ์ ์ผ๋ก ์คํ. ์คํ ์ค์ธ ์์ ์ด ๋๋ ๋ ๊น์ง ๋ค๋ฅธ ์์ ์ ๊ธฐ๋ค๋ฆผ
asynchronous๋ ๋น๋๊ธฐ๋ก ํ์ฌ ์คํ ์ค์ธ ์์ ์ด ์์ด๋ ๋ค๋ฅธ ์์ ์ ์คํ ํ ์ ์๋ค. ์ค๋ ๊ฑธ๋ฆฌ๋ ์์ ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํด์ ๋ฉ์ธ ์ค๋ ๋์์ ํ์๋ก ํ๋ ์์ ์ด ๋ฉ์ถ์ง ์๊ฒ ํ๋ค.
MainActor
UI ๊ด๋ จ ์์ ์ ๋ฉ์ธ ์ค๋ ๋์์ ์คํ๋์ด์ผ ํ๋๋ฐ, MainActor๋ ์ด๋ฅผ ์ฝ๊ฒ ๊ด๋ฆฌํ ์ ์๊ฒ ํด์ค๋ค. UI ์ ๋ฐ์ดํธ์ ๊ด๋ จ๋ ์์ ์ ๋ฉ์ธ ์ค๋ ๋์์ ์คํํ๋๋ก ๋ณด์ฅํ๋ค. ๋น๋๊ธฐ ์์ ์์ ๋ฉ์ธ ์ค๋ ๋๋ก์ ์ ๊ทผ์ ์์ ํ๊ฒ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ์ค๋ ๋ ์์ ์ ์ด๋ฉฐ DispatchQueue.main.async์ ๊ฐ์ ์ฝ๋๋ฅผ ์์ฑํ ํ์๊ฐ ์์ด ๊ฐ๊ฒฐํ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
struct DefaultProductDetailUseCase: ProductDetailUseCase {
func fetchProductDetail(idx: Int) async throws -> Product {
let (data, response) = try await URLSession.shared.data(from: URL(string: "https://dummyjson.com/products/\(idx)")!)
guard let response = response as? HTTPURLResponse,
(200..<300).contains(response.statusCode) else {
throw CustomError.statusCodeError
}
guard let result = try? JSONDecoder().decode(Product.self, from: data) else {
throw CustomError.invalidResponse
}
return result
}
}
๋น๋๊ธฐ ์์ ์ ์ํํ๋ ํจ์๊ฐ ์๋ค๊ณ ํ ๋ ViewModel์์ ์๋์ ๊ฐ์ด ์ฌ์ฉํ ์ ์๋ค.
class ProductDetailViewModel: ObservableObject {
var useCase: ProductDetailUseCase?
@Published var product: Product?
init(useCase: ProductDetailUseCase) {
self.useCase = useCase
}
func getchProductDetail(idx: Int) {
Task {
let product = try await self.useCase?.fetchProductDetail(idx: idx)
await updateUI(with: product)
}
}
@MainActor
private func updateUI(with product: Product?) async {
self.product = product
}
}
MainActor์ ์ฌ์ฉํด์ ๋ฉ์ธ ์ค๋ ๋์์ ๋์ํด์ผ ํ๋ ์ฝ๋๋ฅผ ์คํํ๋ค. ๋น๋๊ธฐ ์์ ํ UI ์ ๋ฐ์ดํธ ์ฝ๋๋ฅผ ๋ฉ์ธ ์ค๋ ๋์์ ์์ ํ๊ฒ ์คํํ ์ ์๋ค.
Async
๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํํ๋ ํจ์๋ฅผ ์ ์ํ๋ ค๋ฉด async ํค์๋๋ฅผ ์ฌ์ฉํด ์ค๋ค.
func fetchProductDetail(idx: Int) async -> Product {
// ์๋ต
}
async ์ฝ๋๋ concurrent ์ปจํ ์คํธ์์๋ง ์คํ ๊ฐ๋ฅํ๋ค. Task ํน์ ๋ค๋ฅธ async ํจ์์์ async ํจ์๋ฅผ ํธ์ถํ ์ ์๋ค.
์๋ฌ๋ฅผ ๋ฐํํ ์ ์๋ ๊ฒฝ์ฐ async throws๋ก ์ ์ํด ์ค๋ค.
func fetchProductDetail(idx: Int) async throws -> Product {
// ์๋ต
}
Await
Task {
let product = try await self.useCase?.fetchProductDetail(idx: idx)
await updateUI(with: product)
}
async ํจ์๋ฅผ ํธ์ถํ๊ธฐ ์ํด์ await ํค์๋๋ฅผ ์ฌ์ฉํ๋ค. await์ผ๋ก ๋งํน๋ ๊ณณ์ potential suspension point(์ ์ฌ์ ์ธ ์ผ์ ์ค๋จ ์ง์ )์ผ๋ก ์ง์ ๋๋ค. ์ฆ ์ด ํจ์๋ฅผ ํธ์ถํ ๋ถ๋ถ๋ ๋๊ธฐ ์ํ๊ฐ ๋ ์ ์๋ค๋ ๊ฒ์ ์๋ฏธํ๋ค.
์ฌ๊ธฐ์ suspension point ์ suspend๋ ์ค๋ ๋๋ฅผ block ์ํค๋ ๊ฒ์ด ์๋๋ผ ๋ค๋ฅธ ๋์์ ์ํํ ์ ์๋๋ก ์ปจํธ๋กค์ ํฌ๊ธฐํ๋ ๊ฒ์ ์๋ฏธํ๋ค. ์ค๋ ๋์ ๋ํ ์ ์ด๊ถ์ system์๊ฒ ์ ๋ฌํ๊ฒ ๋๊ณ , ์์คํ ์ ํด๋น ์ค๋ ๋๋ฅผ ์ฌ์ฉํด ๋ค๋ฅธ ์์ ์ ์ํํ ์ ์๋ค. ์ดํ ์์คํ ์ ๋ค๋ฅธ ํน์ ์ค๋ ๋ ์ ์ด๊ถ์ ์ ๋ฌํด์ suspension point ์ดํ ์ฝ๋๋ฅผ ์ํํ ์ ์๊ฒ ํ๋ค.
task
๋ค๋ฅธ Task์ ํจ๊ป ๋์์ ์คํ ํ ์ ์๋ ๋น๋๊ธฐ ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํ๋ค. SwiftUI์์๋ task view modifier๋ฅผ ์ ๊ณตํ๊ณ ์๋ค.
@inlinable public func task(priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable () async -> Void) -> some View
task modifier๋ ์๋์ ๊ฐ์ด ์ฌ์ฉํ๋ค.
VStack { ์๋ต }
.task {
do {
try await vm.getProductDetail(idx: 1)
} catch {
print("error!")
}
}
getProductDetail(idx:)๋ error๋ฅผ ๋ฐํ ํ ์ ์๋ async throw ์ด๊ธฐ ๋๋ฌธ์ task modifier ๋ด๋ถ์์ try catch๋ฅผ ์ฌ์ฉํ๋ค. ๋ง์ฝ task modifier๊ฐ ์๋ Button์ ๋๋ ์ ๋์ ๊ฐ์ ๋์ ์ดํ์ ์ํ๋์ผ ํ๋ค๋ฉด Task.init() ๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
@frozen public struct Task<Success, Failure> : Sendable where Success : Sendable, Failure : Error {
}
initializer๋ task modifier์ ๋์ผํ๋ค.
@discardableResult
public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success)
Button {
Task {
do {
try await vm.getProductDetail(idx: 1)
} catch {
}
}
} label: {
Text("๋ฒํผ์ผ๋ก ์คํ ํ ๊ฒฝ์ฐ")
}
Task๋ก ์์ฑ๋ ์์ ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ ์ฆ์ ์คํ๋๋ฉฐ await ํค์๋๋ก ํด๋น ์ง์ ์์ ์๋ฃ๋ ๊ฐ์ด ๋์์ฌ ๋๊น์ง ๊ธฐ๋ค๋ฆด ์ ์๋ค.
inout paramters
concurrency๋ ๊ด๋ จ๋๊ฑด ์๋์ง๋ง ์์ฃผ ๋์์ ์ ๋ฆฌํ์๋ฉด, inout์ Swift ๋ด์ ์ ์๋ ํค์๋.
ํจ์์ ๊ฐ์ ์ ๋ฌํ ๋ ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋๋ฐ ์ด๋ ๋งค๊ฐ ๋ณ์์ ๊ฐ์ ๋ณ๊ฒฝํด๋ ์๋ ๋ณ์ ๊ฐ์ ์ํฅ์ ๋ฏธ์น์ง ์๋๋ค. inout์ ์ฌ์ฉํ๋ฉด ํจ์์ ํ๋ผ๋ฏธํฐ ๊ฐ ๋ณ๊ฒฝ์ฌํญ์ ์๋ณธ ๋ณ์์ ์ ์ฉํ ์ ์๋ค.
class ProductDetailViewModel: ObservableObject {
@Published var product: Product?
func removeCurrentProduct() {
self.removeProductRequest(product: &product)
}
private func removeProductRequest(product: inout Product?) {
product = nil
}
// product๋ฅผ fetchํ๋ ์ฝ๋๋ ์ ์ธ๋จ
}
SwiftUI์ View์์ removeCurenProduct๋ฅผ ํธ์ถํ๋ฉด viewModel ๋ด๋ถ์ ์ ์๋ private ํจ์์ธ removeProductRequest๋ฅผ ํธ์ถํ๋ค. ์ด๋ ํ๋ผ๋ฏธํฐ๋ก inout ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ฌํ๋ค. ์ ๋ฌ๋ฐ์ ๋งค๊ฐ๋ณ์์ ๊ฐ์ ๋ณ๊ฒฝํ๋ฉด viewModel์ ์๋ณธ ๋ณ์์ธ @Published๋ก ์ ์๋ product์ ๊ฐ์ด nil๋ก ๋ณ๊ฒฝ๋๋ค.
Button {
vm.removeCurrentProduct()
} label: {
Text("์ํ ์ ๋ณด ์ ๊ฑฐํ๊ธฐ")
}
.buttonStyle(.borderedProminent)
Text(vm.product?.title ?? "")
View ์ฝ๋๋ก ์๋ณธ product ๊ฐ์ ๋ณ๊ฒฝ์ ํ์ธํ ์ ์๋ค.
removeProductRequest(product: &product)
๋งค๊ฐ๋ณ์ ์ ๋ฌ์ &(ampersand)์ ์ฌ์ฉํ๋ค. ์ด๋ ๋งค๊ฐ๋ณ์๊ฐ inout์ผ๋ก ์ฌ์ฉ๋๊ณ ์์์ ๋ช ์์ ์ผ๋ก ์๋ ค์ค๋ค.
Isolated Actor
- ๋์์ฑ์ ์์ ํ๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ํ ํ์
- Actor ๋ด์ ๋ฐ์ดํฐ๋ ๋ค๋ฅธ ์ค๋ ๋๋ ์ปจํ ์คํธ์์ ์ ๊ทผ ํ ์ ์์ง๋ง ํน์ ๋ฉ์๋๋ ํ๋กํผํฐ๋ฅผ isolated๋ก ํ์ํ๋ฉด ํด๋น actor์ ์ํ์ ์ ๊ทผํ๊ณ ์์ ํ ์ ์์
- ๋ชจ๋ Actor ํ์ ์ ์๋ฌต์ ์ผ๋ก Sendable์ ๋ฐ๋ฅธ๋ค. actor๋ mutable state์ ๋ํ isolation์ ๋ณด์ฅํ๋ค.
Sendable
- Sendable ํ์ ์ ์ค์ํ๋ ํ์ ์ ํด๋น ํ์ ์ ์ธ์คํด์ค๋ฅผ ์ค๋ ๋ ๊ฐ ์์ ํ๊ฒ ์ ๋ฌ ํ ์ ์์
- Value Type์ ๋ด๋ถ์ ์ผ๋ก Sendable ํ๋กํ ์ฝ์ ๋ฐ๋ฅด๋ฏ๋ก ์ค๋ ๋์ ์์ ์ ๋ณด์ฅํ๋ค.
- Reference Type์ final ํด๋์ค ํน์ ํด๋์ค ๋ด ์์ ํ์ ์ผ๋ก ์ ์ธํ์ฌ Sendable ํ๋กํ ์ฝ์ ์ฌ์ฉํ ์ ์๋ค.
(์ด๋์ ์ฌ์ฉ๋ ์ ์์์ง๋ ์ ๋ชจ๋ฅด๊ฒ ๋ค๐ค)
public static func run(
priority: TaskPriority? = nil,
operation: @escaping @Sendable (_ send: Send<Action>) async throws -> Void,
catch handler: (@Sendable (_ error: Error, _ send: Send<Action>) async -> Void)? = nil,
fileID: StaticString = #fileID,
line: UInt = #line
) -> Self
- TCA์์ ๋น๋๊ธฐ ์ฒ๋ฆฌ ์ดํ Effect๋ฅผ ๋ฐํํ๊ธฐ ์ํด .run์ ์ฌ์ฉํ๋๋ฐ ์ด๋ operation์ @Sendable์ด ๋ค์ด๊ฐ ์๋ค(TCAํ๋ค๊ฐ ์ฌ๊ธฐ๊น์ง ์๋น ใ ใ ใ ใ ใ )
struct SendableLibraryStruct: Sendable {
var name: String
var bookCount: Int
}
- ๊ฐ ํ์ ์ธ ๊ตฌ์กฐ์ฒด๋ Sendable์ ์ค์ํด์ค๋ค.
final class SendableLibraryClass: Sendable {
let name: String
let bookCount: Int
init(name: String, bookCount: Int) {
self.name = name
self.bookCount = bookCount
}
}
- ์ฐธ์กฐ ํ์ ์ธ class๋ final class์ฌ์ผ ํ๊ณ Sendable์ ์ค์ํด ์ค๋ค.
actor SendableTestActor {
private var libraryStruct = SendableLibraryStruct(name: "", bookCount: 0)
private var libraryClass = SendableLibraryClass(name: "", bookCount: 0)
func updateDataWithStruct(libraryInfo: SendableLibraryStruct) {
self.libraryStruct = libraryInfo
}
func updateDataWithClass(libraryInfo: SendableLibraryClass) {
self.libraryClass = libraryInfo
}
func getNameFromClass() async throws -> String {
try await Task.sleep(until: .now + .seconds(1), clock: .continuous)
return libraryClass.name
}
}
- actor๋ ๋์์ฑ ์ ๊ทผ์ ์์ ํ๊ฒ ๊ด๋ฆฌํ๋ค.
- ๋ด๋ถ์ ๋ฐ์ดํฐ๋ ๋ค๋ฅธ ์ค๋ ๋๋ ์คํ ์ปจํ ์คํธ์์ ์ง์ ์ ๊ทผํ ์ ์๋ค.
- ํน์ ๋ฉ์๋๋ ํ๋กํผํฐ๋ฅผ isolated๋ก ํ์ํ๋ฉด ํด๋น actor ์ํ์ ์์ ํ๊ฒ ์ ๊ทผํ๊ณ ์์ ํ ์ ์๋ค.
- ๋ฐ์ดํฐ์ ๋ฌด๊ฒฐ์ฑ์ ์ ์ง์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
class SendableTestViewModel: ObservableObject {
@Published var userName: String = ""
let testActor = SendableTestActor()
func updateStruct() async {
let libraryInfo = SendableLibraryStruct(name: "kim", bookCount: 19)
await testActor.updateDataWithStruct(libraryInfo: libraryInfo)
}
func updateClass() async {
let libraryInfo = SendableLibraryClass(name: "lee", bookCount: 20)
await testActor.updateDataWithClass(libraryInfo: libraryInfo)
}
@MainActor
func getName() async {
do {
self.userName = try await testActor.getNameFromClass()
} catch {
}
}
}
ViewModel์ actor์ธ SendableTestActor๋ฅผ ์ฌ์ฉํด์ ์ค๋ ๋ ๊ฐ์ ์์ ํ๊ฒ ๊ฐ์ ์ ๋ฌ ํ ์ ์๋ค. updateStruct()์ updateClass๋ ๊ฐ์ ์ ๋ฌ ๋ฐ์ ๋ด๋ถ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ์์ ํ๋ค.
func getNameFromClass() async throws -> String {
try await Task.sleep(until: .now + .seconds(1), clock: .continuous)
return libraryClass.name
}
actor์ธ SendableTestActor์ ํ์ธํด ๋ณด๋ฉด ๋ด๋ถ ๋ฐ์ดํฐ ์ค ์ด๋ฆ์ ๋ฐํํ๋ ๋น๋๊ธฐ ํจ์๊ฐ ์๋ค. ์ด๋ฅผ viewModel์์ ์ ๋ฌ๋ฐ์ ์ฌ์ฉํ ์ ์๋ค.
@MainActor
func getName() async {
do {
self.userName = try await testActor.getNameFromClass()
} catch {
}
}
actor์ ์ฌ์ฉํ๋ฉด actor์ state์ ์์ ํ๊ฒ ์ ๊ทผํ๊ณ ์์ ํ๋ฉฐ ๊ฐ์ ๊ฐ์ ธ์ฌ ์ ์๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ์ ๋ฌด๊ฒฐ์ฑ์ ์ ์งํ๋ฉด์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐ ํ ์ ์๋ค.
References
[WWDC21] Use async/await with URLSession์ ์ ์ฉํด๋ณด์..!
ํด๋น ๊ธ์ [WWDC21] Use async/await with URLSession ๋ณด๊ณ ์์ฑํ์ต๋๋ค. Swift Concurrency ์ ๋ํ ์๊ธฐ๊ฐ ๋ช๋ช ๋์์๋ค. Swift Concurrency์ ๋ํด ๊ฐ๋ตํ๊ฒ ์ค๋ช ํด๋ณด์๋ฉด, ์ฝ๋๋ฅผ ์ ํ์ ์ด๊ณ ๊ฐ๊ฒฐํ๊ฒ ๋ง๋ค๊ณ , Nat
dev-mandos.tistory.com
[Swift] async / await & concurrency
Swift 5.5 ์์ ๋ฑ์ฅํ ๋น๋๊ธฐ์ ๋์์ฑ์ ์ํ ๋ฐฉ๋ฒ์ ์์๋ด ์๋ค
sujinnaljin.medium.com
https://www.hackingwithswift.com/sixty/5/10/inout-parameters
[SwiftUI] Sendable
What is the Sendable protocol in Swift? | Swift Concurrency ๋น๋๊ธฐ ํ๊ฒฝ(async)์์ ํน์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ ์์ ํ์ง ์ ๊ฒํ๋ ํ๋กํ ์ฝ์ค๋ ๋ ์์ ์ ๋ณด์ฅํ๋ ์กํฐ, ํน์ ์กํฐ๋ฅผ ์ฌ์ฉํ๋ ์ฌ๋ฌ ๊ฐ์ ์ค๋ ๋ ์
velog.io
[Swift] Sendable
Sendable ๋ฌธ์์ WWDC 22 > Eliminate data races using Swift Concurrency ๋ฅผ ์กฐํฉํด์ ์ฌ๊ตฌ์ฑํ ๋ด์ฉ์ ๋๋ค. [1] Sendable = safe to share Sendable ํ๋กํ ์ฝ์ concurrencey ์ํฉ์์ ์์ ํ๊ฒ ๊ณต์ ๋ ์ ์๋ ํ์ ์ ๋ํ๋
eunjin3786.tistory.com
+) ChatGPT
์ดํ ๊ณต๋ถํ ๊ฒ
Explore structured concurrency in Swift - WWDC21 - Videos - Apple Developer
When you have code that needs to run at the same time as other code, it's important to choose the right tool for the job. We'll take you...
developer.apple.com
Sendable and @Sendable closures explained with code examples
The Sendable protocol and @Sendable attribute help to eliminate data races and create thread-safety in Swift Concurrency.
www.avanderlee.com
'iOS ๐ > Swift' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[SwiftUI/Swift] ์ค์ํํธ ํ๋กํผํฐ ๋ํผ Property wrapper ๊ธฐ์ด (0) | 2023.01.24 |
---|---|
[Swift] ์ค์ํํธ ๊ตฌ์กฐ์ฒด ์์๋ณด๊ธฐ (0) | 2023.01.24 |