Why This Work Exists
동시성 코드에서 가장 근본적인 문제 중 하나는 data race다. 이 문서는 “값 타입을 쓰면 끝인가?”, “왜 actor가 필요한가?”를 분리해서 이해하기 위해 만든 1편이다.
ZeddiOS의 Swift ) Actor (1) 글을 바탕으로, data race의 원인과 value semantics의 한계, 그리고 왜 actor가 shared mutable state를 위한 언어 차원의 동기화 도구인지 정리한다.
2026-04-09
동시성 코드에서 가장 근본적인 문제 중 하나는 data race다. 이 문서는 “값 타입을 쓰면 끝인가?”, “왜 actor가 필요한가?”를 분리해서 이해하기 위해 만든 1편이다.
흔한 오해는 `struct`만 쓰면 data race가 자동으로 사라진다는 생각이다. 하지만 값 타입이라도 mutable하게 공유하면 여전히 data race는 발생할 수 있다.
더 정확한 표현은 이렇다. 값 타입과 `let`은 shared mutable state를 줄이는 데 유리하지만, 공유하면서 변경해야 하는 상태가 남아 있다면 actor 같은 동기화 수단이 필요하다.
`struct를 쓰면 끝`이 아니라, `공유 mutable state가 필요하면 actor로 간다`가 이 글의 정확한 결론이다.
글의 출발점은 간단하다. 여러 스레드가 동시에 같은 데이터에 접근하고, 그중 하나 이상이 write라면 data race가 난다. 이 문제를 줄이려면 데이터가 변경되지 않거나, 애초에 공유되지 않게 만들어야 한다.
그래서 value semantics가 중요하다. 값 타입은 복사처럼 동작하므로 공유 mutable state를 줄이는 방향으로 설계하기 쉽다. 특히 `let`으로 두면 변경 자체가 막히므로 concurrent access에 더 안전하다.
하지만 여기서 끝이 아니다. `struct`라도 `var`로 공유하며 동시에 수정하면 여전히 data race가 발생할 수 있다. 결국 공유 mutable state가 비즈니스적으로 필요하다면, 그때는 lock이나 serial queue 같은 수동 동기화 대신 actor를 고려하게 된다.
class Counter {
var count: Int = 0
func increment() {
self.count += 1
}
}위 클래스는 여러 스레드에서 동시에 `increment()`를 호출하면 data race가 발생할 수 있다.
struct Counter {
var count: Int = 0
mutating func increment() {
self.count += 1
}
}struct로 바꾸면 value semantics를 활용하기 쉬워지지만, `let`이면 mutating을 못 하고 `var`로 공유하면 다시 race 가능성이 생긴다.
Actor는 shared mutable state를 표현하기 위한 Swift의 동기화 메커니즘이다. Actor 외부에서는 actor의 상태에 직접 난입하는 대신, actor를 통해서만 접근한다. 그 과정에서 Swift가 한 번에 하나의 작업만 mutable state에 접근하도록 보장한다.
actor Counter {
var count: Int = 0
func increment() {
self.count += 1
}
}이 문서 단계에서는 “왜 actor가 필요한가”까지만 정리한다. 외부에서 왜 `await`와 함께 쓰게 되는지는 다음 편에서 더 자세히 다룬다.