λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
πŸ₯– Bread Basics/Swift

Swift 곡식 λ¬Έμ„œ 정리 - λ™μ‹œμ„± (Concurrency)

by BreadDev 2025. 4. 11.
728x90

μ•ˆλ…•ν•˜μ„Έμš”. μ˜€λŠ˜μ€ Swift의 λ™μ‹œμ„±μ— λŒ€ν•΄ μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€. λ™μ‹œμ„±μ€ Swift 5.5μ—μ„œ λ„μž…λœ μ€‘μš”ν•œ κΈ°λŠ₯으둜, 비동기 μ½”λ“œλ₯Ό 더 μ•ˆμ „ν•˜κ³  μ§κ΄€μ μœΌλ‘œ μž‘μ„±ν•  수 있게 ν•΄μ€λ‹ˆλ‹€. λ³΅μž‘ν•œ κ°œλ…μ΄μ§€λ§Œ ν•¨κ»˜ μ°¨κ·Όμ°¨κ·Ό μ‚΄νŽ΄λ³΄λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€!

πŸ“Œ λ™μ‹œμ„±μ΄λž€? (Understanding Concurrency)

Swiftμ—μ„œ λ™μ‹œμ„±μ€ 크게 두 κ°€μ§€ κ°œλ…μ„ ν¬ν•¨ν•©λ‹ˆλ‹€:

  • 비동기(Asynchronous) μ½”λ“œ: μ‹€ν–‰ 쀑에 μΌμ‹œ μ€‘λ‹¨λ˜μ—ˆλ‹€κ°€ λ‚˜μ€‘μ— λ‹€μ‹œ 재개될 수 μžˆλŠ” μ½”λ“œμž…λ‹ˆλ‹€. ν•œ λ²ˆμ— ν”„λ‘œκ·Έλž¨μ˜ ν•œ λΆ€λΆ„λ§Œ μ‹€ν–‰λ©λ‹ˆλ‹€.
  • 병렬(Parallel) μ½”λ“œ: μ—¬λŸ¬ μž‘μ—…μ΄ λ™μ‹œμ— 싀행될 수 μžˆλŠ” μ½”λ“œμž…λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 4μ½”μ–΄ ν”„λ‘œμ„Έμ„œλŠ” 4개의 μž‘μ—…μ„ λ™μ‹œμ— μ‹€ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ™μ‹œμ„±μ˜ 이점: λ„€νŠΈμ›Œν¬ μš”μ²­, 파일 μ²˜λ¦¬μ™€ 같은 μ‹œκ°„μ΄ 였래 κ±Έλ¦¬λŠ” μž‘μ—…μ„ μˆ˜ν–‰ν•˜λŠ” λ™μ•ˆμ—λ„ UIλ₯Ό 응닡성 있게 μœ μ§€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Swift의 λ™μ‹œμ„± λͺ¨λΈμ€ μ“°λ ˆλ“œ μœ„μ— κ΅¬μΆ•λ˜μ–΄ μžˆμ§€λ§Œ, 직접 μ“°λ ˆλ“œλ₯Ό 관리할 ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€. μ‹œμŠ€ν…œμ΄ 비동기 ν•¨μˆ˜μ˜ 싀행을 μžλ™μœΌλ‘œ κ΄€λ¦¬ν•΄μ€λ‹ˆλ‹€.

πŸ“Œ 비동기 ν•¨μˆ˜ μ •μ˜ν•˜κ³  ν˜ΈμΆœν•˜κΈ° (Async Functions)

비동기 ν•¨μˆ˜ μ •μ˜ν•˜κΈ°

async ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•˜μ—¬ 비동기 ν•¨μˆ˜λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€:

func listPhotos(inGallery name: String) async -> [String] {
    // 비동기 λ„€νŠΈμ›Œν‚Ή μ½”λ“œ
    return ["IMG001", "IMG99", "IMG0404"]
}

ν•¨μˆ˜κ°€ λΉ„λ™κΈ°μ μ΄λ©΄μ„œ μ—λŸ¬λ₯Ό 던질 μˆ˜λ„ μžˆλ‹€λ©΄ async throwsλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€:

func fetchUserData(userID: String) async throws -> UserData {
    // λ„€νŠΈμ›Œν¬ μš”μ²­ 쀑 μ—λŸ¬κ°€ λ°œμƒν•  수 있음
}

