본문 바로가기
Swift

Swift: 동시성 코드 작성 노하우

by songmoro 2025. 12. 2.

몇 가지의 동시성 관련 코드 작성 노하우

 

비동기 작업 데이터와 UI 데이터 분리

아래와 같은 개별 파일을 압축하는 코드가 가진 문제점.

 

Published 데이터는 메인 스레드에서 갱신되어야 하는 데이터이고, logs는 동시성 문제를 방지하기 위해 한 번에 한 스레드만 접근해야만 한다.
하지만 logs를 생성하는 작업은 메인 스레드에서 수행될 필요가 없다.

 

아래 코드의 문제는 무거운 작업의 메인 스레드 수행으로 UI 업데이트가 블로킹됨

 

@MainActor
class CompressionState: ObservableObject {
    @Published var files: [FileStatus] = []
    var logs: [String] = []

    func update(url: URL, progress: Double) {
        if let loc = files.firstIndex(where: {$0.url == url}) {
            files[loc].progress = progress
        }
    }

    func update(url: URL, uncompressedSize: Int) {
        if let loc = files.firstIndex(where: {$0.url == url}) {
            files[loc].uncompressedSize = uncompressedSize
        }
    }

    func update(url: URL, compressedSize: Int) {
        if let loc = files.firstIndex(where: {$0.url == url}) {
            files[loc].compressedSize = compressedSize
        }
    }

    func compressAllFiles() {
        for file in files {
            Task {
                let compressedData = compressFile(url: file.url)
                await save(compressedData, to: file.url)
            }
        }
    }

    func compressFile(url: URL) -> Data {
        log(update: "Starting for \(url)")
        let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in
            update(url: url, uncompressedSize: uncompressedSize)
        } progressNotification: { progress in
            update(url: url, progress: progress)
            log(update: "Progress for \(url): \(progress)")
        } finalNotificaton: { compressedSize in
            update(url: url, compressedSize: compressedSize)
        }
        log(update: "Ending for \(url)")
        return compressedData
    }

    func log(update: String) {
        logs.append(update)
    }

 

최종 코드

actor ParallelCompressor {
    var logs: [String] = []
    unowned let status: CompressionState

    init(status: CompressionState) {
        self.status = status
    }

    nonisolated func compressFile(url: URL) async -> Data {
        await log(update: "Starting for \(url)")
        let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in
            Task { @MainActor in
                status.update(url: url, uncompressedSize: uncompressedSize)
            }
        } progressNotification: { progress in
            Task { @MainActor in
                status.update(url: url, progress: progress)
                await log(update: "Progress for \(url): \(progress)")
            }
        } finalNotificaton: { compressedSize in
            Task { @MainActor in
                status.update(url: url, compressedSize: compressedSize)
            }
        }
        await log(update: "Ending for \(url)")
        return compressedData
    }

    func log(update: String) {
        logs.append(update)
    }
}

@MainActor
class CompressionState: ObservableObject {
    @Published var files: [FileStatus] = []
    var compressor: ParallelCompressor!

    init() {
        self.compressor = ParallelCompressor(status: self)
    }

    func update(url: URL, progress: Double) {
        if let loc = files.firstIndex(where: {$0.url == url}) {
            files[loc].progress = progress
        }
    }

    func update(url: URL, uncompressedSize: Int) {
        if let loc = files.firstIndex(where: {$0.url == url}) {
            files[loc].uncompressedSize = uncompressedSize
        }
    }

    func update(url: URL, compressedSize: Int) {
        if let loc = files.firstIndex(where: {$0.url == url}) {
            files[loc].compressedSize = compressedSize
        }
    }

    func compressAllFiles() {
        for file in files {
            Task.detached {
                let compressedData = await self.compressor.compressFile(url: file.url)
                await save(compressedData, to: file.url)
            }
        }
    }
}

 

다른 문제점들도 있긴 하지만 해결에 대한 자세한 과정은 "Swift 동시성 시각화 및 최적화" 참고.

 

동시성 관점에서 몇 가지 정리

  • 연산이 적은 작업은 메인 스레드에서 수행해도 상관없지만, 사용자가 행으로 느낄 만큼 연산이 많아지기 시작한다면, 따로 비동기 처리를 해야 한다.
  • 액터를 통해 연산을 수행할 때 생각해 볼 점 -> 이 작업이 이전 작업에 대한 의존성을 갖는가?
    • 개별 파일에 대한 압축 진행 -> 현재 파일이 이전 파일 혹은 다음 파일에 의존성을 갖지 않음 -> 하나의 액터에서 연산을 직렬화할 필요 없음
      • -> nonisolated 키워드로 연산을 해당 액터에 격리하는 대신 유후 상태인 스레드가 작업을 이어가게 만들 수 있음
      • nonisolated = 비격리

 

최종 코드 동작 방식:

  • nonisolated 메서드를 통해 액터의 serial queue 우회
  • 여러 compressFile 호출을 동시 실행
  • log 호출은 serial 하게 처리

 

unchecked Sendable, @preconcurrency

Swift 6.0의 엄격한 동시성 규정을 임시 허용하기 위해 사용하는 키워드
개발자가 해당 타입은 동시성을 준수한다고 대신 보증

 

결국 Swift 버전이 올라갈수록 동시성 규정이 엄격해질 테니 타입이 동시성을 준수하도록 개선해야 함

 

await MainActor.run

DispatchQueue.main과 같이 메인 스레드에서 UI를 업데이트하기 위해 사용하는 문법

await MainActor.run {
    // ...
}

 

메인 액터 보장 타입의 메서드 호출을 통해 간략하게 사용 가능
(View는 Sendable)

struct ContentView: View {
    func display(_ data: SomeData) { }
}

actor Compressor {
    func compressFile() {
        let data = compress()
        await view.display(data)
    }
}

 

@concurrent func async

메서드를 항상 백그라운드 스레드에서 동작하도록 지시하는 키워드(Swift 6.2+)

메인 스레드에서 수행되지 않아도 되는 비동기 메서드를 백그라운드에서 동작하도록 함

 

@concurrent
func someMethod() async { }

 

참고

https://developer.apple.com/kr/videos/play/wwdc2022/110350
https://developer.apple.com/kr/videos/play/wwdc2025/268
https://developer.apple.com/kr/videos/play/wwdc2025/270
https://developer.apple.com/documentation/Swift/Sendable

'Swift' 카테고리의 다른 글

Swift: CORS Error  (0) 2025.11.17
정렬과 중복 제거가 가능한 모델  (0) 2025.08.30
struct Error vs. enum Error  (1) 2025.08.18
Swift: class func, static func, func  (0) 2025.07.20
Swift: print가 값을 출력하는 방법  (1) 2025.07.13