안녕하세요! 피피아노입니다 🎵
이번 포스팅에서는 Swift에서 동시성 프로그래밍을 할 때 사용하는 Actor에 대해서 정리를 해보려고 합니다!
물론 저번 글에서 동시성에 대해서 다룬 적이 있어서 Actor도 같이 살짝 설명하긴 했지만 자세히 다루진 못해서 이번 포스팅에서 좀 더 구체적으로 다뤄보려고 합니다. 해당 글이 궁금하신 분들은 여기를 참고해주세요!!
그럼 바로 시작하겠습니다!
서론
Swift의 동시성은 여러 작업을 동시에 수행하는 부분에서 성능과 응답성을 향상 시켜줍니다. 하지만 장점이 있는 만큼 단점도 존재합니다.
단점으로는 여러 작업이 동일한 데이터를 동시에 접근할 때 문제가 발생할 수 있고 동시에 접근이 발생하게 되면 읽기 작업과 쓰기 작업이 혼재된다는 점입니다.
여러 작업이 동일한 데이터를 동시에 접근한다는 부분이 이해되지 않을 수도 있는데 쇼핑 앱에서 사용자가 동시에 두 개의 기기에서 같은 계정으로 로그인하여 장바구니를 조작할 때를 예시로 들 수 있습니다.
예를 들어, 한 기기에서 특정 상품을 장바구니에 추가하고 있는 동안, 다른 기기에서 같은 상품을 삭제하려고 시도한다면 데이터 충돌이 일어날 수 있습니다. 이때 데이터 경쟁이 발생해 장바구니가 비정상적으로 동작하거나, 추가와 삭제가 뒤섞여서 앱이 예측하지 못한 결과를 보여줄 수 있습니다. 이런 경우 장바구니에 예상치 못한 아이템이 추가되거나 삭제되지 않는 현상이 생기기도 합니다.
이런 문제를 데이터 경쟁(data race)라고 말하는데 이러한 데이터 경쟁이 발생하게 되면 앱이 충돌하거나 예측할 수 없는 앱 동작으로 이어질 수 있습니다.
그렇다면 데이터 경쟁 문제를 어떻게 해결해야 할까요?
그럴 때 사용하는 것이 바로 오늘 다룰 Actor입니다.
Actor란?
Actor는 한 번에 하나의 작업만 데이터에 접근할 수 있도록 내부 변경 가능 상태에 대한 비동기 접근을 제어하는 Swift 타입입니다. Actor는 참조 타입이고 속성과 생성자, 메서드를 포함하는데 이런 부분에서 클래스와 유사하다고 느끼실 겁니다. Actor는 클래스와 마찬가지로 프로토콜을 준수하고 확장을 사용할 수 있습니다.
Actor 선언 방법
Actor를 정의할 때는 클래스를 선언할 때랑 거의 비슷한데 예를 들어서 클래스는 아래와 선언이 되어 있다고 하면
// class 예시
class ShoppingCartClass {
private var items: [String: Int] = [:]
func addItem(_ item: String, quantity: Int) {
items[item, default: 0] += quantity
}
func getItems() -> [String: Int] {
return items
}
}
Actor는 class 키워드 대신 actor로 바꾼 정도만 차이가 있을 정도로 비슷합니다.
// actor 예시
actor ShoppingCartActor {
private var items: [String: Int] = [:]
func addItem(_ item: String, quantity: Int) {
items[item, default: 0] += quantity
}
func getItems() -> [String: Int] {
return items
}
}
Class와 Actor의 공통점과 차이점
class와 Actor의 공통점과 차이점을 간단하게 짚고 넘어가자면 아래처럼 정리할 수 있습니다.
공통점
- 참조 타입(Reference Type): actor와 class 모두 참조 타입이므로, 인스턴스가 복사되지 않고 동일한 메모리 위치를 참조합니다.
- 프로퍼티와 메서드 정의: actor와 class 모두 프로퍼티와 메서드를 정의할 수 있습니다.
차이점
- 동시성 제어: actor는 동시성 제어가 내장되어 있어, 동일한 데이터에 여러 작업이 동시에 접근하는 데이터 경쟁(data race) 문제를 방지합니다. actor 내부의 프로퍼티에 접근할 때는 비동기적으로 접근해야 하며, Swift가 이를 통해 안전한 동시성 처리를 보장합니다.
- 비동기 호출: actor의 메서드나 프로퍼티에 접근할 때는 await 키워드를 사용해야 합니다. actor 내부 작업은 동시 실행을 방지하며, 안전하게 순차적으로 실행됩니다.
- 상속 불가: actor는 클래스와 달리 상속을 지원하지 않습니다. 대신 protocol을 채택하여 확장하는 방식으로 다형성을 구현할 수 있습니다.
가장 큰 차이점은 Actor는 비동기 함수나 Task 클로저 같은 비동기 콘텍스트 안에서만 생성하거나 접근할 수 있다는 부분입니다.
Actor 호출 및 접근 방법
Actor로 정의한 ShoppingCart라는 인스턴스가 선언되어 있을 때 호출하거나 접근하는 방법은 아래 코드처럼 await 키워드를 사용하면 됩니다.
let cart = ShoppingCart() // ShoppingCart actor의 인스턴스를 생성
// 여러 작업이 동시에 ShoppingCart에 접근하도록 비동기로 호출
Task {
await cart.addItem("Apple", quantity: 3) // 아이템 추가: 비동기 호출, await 필요
}
Task {
await cart.addItem("Banana", quantity: 2) // 다른 아이템 추가: 비동기 호출, await 필요
}
Task {
let success = await cart.removeItem("Apple", quantity: 1) // 아이템 제거: 비동기 호출, await 필요
if success {
print("Item removal successful")
} else {
print("Item removal failed")
}
}
Task {
let items = await cart.getItems() // 장바구니 아이템 조회: 비동기 호출, await 필요
print("Current items in cart: \(items)")
}
소스 코드를 간단하게 설명해보자면
let cart = ShoppingCart()
- ShoppingCart라는 actor의 인스턴스를 생성합니다. actor 인스턴스를 만들 때는 class와 마찬가지로 특별한 비동기 처리가 필요하지 않습니다.
비동기 호출 (Task와 await)
- actor의 메서드에 접근할 때는 await 키워드가 반드시 필요함. 각 작업을 비동기적으로 호출하기 위해 Task 블록 안에서 호출 중
- await cart.addItem(...): await 키워드를 사용하여 addItem 메서드를 호출함. actor의 모든 메서드는 기본적으로 비동기 방식으로 실행 됨
- await cart.removeItem(...): 아이템 제거 메서드도 await를 통해 안전하게 호출함. actor가 동시성을 제어하여 데이터 경쟁을 방지하므로 여러 Task가 동시에 접근해도 안전함
- await cart.getItems(...): 현재 장바구니의 아이템 목록을 조회하는 메서드도 await 키워드를 통해 비동기적으로 호출함
이것도 더 간단하게 요약하자면
- 비동기 접근: actor 메서드를 호출할 때는 항상 await 키워드가 필요
- Task 블록 사용: 비동기 호출을 위해 Task 블록 안에서 actor 메서드에 접근
- 이 방식으로 actor는 동시성 문제를 자동으로 관리하여 데이터 경쟁을 방지
이렇게 요약할 수 있습니다!
데이터 격리(Data Isolation)
데이터 격리는 actor 내부에 정의된 모든 프로퍼티와 메서드를 actor 내부에서만 직접 접근 가능하게 하여 외부에서의 무분별한 접근을 차단하는 개념입니다. 이런 부분이 데이터 보호와 데이터 경쟁 문제를 해결하는 핵심 부분입니다.
- 격리의 목적: actor 내부의 데이터는 오직 actor 자신을 통해서만 접근할 수 있으며, 이때 모든 접근은 비동기적으로 이루어집니다. 이렇게 하면 다른 작업이 동시에 데이터에 접근할 수 없고, 데이터가 안전하게 유지됩니다.
- 외부 접근 제한: 외부에서 actor의 프로퍼티나 메서드에 접근하려면 await 키워드를 사용해 비동기 접근만 가능하도록 제한되어 있어, 동시성으로 인한 문제가 발생하지 않습니다.
Actor에서의 데이터 격리
예를 들어, 장바구니 시스템에서 actor를 사용해 데이터를 격리하는 코드를 살펴보겠습니다.
actor ShoppingCart {
private var items: [String: Int] = [:] // 아이템과 수량을 저장하는 딕셔너리
// 아이템을 추가하는 메서드
func addItem(_ item: String, quantity: Int) {
items[item, default: 0] += quantity
}
// 아이템을 제거하는 메서드
func removeItem(_ item: String, quantity: Int) -> Bool {
guard let currentQuantity = items[item], currentQuantity >= quantity else {
return false
}
items[item] = (currentQuantity == quantity) ? nil : currentQuantity - quantity
return true
}
// 아이템 목록을 가져오는 메서드
func getItems() -> [String: Int] {
return items
}
}
이 코드에서는 ShoppingCart라는 actor가 존재하고 이 actor의 내부에서 items 프로퍼티가 장바구니에 담긴 물건들을 저장하고 있습니다. 그런데 이 코드에서 Shopping actor 내부의 items 프로퍼티는 외부에서 직접 접근할 수 없습니다.
그럼 어떻게 하느냐?
외부에서 접근할 수 있는 유일한 방법은 addItem, removeItem, getItems 같은 메서드를 사용하는 것뿐이고, 이 모든 접근은 비동기적(await 키워드 사용)으로 이루어져야 합니다.
데이터 격리의 작동 방식
Swift의 actor는 내부적으로 actor 프로퍼티에 대한 접근이 일어날 때마다 동시성 제어를 적용하여 다른 비동기 작업들이 데이터에 동시에 접근하지 못하도록 자동으로 차단합니다. 이를 통해 데이터가 안전하게 격리되고, 여러 작업이 같은 데이터를 동시에 수정하려고 할 때 발생할 수 있는 데이터 충돌을 방지합니다.
• 동시 접근 제어: actor 내부의 데이터에 접근하는 모든 작업은 순차적으로 처리되며, 하나의 작업이 완료될 때까지 다른 작업이 대기합니다. 예를 들어, addItem을 호출하는 작업이 실행 중일 때 다른 작업이 items 프로퍼티에 접근하려면 addItem 작업이 끝날 때까지 기다려야 합니다.
• 비동기 호출 (await): actor의 메서드나 프로퍼티에 접근할 때는 항상 await 키워드를 사용하여 비동기 호출로 접근합니다. await는 이 작업이 완료될 때까지 기다리게 하므로, 여러 작업이 동시에 실행되더라도 actor는 작업을 하나씩 순차적으로 처리하여 데이터 격리를 보장합니다.
데이터 격리의 장점
- 안정성: 동시성 관련 오류가 줄어들어 안정적인 데이터 처리가 가능합니다.
- 가독성: actor를 사용하면 동시성 제어 코드가 간결해지고, 데이터 접근 방식이 명확해져 코드 가독성이 높아집니다.
- 유지보수성: 데이터 격리 덕분에 데이터 관련 오류를 사전에 방지할 수 있어 유지보수 작업이 줄어듭니다.
글이 길어지는 관계로 뒷부분은 다음 포스팅에서 이어서 작성하도록 하겠습니다!
오늘은 여기까지 :)
감사합니다.
잘못된 내용이 있거나 더 좋은 내용 피드백은 언제나 환영합니다!
궁금하신 부분은 댓글로 질문 부탁드립니다!
'Apple > Swift' 카테고리의 다른 글
[Swift] n의 배수 고르기 문제 회고 (5) | 2024.11.14 |
---|---|
[Swift] 배열의 유사도 회고 (11) | 2024.11.08 |
[Swift] URL 살펴보기 (0) | 2024.08.07 |
[Swift] 동시성(Concurrency) 톺아보기 (2/2) (2) | 2024.07.25 |
[Swift] 동시성(Concurrency) 톺아보기 (1/2) (0) | 2024.07.23 |