[Swift] 왜 동시성 프로그래밍이 필요할까?

2026. 5. 24. 13:54·Apple/Swift
728x90
반응형

안녕하세요! 피피아노입니다 🎵

 

예전에 동시성에 대한 포스팅을 몇 번 한 적이 있는데 이 동시성이라는 개념이 왜 나왔고 왜 필요한지에 대한 설명이 부족한 것 같아서 이번 포스팅에서 조금 더 자세히 정리를 해보려고 합니다.

 

iOS 앱을 개발하다 보면 네트워크 요청, 이미지 로딩, 파일 I/O처럼 시간이 걸리는 작업을 반드시 다루게 됩니다. 이 작업들을 잘못 처리하면 앱이 멈추거나 UI가 버벅이는 문제가 생깁니다. 동시성 프로그래밍은 바로 이 문제를 해결하기 위한 도구입니다.

 

1. 문제의 시작 — 메인 스레드

iOS 앱은 시작될 때 메인 스레드(Main Thread) 하나가 생성됩니다. 이 스레드는 두 가지 역할을 동시에 담당합니다.

  • UI 렌더링 (버튼, 텍스트, 애니메이션 등)
  • 사용자 이벤트 처리 (터치, 스크롤 등)

UIKit과 SwiftUI 모두 UI 업데이트는 반드시 메인 스레드에서 해야 한다는 규칙이 있습니다.

메인 스레드를 막으면?

메인 스레드가 무거운 작업에 묶여 있으면 UI 업데이트가 멈추고, 사용자는 앱이 "멈췄다"고 느낍니다. 60fps 기준으로 프레임 하나당 약 16ms 안에 처리가 완료되어야 합니다.

2. 스레드란 무엇인가?

프로세스와 스레드

프로세스(Process) 는 실행 중인 프로그램 하나를 의미합니다. iOS 앱을 실행하면 운영체제는 해당 앱을 위한 독립된 프로세스를 만들고, 고유한 메모리 공간(코드, 힙, 데이터 영역)을 할당합니다.

스레드(Thread) 는 그 프로세스 안에서 실제로 코드를 실행하는 단위입니다. 하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 스레드들은 같은 프로세스의 메모리 공간을 공유합니다.

[ 프로세스: MyApp ]
├── 코드 영역     ← 모든 스레드가 공유
├── 힙(Heap)     ← 모든 스레드가 공유 (⚠️ 동시 접근 시 위험)
├── 스레드 1 (메인)
│   └── 스택(Stack), 레지스터, 프로그램 카운터
├── 스레드 2 (백그라운드)
│   └── 스택(Stack), 레지스터, 프로그램 카운터
└── 스레드 3 (백그라운드)
    └── 스택(Stack), 레지스터, 프로그램 카운터

각 스레드는 자신만의 스택을 가집니다. 함수 호출, 지역 변수, 반환 주소가 여기에 쌓입니다. iOS에서 메인 스레드의 기본 스택 크기는 8MB, 백그라운드 스레드는 512KB입니다.

스레드 생성 비용

스레드를 새로 만드는 것은 공짜가 아닙니다. 커널 자료구조 약 1KB, 스택 메모리, 그리고 스레드를 스케줄링하기 위한 오버헤드가 발생합니다. 스레드를 과도하게 생성하면 메모리 압박과 성능 저하로 이어집니다.

 

컨텍스트 스위칭(Context Switching)

CPU 코어는 한 번에 하나의 스레드만 실행할 수 있습니다. 여러 스레드가 동시에 실행되는 것처럼 보이는 이유는 운영체제가 스레드를 아주 빠르게 번갈아 실행하기 때문입니다. 이때 스레드를 전환하는 작업을 컨텍스트 스위칭이라 합니다.

 

전환이 일어날 때마다 현재 스레드의 레지스터 값, 프로그램 카운터 등을 저장하고, 다음 스레드의 상태를 복원해야 합니다. 스레드가 많을수록 이 전환 비용이 쌓여 오히려 성능이 나빠집니다.

 

Thread Explosion 문제

