[TIL] 동시성 프로그래밍

박주하·2025년 6월 2일

동시성 프로그래밍(Concurrency Programming)


먼저 동시성이란 여러 작업이 동시에 진행되는 것처럼 보이도록 만드는 기술을 말한다. 예를 들어, 네트워크 요청을 처리하면서 UI는 멈추지 않고 사용자의 입력을 계속 받을 수 있도록 하는 것이 이에 해당한다.
따라서 동시성 프로그래밍은 여러 작업을 동시에 실행하거나 비동기적으로 처리해서 앱의 반응성을 높이고 사용자 경험을 개선하는 데 핵심적인 역할을 한다.

✅ 그럼 왜 중요한가?

UI가 멈추는 것(freezing)을 방지하여 앱의 빠른 반응성을 유지할 수 있다. 리소스도 효율적으로 사용 가능하고, 네트워크/디스크 작업을 비동기 처리할 수 있다.


동시성 구현 도구들


1. GCD(Grand Central Dispatch)

DispatchQueue.global().async {
    // 백그라운드에서 무거운 작업
    let image = loadImage()

    DispatchQueue.main.async {
        // UI 업데이트는 메인 스레드에서
        imageView.image = image
    }
}

GCD는 스레드를 직접 관리하지 않고 작업 큐(DispatchQueue)를 통해 비동기 처리하는 방식이다. 작업을 실행할 큐에 작업 블록을 제출하고, 해당 큐가 작업을 실행한다.

✅ 1. Main Queue(메인 큐)

  • 메인 스레드에서 실행되는 직렬 큐
  • 직렬 큐: 한 번에 하나의 작업만 수행
  • UI 관련 모든 작업은 반드시 메인 큐에서 수행
  • 백그라운드에서 작업한 후 UI를 갱신할 때 자주 사용

✅ 2. Global Queue(글로벌 큐)

  • 여러가지의 작업을 동시에 처리할 수 있는 병렬 큐
  • 여러 개의 전역 글로벌 큐가 있으며 우선순위(QoS: Quality of Service)에 따라 분류
  • 네트워크, 디스크 I/O, 계산처럼 오래 걸리는 작업은 반드시 글로벌 큐에서 처리

📌 QoS 우선순위 종류

QoS설명사용 예
.userInteractive가장 높은 우선순위, 즉시 UI 반응 필요제스처 처리, 애니메이션 등
.userInitiated사용자가 요청한 작업, 빠른 응답 필요문서 열기, 이미지 처리 등
.default특별한 지정 없는 기본 우선순위일반적인 백그라운드 작업
.utility시간이 오래 걸리는 작업, UI 민감하지 않음다운로드, 백업 등
.background최하 우선순위, 눈에 띄지 않아도 되는 작업데이터 동기화, 로그 정리 등

✅ 장점

  • 간단한 비동기 처리에 적합
  • 경량화된 구조
  • 병렬 처리가 쉽다

🚨 단점

  • 복잡한 흐름을 제어하기 어렵다
  • 콜백 지옥 발생 가능
  • 작업 간 의존성 표현이 어렵다

2. OperationQueue

let queue = OperationQueue()

let operation = BlockOperation {
    print("비동기 작업 실행 중")
}

operation.completionBlock = {
    print("작업 완료")
}

queue.addOperation(operation)

Foundation 프레임워크에 포함된 객체지향적인 동시성 구현 방법이다. GCD보다 더 구조화된 동시성 API로, OperationQueue는 Operation 객체를 큐에 넣어 비동기 작업을 수행하게 해주는 클래스다.

  • Operation: 실행할 작업을 캡슐화한 클래스(BlockOperation, CustomOperation)
  • OperationQueue: 작업을 큐에 등록해서 실행

✅ 장점

  • 작업 간 의존성 설정 가능
  • 취소, 일시정지 등의 제어 가능(operation.cancel())
  • 더 명시적이고 객체지향적

🚨 단점

  • GCD보다 무겁고 복잡
  • 단순 작업에 오버킬

3. Swift Concurrency(async/await)

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: 데이터 경쟁을 막는 안전한 객체

✅ 장점

  • 코드 가독성 뛰어나다(콜백 지옥 제거)
  • 데이터 경쟁 감소
  • 안전한 비동기 흐름 제어

🚨 단점

  • Swift 5.5 이상 / iOS 13+ 이상에서만 사용 가능
  • Actor나 TaskGroup 등의 개념이 익숙해질 때까지 시간이 소요

📌 정리

특징GCDOperationQueueSwift Concurrency
코드 가독성낮음(콜백 중첩)중간높음(동기처럼 읽힘)
작업 제어거의 없음취소/우선순위/의존성 가능Task/Actor 등으로 가능
데이터 경쟁 제어수동 관리수동 관리Actor로 안전하게 처리
추천 용도간단한 백그라운드 작업복잡한 비동기 작업모던 Swift 기반 앱 전체

⚠️ 조심해야 할 문제들


동시성 프로그래밍은 성능을 높일 수 있는 강력한 도구지만, 잘못 사용하면 치명적인 버그를 초래할 수 있다. 그중 대표적인 어려움은 다음 세 가지이다.

1. 데이터 경쟁(Race Condition)

여러 쓰레드(또는 작업)가 동시에 같은 데이터에 접근하고 변경하려 할 때, 실행 순서에 따라 결과가 달라지는 현상이다.

var counter = 0

DispatchQueue.global().async {
    counter += 1  // A 쓰레드
}

DispatchQueue.global().async {
    counter += 1  // B 쓰레드
}
  • 예상 결과: counter == 2
  • 실제 결과: counter == 1(덮어쓰기 발생)

💡해결 방법

  • 직렬 큐 사용
  • Lock(NSLock 등) 사용
  • MainActor / Actor 사용(Swift Concurrency)

2. 데드락(Deadlock)

서로 잠금을 기다리며 영원히 끝나지 않는 상태에 빠지는 문제이다. 서로 기다리기만 해서 아무 일도 진행되지 않는다.

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()
}

💡해결 방법

  • 항상 같은 순서로 잠금(lock)을 걸도록 강제
  • 가능한 락 최소화
  • DispatchSemaphore, DispatchQueue.sync() 사용 주의

3. 복잡한 콜백 지옥

비동기 작업을 콜백으로 중첩해 작성하면, 코드 가독성이 매우 떨어지고 유지보수가 어려워지는 현상이다.

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)
}
  • 들여쓰기 증가 → 유지보수 어려움
  • 에러 핸들링 복잡
  • 흐름 파악 어려움

💡해결 방법

  • Swift Concurrency 도입 (async/await)
  • Combine, RxSwift 등 리액티브 프로그래밍 사용

📌 정리

문제원인해결 방법
데이터 경쟁여러 스레드의 동시 접근락, 직렬 큐, Actor
데드락락을 잘못된 순서로 걸었을 때락 순서 고정, 락 최소화
콜백 지옥중첩된 비동기 코드async/await, Combine

0개의 댓글