iOS Development Guide

Actor (1) - Data Race와 Actor가 필요한 이유

ZeddiOS의 Swift ) Actor (1) 글을 바탕으로, data race의 원인과 value semantics의 한계, 그리고 왜 actor가 shared mutable state를 위한 언어 차원의 동기화 도구인지 정리한다.

Confirmed from source Inference clearly labeled Knowledge item #74
학습 날짜

2026-04-09

Why This Work Exists

동시성 코드에서 가장 근본적인 문제 중 하나는 data race다. 이 문서는 “값 타입을 쓰면 끝인가?”, “왜 actor가 필요한가?”를 분리해서 이해하기 위해 만든 1편이다.

Scope / Non-scope

  • data race의 원인과 shared mutable state를 설명한다.
  • value semantics와 `let`이 왜 도움이 되는지 정리한다.
  • 값 타입만으로 해결되지 않는 경우 actor가 왜 필요한지 설명한다.
  • Actor isolation의 상세 규칙과 `nonisolated`는 2편 이후로 넘긴다.

As-is

흔한 오해는 `struct`만 쓰면 data race가 자동으로 사라진다는 생각이다. 하지만 값 타입이라도 mutable하게 공유하면 여전히 data race는 발생할 수 있다.

To-be

더 정확한 표현은 이렇다. 값 타입과 `let`은 shared mutable state를 줄이는 데 유리하지만, 공유하면서 변경해야 하는 상태가 남아 있다면 actor 같은 동기화 수단이 필요하다.

What The Developer Must Understand Next

핵심 요약

`struct를 쓰면 끝`이 아니라, `공유 mutable state가 필요하면 actor로 간다`가 이 글의 정확한 결론이다.

API / Data Contract

  • `value semantic`은 타입의 성질이고, `let`은 사용 방식이다.
  • `value semantic 타입 + let` 조합은 concurrent access에서 특히 안전하다.
  • `value semantic 타입 + var shared mutation`은 여전히 data race 가능성이 있다.
  • Actor는 shared mutable state를 동기화된 방식으로 접근하게 만든다.

Risks And Decisions Needed

  • `let이 value semantic 타입이다`라고 쓰면 안 된다. `let`은 타입이 아니라 상수 선언 키워드다.
  • `struct면 안전하다`라고 단정하면 안 된다. `var`로 공유하면 여전히 위험하다.
  • Actor는 모든 동시성 문제를 자동 해결하는 도구가 아니라, shared mutable state 동기화에 초점이 있다.

Class Diagram

classDiagram class SharedMutableState { +read() +write() } class ValueType { +copyOnAssignment() } class LetBinding { +immutableBinding() } class Actor { +serializedAccess() } class DataRace { +occursWhenConcurrentWrite() } SharedMutableState --> DataRace : can cause ValueType --> SharedMutableState : may reduce LetBinding --> SharedMutableState : removes mutation Actor --> SharedMutableState : protects

Sequence Diagram

sequenceDiagram participant T1 as Thread 1 participant T2 as Thread 2 participant C as Counter participant A as Actor Counter rect rgb(255,248,240) Note over T1,C: Shared mutable class T1->>C: increment() T2->>C: increment() C-->>T1: race possible end rect rgb(240,253,244) Note over T1,A: Actor T1->>A: await increment() T2->>A: await increment() A-->>T1: handle one at a time A-->>T2: next access after previous completes end

Flowchart

flowchart LR A["State is shared?"] --> B{"Mutable?"} B -->|"No"| C["Data race risk greatly reduced"] B -->|"Yes"| D{"Value type + let?"} D -->|"Yes"| C D -->|"No"| E{"Shared mutation still needed?"} E -->|"No"| F["Refactor to remove sharing"] E -->|"Yes"| G["Use Actor or other synchronization"]

Detailed Reading

글의 출발점은 간단하다. 여러 스레드가 동시에 같은 데이터에 접근하고, 그중 하나 이상이 write라면 data race가 난다. 이 문제를 줄이려면 데이터가 변경되지 않거나, 애초에 공유되지 않게 만들어야 한다.

그래서 value semantics가 중요하다. 값 타입은 복사처럼 동작하므로 공유 mutable state를 줄이는 방향으로 설계하기 쉽다. 특히 `let`으로 두면 변경 자체가 막히므로 concurrent access에 더 안전하다.

하지만 여기서 끝이 아니다. `struct`라도 `var`로 공유하며 동시에 수정하면 여전히 data race가 발생할 수 있다. 결국 공유 mutable state가 비즈니스적으로 필요하다면, 그때는 lock이나 serial queue 같은 수동 동기화 대신 actor를 고려하게 된다.

Concrete Example

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 가능성이 생긴다.

Why Actor Enters

Actor는 shared mutable state를 표현하기 위한 Swift의 동기화 메커니즘이다. Actor 외부에서는 actor의 상태에 직접 난입하는 대신, actor를 통해서만 접근한다. 그 과정에서 Swift가 한 번에 하나의 작업만 mutable state에 접근하도록 보장한다.

actor Counter {
    var count: Int = 0

    func increment() {
        self.count += 1
    }
}

이 문서 단계에서는 “왜 actor가 필요한가”까지만 정리한다. 외부에서 왜 `await`와 함께 쓰게 되는지는 다음 편에서 더 자세히 다룬다.

Document Conclusion

  • data race를 피하려면 shared mutable state를 줄이는 것이 우선이다.
  • 값 타입과 `let`은 이 목적에 유리하다.
  • 하지만 공유하면서 변경해야 하는 상태가 남으면 value semantics만으로는 부족하다.
  • 그때 actor가 언어 차원의 보장과 함께 들어온다.

QA Checklist

  • `struct면 무조건 안전`이라고 쓰지 않았는지 확인한다.
  • `let`과 `value semantic`을 같은 개념처럼 쓰지 않았는지 확인한다.
  • Actor의 역할을 `shared mutable state 동기화`로 정확히 설명했는지 확인한다.
  • 다음 편에서 다룰 내용과 이번 편 범위를 분리했는지 확인한다.

Operations / Rollout Checklist

  • 이 문서는 Actor 시리즈 1편으로 유지한다.
  • 2편에서는 actor isolation, 외부 접근 시 `await`, `nonisolated`를 이어서 정리한다.
  • 인덱스 HTML에 시리즈로 계속 연결한다.