[Concurrency] async-await

정유진·2022년 7월 21일
0

swift

목록 보기
5/24
post-thumbnail

🧸 들어가며, 왜 async-await를 공부하게 되었나

UIKit과 RxSwift를 사용한 토이프로젝트를 마무리하고 이번에는 SwiftUI와 Combine을 주력으로 사용하는 토이프로젝트를 시작해보자 라는 마음을 먹었다. (비록 회사에서 SwiftUI를 쓸 일은 자주 없지만ㅎ) 기술 스택을 서치하던 중, Combine 무용론에 대한 의견을 접하게 되었다. 기존의 나는 background에서 네트워크 통신을 하면서 Combine을 애용해왔기에 충격이 컸다. '나 잘못 쓰고 있었나봐!' 하지만 주니어가 그럴 수도 있지. 서치를 통해 WWDC21에 소개된 async-await를 사용하는 것이 더 적합하다는 의견을 수용하며 내가 정리한 main point는 아래와 같다.

언제 Combine을 사용할 것인가.

  • Combine은 reactive programming framework
  • State의 변화에 따라 화면을 reactive하게 구현해야 할 경우
  • 나는 Combine을 버리지 않고 async와 용도를 달리하여 가져가겠다.
  • 왜냐하면 데이터 파이프 라인을 구성할 때 편하니까
  • 그리고 SwiftUI랑 잘 어울리니까 (반박 환영)

언제 async-await 를 사용할 것인가.

  • 네트워크 호출할 때
  • 어쨌든 별도의 task를 수행해야 할 때
  • 쓸 수 있는 모든 때에

https://wwdcbysundell.com/2021/the-future-of-combine/
고마워요 John!
Combine이 쓸모없다는 것이 아니라 본디 이 프레임워크는 비동기 프로그래밍 구현을 목표로 하고 있지 않다는 것을 알게 해준 당신...

🙄 공식 문서 톺아보기

나는 문서를 꼼꼼히 읽기보다는 일단 무작정 사용해 보는 것을 선호하는 편인데 이번에는 영어 공부도 할 겸 Swift 5.7 의 concurrency chapter를 신중하게 읽어보았다. 그리고 playground에 샘플을 작성해보았는데 오잉 컴파일 에러?ㅠ 프론트 엔드 개발을 했었던 나로서는 async-await 개념이 낯설지 않았으므로 (javascript 짱) '샘플 구현 쯤이야 개꿀~😸' 이라고 생각했는데 내가 바보여서 놓친 부분이 있었다 헤헤. 이를 포함하여 공식 문서에서 기억하면 좋을만한 점들을 요약해보겠다.

asynchronous? parallel code?

  • 당신이 비동기 코드를 쓴다면 당신은 코드의 진행을 중단시켰다가 다시 시작할 수 있다.
  • 비동기 코드를 사용한다면 상대적으로 시간이 걸리는 data fetching, network, parsing 을 진행하면서 UI를 업데이트 할 수 있다. (비동기 코드를 사용하지 않는다면 back에서의 작업이 진행되는 동안 UI가 freezing 🥶 될 것이다. 끔찍해!)
  • 병렬 코드 (번역이 맞나?)는 여러줄의 코드를 동시에 run할 수 있게 해준다. 4코어 프로세스 컴퓨터가 한 번에 4개의 작업을 동시에 진행할 수 있듯이
  • memory-safe하게 여러 작업을 처리할 수 있다! (Lock, Dispatch Queue, Dispatch Group 말고도 우리에게는 다른 옵션이 생겼다 야호!)
  • 하지만 async function이 어떤 thread 를 사용할지에 대해서는 보장할 수 없다.

⭐️ async를 쓸 수 있는 certain place!⭐️

  1. async 함수/메서드/프로퍼티 안에서
  2. static main() 메서드, @main으로 마크 된 클래스, enum 안에서
  3. child task 안에서

이걸 내가 몰랐네! async 함수를 call 하려면 async 함수 안에서 쓰던가 아니면 Task 안에서 써야한다는 것을...

🕶 Sample Code

공식문서의 sample을 약간 응용하여 내 기준 좀더 쉽게 작성해보았다. SwiftUI를 사용했고 버튼을 클릭하면 async 작업이 시작되는 간단한 코드이다. 시간이 소요되는 것을 흉내내기 위해 Task.sleep(nanoseconds:)를 이용하였다.

