Swift 동시성 프로그래밍의 기본 개념 이해
Swift의 동시성 프로그래밍 모델을 이해하기 위해서는 먼저 왜 새로운 동시성 모델이 필요했는지, 그리고 기존 접근법의 한계가 무엇인지 명확히 알아야 합니다. 이 섹션에서는 Swift Concurrency의 등장 배경부터 핵심 개념까지 상세히 설명합니다.
왜 Swift Concurrency가 필요했는가?
기존 GCD(Grand Central Dispatch)와 OperationQueue의 한계
1. 스레드 폭발(Thread Explosion) 문제
// 과도한 스레드 생성 예시
for i in 0..<100 {
DispatchQueue.global().async {
// 각 작업마다 새로운 스레드가 할당될 수 있음
heavyComputation(i)
}
}
GCD는 동시 작업이 증가할 때 너무 많은 스레드를 생성할 수 있습니다. 이는 다음과 같은 문제를 야기합니다:
- 스레드 생성과 컨텍스트 스위칭 오버헤드 증가
- 시스템 리소스(메모리, CPU) 낭비
Thread 1: EXC_BAD_INSTRUCTION
같은 크래시 발생- 스레드 간 지속적인 스위칭으로 인한 앱 성능 저하
2. 복잡한 에러 처리
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(CustomError.noData))
return
}
completion(.success(data))
}.resume()
}
GCD와 콜백 기반 API는 다음과 같은 에러 처리 문제가 있습니다:
- 중첩된 클로저에서 오류 전파가 어려움
- 각 비동기 단계마다 별도의 에러 처리 로직 필요
- 개발자가 completion 호출을 누락할 수 있는 위험성
- 모든 경우의 수를 처리하기 위한 반복적인 코드 작성 필요
3. 우선순위 역전(Priority Inversion) 문제
let lowPriorityQueue = DispatchQueue.global(qos: .background)
let highPriorityQueue = DispatchQueue.global(qos: .userInitiated)
// 낮은 우선순위 작업이 높은 우선순위 작업을 차단할 수 있음
lowPriorityQueue.async {
let resource = obtainSharedResource()
// 리소스 점유 중...
highPriorityQueue.async {
// 공유 리소스를 기다리는 동안 지연됨
useSharedResource(resource)
}
}
GCD의 QoS(Quality of Service)는 때때로 다음과 같은 우선순위 역전 문제를 일으킵니다:
- 낮은 우선순위 작업이 높은 우선순위 작업이 필요한 자원을 점유
- 시스템이 이를 자동으로 해결하지 못하는 상황 발생
- 우선순위 설정 오류로 인한 예측 불가능한 성능 저하
Callback Hell(콜백 지옥) 문제
// 콜백 지옥 예시
fetchUser { user in
fetchUserProfile(userId: user.id) { profile in
fetchProfileImage(url: profile.imageUrl) { image in
fetchImageMetadata(image) { metadata in
applyFilter(image, metadata) { filteredImage in
DispatchQueue.main.async {
self.profileImageView.image = filteredImage
}
}
}
}
}
}
중첩된 콜백 구조는 다음과 같은 심각한 문제를 일으킵니다:
- 들여쓰기 증가로 인한 가독성 저하
- 코드 흐름 파악의 어려움과 직관성 부족
- 휴먼 에러 증가 위험
- 명시적 에러 전파 메커니즘 부재
[weak self]
누락 시 메모리 누수(retain cycle) 발생 가능
Thread-Safety Issues(스레드 안전성 문제)
// 스레드 안전하지 않은 코드
class Counter {
private var value = 0
func increment() -> Int {
value += 1 // 데이터 경쟁 발생 가능 지점
return value
}
}
let counter = Counter()
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
print(counter.increment()) // 예측 불가능한 결과
}
멀티스레드 환경에서 공유 자원에 대한 접근은 다음 문제를 야기합니다:
- 데이터 레이스(Data Race): 여러 스레드가 동시에 같은 메모리에 접근할 때 발생
- 예측 불가능한 결과와 미묘한 버그 발생
- 수동 동기화 메커니즘(lock, semaphore)을 일일이 구현해야 하는 부담
- 데드락(deadlock) 위험성
핵심 개념 상세 설명
1. 동기(Synchronous) vs 비동기(Asynchronous) 작업
동기 작업의 특성:
// 동기 작업 예시
func fetchDataSync() -> Data {
// 네트워크 요청이 완료될 때까지 현재 스레드 차단
let data = /* 네트워크 요청 */
return data
}
// 사용 예
let result = fetchDataSync() // 이 줄이 완료될 때까지 UI 스레드 차단
updateUI(with: result) // 데이터를 받은 후에만 실행
- 호출 스레드 영향: 작업이 완료될 때까지 호출 스레드를 완전히 차단함
- 실행 모델: 순차적 실행 - 한 작업이 끝나야 다음 작업 시작
- UI 영향: 메인 스레드에서 실행 시 앱 프리징 현상 발생 가능
- 에러 처리:
throws
키워드를 통한 직접적 에러 전파 - 코드 구조: 선형적이고 직관적인 코드 흐름
비동기 작업의 특성:
// 비동기 작업 예시 (Swift Concurrency)
func fetchDataAsync() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// 사용 예
Task {
do {
let result = try await fetchDataAsync() // 이 지점에서 일시 중단 가능
updateUI(with: result) // 데이터 수신 후 실행
} catch {
handleError(error)
}
}
- 호출 스레드 영향: 작업 시작 후 즉시 제어권 반환, 스레드 차단 없음
- 일시 중단 포인트:
await
를 통해 작업이 완료될 때까지 실행을 일시 중단 - 스레드 양보:
await
지점에서 스레드 제어권을 포기하고 다른 작업 실행 가능 - 에러 처리:
async throws
와try await
를 통한 통합된 에러 처리 - 컨텍스트 전환: 작업 재개 시 다른 스레드에서 실행될 수 있음
2. 병렬성(Parallelism) vs 동시성(Concurrency) 차이
특성 | 동시성(Concurrency) | 병렬성(Parallelism) |
정의 | 논리적으로 여러 작업이 동시에 진행되는 것처럼 보이게 함 | 물리적으로 여러 작업이 실제 동시에 실행됨 |
실행 방식 | 작업 간 전환을 통한 인터리빙(interleaving) | 동일 시간에 여러 작업 실제 실행 |
필요 조건 | 단일 코어에서도 가능 | 멀티코어 환경 필요 |
Swift 구현 | async/await , Task |
async let , TaskGroup |
작업 단위 | 논리적 작업 분할 | 물리적 스레드 분할 |
주요 목표 | 응답성 향상 | 처리량 증가 |
주의점 | 재진입(reentrancy) 문제 | 데드락 위험 |
동시성과 병렬성의 핵심 차이점:
- 동시성은 "여러 작업이 번갈아가며 실행되어 동시에 진행되는 것처럼 보이는 것"
- 병렬성은 "여러 작업이 실제로 같은 시간(same-time)에 수행되는 것"
- Swift Concurrency는 이 두 개념을 모두 포괄하는 프로그래밍 모델 제공
3. 데이터 경쟁(Data Race)과 안전한 자원 공유
데이터 경쟁 발생 조건:
// 데이터 경쟁 예시
var sharedCounter = 0
Task {
for _ in 0..<1000 {
sharedCounter += 1 // 데이터 경쟁 지점
}
}
Task {
for _ in 0..<1000 {
sharedCounter += 1 // 데이터 경쟁 지점
}
}
데이터 경쟁이 발생하는 조건:
- 두 개 이상의 스레드가 동시에 같은 메모리 위치에 접근
- 그 중 하나 이상이 쓰기(write) 작업일 때
- 접근 간에 동기화 메커니즘이 없을 때
데이터 경쟁의 결과:
- 예측 불가능한 결과값 (예: 두 스레드가 각각 1000번 증가시켜도 결과가 2000이 아닐 수 있음)
- 메모리 손상과 예기치 않은 크래시
- 간헐적으로 발생하여 디버깅이 매우 어려움
안전한 자원 공유 방법:
// Actor를 사용한 안전한 자원 공유
actor SafeCounter {
private var value = 0
func increment() -> Int {
value += 1
return value
}
func getValue() -> Int {
return value
}
}
// 사용 예
let counter = SafeCounter()
Task {
for _ in 0..<1000 {
await counter.increment() // 안전한 접근
}
}
Swift Concurrency에서 제공하는 안전한 자원 공유 메커니즘:
- Actor 모델: 상태 격리를 통한 데이터 경쟁 방지
- Sendable 프로토콜: 스레드 간 안전하게 전달할 수 있는 타입 보장
- 값 타입(Value Types): 복사를 통한 공유 방지로 자연스러운 동시성 안전성 확보
- MainActor: UI 관련 코드를 메인 스레드에서 실행하도록 보장
Swift Concurrency의 핵심 이점
Swift Concurrency 모델이 제공하는 주요 이점:
- 구조화된 동시성: 작업 간의 명확한 계층 구조와 의존성 관리
- 컴파일러 안전성: 데이터 경쟁과 같은 동시성 문제를 컴파일 시간에 감지
- 코드 가독성: 비동기 코드를 동기 코드처럼 간결하게 작성 가능
- 통합된 에러 처리: 동기 코드와 동일한 방식으로 에러 처리 가능
- Cooperative Thread Pool: 효율적인 스레드 관리와 스케줄링
- 취소 전파: 작업 취소가 자동으로 하위 작업에 전파
Swift Concurrency는 이전의 GCD 모델에 비해 더 안전하고, 가독성이 높으며, 유지보수가 쉬운 동시성 프로그래밍 방식을 제공합니다. 이를 통해 개발자는 동시성 관련 버그를 줄이고 더 효율적인 앱을 개발할 수 있습니다.
결론
Swift Concurrency의 기본 개념을 이해하는 것은 현대적인 Swift 앱 개발의 핵심입니다. 기존 GCD와 OperationQueue의 한계를 극복하고, 더 안전하고 직관적인 동시성 프로그래밍 모델을 제공합니다. async/await를 통한 명확한 비동기 작업 표현, Actor를 통한 데이터 안전성 확보, 그리고 구조화된 동시성을 통한 작업 관리는 Swift 애플리케이션의 성능과 안정성을 크게 향상시킵니다.
댓글