동시성 시리즈 - 2. GCD 등장

1️⃣ GCD(Grand Central Dispatch)의 등장

이전 포스트에서 개발자가 직접 스레드를 관리할 때 발생하는 Thread Explosion(스레드 폭발) 문제와 그로 인한 성능 저하, 그리고 복잡한 관리 비용에 대해 알아보았습니다.
애플은 이 문제를 해결하기 위해 iOS 4부터 **GCD(Grand Central Dispatch)**라는 새로운 동시성 프로그래밍 모델을 도입했습니다.

”스레드 관리는 OS가 할게, 너는 작업만 줘.”

GCD의 핵심 철학은 개발자를 스레드 관리의 책임에서 해방시키는 것입니다.

  • 이전 (Thread-based): 개발자가 스레드를 직접 생성하고, 실행하고, 종료시켜야 함
  • GCD (Task-based): 개발자는 수행할 **작업(Task)**을 정의해서 Queue에 넣기만 하면 됨

시스템(OS)은 큐에 들어온 작업을 실행하기 위해 **스레드 풀(Thread Pool)**을 관리하며, 시스템의 부하 상태에 따라 스레드를 생성하거나 재사용합니다.

2️⃣ GCD는 어떤 문제를 해결했나?

1. 스레드 풀 (Thread Pool) 패턴을 통한 최적화

GCD는 내부적으로 스레드 풀을 운영합니다. 매번 작업을 할 때마다 스레드를 생성하고 없애는 것은 오버헤드가 큽니다. 스레드 풀은 미리(혹은 필요할 때) 스레드를 생성해두고, 작업이 끝나면 스레드를 없애지 않고 대기 상태로 두었다가 다음 작업에 재사용합니다.

이를 통해 스레드 생성/삭제 비용을 획기적으로 줄이고, Context Switching 비용을 최소화했습니다.

2. 시스템 레벨의 부하 조절

GCD는 단순한 스레드 풀이 아닙니다. XNU 커널과 깊게 통합되어 있어, 시스템의 전체적인 로드 상태를 파악하고 동적으로 스레드 수를 조절합니다. 만약 시스템이 바쁘다면 스레드 생성을 자제하고, 여유가 있다면 더 많은 스레드를 사용하여 처리를 가속화합니다. 이는 개발자가 개별 앱 수준에서는 절대 구현할 수 없는 OS 레벨의 최적화입니다.

3. 직관적인 비동기 코드 작성 (Block)

Objective-C의 Block (Swift의 Closure) 덕분에, 코드를 함수 단위로 쪼개거나 별도의 메서드를 만들지 않고도, 실행할 코드 덩어리 자체를 큐에 던지는 방식이 가능해졌습니다.

// 코드의 흐름을 끊지 않고 비동기 로직을 작성 가능
DispatchQueue.global().async {
    // 백그라운드 작업 (이미지 다운로드 등)
    let image = downloadImage()
    
    DispatchQueue.main.async {
        // 메인 스레드에서 UI 업데이트
        self.imageView.image = image
    }
}

이 패턴은 당시로서는 혁신적이었고, 복잡한 델리게이트 패턴이나 콜백 함수 분리 없이도 비동기 흐름을 한 눈에 볼 수 있게 해주었습니다.

3️⃣ 하지만 완벽하지 않았다 (GCD의 한계)

GCD는 스레드 관리의 고통을 덜어주었지만, 비동기 프로그래밍의 본질적인 어려움까지 모두 해결하지는 못했습니다.

1. Callback Hell (콜백 지옥)

비동기 작업이 연속될수록 들여쓰기(Indent)가 깊어지는 문제가 발생합니다.

func processImage(completion: @escaping (Result) -> Void) {
    downloadImage { image in
        resizeImage(image) { resizedImage in
            applyFilter(resizedImage) { filteredImage in
                saveToDisk(filteredImage) { url in
                    completion(url)
                }
            }
        }
    }
}

흔히 ‘오른쪽으로 떠나는 여행’이라고 부르는 이 현상은 가독성을 해치고, 에러 처리를 복잡하게 만듭니다.

2. 컴파일 타임이 아닌 런타임 의존적

GCD는 C언어 기반의 라이브러리 위에서 동작하며, 많은 동시성 문제가 **실행 중(Runtime)**에만 발견됩니다. 가장 대표적인 예가 Deadlock이나 Race Condition입니다. 컴파일러는 DispatchQueue.main.sync를 호출해도 문법적으로 틀리지 않다면 경고를 주지 않습니다. 앱을 실행하고 해당 코드가 도달해서야 앱이 멈춰버립니다.

3. 구조적이지 않음 (Unstructured Concurrency)

GCD로 실행한 작업은, 작업을 시작한 함수가 종료되어도 계속 실행될 수 있습니다. 만약 상위 작업이 취소되었을 때, 하위의 비동기 작업들까지 자동으로 취소되게 만드는 것이 매우 어렵습니다 (수동으로 DispatchWorkItem을 관리하거나 CancelFlag를 확인해야 함).

4️⃣ 다음 단계: 더 나은 동시성을 향해

GCD는 훌륭했지만, 더 복잡해지는 앱 환경에서는 작업 간의 의존성 관리취소(Cancel) 기능이 강력하게 필요했습니다. 이를 위해 OperationQueue가 등장하여 객체 지향적인 방식으로 동시성을 다룰 수 있게 해주었지만, 여전히 근본적인 ‘비동기 코드의 복잡성’과 ‘컴파일 타임 안전성’ 문제는 남아있었습니다.

그리고 2021년, Apple은 마침내 **Swift Concurrency (async/await)**를 발표하며 동시성 프로그래밍의 패러다임을 다시 한 번 바꿉니다.