티스토리 뷰

Swift

Swift - Swift Concurrency

DevDiana 2026. 6. 23. 12:44

안녕하세요 Diana 입니다

 

오늘은 Swift의 Swift Concurrency에 대해 자세히 알아보며 Sendable, Actor 등에 대해 자세히 알아보도록 하겠습니다.

 


 

 

✅ Task에 대해

Swift Concurrency에서 우리는 Task를 사용하게 됩니다.
Task는 각자 자신의 자원을 가진 독립적인 일꾼으로 처음부터 끝까지 순차적으로 작업을 수행하고 Await를 만날 경우 작업이 중단될 수도 있습니다.

이때 완벽히 독립적인 경우 우리는 Data Race가 없는 독립성을 보장할 수 있지만 각 Task 별로 데이터의 통신이 불가능해져 쓸모가 없어집니다.

 

Data Race의 근본적인 원인은 하나의 메모리 주소에 실행 흐름이 같은 두 개 이상의 스레드에서 접근하는 것으로 그 중 하나 이상은 반드시 쓰기 작업이 포함되어 있어야 합니다.

CPU 입장에서는 8바이트 값을 쓰는 도중 다른 코어가 해당 값을 읽게 되면 절반만 갱신된 상태가 읽히게 되며 캐시 일관성과 명령어 재 배치까지 곂치게 되면 때마다 결과가 다르게 나오는 Race Condition이 발생하게 됩니다.

 

Task의 데이터 이동에 대해 생각해봅시다.

 

Task는 비동기적 작업을 실행하는 구조체로 구현과 동시에 실행이 됩니다. 하지만 Swift Concurrency는 이런 Task를 스케줄링 하며 현재 상황에 따라 실행을 약간 지연시킬 수 있습니다.

 

Task의 데이터를 옮기는 방법에는 두 가지가 있습니다.

  • 값 타입으로 옮긴다: 바이트를 복사하며 복사가 끝나면 두 Task는 서로 다른 메모리를 가집니다. 공유 자체가 없으니 Data Race가 물리적으로 불가능합니다.
  • 참조 타입으로 옮긴다: 포인터만 복사하며 데이터는 힙에 하나뿐이라 두 Task가 같은 객체를 가리킵니다. 즉 공유 가변 메모리로 Data Race의 가능성을 내포하고 있습니다.

그래서 핵심은 컴파일러가 데이터 이동을 허용하기 위해서는 "두 Task가 같은 가변 메모리를 공유하지 않는다" 는 것을 증명해야 한다는 것 입니다.

 

✅ Sendable이란?

이 증명을 위해 나온 것이 Sendable 입니다.

 

매 호출 지점마다 증명을 하는 것은 전역 분석이라 사실상 불가능합니다. 따라서 Swift는 해당 질문을 값 단위에서 타입 단위로 끌어올립니다.

 

아래와 같이 "이 타입은 도메인을 넘나들어도 Data Race를 만들지 못하는가?" 가 증명이 되면 그 타입에 Sendable을 부여하게 됩니다.

  • 값 타입: 멤버가 전부 Sendable이면 OK (복사되니까). 단 struct 안에 class 참조가 끼어 있으면 그 참조는 복사돼도 공유되므로 재귀적으로 막힙니다 — 그래서 멤버까지 검사합니다.
  • 불변 참조 타입(final + 전부 let): 쓰기가 없으니 동시 읽기는 안전 → Sendable 허용.
  • 락으로 자체 동기화하는 참조 타입: 실제론 안전하지만 컴파일러가 그 락을 이해 못 함 → @unchecked로 "안전은 내가 책임진다" 선언.

Sendable은 컴파일러 차원에서의 전역 분석을 포기하는 대신 타입만 보고 로컬 판단을 할 수 있게 해주는 계약입니다.

 

그럼 이 경계를 어디에 세워야 할까요?

 

Data Race를 막으려면 애초에 "누가 이 메모리를 만질 수 있나"의 경계선이 있어야 합니다. 그래서 Task를 하나의 격리 도메인으로 정의하죠.

  • 도메인 내부: 코드가 순차 실행됨. 동시 접근 자체가 없음. Data Race 불가능
  • 도메인 사이: 오직 Sendable을 통과한 테이터만 통과 가능

이렇게 경계를 그으면 "프로그램 어딘가에 Race가 존재하는가"의 질문이 "경계를 넘는 데이터가 Sendable 인가?"의 국소적인 질문으로 환원됩니다.

 

여기서 모순이 발생합니다.

 

분명 완전히 격리되면 안전은 한데 협력이 불가능합니다. 의미있는 프로그램이 되려면 여러 Task들을 함께 읽고 쓰는 가변 상태가 필요한데 이 공유 가변 상태는 바로 저희가 없애려던 위험요소였단 말이죠.

 

이때 사용하는게 바로 Actor입니다.

 

Actor은 가변상태를 가지되, 그 상태에서 한번에 하나의 Task만 접근하도록 런타임이 강제로 직렬화합니다.

