프로그래밍/Swift Concurrency

Swift Concurrency - 1. 동시성 프로그래밍의 기본 개념 이해

Joo-Topia 2025. 3. 20.
728x90
SMALL

Swift 동시성 프로그래밍의 기본 개념 이해

Swift Concurrency

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 throwstry 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 모델이 제공하는 주요 이점:

  1. 구조화된 동시성: 작업 간의 명확한 계층 구조와 의존성 관리
  2. 컴파일러 안전성: 데이터 경쟁과 같은 동시성 문제를 컴파일 시간에 감지
  3. 코드 가독성: 비동기 코드를 동기 코드처럼 간결하게 작성 가능
  4. 통합된 에러 처리: 동기 코드와 동일한 방식으로 에러 처리 가능
  5. Cooperative Thread Pool: 효율적인 스레드 관리와 스케줄링
  6. 취소 전파: 작업 취소가 자동으로 하위 작업에 전파

Swift Concurrency는 이전의 GCD 모델에 비해 더 안전하고, 가독성이 높으며, 유지보수가 쉬운 동시성 프로그래밍 방식을 제공합니다. 이를 통해 개발자는 동시성 관련 버그를 줄이고 더 효율적인 앱을 개발할 수 있습니다.

 

결론

Swift Concurrency의 기본 개념을 이해하는 것은 현대적인 Swift 앱 개발의 핵심입니다. 기존 GCD와 OperationQueue의 한계를 극복하고, 더 안전하고 직관적인 동시성 프로그래밍 모델을 제공합니다. async/await를 통한 명확한 비동기 작업 표현, Actor를 통한 데이터 안전성 확보, 그리고 구조화된 동시성을 통한 작업 관리는 Swift 애플리케이션의 성능과 안정성을 크게 향상시킵니다.

728x90
SMALL

댓글