1) Content View

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Button("Async Test Start") {
                
                Task.init {
                    let async = AsyncTest()
                    await async.test()
                    print("async test done")
                }
            }
            
        }
    }
    
}
  • AsyncTest 객체는 async 메서드들을 가지고 있는 class 객체인데 viewModel 역할이라고 이해해주시길
  • Task 안에서 작성된 것에 유의하기
  • test()는 async 메서드이기 때문에 함수 앞에 await 태그가 사용되었다.
  • 이 await를 통해 비동기 코드를 동기 코드처럼 순차 사용할 수 있다.
  • async-await를 사용하지 않는다면 테스트의 진행과 상관없이 "test done"이 로그에 바로 찍힐 것이다.
  • 하지만 위 코드에서는 await를 통해 test() 메서드가 return 될 때까지 task가 suspended 되므로 test 작업을 마친 후에 "test done"이 찍히게 된다.
cf) Task를 시작하는 다른 방법도 있다. (SwiftUI)
Text("async test")
	.task {
		let group = TaskGroupTest()
		await group.test()
     }

하지만 나는 button의 action으로 동작하기 원했기 때문에 action의 closure 안에서 Task를 init하여 사용했다.

2) AsyncTest

class AsyncTest {
    func test() async {
              
        let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
        move(firstPhoto, from: "Summer Vacation", to: "Road Trip")
        
        let photoNames = await listPhotos(inGallery: "Summer Vacation")
        
        // async let
        async let first = downloadPhotos(name: photoNames[0])
        async let second = downloadPhotos(name: photoNames[1])
        async let third = downloadPhotos(name: photoNames[2])
        
        let photos = await [first, second, third]
        show(photos: photos)
    }
    
    private func listPhotos(inGallery name: String) async -> [String] {
        do {
            try await Task.sleep(nanoseconds: 2)
        } catch let error {
            print(error.localizedDescription)
        }
        return ["IMG001", "IMG99", "IMG0404"]
    }
    
    private func downloadPhotos(name: String) async -> String {
        do {
            try await Task.sleep(nanoseconds: 3)
        } catch let error {
            print(error.localizedDescription)
        }
        return "IMG001"
    }
    
    ...
}
  • listPhotos(), downloadPhotos() 가 시간이 소요되는 작업을 Task.sleep으로 흉내낸 async 메서드들이다.
  • 다시 부분으로 나누어 살펴보겠다.

🐣 1번 예제

 let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
 move(firstPhoto, from: "Summer Vacation", to: "Road Trip")
  • 만약 비동기 처리를 하지 않았다면 firstPhoto에 사진이 담기기도 전에 move 메서드가 호출될 것이다.
  • 우리는 firstPhoto가 확보되었다는 보장이 필요하다.
  • 따라서 ListPhotos()가 사진 배열을 return할 때까지 기다릴 것이다. await!
  • 다시 한 번 말하지만 await를 쓰면 after에 쓰인 함수 및 메서드가 return 할 때까지 기다린다.
  • 만약에 메서드가 throw를 하는 녀석이면 try await 라고 적었을 것이다.

🐥 2번 예제 async-let

let photoNames = await listPhotos(inGallery: "Summer Vacation")
 
async let first = downloadPhotos(name: photoNames[0])
async let second = downloadPhotos(name: photoNames[1])
async let third = downloadPhotos(name: photoNames[2])
        
let photos = await [first, second, third]
show(photos: photos)
  • 개인적으로 아주 fancy하다고 생각되는 async let 이다.
  • 우리는 첫 번째 await를 통해 모든 사진의 이름 리스트를 반환 받기까지 기다렸다.
  • 이 이름 리스트를 기반으로 다운로드를 진행하려 하는데 이름이 first, second, third 라고 해서 다운로드도 순차적으로 진행할 필요는 없다.
  • 다운로드는 순서를 지켜야 할 필요가 없고 독립적으로 또한 동시에 실행되면 그만이다.
  • async let 을 통해 각 다운로드는 independently 하게 at the same time 에 실행된다.
  • 하지만 사진을 show() 하기 위해서는 모든 다운로드가 끝나고 온전한 photos 가 완성되어야 한다.
  • 따라서 photos라는 배열이 완성되기를 기다릴 것이다. await!

https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html

🧤 결론

Swift로 비동기 코드를 구현하는 것은 새삼스러운 일이 아니긴 하지만 과거에 사용했던 기술(?) 보다는 확실히 async-await 가 간단하고 좋다. 하지만 iOS 13+에서 지원하기 때문에 당장 현업에는 적용하기가 어렵게 되었다. 아쉽다. 그래서 토이 프로젝트에서 잘 써먹어 볼 생각이다.

profile
느려도 한 걸음 씩 끝까지

0개의 댓글