이전 글에서는 actor 내부의 비동기 함수도 await 를 기준으로 다른 작업과 교차 실행될 수 있다는 것을 알아봤습니다.
그런데 actor 의 모든 메서드는 반드시 actor 내부에서 실행되어야 할까요? 반대로 actor 외부에서는 항상 await 를 사용해야 할까요?
이번 글에서는 Actor Isolation 과 격리 범위를 조절하는 isolated, nonisolated 에 대해 알아보겠습니다.
1. Actor Isolation 이란?
actor 의 프로퍼티와 인스턴스 메서드는 기본적으로 해당 actor 에 격리 됩니다.
actor BankAccount {
private var balance = 0
func deposit(_ amount: Int) {
balance += amount
}
func currentBalance() -> Int {
balance
}
}
actor 내부에서는 자신의 상태에 동기적으로 접근할 수 있습니다.
func deposit(_ amount: Int) {
balance += amount
}
하지만 actor 외부에서 메서드를 호출하려면 await 가 필요합니다.
let account = BankAccount()
await account.deposit(10_000)
let balance = await account.currentBalance()
deposit() 자체는 동기 함수지만, 호출자는 actor 에서 자신의 차례가 올 때까지 기다려야 할 수 있습니다.
따라서 actor 외부에서 접근하면 잠재적ㅇ니 중단 지점이 되고 await 를 사용해야 합니다.
이처럼 actor 의 상태에 접근하는 코드를 해당 actor 안에서만 실행하도록 제한하는 것을 Actor Isolation 이라고 합니다.
2. nonisolated 란?
actor 의 모든 코드가 반드시 actor 의 보호를 받아야 하는 것은 아닙니다.
예를들어 계좌번호 생성 이후 변경되지 않는 값이 있다고 가정해보겠습니다.
actor BankAccount {
nonisolated let accountNumber: String
private var balance = 0
init(accountNumber: String) {
self.accountNumber = accountNumber
}
}
accountNumber 는 변경되지 않는 값이므로 actor 상태와 관계없이 안전하게 읽을 수 있습니다.
let account = BankAccount(accountNumber: "123-456")
print(account.accountNumber)
nonisolated 로 선언했기 때문에 await 가 필요하지 않습니다.
프로퍼티가 아닌 메서드에서도 사용할 수 있는데요.
actor BankAccount {
nonisolated let accountNumber: String
private var balance = 0
init(accountNumber: String) {
self.accountNumber = accountNumber
}
nonisolated func maskedAccountNumber() -> String {
"****-\(accountNumber.suffix(3))"
}
}
외부에서는 일반적인 동기 메서드처럼 호출할 수 있습니다.
print(account.maskedAccountNumber())
하지만 nonisolated 메서드에서는 actor 에 격리된 상태에 접근할 수 없습니다.
nonisolated func printBalance() {
print(balance)
// Error:
// Actor-isolated property 'balance' cannot be
// referenced from a nonisolated context
}
nonisolated 함수는 actor 밖에서도 동시에 호출될 수 있기 때문입니다.
정리하면 nonisolated 는 다음 의미를 갖습니다.
"이 선언은 actor 의 격리 영역에 포함되지 않으며, actor 상태에 접근하지 않습니다."
3. nonisolated 는 백그라운드 실행이 아니다.
nonisolated 를 사용하면 해당 함수가 백그라운드 Thread 에서 실행된다고 오해하기 쉽습니다.
하지만 nonisolated 는 실행할 Thread 를 지정하는 기능이 아닙니다.
nonisolated func maskedAccountNumber() -> String {
"****-\(accountNumber.suffix(3))"
}
이 코드는 단지 특정 actor 에 격리되지 않았다는 뜻입니다.
즉, 다음 두 질문을 구분해야 합니다.
- 이 코드는 어떤 actor 에 격리되어 있는가?
- 이 코드는 어느 executor 또는 Thread 에서 실행되는가?
nonisolated 는 첫 번째 질문에 대한 기능입니다.
4. isolated 파라미터란?
nonisolated 가 actor 의 격리에서 벗어나는 기능이라면, isolated 파라미터는 일반 함수를 특정 actor 의 격리 영역에서 실행하도록 만듭니다. 다음과 같은 actor 가 있다고 가정하겠습니다.
actor BankAccount {
private var balance = 0
private var transactionCount = 0
func deposit(_ amount: Int) {
balance += amount
transactionCount += 1
}
func currentBalance() -> Int {
balance
}
func currentTransactionCount() -> Int {
transactionCount
}
}
외부에서 여러 값을 조회하려면 각각 await 가 필요합니다.
let balance = await account.currentBalance()
let count = await account.currentTransactionCount()
이 두 호출 사이에는 다른 actor 작업이 실행될 수도 있습니다. 따라서 balance 와 count 가 서로 다른 시점의 값일 가능성이 있습니다.
이때 isolated 파라미터를 사용할 수 있습니다.
func printSummary(
of account: isolated BankAccount
) {
let balance = account.currentBalance()
let count = account.currentTransactionCount()
print("잔액: \(balance)")
print("거래 횟수: \(count)")
}
account 가 isolated 로 선언됐기 때문에 함수 전체가 전달받은 BankAccount 의 격리 영역에서 실행됩니다.
함수 내부에서는 await 없이 actor 메서드를 호출할 수 있습니다.
account.currentBalance()
account.currentTransactionCount()
외부에서는 함수를 호출할 때 한 번만 await 를 사용합니다.
await printSummary(of: account)
printSummary() 안에 다른 await 가 없다면 두 값을 읽는 사이에 다른 actor 작업이 끼어들 수 없습니다.
따라서 동일한 시점의 상태를 조회할 수 있습니다.
5. actor 메서드의 self 도 isolated 다
사실 actor 의 인스턴스 메서드는 기본적으로 self 가 격리된 것처럼 동작합니다.
actor BankAccount {
private var balance = 0
func deposit(_ amount: Int) {
balance += amount
}
}
개념적으로는 다음과 비슷합니다.
func deposit(
account: isolated BankAccount,
amount: Int
) {
account.deposit(amount)
}
actor 메서드에서는 이미 self 의 격리 영역에 있으므로 자신의 프로퍼티와 메서드에 await 없이 접근할 수 있습니다.
isolated 파라미터는 이 동작을 actor 외부의 함수에서도 사용할 수 있게 해주는 기능이라고 이해할 수 있습니다.
6. isolated 를 사용해도 Reentrancy 는 발생합니다.
isolated 파라미터를 사용한다고 해서 함수 전체가 항상 원자적으로 실행되는 것은 아닙니다.
func process(
account: isolated BankAccount
) async {
let balance = account.currentBalance()
await requestApproval()
// ⭐ await 중 account 상태가 변경됐을 수 있습니다.
account.deposit(balance)
}
함수 내부에서 await 를 만나면 이전 글에서 살펴본 actor reentrancy 가 발생할 수 있습니다.
따라서 isolated 를 사용하더라도 await 전후의 상태가 동일하다고 가정해서는 안됩니다.
- isolated : 어떤 actor 에서 실행되는지를 결정
- await : 현재 실행이 중단되고 다른 작업이 끼어들 수 있음
두 개념은 서로 다른 역할을 합니다.
7. 언제 사용해야 할까?
기본적으로 actor 의 기본 격리를 그대로 사용하는 것이 좋습니다.
nonisolated 는 다음과 같은 경우 사용할 수 있습니다.
- 생성 이후 변경되지 않는 식별자를 외부에 제공할 때
- actor 상태가 필요 없는 순수 계산을 수행할 때
- 동기 프로토콜 요구 사항을 구현해야 할 때
- actor 와 무관하게 안전하게 호출할 수 있는 기능일 때
isolated 파라미터는 다음과 같은 경우 유용합니다.
- 함수에서 같은 actor 의 여러 메서드를 호출할 때
- 여러 actor 상태를 하나의 동기 구간에서 읽어야 할 때
- actor 를 사용하는 공통 헬퍼 함수를 만들 때
단순히 await 를 없애기 위해 nonisolated 를 사용하는 것은 피해야 합니다.
특히 nonisolated(unsafe) 는 컴파일러의 격리 검사를 우회하므로, 안전성을 직접 증명할 수 있는 제한적인 상황이 아니라면
사용하지 않는 것이 좋습니다.
8. 정리
Actor Isolation 은 actor 의 상태에 접근하는 코드를 해당 actor 의 격리 영역에서만 실행하도록 제한합니다.
- actor 의 인스턴스 프로퍼티와 메서드는 기본적으로 actor 에 격리됩니다.
- nonisolated 는 선언을 actor 의 격리 영역에서 제외합니다.
- nonisolated 코드에서는 actor 의 가변 상태에 접근할 수 없습니다.
- isolated 파라미터는 함수를 전달받은 actor 의 격리 영역에서 실행하게 합니다.
- isolated 함수에서도 await 를 만나면 Reentrancy 가 발생할 수 있습니다.
- nonisolated 는 백그라운드 실행을 의미하지 않습니다.
지금까지 actor 가 어떤 코드를 보호하고, 격리된 코드에 어떻게 접근하는지 알아봤습니다.
그렇다면 서로 다른 actor 나 Task 사이로 데이터를 전달할 때는 어떤 조건이 필요할까요?
다음 글에선 동시성 경계를 안전하게 넘어갈 수 있는 타입을 표현하는 Sendable 과 @Sendable 에 대해 알아보겠습니다!
'Concurrency' 카테고리의 다른 글
| [Concurrency] Actor 인데 값이 꼬일 수 있다? Actor Reentrancy 알아보기 (1) | 2026.09.16 |
|---|---|
| [Concurrency] Actor 와 Lock, 언제 무엇을 써야 할까? (0) | 2026.09.14 |
| [Concurrency] TaskGroup 으로 타임아웃을 구현했는데 함수가 끝나지 않았던 이유 (0) | 2026.07.24 |
| [Concurrency] async let 과 TaskGroup (0) | 2026.05.09 |
| [Concurrency] for await 알아보기 - AsyncSequence와 비동기 반복문 (0) | 2026.05.04 |