즉, 개발자가 손으로 락을 거는 대신, 타입 시스템이 상호배제를 보장하는 것이죠. 상태는 Actor의 격리 도메인 안에 살고 밖에서 접근하려면 줄을 서야 하니 진입이 잠재적으로 중단 됩니다.

  • Actor: 공유 가변 상태가 필요하다 와 공유 가변 상태는 위험하다를 직렬화해 화해시킨 장치

여기서 설계 선택이 하나 더 끼어듭니다. Actor가 어떤 작업을 await에서 멈출 때, 그 작업을 붙잡은 채 기다릴것이냐 vs 풀어주고 다른 작업을 받을 것(Reentrant)이냐. Swift는 Reentrant를 선택했습니다. 그렇지 않으면 서로의 Actor을 기다리다 데드락이 나기 때문입니다.

 

하지만 이런 Actor도 Race Condition에서 자유롭지 못합니다. Actor 내부에 Await가 있는 경우 전후로 Actor의 상태가 바뀔 수 있습니다. 메모리는 깨지지 않지만 논리적 불변식이 깨질 수 있죠. 따라서 Actor 안의 한 단위 작업은 Await 없이 동기로, 트랜잭션처럼 짜라 는 권고가 따라붙고 있습니다.

 

그럼 Lock과 Actor의 차이는 무엇일까요?

둘다 "한번에 하나만"을 보장한다는 점에서 비슷하지만 핵심 차이는 Blocking이냐 Suspending이냐 입니다.

 

Lock은 스레드를 막고 Actor은 작업을 미룹니다.

 

Lock(NSLock, mutex)는 스레드 자체를 블록시킵니다. 그 스레드는 OS 입장에서 잠든 상태가 되며 아무 일도 못하죠.

스레드는 비싼 자원이기 때문에 Lock 경합이 심하면 스레드가 줄줄이 잠들면서 스레드 풀이 고갈됩니다. 이게 바로 Thread Explosion의 원인이죠.

 

Actor은 스레드가 아닌 Task를 멈춥니다. 해당 Task는 나중에 깨워달라는 등록 이후에 스레드에서 내려옵니다(Suspend). 그렇게 시스템으로 Suspend된 스레드는 곧바로 다른 작업을 이어나갈 수 있게 되죠.

즉, Lock은 스레드를 인질로 잡고 대기한다는 것이고 Actor은 작업을 큐에 넣고 스레드는 시스템으로 돌려보냅니다.

Actor의 경우 스레드는 보통 코어 수에 맞춰 동작해 CPU의 부담도 줄어듭니다.

 

또 다른 차이점은 어디서 멈추는가 입니다.

 

Lock은 블로킹 시점을 알기 어려우며 어느 줄에서나 우리도 모르게 멈출 수 있습니다. 하지만 Actor은 반드시 await에서만 멈추며 멈추는 지점이 문법에 강제 노출됩니다.

 

그럼 이 멈추는 동안 무슨일이 일어날까요

 

Reentrancy를 생각해보면 Lock은 재진입을 하지 않습니다. 

스레드 A가 락을 잡고 임계 영역 안에서 멈춰잇으면, 그 락은 완전히 잠긴상태며 임계 영역 중간에 다른 작업이 끼어들 여지가 없죠.

따라서 Lock이 걸려있는 동안 데드락이 걸릴 지언정 상태는 절대 바뀌지 않습니다.

A가 B의 락을, B가 A의 락을 기다리면 둘다 영원히 잠들게 되는 것이죠.

 

Actor은 다릅니다. Actor은 재진입을 하며 작업이 await에서 멈추면 Actor은 풀려나 다른 작업을 받습니다. 스레드가 잠든 것이 아니기 때문에 데드락이 발생하지는 않지만 Await 전후로 Actor 상태가 바뀔 수 있다는 위험이 발생합니다. 이를 표로 정리하면 아래와 같습니다.

 

  Lock Actor
멈추는 대상 스레드 (blocking) 작업 (suspending)
멈출 수 있는 위치 어디서나 (암묵적) await만 (명시적)
임계 영역 중간 끼어들기 불가능 await에서 가능 (reentrant)
데드락 가능 구조적으로 회피
자리 비운 동안 상태 보존 보장됨 보장 안 됨

 

추가적으로 Lock은 임계 영역을 보호한다는 특징을 가지고 있으며 Actor은 데이터 자체를 격리합니다.

 

오늘은 이렇게 Concurrency에 대해 알아보며 Swift 내부에서 정확한 데이터 전달을 위한 방법들 또한 알아봤습니다.

솔직히 적고나니 너무 두서 없이 적은 감이 없지 않아 있긴 한데 개인 기록용이라는 핑계하에 넘어가봅니다 ㅎㅎ..

 

출처

https://developer.apple.com/videos/play/wwdc2022/110351/

https://developer.apple.com/documentation/swift/sendable
https://developer.apple.com/documentation/Swift/Actor

https://eunjin3786.tistory.com/572

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/08   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함