지난 글에서 짧게 지나갔던 비동기에 대해서 자세하게 알아보려고 한다.
먼저 동기와 비동기의 차이에 대해서 알아보자!
동기(Sync)
국어 사전의 동기를 검색하면 나오는 결과이다. 가장 마지막 줄의 풀이가 우리가 찾던 동기의 뜻이다.
우리가 찾는 동기의 뜻이지만 아주 복잡하게 쓰여있다...😱
동기 처리는 작업을 순차적으로 진행한다는 뜻이다. 순차적으로 작업이 진행되는 동안 다른 작업을 할 수 없다. 모든 작업이 다 끝나야만 다음 작업을 시행할 수 있다. 예를 들어, 검색어를 입력하고 검색을 시작하면 검색 결과가 나올 때까지 다른 일을 할 수 없다. 동기 처리를 진행하게 되면 여러 작업을 동시에 실행할 때 각 작업이 완료된 이후에 다른 작업이 진행되기 때문에 성능 저하가 발생할 수 있습니다. 하지만 동기 처리가 무조건 나쁜 것은 아니다. 간단하고 직관적인 코드를 작성할 수 있다는 장점이 있다.
비동기(Async)
비동기는 동기와 달리 작업이 순차적으로 처리되지 않는다. 동기와 비동기의 차이는 동시에 여러 작업을 처리하는 경우에 그 차이를 설명하기 쉽다. 동기에서 설명한 검색을 다시 한번 예로 들어 설명하면 검색하고 검색 결과를 기다리는 동안 음악을 재생하거나 다른 작업을 실행할 수 있다. 검색과 음악 재생이 각각 독립적으로 실행된다. 이를 작업이 병렬적으로 이루어진다고 설명한다. 비동기 처리는 주로 I/O 작업이나 네트워크 요청 등의 시간이 많이 소요되는 작업을 처리할 때 사용한다. 비동기 처리를 통해서 효율적으로 작업을 처리할 수 있다. 더불어서 사용자 경험도 개선할 수 있다. 예를 들어 서버에서 사진을 불러오는 동안에 다른 UI는 독립적으로 작동할 수 있다. 비동기 처리에도 몇 가지 단점이 존재한다. 가장 큰 단점은 코드의 복잡성이다. 여러 작업을 병렬적으로 진행하기 때문에 코드의 흐름이 복잡하다. 이에 따라 디버깅과 테스트 시에 어려움을 겪을 가능성이 높다.
Swift에서 비동기 처리를 구현하는 2가지 대표적인 방법을 살펴보자!!
1. 콜백 함수
비동기 처리 방식 중 하나인 콜백 함수는 다른 함수에 인자로 전달되어, 특정 작업이 끝난 후 실행되는 함수이다. 콜백 함수를 사용하면 비동기 작업에서 순차적인 작업을 보장할 수 있다. 가장 전통적인 방식이다.
func someFunction(callback: () -> Void) {
print("함수 실행 중...")
callback() // 콜백 함수 호출
}
someFunction {
print("콜백 함수 실행됨!")
}
swift로 작성한 콜백 함수의 예시이다. callback이라는 함수를 인자로 받아서 콜백 함수를 호출하여 전달된 함수를 실행한다. 비동기 처리 이후에 전달된 결과가 다음 동작의 함수를 호출하는 방식이다.
func step1(completion: @escaping () -> Void) {
DispatchQueue.global().async {
print("Step 1 실행")
sleep(1)
completion()
}
}
func step2(completion: @escaping () -> Void) {
DispatchQueue.global().async {
print("Step 2 실행")
sleep(1)
completion()
}
}
func step3(completion: @escaping () -> Void) {
DispatchQueue.global().async {
print("Step 3 실행")
sleep(1)
completion()
}
}
// 콜백 헬 발생 (콜백 중첩)
step1 {
step2 {
step3 {
print("모든 작업 완료")
}
}
}
콜백 함수에는 단점이 존재하는데 바로 "콜백 헬(Callback Hell)"이다. 위의 보이는 코드가 발로 콜백 헬이 발생하는 코드이다. 연속적으로 콜백 함수를 사용하면 코드가 복잡해지고 가독성이 떨어진다. 이로 인행 유지 보수성이 크게 떨어지게 된다. 위의 작성된 코드들에 경우에는 따로 에러 핸들링이 작성되어있지 않다. 실제 코드를 사용할 때 에러 핸들링까지 추가된다고 생각하면 가독성은 더 떨어질 것이다.
2. Async / Await 패턴
Async / Await 패턴은 콜백 헬을 피하고 비동기 처리를 간결하고 이해하기 쉽게 만들어 준다.
import Foundation
func someFunction() async {
print("함수 실행 중...")
await callbackFunction() // 비동기 콜백 호출
}
func callbackFunction() async {
print("콜백 함수 실행됨!")
}
// Task를 사용해 비동기 함수 호출
Task {
await someFunction()
}
위에서 작성한 예시를 Async / Await 패턴을 이용해서 다시 작성한 코드이다. 콜백 함수로 작성했을 때보다 직관적이다.
async는 비동기 함수를 정의할 때 사용된다. async 함수는 비동기적으로 실행되면서 내부에 await을 사용할 수 있다. async 함수를 사용할 때는 반드시 await을 사용해야 한다.
await은 비동기 함수가 종료되면 다음 코드를 실행하는 역할을 한다. 이를 통해서 콜백 없이 순차적으로 실행되는 동기 처리처럼 코드를 작성할 수 있다. await은 반드시 Task 또는 async 함수 안에서만 사용할 수 있다.
에러 처리 시에도 기존의 콜백 함수보다 간단하게 처리할 수 있다. 콜백 함수는 Result<T, Error> 또는 do-catch를 이용해서 오류를 해결하지만 async / await 패턴은 try await으로 간결하게 처리할 수 있다.
실제로 사용해 본 비동기 처리 (Async/Await 패턴)
여러 사례 중에서 Github의 API를 사용하는 사례를 가져왔다. 위의 함수는 Github에서 사용자의 이름을 검색했을 때 사용자의 정보를 가져오는 API를 구현하는 함수이다. 위의 네트워크 요청의 경우 검색을 통해서 사용자 정보를 가져오기 때문에 동기로 처리하게 된다면 사용자 정보를 가져오는 동안 UI가 업데이트되지 않아서 사용자 경험을 저하시킨다. 그래서 비동기처리를 통해서 검색에 해당하는 사용자 정보를 가져오고 그동안 UI가 업데이트된다. 스크롤을 해서 새로운 사용자가 추가되더라도 UI는 정상적으로 작동하고 다른 thread에서는 계속해서 사용자 정보를 공급한다. 이게 비동기 처리가 필요한 이유다!
사실 Swift에서 사용하는 또다른 비동기 처리가 있는데... 바로! RxSwift이다!
사실 이전 포스트에 자세하게 설명했기 때문에 궁금하다면 이전 포스트를 참고하길 바란다.
✅RxSwift에 대해서 설명한 포스트
'테크' 카테고리의 다른 글
정보 보안 기법 (0) | 2025.05.23 |
---|---|
PK,FK 그리고 Index (0) | 2025.04.10 |
RDBMS와 NoSQL (0) | 2025.03.25 |