Q. Swift Concurrency란 무엇인가요?
Swift Concurrency는 비동기·동시성 코드를 안전하고 간결하게 작성하기 위한 Swift의 공식 동시성 모델입니다.
기존의 GCD, OperationQueue 기반 방식의 한계를 보완하기 위해 도입되었으며,
언어 차원에서
- 비동기 코드의 가독성을 높이고
- 데이터 레이스를 방지하며
- 안정적인 동시성 처리를 가능하게 합니다.
Q. Swift Concurrency가 등장하게 된 배경은 무엇인가요?
기존 비동기 처리 방식에는 다음과 같은 문제가 있었습니다.
- completion handler 중첩으로 인한 가독성 저하 (콜백 지옥)
- 실행 스레드 추적의 어려움
- 공유 데이터 접근 시 Race Condition 발생 가능성
Swift Concurrency는 이러한 문제를 라이브러리 차원이 아니라 언어 차원에서 해결하기 위해 등장했습니다.
Q. async / await는 무엇인가요?
async / await는 비동기 작업을 동기 코드처럼 작성할 수 있게 해주는 문법입니다.
async는 이 함수가 중단(suspend)될 수 있음을 의미하고,
await는 비동기 작업의 결과를 기다린다는 의미입니다.
내부적으로는 스레드를 블로킹하지 않고, 작업이 완료되면 이어서 실행됩니다.
Q. async / await는 GCD와 어떤 차이가 있나요?
GCD의 DispatchQueue.async와 달리,
- 비동기 함수가 값을 반환할 수 있고
- throws를 통해 에러를 전달할 수 있습니다.
- 코드 흐름이 위에서 아래로 읽혀 가독성이 좋습니다.
즉, 비동기 작업을 “흐름 중심”으로 표현할 수 있다는 점이 가장 큰 차이입니다.
Q. Task란 무엇인가요?
Task는 Swift Concurrency에서 비동기 작업의 실행 단위입니다.
모든 비동기 코드는 반드시 Task 안에서 실행되며,
- 메인 스레드를 막지 않고 작업을 실행하고
- 구조적 동시성(Structured Concurrency)을 따르며
- 취소(cancellation)와 우선순위를 지원합니다.
Q. 왜 Task가 필요한가요?
SwiftUI의 버튼 액션처럼 동기 컨텍스트에서는 async 함수를 직접 호출할 수 없습니다.
이때 Task를 사용하면 비동기 컨텍스트를 만들어 async / await를 사용할 수 있습니다.
즉, Task는 동기 코드와 비동기 코드 사이의 다리 역할을 합니다.
Q. Swift Concurrency와 GCD의 가장 큰 차이는 무엇인가요?
가장 큰 차이는 “개발자가 스레드를 관리하느냐, 시스템이 관리하느냐”입니다.
Swift Concurrency에서는 개발자가 “무엇을 비동기로 할지”만 선언하고, 실제 실행 방식은 시스템에 맡깁니다.
Q. Actor란 무엇인가요?
Actor는 동시성 환경에서 공유 상태를 안전하게 보호하기 위한 타입입니다.
Actor 내부의 상태는 ⭐️ 한 번에 하나의 Task만 접근 ⭐️할 수 있어 데이터 레이스를 방지합니다.
즉, Actor는 락(lock)이나 Serial Queue를 직접 작성하지 않고도 동시성 안전성을 보장합니다.
Q. Actor에서 왜 await가 필요한가요?
Actor 외부에서 접근할 경우, 다른 Task가 이미 Actor를 사용 중일 수 있기 때문에 대기(await)가 필요합니다.
이를 통해 Actor는 ⭐️내부 상태 접근을 자동으로 직렬화⭐️합니다.
Q. Actor와 Serial Queue의 차이점은 무엇인가요?
Serial Queue는 동시성 안전성을 개발자가 직접 보장해야 하지만, Actor는 언어 차원에서 자동으로 동시 접근을 제어합니다.
즉, Actor는 “동시성 안전한 객체”를 제공하는 개념입니다.
Q. MainActor는 무엇인가요?
MainActor는 UI 업데이트를 반드시 메인 스레드에서 실행하도록 보장하는 Actor입니다.
UIKit과 SwiftUI의 UI 코드는 메인 스레드에서만 실행되어야 하므로, 비동기 환경에서는 MainActor가 필요합니다.
Q. async let은 언제 사용하나요?
서로 독립적인 비동기 작업을 병렬로 실행하고 싶을 때 사용합니다.
작업 개수가 고정되어 있고 결과를 한 번에 기다리는 경우에 적합합니다.
async let은 서로 독립적인 비동기 작업을 동시에 시작하고, 마지막에 결과를 모아 한 번에 처리하기 위해 사용하는 문법입니다.
Q. TaskGroup은 언제 사용하나요?
비동기 작업의 개수가 동적이거나, 여러 작업의 결과를 순차적으로 수집해야 할 때 사용합니다.
- async let → 단순한 병렬 처리
- TaskGroup → 복잡한 병렬 작업 관리
용도로 구분합니다.
Q. Swift Concurrency에서 병렬 처리 시 주의할 점은 무엇인가요?
병렬 처리는 시간 단축에 유리하지만, 공유 데이터 접근 시
- Race Condition: 여러 작업이 동시에 실행되며 실행 순서에 따라 결과가 달라지는 상황
- Data Race: 여러 스레드/Task가 동시에 같은 메모리를 읽고 쓰며 데이터 무결성이 깨지는 상황
- Deadlock: 두 개 이상의 작업이 서로 자원을 기다리며 영원히 멈춰버리는 상태
위험이 발생할 수 있습니다.
따라서 공유 상태를 가지는 객체는 Actor로 감싸 직렬화하는 것이 중요합니다.
Q. Swift Concurrency와 RxSwift의 차이는 무엇인가요?
- Swift → Concurrency 단발성 비동기 작업에 적합
- RxSwift → 이벤트 스트림 처리에 강점
두 기술은 경쟁 관계라기보다 문제 성격에 따라 선택하는 도구라고 생각합니다.
Q. Task Cancellation이란 무엇인가요?
Task Cancellation은 실행 중인 비동기 작업에 대해 취소 요청을 보내는 기능입니다.
Swift Concurrency의 취소는 강제 종료가 아니라 협조적 취소(Cooperative Cancellation) 방식입니다.
즉, 작업 내부에서 취소 여부를 확인하고 직접 중단 처리해야 합니다.
Q. async / await에서 self 캡처 문제는 없나요?
async / await는 클로저 기반이 아닌 함수 흐름 기반이기 때문에 RxSwift보다 self 캡처 문제가 적습니다.
다만, Task를 프로퍼티로 보관하는 경우에는 생명주기 관리와 취소 처리가 필요합니다.
Q. Swift Concurrency에서 메모리 관리는 어떻게 되나요?
Swift Concurrency는 기존 ARC 기반 메모리 모델을 그대로 사용합니다.
다만 Task는 명시적으로 cancel하지 않으면 계속 실행될 수 있으므로,
ViewController나 ViewModel의 생명주기에 맞춰 Task를 관리하는 것이 중요합니다.
Q. async / await는 항상 메인 스레드에서 실행되나요?
아닙니다.
async 함수는 어떤 스레드에서 실행될지 보장하지 않으며, UI 업데이트는 반드시 MainActor에서 수행해야 합니다.
Q. Swift Concurrency를 언제 사용하는 것이 적절하다고 생각하나요?
- 네트워크 요청
- 단발성 비동기 작업
- UI 이벤트 기반 비동기 처리
반면,
- 복잡한 이벤트 스트림
- 지속적인 상태 바인딩
이 필요한 경우에는 RxSwift가 더 적합할 수 있다고 생각합니다.
Q. async/await를 실제로 적용하면서 가장 크게 느낀 변화는 무엇이었나요?
기존 completion handler 기반 코드에서는 비동기 흐름이 중첩되면서 가독성이 떨어지고,
에러 처리 위치도 분산되는 문제가 있었습니다.
async/await로 전환한 이후에는 비동기 로직을 동기 코드처럼 위에서 아래로 읽을 수 있게 되었고,
에러 처리 또한 do-catch로 한 곳에서 관리할 수 있어 코드 이해와 유지보수가 훨씬 쉬워졌습니다.
Q. async 함수 안에서 여러 비동기 작업을 호출할 때, 어떤 기준으로 순차/병렬을 나누시나요?
작업 간 의존성이 있는 경우에는 순차 실행, 서로 독립적인 작업인 경우에만 병렬 실행을 선택합니다.
병렬 실행이 항상 좋은 것은 아니기 때문에, 데이터 간 의존성이 없는지 먼저 판단한 뒤
async let 또는 TaskGroup을 사용했습니다.
Q. async let을 사용할 때 주의했던 점은 무엇인가요?
async let은 병렬 실행을 쉽게 만들 수 있지만,
작업 간 의존성이 있을 경우 사용하면 예상치 못한 실행 순서 문제가 발생할 수 있습니다.
또한 async let으로 선언한 작업은 반드시 await로 결과를 소비해야 하며,
그렇지 않으면 의도하지 않은 동작이나 경고가 발생할 수 있어 사용 범위를 명확히 제한했습니다.
Q. Task는 언제 직접 생성하고, 언제 생성하지 않나요?
이미 async 컨텍스트 안에 있다면 불필요하게 Task를 생성하지 않습니다.
Task는 주로
- SwiftUI 버튼 액션처럼 sync 컨텍스트에서 async 함수를 호출해야 할 때
- View 생명주기와 함께 시작되는 비동기 작업을 실행할 때
에만 사용했습니다.
즉, sync → async 전환 지점에서만 Task를 사용했습니다.
Q. ViewModel에서 Task는 어떻게 관리하셨나요?
ViewModel에서 Task를 프로퍼티로 보관하는 경우에는
ViewModel의 생명주기와 함께 관리했습니다.
- 중복 요청 방지를 위해 기존 Task를 취소하고 새 Task 생성
- deinit에서 task?.cancel() 호출
을 통해 불필요한 작업 실행과 리소스 낭비를 방지했습니다.
Q. Task.cancel()을 호출했는데 작업이 계속 실행된 적은 없었나요?
있었습니다.
Swift Concurrency의 취소는 강제 종료가 아니라 협조적 취소(Cooperative Cancellation)이기 때문에,
작업 내부에서 취소 상태를 직접 확인하지 않으면 계속 실행될 수 있다는 점을 경험했습니다.
이후에는 Task.isCancelled 또는 Task.checkCancellation()을 사용해 취소 여부를 명시적으로 처리했습니다.
Q. async/await 환경에서 UI 업데이트 문제를 겪은 적은 없었나요?
async 함수는 어떤 스레드에서 실행될지 보장하지 않기 때문에, UI 업데이트 시점에 문제가 발생한 적이 있었습니다.
이 문제를 해결하기 위해 UI 업데이트가 필요한 함수에는 @MainActor를 명시하거나, MainActor.run을 사용해
메인 스레드 실행을 보장했습니다.
Q. @MainActor를 함수에 붙이는 기준은 무엇인가요?
해당 함수가 UI 상태를 직접 변경하거나 View와 밀접하게 연결된 로직이라면 @MainActor를 붙였습니다.
반대로, 비즈니스 로직이나 데이터 처리 로직에는 불필요하게 MainActor를 사용하지 않도록 분리했습니다.
Q. Actor를 실제로 도입하게 된 계기는 무엇이었나요?
여러 Task에서 같은 공유 상태에 접근해야 하는 상황에서 Race Condition 가능성을 직접 경험했기 때문입니다.
Lock이나 Serial Queue로도 해결할 수 있었지만,
Actor를 사용하면 언어 차원에서 동시성 안전성이 보장되기 때문에 의도를 더 명확하게 표현할 수 있다고 판단했습니다.
Q. Actor를 사용하면서 불편했던 점은 없었나요?
Actor 외부 접근 시 모든 접근에 await가 필요해 호출 코드가 다소 번거로워지는 점은 있었습니다.
하지만 그만큼 동시성 안전성이 명확해지고, 실수할 여지가 줄어든다는 점에서 충분히 감수할 수 있는 트레이드오프라고 생각했습니다.
Actor = 동시성 안전 기능이 추가된 클래스 같은 타입
Actor는 “동시성 안전한 클래스”라고 생각하면 됩니다.
Actor = 공유 데이터에 동시에 접근 못 하게 자동으로 막아주는 객체
- class처럼 참조 타입
- 상태 있음
- 동시에 접근 못함
Q. Actor를 왜 사용하나요?
공유 상태에 여러 Task가 동시에 접근할 때 Race Condition을 방지하기 위해 사용합니다.
기존에는 Lock이나 Serial Queue를 사용해 동시성을 직접 제어해야 했지만,
Actor는 언어 차원에서 접근을 자동으로 직렬화해주기 때문에 더 안전하고 의도가 명확한 코드 작성이 가능합니다.
Q. Actor 내부에서 또 다른 async 작업을 호출해도 괜찮나요?
가능하지만 주의가 필요합니다.
**Actor 내부에서 await**를 만나면 다른 Task가 Actor에 접근할 수 있기 때문에,
상태 일관성이 깨지지 않도록 상태 변경 시점을 신중하게 설계했습니다.
Q. Serial Queue 대신 Actor를 선택한 이유는 무엇인가요?
Serial Queue는 동시성 안전성을 개발자가 직접 보장해야 하지만, Actor는 언어 차원에서 자동으로 보호해줍니다.
즉, 동시성 제어를 “규칙”이 아니라 “타입”으로 표현할 수 있다는 점이 Actor를 선택한 가장 큰 이유였습니다.
Q. Swift Concurrency를 도입한 이후 테스트는 어떻게 달라졌나요?
기존에는 XCTestExpectation을 사용해 비동기 테스트를 작성해야 했지만,
async 테스트를 사용하면서 테스트 코드도 동기 코드처럼 작성할 수 있어 가독성과 안정성이 크게 향상되었습니다.
Q. Swift Concurrency를 사용하면서도 여전히 주의해야 할 점은 무엇이라고 생각하나요?
- Task 생명주기 관리
- 취소 처리 누락
- MainActor 누락으로 인한 UI 이슈
- 병렬 처리 남용
Swift Concurrency가 모든 문제를 자동으로 해결해주지는 않기 때문에,
동시성 설계에 대한 판단은 여전히 개발자의 책임이라고 생각합니다.
Q. 실제 사용해보면서, Swift Concurrency의 핵심 가치는 무엇이라고 느꼈나요?
Swift Concurrency의 가장 큰 가치는 비동기 로직을 안전하고 예측 가능하게 만들었다는 점이라고 생각합니다.
Q. 기존 GCD + Completion Handler 방식의 문제점은 무엇인가요?
GCD와 completion handler 방식은 비동기 작업을 처리할 수는 있지만,
코드가 복잡해질수록 다음과 같은 문제가 발생했습니다.
- completion handler 중첩으로 인한 콜백 지옥
- 비동기 흐름을 위에서 아래로 파악하기 어려움
- 에러 전달 방식이 일관되지 않음
- 어떤 스레드에서 실행되는지 추적이 어려움
- 공유 데이터 접근 시 Race Condition 발생 가능성
특히 로직이 길어질수록 가독성과 유지보수성이 급격히 떨어졌습니다.
Q. Swift Concurrency는 이런 문제를 어떻게 해결하나요?
Swift Concurrency는 비동기 처리를 언어 차원에서 구조화함으로써
이 문제들을 해결합니다.
- async / await로 비동기 코드를 동기 코드처럼 작성
- throws를 통한 일관된 에러 전달
- Task, Actor를 통해 실행 흐름과 동시성 안전성 명확화
즉, 비동기 코드를 “콜백”이 아니라 “흐름”으로 표현할 수 있게 되었습니다.
Q. 코드 가독성 측면에서 GCD와 Swift Concurrency의 차이는 무엇인가요?
GCD 기반 코드는 비동기 작업이 중첩될수록 실행 순서를 파악하기 어렵습니다.
반면 Swift Concurrency는 await를 기준으로 코드 흐름이 명확하게 드러나
비동기 로직을 위에서 아래로 자연스럽게 읽을 수 있습니다.
이로 인해 코드를 처음 보는 사람도 로직을 이해하기 쉬워졌습니다.
Q. 에러 처리 방식의 차이는 무엇인가요?
GCD에서는 에러를 completion handler의 파라미터로 직접 전달해야 했고,
에러 처리 방식이 함수마다 달라질 수 있었습니다.
Swift Concurrency에서는 throws와 do-catch를 사용해 동기 코드와 동일한 방식으로 에러를 처리할 수 있습니다.
이로 인해 에러 처리 흐름이 일관되고 명확해졌습니다.
Q. 결과 반환 방식은 어떻게 다른가요?
GCD에서는 비동기 작업의 결과를 completion handler를 통해 전달해야 했습니다.
Swift Concurrency에서는 비동기 함수가 값을 직접 반환할 수 있어 함수의 책임과 역할이 더 명확해졌습니다.
Q. 스레드 관리 측면에서 어떤 차이가 있나요?
GCD에서는 개발자가 직접 어떤 큐에서 실행할지 관리해야 했고, 이로 인해 실수가 발생할 여지가 있었습니다.
Swift Concurrency에서는 개발자가 “무엇을 비동기로 할지”만 선언하면, 실제 스레드 관리는 시스템이 담당합니다.
UI 업데이트가 필요한 경우에만 MainActor를 명시적으로 사용하면 됩니다.
Q. 취소(Cancellation) 처리는 어떻게 다른가요?
GCD에서는 비동기 작업 취소를 직접 구현해야 했고, 취소 여부를 관리하기가 쉽지 않았습니다.
Swift Concurrency에서는 Task가 기본적으로 취소 상태를 관리하며,
Task.isCancelled 등을 통해 취소를 구조적으로 처리할 수 있습니다.
Q. 동시성 안전성 측면에서 어떤 차이가 있나요?
GCD에서는 Serial Queue나 Lock을 사용해 동시성 안전성을 개발자가 직접 보장해야 했습니다.
Swift Concurrency에서는 Actor를 통해 공유 상태에 대한 접근을 언어 차원에서 직렬화할 수 있어
Race Condition을 예방할 수 있습니다.
Q. 그럼 GCD는 이제 안 쓰나요?
아니요. GCD는 여전히 저수준 동시성 제어가 필요한 상황이나 레거시 코드 유지보수에서는 유효합니다.
다만 새로운 코드나 비즈니스 로직 중심의 비동기 처리에서는 Swift Concurrency가 더 안전하고 표현력이 좋다고 생각합니다.
Q. Swift Concurrency가 항상 더 좋은 선택인가요?
항상 그렇지는 않습니다.
- 단순한 백그라운드 작업
- 이미 GCD 기반으로 안정화된 코드
에서는 굳이 Swift Concurrency로 전환하지 않아도 된다고 생각합니다.
중요한 것은 문제의 복잡도와 팀의 상황에 맞는 선택이라고 생각합니다.
Q. 실제 사용해보면서, GCD 대비 Swift Concurrency의 가장 큰 장점은 무엇이었나요?
가장 큰 장점은 비동기 로직을 안전하고 예측 가능하게 만들 수 있었다는 점입니다.
단순히 코드가 짧아진 것이 아니라, 비동기 처리 자체를 실수하기 어려운 구조로 만들 수 있었습니다.
Q. GCD 기반 비동기 코드에서 에러 처리는 어떻게 하나요?
GCD 기반 비동기 코드에서는 에러를 completion handler의 파라미터로 전달합니다.
보통 다음과 같은 형태를 사용합니다.
funcfetchUser(completion:@escaping (User?,Error?) ->Void) {
DispatchQueue.global().async {
let success=Bool.random()
if success {
let user=User(id:1, name:"홍길동")
completion(user,nil)
}else {
completion(nil,NetworkError.failed)
}
}
}
이 방식의 특징은 성공/실패를 개발자가 직접 분기 처리해야 한다는 점입니다.
Q. GCD 방식의 에러 처리에는 어떤 문제가 있나요?
GCD 방식의 에러 처리에는 다음과 같은 문제가 있습니다.
1️⃣ 에러 전달 방식이 일관되지 않음
- 어떤 함수는 (Result<T, Error>)
- 어떤 함수는 (T?, Error?)
- 어떤 함수는 Bool 플래그 사용
2️⃣ 콜백 중첩 시 에러 처리 분산
fetch User { user, errorin
if let error= error {
// 여기서 처리
return
}
fetchPosts(user: user!) { posts, errorin
if let error= error {
// 또 여기서 처리
return
}
// 성공 로직
}
}
에러 처리가 여러 위치에 흩어져
가독성과 유지보수가 어려워집니다.
Q. Swift Concurrency에서는 에러를 어떻게 처리하나요?
Swift Concurrency에서는 비동기 함수가 throws를 통해 에러를 던지고, 호출부에서 do-catch로 처리합니다.
func fetchUser() async **throws** ->User {
let success = Bool.random()
if success {
return User(id:1, name:"홍길동")
} else {
throw NetworkError.failed
}
}
호출부는 다음과 같이 작성합니다.
Task {
do {
let user=tryawait fetchUser()
print(user)
} catch {
print("에러 발생:", error)
}
}
에러 처리 흐름이 동기 코드와 동일한 형태로 표현됩니다.
Q. 두 방식의 가장 큰 차이는 무엇인가요?
가장 큰 차이는 에러가 “값”으로 전달되느냐, “흐름 제어”로 전달되느냐입니다.
- GCD → 에러를 completion handler의 파라미터로 전달
- Swift Concurrency → 에러를 throw로 전달하고 흐름을 중단
Swift Concurrency 방식이 에러 전파 경로를 더 명확하게 만듭니다.
Q. 여러 비동기 작업을 연쇄적으로 호출할 때, 에러 처리 차이는 어떻게 나타나나요?
GCD 방식
fetchUser { user, errorin
iflet error= error {
print(error)
return
}
fetchPosts(user: user!) { posts, errorin
iflet error= error {
print(error)
return
}
fetchComments(posts: posts!) { comments, errorin
iflet error= error {
print(error)
return
}
print(comments)
}
}
}
- 에러 처리 코드가 중복됨
- 성공/실패 흐름이 섞여 있음
Swift Concurrency 방식
funcloadData()async {
do {
let user=tryawait fetchUser()
let posts=tryawait fetchPosts(user: user)
let comments=tryawait fetchComments(posts: posts)
print(comments)
}catch {
print("에러 발생:", error)
}
}
- 에러 처리 위치가 한 곳
- 흐름이 명확함
Q. 병렬 비동기 작업에서 에러 처리는 어떻게 달라지나요?
GCD 방식
let group=DispatchGroup()
var userError:Error?
var postsError:Error?
group.enter()
fetchUser {_, errorin
userError= error
group.leave()
}
group.enter()
fetchPosts {_, errorin
postsError= error
group.leave()
}
group.notify(queue: .main) {
iflet error= userError?? postsError {
print("에러:", error)
return
}
print("모두 성공")
}
- 에러 상태를 직접 관리해야 함
- 코드 복잡도 증가
Swift Concurrency 방식
func loadData()async {
do {
async let user= fetchUser()
async let posts= fetchPosts()
let_=tryawait (user, posts)
print("모두 성공")
}catch {
print("에러 발생:", error)
}
}
- 에러가 자동으로 상위로 전파됨
- 첫 에러 발생 시 흐름 종료
Q. 에러 처리 관점에서 Swift Concurrency의 가장 큰 장점은 무엇인가요?
에러를단순한 “콜백 값”이 아니라 프로그램 흐름의 일부로 다룰 수 있다는 점입니다.
이로 인해
- 에러 전파 경로가 명확해지고
- 중복 코드가 줄어들며
- 유지보수가 쉬워집니다.
'기타' 카테고리의 다른 글
| TCA QnA (0) | 2026.06.10 |
|---|---|
| TCA란? (0) | 2026.06.10 |
| Xcode 빌드 환경 구성 한 번에 정리하기 (Scheme, Configuration, xcconfig...) (0) | 2026.04.27 |
| GitHub 403 에러 (Permission denied) 해결 방법 (0) | 2025.04.04 |
| Git 잔디가 안심어짐 (0) | 2024.08.16 |