목차
저는 비동기 처리를 알아보던 중 동시성을 접했어요. 그래서 '비동기 = 동시성' 이라고 생각하고 있었는데
API호출을 어떻게 하면 더 빠르게 호출할 수 있을까에 대해서 알아보던 중 동시성을 다시금 공부하게 됐습니다.
목차
1. 동시성이란 무엇일까?
- 동기(Synchronous) VS 비동기(Asynchronous)
- 직렬처리(Serial) VS 병렬처리(Parallel)
2. Async/await를 이용하여 비동기처리해보자
3. 비동기 함수 병렬처리(Parallel)하기
- async let
- 작업과 작업 그룹 (Tasks and Task Groups)
1. 동시성이란 무엇일까?
예시를 들어보겠습니다.
카페에 알바생이 3명이 있다. 음료 주문이 5잔 들어온 상황이다. 어떻게 음료를 제조하면 좋을까?
상황 1
- 알바생1이 음료를 하나를 만들고 또 하나를 만들고 이렇게 순서대로 5잔을 모두 만든다.
상황 2
- 알바생1이 커피를 만들고 있다. 원두를 추출하는 동안 에이드를 만들고 다시 커피를 완성했다. 이렇게 상황1보다 효율적으로 5잔을 모두 만든다.
상황 3
- 알바생 3명이 분담하여 만든다.
여기서 설명하려는 개념이 나옵니다.
1. 동기(Synchronous) VS 비동기(Asynchronous)
2. 직렬처리(Serial) VS 병렬처리(Parallel)
동기는 일을 하나씩 처리하는 것이고 비동기는 일을 하나를 시작해놓고 도중에 다른 일을 처리하고 다시 하던 일을 마무리하는 것입니다.
상황1은 동기, 상황2는 비동기라고 할 수 있습니다.
직렬처리는 혼자서 일을 다하는 것이고 병렬처리는 여러명에서 일을 하는 것입니다.
상황1, 2는 직렬처리이고, 상황3는 병렬처리입니다.
알바생은 스레드(thread)라고 생각하면 됩니다.
그래서 동시성이란?
동기-비동기, 직렬처리-병렬처리를 적절하게 사용해서 작업을 효율적으로 처리하는 것을 말합니다.
Q. 당연히 비동기, 병렬처리 방식을 사용하면 더 효율적으로 작업을 할 수 있을 것 같은데 동기, 직렬처리도 함께 사용하나요?
- 일반적으로 장점은 작업 처리 속도가 빨라지는 것이고, 단점은 코드가 복잡해지는 것입니다.
- 또 앞의 작업이 끝나야 다음 작업을 시작할 수 있는 순서가 있는 경우입니다.
예를 들어서 위의 예시에서는 음료 1잔 만들기를 작업으로 봤습니다.
이번에는 음료 1잔을 만드는 하위 과정들을 하나의 작업으로 보겠습니다.
아이스아메리카노를 만드는 과정을 생각해 볼게요. 마침 준비한 원두가 떨어져서 처음부터 준비한다고 가정해볼게요.
원두를 볶는다 -> 원두를 그라인딩한다 -> 에스프레소를 추출한다 -> 얼음컵을 준비한다 -> 에스프레소와 얼음컵을 혼합한다.
이 일련의 과정은 순서가 중요합니다. 예를 들어 알바생1이 원두를 볶고 있는데, 다른 알바생이 원두를 그라인딩을 할 수는 없잖아요? 이런 경우에는 병렬처리보단 직렬처리가 더 적절하다고 볼 수 있습니다.
또 같은 예로 비동기에 대해서 설명하자면 원두를 볶는 과정을 함수 roastBeans라고 해보겠습니다. roastBeans는 시간이 걸리는 과정입니다.
코드가 실행되는 과정을 보면 동기로 실행이 됩니다. 위에서부터 한줄씩 실행되는데
// 원두를 볶는다 -> 원두를 그라인딩한다 -> 에스프레소를 추출한다 -> 얼음컵을 준비한다 -> 에스프레소와 얼음컵을 혼합한다.
roastBeans()
grindBeans()
brewEspresso()
prepareIceCup()
mix()
이때 roastBeans처럼 시간이 걸리는 함수가 있을 때 비동기 처리가 되지 않았다면, roastBeans가 처리되고 있는 중 grindBeans가 실행되는 경우가 있습니다. 그렇게 될 경우 다 볶아지지 않은 원두로 커피를 만들면 맛에 문제가 있겠죠? 이럴 경우엔 비동기처리를 해주어야합니다. 일반적으로 비동기처리는 네트워크 관련 작업을 할 때 필요합니다.
2. Async/await를 이용하여 비동기처리해보자
비동기 함수에 대해서 조금 더 설명을 해보자면
동기 함수는 실행되거나 완료될 때까지 실행되거나, 오류가 발생하거나, 반환되지 않는 함수입니다.
이와 대조적으로 비동기함수는 위 세 가지 중 하나를 수행하지만, 무언가를 기다리고 있을 때 중간에 일시중지 될 수 있습니다.
예시와 함께 설명하겠습니다.
사진전이 있습니다. 요시고, 사울레이터 등등
요시고 작가님이 사진 전시회를 하고 있는데,
✅ 1. 그 전시회의 사진 이름들을 모아둔 리스트를 뽑고
✅ 2. 리스트를 기반으로 이미지를 다운받으려고 합니다.
func listPhotos(name: String) -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
func downloadPhoto(photoName: String) -> Image {
let photo = // ... some asynchronous networking code ...
return photo
}
downloadPhoto는 사진 1장을 다운로드 받는 메소드입니다.
사진을 다운로드하는 것을 코드로 작성해보면 아래와 같습니다. 일단 리스트를 받아오고 사진을 다운로드 받습니다.
let photoNames = listPhotos(name: "요시고") // ["그림1", "그림2", "그림3"]
let firstPhoto = downloadPhoto(photoName: photoNames[0])
listPhotos 메소드가 빠르게 완료가 된다면 downloadPhoto는 옳바른 값을 반환할 것입니다.
하지만 네트워크 통신이라고 가정했을 때 시간이 걸린다면 지금처럼 동기 함수로 진행할 경우 listPhotos가 실행되고 있는 중에 downloadPhoto가 실행된다면 잘못된 값을 반환할 수 있습니다.
그래서 기존의 코드를 비동기 함수로 바꿔서 적용을 하면
listPhotos에 'async'라는 단어가 추가됐습니다. 이 함수는 비동기라는 표시입니다.
func listPhotos(name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
func downloadPhoto(photoName: String) -> Image {
let photo = // ... some asynchronous networking code ...
return photo
}
그리고 코드를 다시 작성하면 첫번째 코드에 'await'가 추가됐습니다.
위에서 비동기 코드는 실행이 완료될 때까지 일시정지할 수 있다고 언급했었는데요, await는 listPhotos가 다 준비될 때까지 '잠시 기다리겠다' 라는 의미입니다. 이렇게 비동기처리를 해주면 photos는 listPhotos 메소드가 완료될때까지 기다리고 다음 코드로 넘어가게 됩니다. 그리고 의도한 값을 반환받을 수 있게 됩니다.
let photoNames = await listPhotos(name: "요시고") // ["그림1", "그림2", "그림3"]
let firstPhoto = downloadPhoto(photoName: photoNames[0])
3. 비동기 함수 병렬처리(Parallel)하기
이번엔 사진을 여러장 다운 받아보겠습니다.
let photoNames = await listPhotos(name: "요시고") // ["그림1", "그림2", "그림3"]
let firstPhoto = downloadPhoto(photoName: photoNames[0])
let secondPhoto = downloadPhoto(photoName: photoNames[1])
let thirdPhoto = downloadPhoto(photoName: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
이렇게 진행하게 되면 firstPhoto, secondPhoto, thirdPhoto가 각각 다운로드되고 photos 배열에 저장됩니다. 이때 downloadPhoto도 네트워크 통신이라고 가정하게되면 downloadPhoto끼리는 순서가 상관 없지만 photos 배열에 저장될 때까지 다운로드가 완료되지 않는다면 빈 값이 저장될 수 있습니다.
그래서 downloadPhoto도 마찬가지로 비동기 처리를 해주어서 photos에 저장될 때는 전부 다운로드 된 상태로 만들어보겠습니다.
func downloadPhoto(photoName: String) async -> Image {
let photo = // ... some asynchronous networking code ...
return photo
}
let photoNames = await listPhotos(name: "요시고") // ["그림1", "그림2", "그림3"]
let firstPhoto = await downloadPhoto(photoName: photoNames[0])
let secondPhoto = await downloadPhoto(photoName: photoNames[1])
let thirdPhoto = await downloadPhoto(photoName: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
이렇게 해주면 첫 번째 사진이 다 받아지면 두 번째 사진, 그리고 세 번째 사진 순서대로 코드가 진행됩니다.
일단 다 완료는 됐지만, 위에서 동시성에 대해 설명할 때 비동기와 병렬처리가 있다고 말했었습니다.
지금 코드는 비동기처리는 해주었지만, 한 스레드에서 세 개의 다운로드 작업을 실행하고 있습니다. 모든 작업이 한 스레드에서 진행되는 직렬처리이다 보니 작업이 오래걸릴 수 밖에 없습니다.
그렇다면 어떻게 해야할까요?
downloadPhoto를 각자 다른 스레드로 보내어 작업을 처리하려고 합니다.
async let
async let을 사용하면 자동으로 병렬처리가 되어 각 코드가 다른 스레드에서 실행됩니다. 그리고 마지막 코드에 await를 넣어주어 각 사진들이 다운로드가 완료되면 photos 이후의 코드가 실행될 수 있습니다.
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
async let을 사용하면 수가 적거나 일정한 수가 정해진 사진에 대해서 병렬처리를 할 수 있습니다.
그렇지만 예를 들어 사진전의 모든 사진을 받으려고 한다면 수가 많아지거나 셀 수 없어지는 경우가 생깁니다. 이럴 경우 async let을 사용하긴 어려워집니다. 이럴 때는 다른 방법이 있습니다.
작업과 작업 그룹(Tasks and Task Groups)
withTaskGroup 혹은 withThrowingTaskGroup(에러를 던질 필요가 있을 때)을 호출하여 사용할 수 있습니다.
func downloadPhotos(photoNames: [String]) async -> [Image]{
await withTaskGroup(of: Data.self) { taskGroup in
for name in photoNames {
taskGroup.addTask {
return await downloadPhoto(named: name)
}
}
var photos: [Image] = []
for await image in taskGroup {
phoros.append(image)
}
return photos
}
결론
동시성에 대해 개념을 알 수 있었고, 비동기와 병렬처리를 어떻게 사용하는지 알아보았습니다.
저는 스위프트 공식문서에서 동시성을 처음 공부했는데 하면서 아쉬웠던 점이 뭔가 세세하게 알려주기 보단 이정도 보여줬으면 알아들었지? 이런 느낌이었습니다. 그래서 저는 제가 이해한 부분과 추가로 공부했던 부분을 정리해서 블로깅했습니다.
잘못된 부분이 있으면 피드백 주시면 감사하겠습니다.
참고:
https://bbiguduk.gitbook.io/swift/language-guide-1/concurrency
동시성 (Concurrency) - Swift
한 동시성 도메인에서 다른 동시성 도메인으로 공유될 수 있는 타입을 전송 가능 타입 (sendable type) 이라고 합니다. 예를 들어, 액터 메서드로 호출될 때 인수로 전달되거나 작업의 결과로 반환될
bbiguduk.gitbook.io
https://www.swiftbysundell.com/articles/swift-concurrency-multiple-tasks-in-parallel/
Using Swift’s concurrency system to run multiple tasks in parallel | Swift by Sundell
One of the benefits of Swift’s built-in concurrency system is that it makes it much easier to perform multiple, asynchronous tasks in parallel, which in turn can enable us to significantly speed up operations that can be broken down into separate parts.
www.swiftbysundell.com
https://www.youtube.com/watch?v=zRJOte7TaPw&t=364
'Swift' 카테고리의 다른 글
CoreLocation) 문서 공부2 (CLLocationManager) (4) | 2024.02.06 |
---|---|
CoreLocation) 문서공부1 (현재 위치 받기, CLLocation 클래스 파헤치기) (1) | 2024.02.01 |
CoreLocation) 위치정보 이용 관련 시뮬레이터 활용방법 (2) | 2024.02.01 |
CoreLocation) 위치정보 이용에 필요한 승인 권한 요청(사용중, 항상) (0) | 2024.02.01 |
Realm - Updating UITableView 관련 이슈 및 해결(Xcode 이슈) / 이게 되네.. (0) | 2024.01.16 |