Swift Concurrency는 동시성 프로그래밍을 가능하게 해줌
동시성 프로그래밍은 뭘까요?
스위프트에서는 동시성을 "비동기 및 병렬코드의 일반적인 조합"이라고 지칭한다고 함
비동기가 뭔가요..?
네트워크나 파일 구문분석같은 경우에는 시간 소요가 많음
이런 작업들이 계속되는 동안 UI업데이트와 같은 작업을 계속 진행할 수 있고, 이것을 비동기로 수행된다고 함
그럼 병렬코드는 뭐에요..?
여러 코드가 동시에 실행됨을 의미함. 코어가 4개라면 이 네개에서 코드의 실행이 동시에 일어남
근데 GCD를 쓰면 우리가 DispatchQueue를 통해서 동시성 프로그래밍을 할 수 있는데 왜 Swift Concurrency를 쓸까
이유가 여러개로 알고 있지만 오늘은 사용적인 관점만 보도록 하겠음!!
평소에 사용하던 URLSession + Completion Handler 형태의 네트워크 요청 함수임
runDataTask가 뭐냐면 통신 완료후 작업은 메인쓰레드에서 처리할 수 있도록 구성한 함수임
여기서 GCD를 사용하면 문제가 생기는 지점을 찾아볼 수 있음
(1) 오류 처리 관점
completionHandler에서 에러를 지정해주지 않을 수 있고 이로 인해 에러 핸들링의 안정성이 떨어질 수 있음
(2) 옵셔널 처리 관점
이미지가 있을수도 없을수도, 에러가 있을수도 없을수도 총 4개의 경우의 수가 나옴
이걸 해결하기 위해 ResultType을 사용할 수 있음
(3) 콜백지옥
비동기 통신을 계속 여러번 한다고 할 때 콜백이 계속 중첩되서 코드 가독성이 떨어질 것임
이 외에도 Swift Concurrency는 쓰레드 익스플로전 문제와 우선순위역전문제를 해결할 수 있음! 이것은 추후에 다뤄보겠음
이제는 코드로 GCD와 Swift Concurrency를 봐보도록 하겠음
아래는 GCD로 구성한 네트워크 통신 코드임
func callNetworkByGCD(_ url: URL, completion: @escaping (UIImage?, APIError?) -> Void) {
let request = URLRequest(url: url)
URLSession.shared.runDataTask(request: request) { data, response, error in
if let _ = error {
completion(nil, .fetchError)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completion(nil, .badID)
return
}
guard let data = data else {
completion(nil, .badImage)
return
}
guard let image = UIImage(data: data) else {
completion(nil, .badImage)
return
}
completion(image, nil)
}
}
Swift Concurrency로 코드를 변경해보면 throws를 통해 에러를 던져버릴 수 있음
그리고 반환값을 사용해서 completionHandler를 사용하지 않아도 바로 반환값을 받아서 사용할 수 있음
await이랑 그럼 async 키워드는 대체 뭔가요?
await은 해당 지점에서 실행이 중단될 수 있다는 것을 의미함
일반적인 함수에서는 실행의 중단이라는 개념이 없음. 그냥 스코프가 끝나면 종료되는 것임
하지만 비동기 작업에서는 중단의 개념이 있고, 그러면 저 함수도 중단될 수 있음을 알려함. 그게 async 키워드임!
func callNetworkByConcurrency(_ url: URL) async throws -> UIImage {
let (data, response) = try await URLSession.shared.data(from: url)
print(#function, Thread.isMainThread)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw APIError.badID }
guard let image = UIImage(data: data) else { throw APIError.badImage }
return image
}
그래서 구성은 하겠는데 사용은 어떻게 하나요?
비동기 작업을 수행하려면 함수에 async를 붙여야 한댔음. 근데 얘는 왜 async는 없고 Task라는 애가 있음
async로 함수를 선언해버리면, 이 함수를 호출하는 viewDidLoad같은 함수에도 async가 붙어야됨
그러면 연관된 모든 함수에 async를 붙여야 될텐데 이런 비동기-동기식 코드 사이 격차를 해소하고자 나온 게 Task임
비동기 컨텍스트를 제공해주고, 비동기 - 동기 작업 사이 브릿지 역할을 한다 정도로 생각하면 될 것 같음
근데 신기한게 여기서 어디서도 메인쓰레드로 전환해주는 코드를 작성하지 않았음
이건 UIViewController가 MainActor로 선언되어있기 때문임
Swift Concurrency + MainActor를 사용하면 메인스레드에서 UI관련 작업들이 적절하게 디스패치 된다고 함
그렇기 때문에 print 문으로 찍어보면 APIManager의 함수는 메인쓰레드가 아님
하지만 나머지 작업은 알아서 메인쓰레드에서 동작함
private func fetchImageByConcurrency() {
print("11111", Thread.isMainThread)
Task {
do {
print("22222", Thread.isMainThread)
let image = try await APIManager.shared.callNetworkByConcurrency(Nasa.photo)
print("33333", Thread.isMainThread)
nasaImageView2.image = image
} catch {
self.view.makeToast("오류가 발생하였습니다")
}
}
print("44444", Thread.isMainThread)
}
async-await을 WWDC에서 설명한 자료인데, await을 만난 순간 시스템에게 쓰레드 제어권을 줌
이 제어권을 줌으로써 "너 할일 많은 거 아니까 알아서 중요한거 먼저해!"라고 말해줌
그리고 중요한 일들이 끝나게 되면 시스템이다시 쓰레드 제어권을 돌려주게 됨(resume)
Swift Concurrency는 테스트 코드 관점에서도 상당히 간추려서 작성할 수 있음
테스트코드는 일반적으로 동기로 작동하기 때문에 expectaion & wait & fullfill 을 통해서 항상 테스트 코드를 작성해야 했음!
하지만 Swift Concurrency는 async-await을 통해서 간단하게 테스트 코드를 작성해볼 수 있음
func testAPIManager_Validation_GCD_ReturnSuccess() throws {
let expectation = XCTestExpectation(description: "mock network completion")
sut.callNetworkByGCD(NasaViewController.Nasa.photo) { image, error in
XCTAssertNotNil(image)
expectation.fulfill()
}
wait(for: [expectation], timeout: 20.0)
}
func testAPIManager_Validation_Concurrency_ReturnSuccess() async throws {
let result = try await sut.callNetworkByConcurrency(NasaViewController.Nasa.photo)
XCTAssertNotNil(result)
}
그러면 또 궁금한게 여러 비동기 작업을 할 때 주로 DispatchGroup을 사용하는데, 이건 어떤식으로 구성할 수 있을까
private func fetchMultipleImage() {
let url1 = Nasa.one.photoURL
let url2 = Nasa.two.photoURL
let url3 = Nasa.three.photoURL
Task {
async let image1 = APIManager.shared.callNetworkByConcurrency(url1)
async let image2 = APIManager.shared.callNetworkByConcurrency(url2)
async let image3 = APIManager.shared.callNetworkByConcurrency(url3)
let images = try await [image1, image2, image3] //오류 발생시 알 수 있는 지점
nasaImageView1.image = images[0]
nasaImageView2.image = images[1]
nasaImageView3.image = images[2]
}
}
async let이라는 개념이 등장함. 근데 생각해보면 저희가 "try await 네트워크 작업" 이런식으로 사용을 했는데
그거랑 이거랑 뭐가 다르지? 라고 생각할 수 있음.
만약에 async let이 아니라 try await을 통해서 각각을 실행시키면 순차 처리됨. 각 사진이 다음 사진이 다운로드 되기 전에 완전히 다운로드 된다는 것임.
근데 대부분 여러장 같이 받아와서 표시함. 그렇기 때문에 async let으로 병렬로 비동기 함수를 호출해주는 것임
그리고 이 병렬처리되는 작업들을 한번에 await해주면, 각 작업이 다 끝날때까지 기다리고 한번에 이미지 표시가 가능해짐
이 구조에 대해서 WWDC에서는 Child Task를 만든다고 함
실행흐름이 두개로 나뉘어서 부모 테스크는 자식 테스크(다운로드 작업)을 생성하고 다음 작업을 실행함
그리고 계속 다음 작업들을 하다가 자식 테스크가 사용하는 곳에서 부모가 자식 테스크들을 기다리게 됨(await)
그리고 에러가 발생하는 곳도 실제로 데이터가 쓰이는 곳(await)구문에서 발생한다고 함!
참고 자료
'iOS > iOS 지식' 카테고리의 다른 글
[iOS] StackView의 Distribution과 Alignment (0) | 2024.11.28 |
---|---|
Swift Concurrency(2) - Continutation (0) | 2024.11.11 |
[Swift] Protocol(1) - (some과 any는 왜 필요할까?) (0) | 2024.10.07 |
[Swift] Struct와 Class (1) - Class 안에 Class, Struct 안에 Class (0) | 2024.10.05 |
[iOS] ViewController 생명주기(2) - 실험 (2) | 2024.10.04 |