먼저 동시성이란 여러 작업이 동시에 진행되는 것처럼 보이도록 만드는 기술을 말한다. 예를 들어, 네트워크 요청을 처리하면서 UI는 멈추지 않고 사용자의 입력을 계속 받을 수 있도록 하는 것이 이에 해당한다.
따라서 동시성 프로그래밍은 여러 작업을 동시에 실행하거나 비동기적으로 처리해서 앱의 반응성을 높이고 사용자 경험을 개선하는 데 핵심적인 역할을 한다.
UI가 멈추는 것(freezing)을 방지하여 앱의 빠른 반응성을 유지할 수 있다. 리소스도 효율적으로 사용 가능하고, 네트워크/디스크 작업을 비동기 처리할 수 있다.
DispatchQueue.global().async {
// 백그라운드에서 무거운 작업
let image = loadImage()
DispatchQueue.main.async {
// UI 업데이트는 메인 스레드에서
imageView.image = image
}
}
GCD는 스레드를 직접 관리하지 않고 작업 큐(DispatchQueue)를 통해 비동기 처리하는 방식이다. 작업을 실행할 큐에 작업 블록을 제출하고, 해당 큐가 작업을 실행한다.
📌 QoS 우선순위 종류
| QoS | 설명 | 사용 예 |
|---|---|---|
.userInteractive | 가장 높은 우선순위, 즉시 UI 반응 필요 | 제스처 처리, 애니메이션 등 |
.userInitiated | 사용자가 요청한 작업, 빠른 응답 필요 | 문서 열기, 이미지 처리 등 |
.default | 특별한 지정 없는 기본 우선순위 | 일반적인 백그라운드 작업 |
.utility | 시간이 오래 걸리는 작업, UI 민감하지 않음 | 다운로드, 백업 등 |
.background | 최하 우선순위, 눈에 띄지 않아도 되는 작업 | 데이터 동기화, 로그 정리 등 |
✅ 장점
🚨 단점
let queue = OperationQueue()
let operation = BlockOperation {
print("비동기 작업 실행 중")
}
operation.completionBlock = {
print("작업 완료")
}
queue.addOperation(operation)
Foundation 프레임워크에 포함된 객체지향적인 동시성 구현 방법이다. GCD보다 더 구조화된 동시성 API로, OperationQueue는 Operation 객체를 큐에 넣어 비동기 작업을 수행하게 해주는 클래스다.
Operation: 실행할 작업을 캡슐화한 클래스(BlockOperation, CustomOperation)OperationQueue: 작업을 큐에 등록해서 실행✅ 장점
operation.cancel())🚨 단점
func fetchImage() async -> UIImage {
// 비동기 작업
let url = URL(string: "https://example.com/image.png")!
let (data, _) = try! await URLSession.shared.data(from: url)
return UIImage(data: data)!
}
// 사용처 (비동기 context에서만 가능)
Task {
let image = await fetchImage()
imageView.image = image
}
코드가 동기처럼 읽히면서 내부적으로는 비동기로 실행되기 때문에 가독성과 안전성이 뛰어나다.
async: 비동기 함수await: 비동기 함수의 결과를 기다린다Task: 새로운 비동기 컨텍스트 생성Actor: 데이터 경쟁을 막는 안전한 객체✅ 장점
🚨 단점
| 특징 | GCD | OperationQueue | Swift Concurrency |
|---|---|---|---|
| 코드 가독성 | 낮음(콜백 중첩) | 중간 | 높음(동기처럼 읽힘) |
| 작업 제어 | 거의 없음 | 취소/우선순위/의존성 가능 | Task/Actor 등으로 가능 |
| 데이터 경쟁 제어 | 수동 관리 | 수동 관리 | Actor로 안전하게 처리 |
| 추천 용도 | 간단한 백그라운드 작업 | 복잡한 비동기 작업 | 모던 Swift 기반 앱 전체 |
동시성 프로그래밍은 성능을 높일 수 있는 강력한 도구지만, 잘못 사용하면 치명적인 버그를 초래할 수 있다. 그중 대표적인 어려움은 다음 세 가지이다.
여러 쓰레드(또는 작업)가 동시에 같은 데이터에 접근하고 변경하려 할 때, 실행 순서에 따라 결과가 달라지는 현상이다.
var counter = 0
DispatchQueue.global().async {
counter += 1 // A 쓰레드
}
DispatchQueue.global().async {
counter += 1 // B 쓰레드
}
counter == 2counter == 1(덮어쓰기 발생)서로 잠금을 기다리며 영원히 끝나지 않는 상태에 빠지는 문제이다. 서로 기다리기만 해서 아무 일도 진행되지 않는다.
let lockA = NSLock()
let lockB = NSLock()
DispatchQueue.global().async {
lockA.lock()
lockB.lock()
// 작업 수행
lockB.unlock()
lockA.unlock()
}
DispatchQueue.global().async {
lockB.lock()
lockA.lock() // 여기서 A가 풀릴 때까지 대기 → 서로 기다림
lockA.unlock()
lockB.unlock()
}
DispatchSemaphore, DispatchQueue.sync() 사용 주의비동기 작업을 콜백으로 중첩해 작성하면, 코드 가독성이 매우 떨어지고 유지보수가 어려워지는 현상이다.
func loadUserData() {
fetchUserID { id in
fetchProfile(id: id) { profile in
fetchPosts(userID: id) { posts in
print("모든 데이터 처리 완료")
}
}
}
}
// async / await로 해결
func loadUserData() async {
let id = await fetchUserID()
let profile = await fetchProfile(id: id)
let posts = await fetchPosts(userID: id)
}
async/await)| 문제 | 원인 | 해결 방법 |
|---|---|---|
| 데이터 경쟁 | 여러 스레드의 동시 접근 | 락, 직렬 큐, Actor |
| 데드락 | 락을 잘못된 순서로 걸었을 때 | 락 순서 고정, 락 최소화 |
| 콜백 지옥 | 중첩된 비동기 코드 | async/await, Combine |