μ€‘μš”: asyncλŠ” λ°˜ν™˜ ν™”μ‚΄ν‘œ(->) μ•žμ— μœ„μΉ˜ν•˜κ³ , throwsκ°€ μžˆλ‹€λ©΄ throws μ•žμ— asyncλ₯Ό μž‘μ„±ν•©λ‹ˆλ‹€.

비동기 ν•¨μˆ˜ ν˜ΈμΆœν•˜κΈ°

비동기 ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•  λ•ŒλŠ” await ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•˜μ—¬ 잠재적인 쀑단 지점을 ν‘œμ‹œν•©λ‹ˆλ‹€:

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

이 μ½”λ“œμ˜ μ‹€ν–‰ 흐름은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€:

  1. listPhotos ν•¨μˆ˜ 호좜 μ‹œ awaitμ—μ„œ μΌμ‹œ 쀑단될 수 있음
  2. ν•¨μˆ˜κ°€ λ°˜ν™˜λ˜λ©΄ sortedNames와 name을 계산 (μΌμ‹œ 쀑단 μ—†μŒ)
  3. downloadPhoto 호좜 μ‹œ λ‹€μ‹œ awaitμ—μ„œ μΌμ‹œ 쀑단될 수 있음
  4. μ΅œμ’…μ μœΌλ‘œ show(photo) μ‹€ν–‰

쀑단 지점(Suspension Points): awaitλŠ” μ½”λ“œκ°€ μΌμ‹œ 쀑단될 수 μžˆλŠ” 지점을 λͺ…μ‹œμ μœΌλ‘œ ν‘œμ‹œν•©λ‹ˆλ‹€. 이런 λͺ…μ‹œμ  ν‘œμ‹œ 덕뢄에 비동기 μ½”λ“œμ˜ 흐름을 더 μ‰½κ²Œ 이해할 수 μžˆμŠ΅λ‹ˆλ‹€.

비동기 ν•¨μˆ˜λŠ” λ‹€μŒ μœ„μΉ˜μ—μ„œλ§Œ ν˜ΈμΆœν•  수 μžˆμŠ΅λ‹ˆλ‹€:

  • λ‹€λ₯Έ 비동기 ν•¨μˆ˜, λ©”μ„œλ“œ, ν”„λ‘œνΌν‹° λ‚΄λΆ€
  • @main νƒ€μž…μ˜ static main() λ©”μ„œλ“œ λ‚΄λΆ€
  • κ΅¬μ‘°ν™”λ˜μ§€ μ•Šμ€ λ™μ‹œμ„± μž‘μ—… λ‚΄λΆ€

πŸ“Œ 비동기 μ‹œν€€μŠ€ (Asynchronous Sequences)

μ»¬λ ‰μ…˜μ˜ μš”μ†Œλ₯Ό λΉ„λ™κΈ°μ μœΌλ‘œ ν•˜λ‚˜μ”© μ²˜λ¦¬ν•΄μ•Ό ν•  λ•ŒλŠ” 비동기 μ‹œν€€μŠ€λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€:

import Foundation

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}

for-await-in 루프: 일반 for-in 루프와 λΉ„μŠ·ν•˜μ§€λ§Œ, 각 λ°˜λ³΅μ—μ„œ λ‹€μŒ μš”μ†Œλ₯Ό μ‚¬μš©ν•  수 μžˆμ„ λ•ŒκΉŒμ§€ 기닀릴 수 μžˆμŠ΅λ‹ˆλ‹€.

μžμ‹ λ§Œμ˜ 비동기 μ‹œν€€μŠ€λ₯Ό λ§Œλ“€λ €λ©΄ AsyncSequence ν”„λ‘œν† μ½œμ„ κ΅¬ν˜„ν•˜λ©΄ λ©λ‹ˆλ‹€.

πŸ“Œ λ³‘λ ¬λ‘œ 비동기 ν•¨μˆ˜ ν˜ΈμΆœν•˜κΈ° (Parallel Async Calls)

μ—¬λŸ¬ 비동기 μž‘μ—…μ„ 순차적으둜 μ‹€ν–‰ν•˜λ©΄ 각 μž‘μ—…μ€ 이전 μž‘μ—…μ΄ μ™„λ£Œλ  λ•ŒκΉŒμ§€ κΈ°λ‹€λ¦½λ‹ˆλ‹€:

// 순차적 μ‹€ν–‰ - 각 λ‹€μš΄λ‘œλ“œκ°€ 이전 λ‹€μš΄λ‘œλ“œ μ™„λ£Œ ν›„ μ‹œμž‘
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

