Swift

[iOS] About Swift Concurrency

dsungc 2025. 1. 4. 11:39

드디어..!

Swift Concurrency에 대해 글을 쓰는 날이 왔습니다..!

코드적으로 사용하는 것은 어렵지 않았으나 그 외적인 내용이 굉장히 많더군요..!

 

1. Swift Concurrency 도입 배경

기존 비동기 프로그래밍 - GCD 에서의 문제점

 

콜백지옥(callback hell)

 

기존의 비동기 함수 정의 방식

- 오래 걸려서 얻는 결과값을 콜백 클로저 형태로 돌려받음

// 콜백 함수 형태로 사용
func getImage(completionHandler: @escaping (UIImage?) -> Void) {
    DispatchQueue.global().async {
        sleep(5)
        let image = UIImage(systemName: "person")
        completionHandler(image)
    }
}

- 위의 코드를 무한히 호출하게 되면 엄청난 중첩코드가 발생 >> 유지보수 관점에서 좋지 않음

 

 

코드의 복잡성

Race Condition이나 메모리 누수와 같은 동시성 관련 버그 발생 가능

 

2. Swift Concurrency 의 장점

코드 가독성 향상

- async/await 로 비동기 작업을 동기 코드처럼 직과적으로 작성 가능

 

안정성 강화

- Acotr로 데이터 경쟁 문제 방지

- Structured Concurrency로 작업의 생명 주기를 명확히 관리

 

성능최적화

- 작업 스케줄리과 스레드 풀을 통해 시스템 리소스 효율적으로 사용

 

에러 처리 간소화

- try/await 를 통해 에러 처리 명확하게 명시

 

기존 비동기 코드와 호환

- Continuation을 사용해 클로저 기반 API를 async/await로 변환

 

 

3. async/await

async

- 함수가 비동기 작업을 수행함을 나타냄

- 하나의 함수 자체가 실행 / 중간에 멈춤 / 재개 되는 것이 가능한 함수 >> 새로운 방식의 비동기 처리/함수

- 콜백방식이 아닌 값을 직접 리턴하는 것도 가능

 

await

- 비동기 작업의 완료를 기다림

- 잠수 멈출 수 있는 중간 포인트 개념 - 스레드 yielding 이라고도 불림 > 스레드 제어권을 운영체제에 넘김

- 비동기 함수를 호출하는 것이 새로운 작업을 생성하는 건 X

 

// 비동기 함수 정의
func asyncMethod() async -> String {
	// 비동기 작업
	return result
}
// 비동기 함수의 실행
Task {
	let result = await asyncMethod()
}

 

 

4. Task 와 Structured Concurrency

 

Task

- 비동기처리를 할 수 있는 하나의 작업 단위

- 생성 후, 즉시 비동기 처리 시작

- 비동기 함수를 Task 내부에서만 호출 가능 >> 비동기 실행 컨텍스트(비동기함수가 실행될 수 있는 실행환경)를 만드는 것

- 각 Task는 실행 중인 상태와 데이터를 안전하게 과리, 서로 다른 작업간에는 상태 공유 X

- 작업 내부의 코드는 순차적으로 동작

- 컴파일 시점에 thread-safe 하지 않은 코드 작성 불가능 하도록 함 >> Race Condition 문제 방지

- 다른 Task와는 병렬적으로 실행, 런타임에서 적절한 스레드에 작업을 분해하여 실행

 

 

5. 작업 스케줄링

스레드 풀을 통해 작업을 스레드에 효율적으로 분배

 

스레드 재사용

- await로 작업이 중단되면 스레드를 다른 작업에 할당

- await 이후 작업이 다른 스레드에서 재개될 수 있음

 

스레드 풀 관리

- CPU 코어 수와 시스템 부하를 고려하의 최적의 스레드만 생성

 

성능 최적화

- 불필요한 컨텍스트 스위칭 최소화

 

GCD

- 고정된 스레드 풀 >> 작업 수 증가 > Context Switching 많아짐

- 작업의 중단 및 재개 시, 작업이 실행 중이던 스레드를 해제하지 않고 대기

- 이는 한정된 스레드 자원 내 불필요한 스레드 대기와 스위칭 유발

- 결국 개발자가 스레드와 작업간의 효율적 매핑을 해야함

 

Swift Concurrency

- 스레드 재활용 >> await로 중단되면 현재 스레드는 즉시 해제되어 다른 작업 처리 가능

- 중단 시, 다른 작업이 처리 가능 하기 때문에 컨텍스트 스위칭 최소화, 불필요한 대기 스레드 역시 치소화

- 적절한 스레드 관리 개발자가 XX

- Task의 우선순위를 기반으로, 중요한 작업이 자동으로 먼저 실행되도록 관리

6. Actor

- Race Condition을 방지하기 위해 설계된 동시성 모델

- Acotr 내부의 상태는 단일 작업만 접근할 수 있도록 보장

 

사용 예시

actor SharedResource {
    private var data: [String] = []

    func addItem(_ item: String) {
        data.append(item)
    }

    func getAllItems() -> [String] {
        return data
    }
}

let resource = SharedResource()

Task {
    await resource.addItem("First")
}

Task {
    await resource.addItem("Second")
}

Task {
    print(await resource.getAllItems()) // ["First", "Second"]
}

- actor를 활용하여 SharedResource를 Race Condition이 안 일어나도록 정의

 

Race condition 발생 예제

var sharedValue = 0

func incrementSharedValue() {
    sharedValue += 1
}

Task {
    incrementSharedValue()
}

Task {
    incrementSharedValue()
}

print(sharedValue) // 데이터 충돌로 인해 예측 불가능한 값 출력 가능

 

- 기대 결과값은 2 이지만, 2 외에 다른 값이 나올 수 있음

 

고로 다시 정리해보면

 

- 공유 상태를 안전하게 관리

- 동시성 문제가 발생 가능성이 있을 때

- 데이터 무결성 유지

 

위의 조건이 필요할 때 사용

 

 

하지만 반드시 사용해야하는 것은 아님

 

 

7. Continuation

- 기존의 클로저 기반 비동기 API를 async/await로 변환하는 API

 

CheckedContinuation

- 컴파일러가 작업 완료 여부를 확인

- 일반적으로 사용

UnsafeContinuation:

- 개발자가 작업 완료를 보장해야 하며, 컴파일러가 확인하지 않음

- 성능 최적화가 필요한 특수한 상황에 사용

 

왜 변환? 

기존의 방식은 태스크마다 스레드를 새로 생성해서 비동기 처리

but Continuation을 통해 async/await 형식으로 바꾸면

스레드를 재활용할 수 있게 코드를 바꿔주는 것!

 

func fetchData() async throws -> String {
    try await withCheckedContinuation { continuation in
        fetchData { data, error in
            if let data = data {
                continuation.resume(returning: data)
            } else if let error = error {
                continuation.resume(throwing: error)
            }
        }
    }
}

 

 

사용법은 쉬운데..

참 오랫동안 이해가 안돼서 미루고 미루웠던.. Swift Concurrency 였습니다..

드디어 코드적으로도 개념적으로도 이해가 되는 날이..! 왔네요 ㅎ

.

.

.

.