작년에 SyncSwift 2022 현장에서 김윤재님의 발표를 직접봤었다.
그때는 Concurrency에 대해 잘 모르고 있어서 이해하긴 어려웠지만 분명 좋은 내용이라는 것은 느낄 수 있었다. 그래서 나중에 다시 볼 수 있다면 좋겠다라는 생각을 했었는데 다행히 유튜브와 Line개발 블로그에 해당 내용이 잘 업로드 되어 있었다.
SyncSwift 2022 김윤재님의 Swift Concurrency 적응기를 보고 정리해보는 글입니다.
유튜브: https://www.youtube.com/watch?v=jdtgniY-8wA&ab_channel=AsyncSwiftKorea
블로그: https://engineering.linecorp.com/ko/blog/about-swift-concurrency
SE-0296을 참고하셨다고 같이 참고하여 작성했다.
파멸의 피라미드, 콜백 아도겐 ㅋㅋㅋㅋㅋ
콜백 ⬆️ + 줄바꿈 ⬆️ = 가독성이 낮아짐
기존 코드를 보면 코드가 실행되는 위치를 읽고 추적하기 어렵게 만든다.
func processImageData1(completionBlock: (_ result: Image) -> Void) {
loadWebResource("dataprofile.txt") { dataResource in
loadWebResource("imagedata.dat") { imageResource in
decodeImage(dataResource, imageResource) { imageTmp in
dewarpAndCleanupImage(imageTmp) { imageResult in
completionBlock(imageResult)
}
}
}
}
}
processImageData1 { image in
display(image)
}
개발자가 직접 에러를 처리해주어야 한다. 에러를 처리하지 않아도 컴파일 시점에 에러가 발생하지 않아 앱 실행 중 에러가 발생 할 수 있다.
Result타입을 사용하여 어느정도 개선이 가능하지만 여전히 가독성이 좋지 않다.
비동기 함수를 조건부로 실행하는건 매우 번거롭고 코드의 가독성을 떨어뜨린다.
func processImageData3(recipient: Person, completionBlock: (_ result: Image) -> Void) {
let swizzle: (_ contents: Image) -> Void = {
// ... eventually completionBlock을 호출하는 계속 클로저
}
if recipient.hasProfilePicture {
swizzle(recipient.profilePicture)
} else {
decodeImage { image in
swizzle(image)
}
}
}
이 코드와 설명이 처음엔 이해가 안됐는데 아래와 같다.
processImageData3 함수는 recipient 객체가 프로필 사진을 가지고 있는지 확인하고, 사진이 있을 경우에는 바로 처리하고, 사진이 없을 경우에는 비동기적으로 이미지를 디코딩한 후에 처리하는 역할을 하고있다.
이 예시 코드에서 언급한 문제는?
조건부 실행의 번거로움
: 보통은 조건을 확인하고 처리를 위한 로직을 작성하지만, 위의 코드에서는 처리를 위한 로직이 먼저 작성되고 조건을 확인하는 비동기 함수 호출이 포함되어 있다. 따라서 코드의 구조가 비효율 적이며 가독성이 떨어진다.
완료 핸들러의 사용
: 비동기 작업이 완료된 후에 실행되는 클로저인 completion handler가 사용되는데 이는 코드의 복잡성을 증가시킨다.
실수가 발생할 가능성이 많다. completion 블록 호출을 잊거나 혹은 블록 호출 후 반환을 잊을 수 도 있다.
코드 작성중에는 에러 확인할 수 없어서 실수 가능성이 있다.
func processImageData4a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
loadWebResource("dataprofile.txt") { dataResource, error in
guard let dataResource = dataResource else {
return // <- 블록 호출을 잊었음
}
loadWebResource("imagedata.dat") { imageResource, error in
guard let imageResource = imageResource else {
completionBlock(image) // <- 블록 호출 후 반환을 잊었음
}
...
}
}
}
많은 사람들이 Comletion handlers를 통해 비동기 코드를 작성하는 방식을 어색해하고 어려워해서 제대로 사용하지 못하는 경우가 많다고 한다 ㅠㅠ (공감)
Swift concurrenct가 등장하게된 배경에 대해서 알아봤는데
Problem1 에서 언급한 문제를 Swift Concuttency로 작성하면 아래처럼 가독성이 좋은 코드로 표현 가능하다.
func processImageData() async throws -> Image {
let dataResource = try await loadWebResource("dataprofile.txt")
let imageResource = try await loadWebResource("imagedata.dat")
let imageTmp = try await decodeImage(dataResource, imageResource)
let imageResult = try await dewarpAndCleanupImage(imageTmp)
return imageResult
}
기존 방식에서는 에러처리를 위해 작성해야 하지만 누락되어도 컴파일 에러가 발생하지 않아 개발자가 주의해야 할 필요가 있었지만 Swift Concurrency로 작성하면 에러 핸들링은 throw로, 데이터 전달은 return으로 분리하여 실수를 방지할 수 있다.
동시성 프로그래밍을 할 때 여러 코드에서 하나의 변수에 접근할 수 있기 때문에 동기화 문제를 올바르게 처리해주어야 한다.
Swift Concurrency를 사용하면 동기화가 제대로 처리되지 않았다면 컴파일 에어를 발생시켜 실수를 미연에 방지해준다.
GCD를 이용한 코드 작성
"thread explosion"이 발생하지 않도록 주의해야 한다. "thread explosion"는 너무 많은 스레드가 생성되어 컨텍스트 스위칭과 성능 저하를 유발하는 현상을 의미한다.
이를 방지하기 위해 하나의 서브시스템에 하나의 Dispatch Queue를 할당하는 것이 권장된다.
Swift Concurrency
스레드 관리를 보다 편리하게 제공한다.
await 키워드를 사용하여 비동기 작업이 중단될 때, CPU가 스레드를 전환하는 대신, 같은 스레드 내에서 다음 작업을 실행한다.
결국 각 스레드는 여러 작업을 순차적으로 처리할 수 있게 되며, 스레드 간의 전환에 따른 오버헤드를 줄일 수 있다.
즉 GCD를 사용할 때에는 너무 많은 스레드가 생성되지 않도록 주의해야 하지만 Swift Concurrency를 사용하면 스레드 관리가 보다 편리하고 효율적으로 이루어 진다는 것이다.
발표 영상이나 블로그를 보면 직접 테스트한 자료가 있다!
Dispatch Queue에서의 우선순위 역전
: Dispatch Queue를 사용하여 동시성 프로그래밍을 할 때 여러러 작업이 하나의 큐에 추가될 수 있고 이때 각 작업은 서로 다른 우선순위(QoS - Quality of Suervice)를 사질 수 있다.
GCD는 이러한 상황에서 앞서 추가한 작업들(Background Qos작업들)의 우선순위를 높여서 새로 추가된 작업(user Initiated Qos작업)이 너무 오래 기다리지 않도록 한다.
Swift Concurrency 에서의 해결
: Dispatch Queue와 다르게 작업이 실행되는 순서가 FIFO가 아니다. 따라서 우선순위가 높은 작업이 먼저 실행될 수 있다.
즉 Dispatch Queue는 우선순위 역전을 방지하기 위해 앞선 작업들의 우선순위를 높이지만 Swift Concurrency는 우선순위가 높은 작업이 먼저 실행되게 한다.
그런데 이 내용을 보면서 의문이 있다.
Dispatch Queue에서 우선순위 역전을 해결하기 위해 중요한 작업보다 앞선 작업들의 우선순위도를 높이는 방식을 사용한다고 했는데 이는 아무런 이점도 주지 않는거 아닌가?라는 생각이 든다.
앞선 작업을의 우선순위가 높거나 낮아도 어차피 순서대로 작업이 처리되길 기다려야 하는거 아닌가? 헷갈린다...
그래서 QoS가 높은 경우와 낮은 경우 어떤 차이가 있는지 알아보았다
즉 별로 중요하지 않은 작업들을 중요하다고 명시해서 빠르게 처리하게 한다는 것이었다!
이후에는 Actor에 대한 설명을 포함하고 있는데 Actor와 관련된 내용은 추후에 따로 정리하고 작성하도록 하겠다
Swift Concurrnecy에 대한 내용에 대해 한번 정리한적 있어서 이해하기 편했고 재밌게 볼 수 있었다. 그리고 직접 테스트까지 하는 과정이 흥미로웠다.