μž‘μ—…λ“€μ„ λ³‘λ ¬λ‘œ μ‹€ν–‰ν•˜λ €λ©΄ async let을 μ‚¬μš©ν•©λ‹ˆλ‹€:

// 병렬 μ‹€ν–‰ - λͺ¨λ“  λ‹€μš΄λ‘œλ“œκ°€ λ™μ‹œμ— μ‹œμž‘
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

// λͺ¨λ“  κ²°κ³Όκ°€ ν•„μš”ν•  λ•Œ await
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

순차적 vs 병렬:

  • λ‹€μŒ μ½”λ“œκ°€ 이전 ν•¨μˆ˜μ˜ 결과에 μ˜μ‘΄ν•œλ‹€λ©΄ 일반 awaitλ₯Ό μ‚¬μš©ν•˜μ„Έμš”.
  • μ—¬λŸ¬ 독립적인 μž‘μ—…μ„ λ™μ‹œμ— μ‹€ν–‰ν•˜λ €λ©΄ async let을 μ‚¬μš©ν•˜μ„Έμš”.

πŸ“Œ μž‘μ—…κ³Ό μž‘μ—… κ·Έλ£Ή (Tasks and Task Groups)

Swift λ™μ‹œμ„±μ˜ 핡심 λ‹¨μœ„λŠ” μž‘μ—…(Task)μž…λ‹ˆλ‹€. μž‘μ—…μ€ λΉ„λ™κΈ°μ μœΌλ‘œ μ‹€ν–‰ν•  수 μžˆλŠ” μž‘μ—… λ‹¨μœ„μž…λ‹ˆλ‹€.

μž‘μ—… 그룹으둜 μ—¬λŸ¬ μž‘μ—… κ΄€λ¦¬ν•˜κΈ°

μ—¬λŸ¬ μž‘μ—…μ„ λ™μ μœΌλ‘œ μƒμ„±ν•˜κ³  κ΄€λ¦¬ν•˜λ €λ©΄ μž‘μ—… κ·Έλ£Ή(Task Group)을 μ‚¬μš©ν•©λ‹ˆλ‹€:

await withTaskGroup(of: Data.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    
    for name in photoNames {
        group.addTask {
            return await downloadPhoto(named: name)
        }
    }

    for await photo in group {
        show(photo)
    }
}

μž‘μ—… κ·Έλ£Ήμ—μ„œ κ²°κ³Όλ₯Ό μˆ˜μ§‘ν•˜κ³  λ°˜ν™˜ν•˜λ €λ©΄:

let photos = await withTaskGroup(of: Data.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    
    for name in photoNames {
        group.addTask {
            return await downloadPhoto(named: name)
        }
    }

    var results: [Data] = []
    for await photo in group {
        results.append(photo)
    }
    
    return results
}

ꡬ쑰적 λ™μ‹œμ„±(Structured Concurrency): μž‘μ—…λ“€ 사이에 λͺ…μ‹œμ μΈ λΆ€λͺ¨-μžμ‹ 관계가 μžˆμ–΄ μž‘μ—… 관리와 μ·¨μ†Œκ°€ 더 μ‰¬μ›Œμ§‘λ‹ˆλ‹€.

μž‘μ—… μ·¨μ†Œ (Task Cancellation)

SwiftλŠ” ν˜‘λ ₯적 μ·¨μ†Œ λͺ¨λΈ(cooperative cancellation)을 μ‚¬μš©ν•©λ‹ˆλ‹€. μž‘μ—…μ΄ μ·¨μ†Œλ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜λŠ” 방법은 두 κ°€μ§€μž…λ‹ˆλ‹€:

  1. Task.checkCancellation()을 ν˜ΈμΆœν•˜μ—¬ μ·¨μ†Œ μ‹œ μ—λŸ¬λ₯Ό λ°œμƒμ‹œν‚΄
  2. Task.isCancelled ν”„λ‘œνΌν‹°λ₯Ό ν™•μΈν•˜μ—¬ 직접 μ·¨μ†Œ 처리
 
let photos = await withTaskGroup(of: Optional<Data>.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    
    for name in photoNames {
        let added = group.addTaskUnlessCancelled {
            // μ·¨μ†Œ μ—¬λΆ€ 확인
            guard !Task.isCancelled else { return nil }
            return await downloadPhoto(named: name)
        }
        guard added else { break }
    }

    var results: [Data] = []
    for await photo in group {
        if let photo { results.append(photo) }
    }
    return results
}