GCD를 사용할 때 흔히 발생하는 문제가 있습니다. DispatchQueue.global().async를 남발하면 시스템이 계속 새 스레드를 만들어냅니다.

// 이미지 100개를 동시에 다운로드하려는 코드
for url in imageURLs {
    DispatchQueue.global().async {
        let image = downloadImage(from: url) // 블로킹 I/O
        // ...
    }
}

이 코드는 최대 100개의 스레드를 생성하려 시도할 수 있습니다. 각 스레드가 I/O를 기다리며 블로킹되면, GCD는 작업이 처리되지 않는다고 판단해 새 스레드를 계속 추가합니다. 이를 Thread Explosion이라 하며, 다음 결과로 이어집니다.

  • 메모리 사용량 급증 (스레드당 512KB 스택)
  • 과도한 컨텍스트 스위칭으로 CPU 낭비
  • 최악의 경우 앱 크래시

Swift Concurrency의 async/await는 이 문제를 스레드를 블로킹하지 않고 일시 중단(suspend) 하는 방식으로 해결합니다. 스레드 자체는 해제되고, 다른 작업에 재사용됩니다.

데이터 레이스(Data Race)

스레드들이 힙 메모리를 공유하기 때문에 여러 스레드가 같은 데이터에 동시에 접근하면 예측 불가능한 결과가 생깁니다.

var count = 0

DispatchQueue.global().async { count += 1 } // 스레드 A
DispatchQueue.global().async { count += 1 } // 스레드 B

// 결과가 2일 수도 있고, 1일 수도 있습니다

count += 1은 실제로 세 단계입니다: 읽기 → 더하기 → 쓰기. 두 스레드가 동시에 count를 읽으면 둘 다 같은 초기값을 읽고, 각자 1을 더해 같은 값을 두 번 쓰는 상황이 발생합니다.

데이터 레이스는 재현하기 어렵습니다

발생 여부가 스케줄링 타이밍에 달려 있어서 개발 환경에서는 멀쩡하다가 프로덕션에서 터지는 경우가 많습니다. Xcode의 Thread Sanitizer(TSan) 를 활성화하면 개발 중에 탐지할 수 있습니다.

Swift Concurrency는 Actor를 통해 이 문제를 컴파일 타임에 방지할 수 있는 구조를 제공합니다.

3. 동기(Synchronous) 방식의 한계 — 실제 문제 상황

아래 코드를 보겠습니다. 서버에서 데이터를 가져와 테이블뷰를 갱신하는 흔한 패턴입니다.

func loadData() {
    let url = URL(string: "https://api.example.com/items")!
    let data = try! Data(contentsOf: url) // ❌ 메인 스레드 블로킹
    let items = try! JSONDecoder().decode([Item].self, from: data)
    tableView.reloadData()
}

Data(contentsOf:) 는 응답이 올 때까지 메인 스레드를 완전히 점유합니다. 네트워크 상태가 좋지 않다면 사용자는 수 초간 아무런 반응도 없는 앱을 마주하게 됩니다.

 

4. 기존 해결책 — GCD와 Completion Handler

이 문제를 해결하기 위해 오랫동안 GCD(Grand Central Dispatch) 와 Completion Handler 패턴이 사용됐습니다.

func loadData(completion: @escaping (Result<[Item], Error>) -> Void) {
    DispatchQueue.global().async {
        do {
            let url = URL(string: "https://api.example.com/items")!
            let data = try Data(contentsOf: url)
            let items = try JSONDecoder().decode([Item].self, from: data)
            DispatchQueue.main.async {
                completion(.success(items))
            }
        } catch {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }
}

백그라운드 스레드에서 작업하고, 완료되면 메인 스레드로 결과를 전달하는 패턴입니다. 동작은 하지만 문제가 있습니다.

GCD + Completion Handler의 단점
  • 콜백 지옥(Callback Hell): 비동기 작업이 중첩될수록 들여쓰기가 깊어지고 코드 가독성이 떨어집니다.
  • 에러 처리의 불편함: Result 타입이나 두 개의 옵셔널 파라미터로 처리해야 합니다.
  • 실행 흐름 추적 어려움: 코드가 위에서 아래로 읽히지 않아 디버깅이 힘듭니다.
  • 스레드 안전성: 개발자가 직접 DispatchQueue를 관리해야 하며, 실수하기 쉽습니다.

5. Swift Concurrency의 등장

Swift 5.5(Xcode 13, iOS 15)부터 async/await 문법이 도입됐습니다. 비동기 코드를 동기 코드처럼 읽히게 작성할 수 있습니다.

func loadData() async throws -> [Item] {
    let url = URL(string: "https://api.example.com/items")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Item].self, from: data)
}

