| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 영화일기
- swift concurrency
- 코딩공부
- sopt ios
- 백준
- 영화후기
- SOPT
- 리뷰
- sopt 35기
- 플러터
- 영화리뷰
- 토이프로젝트
- 독서일기
- 자바
- IOS
- 키노
- Flutter Toy Project
- SWIFT
- 영화기록
- 오블완
- 티스토리챌린지
- 자바 스터디
- java
- 영화
- 자바공부
- 새벽녘 소소한 기록
- MVVM-C
- Flutter
- 프로그래머스
- 일기
- Today
- Total
새벽의 기록
[iOS] Swift Concurrency 박살내기 #1 - iOS에서의 동시성과 비동기 프로그래밍 방법 본문
iOS에서의 동시성 프로그래밍과 iOS의 비동기 프로그래밍을 검색하면 두 키워드 모두 GCD, Operation, async/await이 나올 것이다.
동시성과 비동기는 엄연히 다른 개념인데 왜 두 키워드 모두 같은 결과가 나올까?
왜냐하면 이 둘은 매우 밀접하게 연관되어 있는 관계이기 때문이다.
비동기(목표)를 달성하기 위한 기술적 수단으로 동시성(방법)을 사용하기 때문에 같이 검색되는 것이다.
GCD, Operation, async/await는 모두 이 '비동기'라는 목표를 '동시성'이라는 방법으로 구현하도록 도와주는 도구들이다.
동시성과 비동기의 개념이 헷갈릴 수 있어서 이것부터 정리하고 가겠다.
개념 바로잡기: 동시성 vs 비동기
동시성은 여러 작업을 동시에 처리하는 능력이며, 비동기 프로그래밍은 작업의 완료를 기다리지 않고 다른 작업을 수행하는 방식이다.
이해를 돕기 위해 카페에 커피를 주문하는 상황을 비유로 들어보겠다.
여기서 나는 메인 스레드(UI 스레드)이고, 바리스타는 CPU(작업 처리자)이다.
비동기(Asynchrony): "기다리지 않는다"
비동기는 작업을 요청한 후, 그 작업이 끝날 때까지 기다리지 않고(Non-Blocking) 바로 다음 일을 처리하는프로그래밍 모델이다.
- 동기 (Sync): 카페 키오스크에서 주문(작업 요청)하고, 커피가 나올 때까지 그 자리에서 계속 기다린다.커피를 받아야만 카페를 나갈 수 있다. (Blocking)
- 비동기 (Async): 키오스크에서 주문(작업 요청)하고 진동벨을 받는다. 그리고 자리에 가서 앉아 다른 일(웹 서핑, 친구와 대화)을 한다. 나중에 진동벨이 울리면(Callback) 커피를 받으러 간다.
iOS 앱에서 메인 스레드(나)가 네트워크 통신이나 대용량 파일 읽기 같은 오래 걸리는 작업을 동기(Sync)로 요청하면, 앱 전체가 멈춘다(Blocking).
이는 커피가 나올 때까지 키오스크 앞에 서 있는 것과 같다.
비동기(Async)는 이 문제를 해결한다.
메인 스레드가 오래 걸리는 작업을 요청하면(주문) 바로 리턴(진동벨 받기)한다.
그리고 메인 스레드는 계속해서 사용자 터치에 반응하고 UI를 그리는 등 다른 작업을 한다.
그리고 나중에 작업이 완료되면(진동벨 울림) 결과를 받아 UI를 업데이트한다.
비동기의 핵심: "작업을 시키고, 일단 나는 내 할 일 하러 간다."
Concurrency vs. Parallelism (동시성 vs. 병렬성)
이제 내가 자리를 비운 사이 커피가 어떻게 만들어지는지, 즉 바리스타(CPU)의 관점을 보겠다.
동시성 (Concurrency): 여러 작업을 동시에 다루는 것
바리스타가 한 명뿐이라고 가정한다.
이 바리스타는 A 손님의 에스프레소를 추출하는 동안 B 손님의 우유를 스팀하고, C 손님의 주문을 받는 등 여러 작업을 번갈아 가며 (Context Switching) 처리한다.
겉보기에는 여러 작업이 동시에 진행되는 것처럼 보이지만, 실제로는 한순간에 하나의 작업만 처리한다.
이것이 동시성이다. 따라서 싱글 코어 CPU에서도 동시성은 가능하다.
병렬성 (Parallelism): 여러 작업을 물리적으로 동시에 실행하는 것
바리스타가 두 명 이상이라고 가정한다.
바리스타 1이 A 손님의 에스프레소를 추출하는 순간에, 바리스타 2가 B 손님의 우유를 스팀한다.
이것이 병렬성이며, 멀티 코어 CPU가 있어야만 가능하다.
그래서 왜 같이 검색되는데? (무슨 관계길래?)
"나는 비동기(Async)로 주문하고 싶다. (목표)"
"그러려면 내가 아닌 누군가(바리스타)가 내 커피를 동시성(Concurrency)으로 처리해야 한다. (방법)"
이것이 핵심이다.
iOS 앱(메인 스레드)이 비동기로 동작하려면, 즉 UI를 멈추지 않으려면, 오래 걸리는 작업을 다른 스레드에게 맡겨야 한다.
또한 시스템은 이 다른 스레드들을 효율적으로 관리해야 한다.
여러 앱, 여러 작업의 요청을 받아 동시성(때로는 병렬성)을 활용해 처리한다.
GCD, Operation, async/await는 바로 이 다른 스레드에게 작업을 맡기는 과정을 쉽게 하도록 도와주는 도구다.
개발자들은 이 도구들을 사용해 비동기 프로그래밍을 구현한다.
GCD(Grand Central Dispatch)
스레드 관리를 OS에 맡기고 개발자는 작업(Task)을 대기열(Queue)에 넣는 저수준 도구.
작업(Task)과 큐(Queue)
GCD는 두 가지 핵심 요소로 동작한다.
- 작업 (Task): 실행해야 할 코드 조각으로, Swift에서는 주로 클로저(Closure) 형태로 표현된다.
- DispatchQueue (디스패치 큐): 요청한 작업(Task)들을 선입선출(FIFO) 순서로 담아두는 대기열.
개발자가 할 일은 작업(클로저)을 만들어 큐에 넣는 것 뿐이다.
그러면 GCD는 이 큐에서 작업을 하나씩 꺼내어 스레드 풀(Thread Pool)이라는 시스템이 관리하는 스레드 집합에 분배하여 실행시킨다.
두 가지 작업 방식: async vs sync
큐에 작업을 넣는 방식은 두 가지다.
이는 작업을 시킨 스레드가 기다릴 것인가?를 결정한다.
async(Asynchronous, 비동기)
- 큐에 작업을 던져놓고(dispatch), 즉시 리턴한다.
- 작업이 완료될 때까지 기다리지 않는다. (Non-Blocking)
- 사용 예: 네트워크 요청(작업) 백그라운드 큐에 보내놓고, 계속 UI 그리기. (가장 일반적인 사용법)
sync(Synchronous, 동기)
- 큐에 작업을 보내고, 그 작업이 완료될 때까지 그 자리에서 멈춰서 기다린다. (Blocking)
- 사용 예: 주로 데이터 동기화, Data Race 방지용
Main Queue, Global Queue
Main Queue (DispatchQueue.main)
- Serial Queue (직렬 큐)
- 앱의 메인 스레드에서 작업을 처리하는 유일한 큐.
- 이 큐에 들어온 작업은 무조건 순서대로, 한 번에 하나씩만 실행.
- 모든 UI 업데이트(UILabel 텍스트 변경, 버튼 활성화 등)는 반드시 Main Queue에서 수행해야 한다.
Global Queues (DispatchQueue.global())
- Concurrent Queue (동시 큐)
- 시스템이 기본으로 제공하는 백그라운드 작업용 큐.
- 이 큐에 들어온 작업은 동시에 여러 개가 여러 스레드에 분배되어 실행된다. (작업이 끝나는 순서는 보장되지 않음)
- Global Queue에 작업을 보낼 때는 이 작업이 얼마나 중요한지 우선순위(QoS)를 알려줘야 한다. 시스템은 이 QoS를 보고 스레드 배분, CPU 사용량, 전력 소모 등을 최적화한다.
Global Queues의 우선순위(QoS)
- .userInteractive (최고):
- 지금 당장 사용자와 상호작용하는 작업. (예: UI 애니메이션, 스크롤 반응)
- 즉각 반응이 없으면 앱이 멈춘 것처럼 보인다. (메인 스레드 급)
- .userInitiated (높음):
- 사용자가 시작한 작업이며 즉각적인 결과가 필요함. (예: 버튼을 눌러 문서 열기, 이메일 전송)
- .default (기본):
- 기본값.
- .utility (낮음):
- 시간이 다소 걸리는 작업. (예: 대부분의 네트워크 요청, 데이터 다운로드)
- 사용자가 진행 과정을 인지할 필요는 없다.
- .background (최저):
- 사용자가 전혀 인지하지 못하는 백그라운드 작업.
GCD 예시
// UI 로딩 시작
loadingIndicator.startAnimating()
// 1. 오래 걸리는 작업(네트워킹)을 .utility 우선순위의 Global Queue에 비동기(async)로 보냄
DispatchQueue.global(qos: .utility).async {
// (백그라운드 스레드에서 실행)
print("백그라운드 작업 시작... 🏃♂️")
let data = try? Data(contentsOf: someLargeImageURL) // 예: 용량 큰 이미지 다운로드
let image = UIImage(data: data ?? Data())
print("백그라운드 작업 완료! ✅")
// 2. 작업이 끝나면, UI 업데이트를 위해 Main Queue에 비동기(async)로 보냄
// 콜백 중첩
DispatchQueue.main.async {
// (메인 스레드에서 실행)
print("메인 스레드에서 UI 업데이트 🖼️")
self.imageView.image = image
self.loadingIndicator.stopAnimating()
}
}
// 3. async로 보냈기 때문에, 위 작업이 끝나길 기다리지 않고 바로 다음 코드 실행
print("메인 스레드에서 다른 일 하는 중... 🎨")
[출력 순서]
/*
메인 스레드에서 다른 일 하는 중... 🎨
백그라운드 작업 시작... 🏃♂️
백그라운드 작업 완료! ✅
메인 스레드에서 UI 업데이트 🖼️
*/
GCD 단점
콜백 지옥 (Callback Hell)
위 예시처럼 작업이 중첩되면 가독성이 급격히 떨어진다.
또한 콜백 안에서 수동으로 Result 타입을 전달하는 등 에러처리가 번거롭고 실수할 확률이 높다.
복잡한 제어 불가
일단 큐에 보낸 작업은 취소하기가 거의 불가능하다. 또한 A 작업이 끝나야 B 작업 시작 같은 의존성 관리가 복잡하다.
Operation
Operation은 GCD의 단점(특히 의존성 관리)을 보완하기 위해 나온 도구다.
GCD 위에 구축된 래퍼(Wrapper)이며, 더 복잡한 동시성 제어를 가능하게 한다.
해야 할 '작업' 자체를 하나의 객체로 만든다. 그리고 이 객체들을 OperationQueue라는 대기열에 넣는다
GCD 대비 장점
의존성(Dependencies) 관리
operationB.addDependency(operationA) → B 작업은 A 작업이 '완료(finished)' 상태가 되기 전까지 절대 시작하지 마라.
취소(Cancellation)
operation.cancel()을 호출하여 작업을 취소할 수 있다.
상태(State) 모니터링
isReady, isExecuting, isFinished 같은 상태 값을 가지며, 이 상태 변화를 감지할 수 있다.
동시 실행 제어
queue.maxConcurrentOperationCount = 2 → 이 큐에서는 동시에 최대 2개의 작업만 병렬로 실행해라.
Operation 예시
// 1. 작업(Operation)들을 정의
let fetchProfileOperation = BlockOperation {
print("1. 프로필 이미지 다운로드 중...")
// ... 다운로드 ...
}
let resizeOperation = BlockOperation {
print("2. 이미지 리사이징 중...")
// ... 리사이징 ...
}
let applyFilterOperation = BlockOperation {
print("3. 필터 적용 중...")
// ... 필터 적용 ...
}
// 2. 의존성 설정 (B는 A에 의존, C는 B에 의존)
resizeOperation.addDependency(fetchProfileOperation)
applyFilterOperation.addDependency(resizeOperation)
// 3. 큐에 추가 (순서 상관없이 넣어도 의존성에 따라 실행됨)
let queue = OperationQueue()
queue.addOperations([applyFilterOperation, fetchProfileOperation, resizeOperation], waitUntilFinished: false)
// [실행 결과]
// 1. 프로필 이미지 다운로드 중...
// 2. 이미지 리사이징 중...
// 3. 필터 적용 중...
Operation 단점
GCD보다 무거우며 여전히 콜백 지옥을 유발할 수 있다.
이 콜백 지옥이 되게 큼
async/await (Swift Concurrency)
Swift 5.5 부터 도입된 비동기 코드를 동기 코드처럼 작성하게 도와주는 문법
async (Asynchronous Function)
- 함수가 비동기적으로 실행될 수 있음을 나타낸다.
- 이 함수는 일시 중단(suspend)될 수 있다.
await (Suspension Point)
- async 함수를 호출할 때 사용한다.
- await를 만나면, 해당 작업이 완료될 때까지 현재 함수를 일시 중단시킨다.
- 작업이 완료되면, await 다음 줄부터 코드가 다시 실행된다.
중요한 점은 스레드를 차단(Block)하는 GCD의 sync와는 다르다는 것이다.
일시 중단함으로써 스레드의 제어권을 시스템에 넘겨주고, 스레드는 다른 작업을 하러 갈 수 있다.
(차단 하면 그 스레드는 작업을 못 하고 멈춤)
제어권을 넘겨주고 급한 작업들이 완료되면, 시스템은 이 함수의 나머지 부분을 다시 이어서(resume) 실행한다.
이때, 꼭 이전과 동일한 스레드가 아닐 수도 있다! 즉, 제어권을 넘겨준 작업이 끝나야 재개되는 것이 아니라 이전에 실행되고 있던 다른 작업들 중에 작업이 완료돼서 스레드가 남으면 그 스레드를 사용할 수도 있다는 뜻
콜백 지옥 해결
앞서 언급했든 기존 GCD나 Operation은 작업이 완료되는 시점을 클로저(콜백)로 받아야 했다.
따라서 작업이 여러 개 중첩되면 코드가 피라미드처럼 안으로 파고드는 콜백 지옥이 펼쳐졌다.
[Before - GCD]
func fetchAndProcessImage(completion: @escaping (UIImage) -> Void) {
downloadImageData { data in // 1. 콜백
processImage(data: data) { image in // 2. 콜백
applyFilter(image: image) { finalImage in // 3. 콜백
// 4. UI 업데이트 (메인 스레드에서)
DispatchQueue.main.async {
completion(finalImage)
}
}
}
}
}
[After - async/await]
func fetchAndProcessImage() async throws -> UIImage {
let data = try await downloadImageData()
let image = try await processImage(data: data)
let finalImage = try await applyFilter(image: image)
return finalImage
}
// 호출부분
Task {
let image = try await fetchAndProcessImage()
self.imageView.image = image // UI 업데이트
}
Task나 Actor같은 개념은 아예 따로 다뤄야 할 것 같아서 일단 여기까지…
좀 더 내부적이고 수준 높은 설명은
https://sujinnaljin.medium.com/swift-async-await-concurrency-bd7bcf34e26f
[Swift] async / await & concurrency
Swift 5.5 에서 등장한 비동기와 동시성을 위한 방법을 알아봅시다
sujinnaljin.medium.com
이 블로그만큼 자세히 되어 있는 곳을 본 적 없다.
도저히 이 정도 수준으로 정리할 자신이 없어서 샤라웃과 함께 첨부함
참고
https://engineering.linecorp.com/ko/blog/about-swift-concurrency
https://yudonlee.tistory.com/39
https://brownsoo.medium.com/swift-concurrency-훑어보기-15c6a871f79a
https://tdcian.tistory.com/408
https://sujinnaljin.medium.com/swift-async-await-concurrency-bd7bcf34e26f
'[iOS]' 카테고리의 다른 글
| [iOS] Swift Concurrency 박살내기 #3 - Actor (0) | 2025.11.20 |
|---|---|
| [iOS] Swift Concurrency 박살내기 #2 - 구조화된 동시성 (Structured Concurrency) vs 구조화되지 않은 동시성 (Unstructured Concurrency) (Task, TaskGroup, async let) (0) | 2025.10.20 |
| [iOS] Swift Concurrency 박살내기 #0 - 동시성과 비동기 (0) | 2025.10.15 |
| [iOS] SwiftUI에서 MVVM-C 패턴 톺아보기 (0) | 2025.10.02 |
| [iOS] 클린아키텍처 공부할 때 주저리주저리 썼던 거 (0) | 2025.10.01 |