ν˜‘λ ₯적 μ·¨μ†Œ: μž‘μ—…μ΄ μ·¨μ†Œλ˜λ©΄ μžλ™μœΌλ‘œ μ€‘λ‹¨λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λŒ€μ‹ , μž‘μ—…μ€ μ·¨μ†Œλ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜κ³  적절히 λ°˜μ‘ν•΄μ•Ό ν•©λ‹ˆλ‹€.

πŸ“Œ κ΅¬μ‘°ν™”λ˜μ§€ μ•Šμ€ λ™μ‹œμ„± (Unstructured Concurrency)

λ•Œλ‘œλŠ” ν˜„μž¬ ꡬ쑰와 관계없이 μž‘μ—…μ„ μ‹€ν–‰ν•΄μ•Ό ν•  λ•Œκ°€ μžˆμŠ΅λ‹ˆλ‹€. 이럴 λ•Œ κ΅¬μ‘°ν™”λ˜μ§€ μ•Šμ€ λ™μ‹œμ„±μ„ μ‚¬μš©ν•©λ‹ˆλ‹€:

// ν˜„μž¬ μ•‘ν„°μ—μ„œ κ΅¬μ‘°ν™”λ˜μ§€ μ•Šμ€ μž‘μ—… 생성
let handle = Task {
    return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value

// λΆ„λ¦¬λœ μž‘μ—… 생성 (ν˜„μž¬ 앑터와 무관)
let detachedHandle = Task.detached {
    return await processPhoto(newPhoto)
}

주의: κ΅¬μ‘°ν™”λ˜μ§€ μ•Šμ€ μž‘μ—…μ€ μœ μ—°ν•˜μ§€λ§Œ, 그만큼 관리에 λŒ€ν•œ μ±…μž„λ„ ν½λ‹ˆλ‹€.

πŸ“Œ μ•‘ν„° (Actors)

μ•‘ν„°λŠ” λ™μ‹œμ„± μ½”λ“œ 간에 데이터λ₯Ό μ•ˆμ „ν•˜κ²Œ κ³΅μœ ν•  수 있게 ν•΄μ£ΌλŠ” μ°Έμ‘° νƒ€μž…μž…λ‹ˆλ‹€:

actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int

    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }
    
    func update(with measurement: Int) {
        measurements.append(measurement)
        if measurement > max {
            max = measurement
        }
    }
}

μ•‘ν„° μ‚¬μš© 방법:

let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
// μ•‘ν„°μ˜ ν”„λ‘œνΌν‹°λ‚˜ λ©”μ„œλ“œμ— μ ‘κ·Όν•  λ•ŒλŠ” await ν•„μš”
print(await logger.max)  // 25

// λ‹€λ₯Έ μΈ‘μ •κ°’ μΆ”κ°€
await logger.update(with: 30)
print(await logger.max)  // 30

μ•‘ν„° 뢄리(Actor Isolation): μ•‘ν„°λŠ” ν•œ λ²ˆμ— ν•˜λ‚˜μ˜ μž‘μ—…λ§Œ λ³€κ²½ κ°€λŠ₯ν•œ μƒνƒœμ— μ ‘κ·Όν•  수 μžˆλ„λ‘ 보μž₯ν•©λ‹ˆλ‹€. 이λ₯Ό 톡해 데이터 경쟁 쑰건을 λ°©μ§€ν•©λ‹ˆλ‹€.

μ•‘ν„° λ‚΄λΆ€μ—μ„œλŠ” μžμ‹ μ˜ ν”„λ‘œνΌν‹°μ— μ ‘κ·Όν•  λ•Œ await 없이 직접 μ ‘κ·Όν•  수 μžˆμŠ΅λ‹ˆλ‹€:

extension TemperatureLogger {
    func convertFahrenheitToCelsius() {
        // μ•‘ν„° λ‚΄λΆ€μ—μ„œλŠ” await 없이 ν”„λ‘œνΌν‹°μ— μ ‘κ·Ό κ°€λŠ₯
        for i in measurements.indices {
            measurements[i] = (measurements[i] - 32) * 5 / 9
        }
    }
}

πŸ“Œ 전솑 κ°€λŠ₯ νƒ€μž… (Sendable Types)

λ™μ‹œμ„± 도메인 간에 데이터λ₯Ό μ•ˆμ „ν•˜κ²Œ μ „λ‹¬ν•˜λ €λ©΄ ν•΄λ‹Ή νƒ€μž…μ΄ '전솑 κ°€λŠ₯(Sendable)'ν•΄μ•Ό ν•©λ‹ˆλ‹€:

