Neoself의 기술 블로그

Swift에서의 동시성 프로그래밍(Concurrent Programming) 본문

개발지식 정리/Swift

Swift에서의 동시성 프로그래밍(Concurrent Programming)

Neoself 2024. 10. 21. 17:01

1.  동시성

동시성 프로그래밍은 엄밀히 따지면, 동시성과 병렬성을 모두 포함하는 개념이라고 볼 수 있습니다. 즉, 여러 작업을 동시에 처리하는 프로그래밍 방식입니다.

 

여기서 동시성 프로그래밍(Concurrent Programming)과 병렬성 프로그래밍(Parrallel Programming)의 가장 큰 차이는 무엇일까요? 

둘다 여러 작업을 동시에 처리한다는 방향을 공유하고 있지만, 동시성이 한번에 하나의 작업을 번갈아가며 처리를 하는 방식이라면, 병렬성은 멀티코어 환경을 바탕으로 여러작업을 실제로 동시에 처리하는 방식을 일컫습니다.

즉 동시성 프로그래밍은 소프트웨어적 쓰레드에서 동시에 일을 하는 개념이기 때문에, 보다 개발자 개입이 중요한 처리방식입니다.

 

아래 다이어그램을 보면 동시성과 병렬성의 동작방식 차이를 더 쉽게 이해할 수 있을 것 같습니다! 코어가 1개일 경우에는, 각 작업을 주로 시분할의 원리에 따라 쪼갠 후에, 각 작업을 번갈아서 수행하는 과정을 거치게 됩니다. 하지만, 멀티코어의 경우 각 쓰레드에 일괄적으로 할당이 가능하기 때문에 번갈아서 수행하는 과정없이, 말 그대로 동시에 여러 작업을 처리하게 됩니다.

iOS 및 MacOS에서는 아래 두가지 방식을 통해 위 동시성을 구현할 수 있습니다.

  1. DispatchQueue: 작업을 큐에 제출하고 시스템이 관리하는 스레드 풀에서 실행
  2. Swift Concurrency: 구조화된 동시성을 제공하며 async/await 패턴을 사용

Swift 5.5가 되면서 Swift Concurrency가 추가된 것인데, 이번 글에서는 DispatchQueue를 중심으로 설명드리겠습니다

let queue = DispatchQueue(label: "com.example.myQueue", attributes: .concurrent)
// 여기서 자료구조를 공부하신 분은 익숙한 워딩이 보이실 겁니다. 
// Queue는 First in First Out을 이행하는 자료구조로, DispatchQueue에서 작업을 관리하고 실행하는 데이터 구조로써 활용됩니다.

queue.async {
    for i in 1...5 {
        print("비동기 작업 \(i)")
        Thread.sleep(forTimeInterval: 1)
    }
}

for i in 1...3 {
    print("메인 스레드 작업 \(i)")
    Thread.sleep(forTimeInterval: 0.5)
}

// 메인 스레드 작업 1
// 비동기 작업 1
// 메인 스레드 작업 2
// 메인 스레드 작업 3
// 비동기 작업 2
// 비동기 작업 3
// 비동기 작업 4
// 비동기 작업 5

위 코드는 DispatchQueue 도구를 활용해 별도의 큐를 생성한 다음, queue.async 클로저를 통해 생성한 큐에 작업을 추가하고 있습니다. 메인스레드에서 print("메인스레드 작업 \(i)")를 반복수행하는 동안, 시스템 레벨에서 스레드 풀을 관리하는 GCD(Global Central Dispatch)로 queue.async를 통해 추가된 작업들이 제출되며, GCD는 시스템 리소스 상황에 따라 사용 가능한 스레드에 제출받은 작업들을 할당해줍니다. 즉, 동기처리 작업들이 실행되는 메인스레드와는 독립적으로 동작하기 때문에 메인스레드 작업 3개가 모두 완료되는 1.5초 이후에, 2초부터 1초씩 처리되는 비동기작업 2부터 5가 처리완료되는 것입니다.

 

DispatchQueue.main.async 또한 메인스레드와 GCD 가 독립적으로 실행되기 때문에 필요한 메서드입니다. UI 작업의 경우 무조건 메인스레드에서 실행되어야, 원활한 동작이 가능합니다. 때문에 비동기 작업 도중 UI를 변경해야하는 작업, 예를 들어 뷰모델 내부에서 @Published 변수를 변경하는 상황이면, 위 메서드를 통해 GCD에서 메인스레드로 전환이 필요합니다.

 

2.  비동기 처리

자 그럼 앞서 말씀드렸던 비동기 작업은 정확히 무엇을 의미하는 것일까요?

비동기를 말씀드리기에 앞서, 반대되는 개념인 동기처리부터 말씀드리겠습니다.

func makeCoffee() {
    grindBeans()
    brewCoffee()
    serveCoffee()
}

makeCoffee()
doNextTask() // makeCoffee()가 완전히 끝난 후에 실행됩니다.

동기처리의 경우 커피를 제조하고, 서빙하는 과정을 생각해보면 됩니다. 커피가 사용자에게 서빙되기 전에는 커피콩을 가는 과정, 그리고 커피를 볶는 과정이 선행되어야만 합니다. 커피가 다 만들어지기 전에는 커피를 서빙할 수가 없죠.

이처럼 저희에게 익숙한 Top-down 순서로, 한 작업이 끝날때 까지 다음 작업을 시작하지않고 기다리는 것이 바로 동기처리라고 칭합니다.

 

하지만, 작업이 수행되는 시간동안 메인 스레드, 즉 UI 쓰레드를 계속 점유해야 하기 때문에, 클라이언트 단에서 약속된 시간 안에 작업완료가 보장받기 어렵거나, 소요시간이 큰 작업을 수행할 때에는, 앱이 사용자 입력에 반응하지 않는 UI Freezing 현상이 발생할 수 있습니다. 여기에는 메인 쓰레드를 항시 점유해야 원활히 동작하는 애니메이션도 포함됩니다.

 

때문에 위와 같은 상황에서는 메인 쓰레드가 아닌, 백그라운드 쓰레드에서 작업을 수행하는 비동기 처리를 주로 사용합니다. 여기서 UI Freezing 현상을 초래할 수 있는 작업들은 아래와 같습니다.

  1. 네트워크 요청 (ex. API 호출, 파일 다운로드) : 네트워크 지연 시간이 불예측적임. 
  2. 파일 입출력 (ex. 대용량 파일 읽기/쓰기, 로그 기록) : 디스크 접근 속도가 느릴 수 있음.
  3. 이미지 처리 : 이미지 리사이징과 같이 큰 리소스가 할애되는 작업.

동시성 프로그래밍과 비동기 처리 간의 차이에 대해 헷갈리시는 분들이 있을 것 같습니다.

둘 다 시스템 리소스를 효율적으로 처리하기 위한 프로그래밍 기법이지만, 동시성 프로그래밍이 2개이상의 스레드에서 작업을 동시에 처리한다는 것에 중점을 둔다면, 비동기 처리의 경우 작업의 완료를 기다리지 않고, 다음 작업을 시작하는데에 중점을 둡니다.

 

이 두 개념은 상호보완적 관계. 더 쉽게 풀어 말하자면, 동시성을 구현하는 핵심 매커니즘 중 하나라고 이해하시면 될 것 같습니다.

 

다음은 Swift에서 사용되는 비동기 패러다임에 대해 다루도록 하겠습니다. 감사합니다.