위에서 아래로 자연스럽게 읽힙니다. 중첩 클로저도 없고, DispatchQueue도 없습니다.

호출하는 쪽도 마찬가지입니다.

Task {
    do {
        let items = try await loadData()
        await MainActor.run {
            self.items = items
            tableView.reloadData()
        }
    } catch {
        print("Error:", error)
    }
}
Swift Concurrency의 핵심 장점
  • 가독성: 코드 흐름이 위→아래로 선형적입니다.
  • 에러 처리: Swift의 throws/try 문법을 그대로 사용합니다.
  • 컴파일 타임 안전성: 컴파일러가 스레드 안전성 문제를 일부 잡아줍니다.
  • 구조화된 동시성(Structured Concurrency): async let, TaskGroup으로 병렬 작업도 명확하게 표현 가능합니다.

감사합니다.

 

잘못된 내용이 있거나 더 좋은 내용 피드백은 언제나 환영합니다!

궁금하신 부분은 댓글로 질문 부탁드립니다! 

728x90
반응형

'Apple > Swift' 카테고리의 다른 글

[Swift] 좋은 코드 만들기  (0) 2026.06.15
[Swift] ARC(Automatic Reference Counting)란 무엇인가  (2) 2026.05.16
[Swift] 왜 Class는 Initializer를 자동으로 만들어주지 않을까?  (1) 2026.04.21
[Swift] SwiftData 상속과 스키마 마이그레이션 알아보기  (3) 2026.03.18
[Swift] Swift 6로 마이그레이션 하기  (3) 2026.03.04
'Apple/Swift' 카테고리의 다른 글
  • [Swift] 좋은 코드 만들기
  • [Swift] ARC(Automatic Reference Counting)란 무엇인가
  • [Swift] 왜 Class는 Initializer를 자동으로 만들어주지 않을까?
  • [Swift] SwiftData 상속과 스키마 마이그레이션 알아보기
P_Piano
P_Piano
Apple 생태계 개발자가 되기 위해 학습과 경험을 기록하고 있습니다.
    반응형
    250x250
  • P_Piano
    피피아노의 개발 일지
    P_Piano
  • 전체
    오늘
    어제
    • 분류 전체보기 (235) N
      • Apple (151)
        • iOS (25)
        • visionOS (5)
        • Swift (80)
        • UIKit (2)
        • SwiftUI (27)
        • RxSwift (2)
        • Xcode (6)
        • Metal (2)
      • Apple Developer Academy (1)
      • C언어 (5)
      • C++ (8)
      • Dart (1)
      • Python (3)
      • JavaScript (17)
      • Git (1)
      • CS (40)
        • 디자인 패턴 (6)
        • 네트워크 (20)
        • 운영체제 (8)
        • Database (5)
        • 자료구조 (0)
      • IT 지식 (2)
      • IT 뉴스 (4)
      • 경험 (2) N
      • 출처 표기 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    이니셜라이저
    회고
    클래스
    Initializers
    운영체제
    함수
    변수
    디자인패턴
    Xcode
    오블완
    visionOS
    동시성
    SWIFT
    데이터베이스
    Vision Pro
    비동기
    제어문
    프로퍼티 래퍼
    상속
    ios
    네트워크
    Apple
    swiftUI
    티스토리챌린지
    프로세스
    코딩테스트
    스위프트
    자바스크립트
    UIKit
    배열
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
P_Piano
[Swift] 왜 동시성 프로그래밍이 필요할까?
상단으로

티스토리툴바