// 전솑 κ°€λŠ₯ νƒ€μž… μ„ μ–Έ
struct TemperatureReading: Sendable {
    var measurement: Int
}

// μ•‘ν„° λ©”μ„œλ“œμ— 전솑 κ°€λŠ₯ν•œ νƒ€μž… 전달
extension TemperatureLogger {
    func addReading(from reading: TemperatureReading) {
        measurements.append(reading.measurement)
    }
}

let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
let reading = TemperatureReading(measurement: 45)
await logger.addReading(from: reading)

λ‹€μŒκ³Ό 같은 νƒ€μž…μ€ μžλ™μœΌλ‘œ 전솑 κ°€λŠ₯ν•©λ‹ˆλ‹€:

  • 전솑 κ°€λŠ₯ν•œ ν”„λ‘œνΌν‹°λ§Œ κ°€μ§„ ꡬ쑰체
  • 전솑 κ°€λŠ₯ν•œ μ—°κ΄€κ°’λ§Œ κ°€μ§„ μ—΄κ±°ν˜•
  • λ³€κ²½ λΆˆκ°€λŠ₯ν•œ 클래슀(읽기 μ „μš© ν”„λ‘œνΌν‹°λ§Œ κ°€μ§„)

νƒ€μž…μ„ λͺ…μ‹œμ μœΌλ‘œ 전솑 λΆˆκ°€λŠ₯ν•˜κ²Œ ν‘œμ‹œν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€:

struct FileDescriptor {
    let rawValue: CInt
}

// 이 νƒ€μž…μ„ 전솑 λΆˆκ°€λŠ₯ν•˜κ²Œ ν‘œμ‹œ
@available(*, unavailable)
extension FileDescriptor: Sendable { }

πŸ“Œ 정리

Swift의 λ™μ‹œμ„± μ‹œμŠ€ν…œμ€ λ³΅μž‘ν•œ 비동기 μ½”λ“œλ₯Ό μ•ˆμ „ν•˜κ³  μ΄ν•΄ν•˜κΈ° μ‰½κ²Œ μž‘μ„±ν•  수 μžˆλ„λ‘ λ„μ™€μ€λ‹ˆλ‹€. μ£Όμš” κ°œλ…μ„ μš”μ•½ν•΄λ³΄λ©΄:

  1. 비동기 ν•¨μˆ˜(async/await): μ½”λ“œ 싀행을 μΌμ‹œ μ€‘λ‹¨ν•˜κ³  λ‚˜μ€‘μ— μž¬κ°œν•  수 있음
  2. 병렬 μ‹€ν–‰(async let): μ—¬λŸ¬ 비동기 μž‘μ—…μ„ λ™μ‹œμ— μ‹œμž‘ν•˜κ³  λͺ¨λ“  κ²°κ³Όκ°€ ν•„μš”ν•  λ•Œ κΈ°λ‹€λ¦Ό
  3. μž‘μ—…κ³Ό μž‘μ—… κ·Έλ£Ή: λ™μ‹œμ„± μ½”λ“œμ˜ λ‹¨μœ„μ™€ κ΅¬μ‘°ν™”λœ 관리 방법
  4. μ•‘ν„°: 데이터 레이슀 없이 μƒνƒœλ₯Ό μ•ˆμ „ν•˜κ²Œ κ³΅μœ ν•˜λŠ” 방법
  5. 전솑 κ°€λŠ₯ νƒ€μž…: λ™μ‹œμ„± 경계λ₯Ό λ„˜μ–΄ μ•ˆμ „ν•˜κ²Œ 전달될 수 μžˆλŠ” 데이터 νƒ€μž…

Swift의 λ™μ‹œμ„± λͺ¨λΈμ€ 특히 λ‹€μŒκ³Ό 같은 이점이 μžˆμŠ΅λ‹ˆλ‹€:

  • μ½”λ“œκ°€ 더 읽기 쉽고 μœ μ§€λ³΄μˆ˜ν•˜κΈ° 쉬움
  • 컴파일 μ‹œκ°„μ— 더 λ§Žμ€ 였λ₯˜λ₯Ό μž‘μ„ 수 있음
  • 데이터 λ ˆμ΄μŠ€μ™€ 같은 λ™μ‹œμ„± 버그λ₯Ό λ°©μ§€ν•˜λŠ” 데 도움됨
  • μ“°λ ˆλ“œ 관리λ₯Ό 직접 ν•  ν•„μš”κ°€ μ—†μŒ