🔥 동시성 프로그래밍
강의 목차
Swift 는 동시성 프로그래밍을 위한 다양한 기능을 제공하고 있어요. 이번 글에서는 async
, await
, async let
, Task
, task group
, actor
등 Swift 의 동시성 프로그래밍 기능에 대해 차근차근 살펴보도록 하겠습니다.
async
로 비동기 함수 만들기
async
키워드를 함수 앞에 붙이면 해당 함수가 비동기로 실행된다는 것을 나타낼 수 있어요. 다음은 async
로 비동기 함수를 만드는 예시 코드입니다.
func fetchUserID(from server: String) async -> Int { // 서버가 "primary" 인 경우 97 을 반환 if server == "primary" { return 97 } // 그 외의 경우 501 을 반환 return 501 }
swift
이 코드는 fetchUserID
라는 비동기 함수를 정의하고 있어요. 이 함수는 server
라는 문자열 매개변수를 받아서 서버에 따라 다른 사용자 ID 를 반환해요. 만약 server
가 "primary" 라면 97 을 반환하고, 그 외의 경우에는 501 을 반환하도록 되어 있어요. 함수 선언부에 async
키워드가 붙어 있는 것을 볼 수 있는데, 이는 이 함수가 비동기적으로 실행된다는 것을 나타내요.
await
로 비동기 함수 호출하기
비동기 함수를 호출할 때는 await
키워드를 함수 호출 앞에 붙여주어야 해요. await
를 붙이면 해당 비동기 함수가 완료될 때까지 대기한 후 결과를 받아올 수 있습니다. 아래는 await
를 사용하여 비동기 함수를 호출하는 예시 코드예요.
func fetchUsername(from server: String) async -> String { // fetchUserID 비동기 함수를 호출하고 결과를 기다림 let userID = await fetchUserID(from: server) // 받아온 userID 가 501 이면 "John Appleseed" 를 반환 if userID == 501 { return "John Appleseed" } // 그 외의 경우 "Guest" 를 반환 return "Guest" }
swift
fetchUsername
이라는 비동기 함수가 정의되어 있어요. 이 함수는 server
매개변수를 받아서 해당 서버의 사용자 이름을 반환하는데요, 먼저 await
키워드를 사용해 fetchUserID
비동기 함수를 호출하고 그 결과를 userID
상수에 저장해요. 그리고 userID
값에 따라 사용자 이름을 반환하는데, 만약 userID
가 501 이면 "John Appleseed" 를, 그 외의 경우에는 "Guest" 를 반환하도록 되어 있어요.
async let
으로 비동기 함수 병렬 실행하기
async let
을 사용하면 여러 개의 비동기 함수를 병렬로 실행할 수 있어요. async let
으로 선언한 상수는 해당 비동기 함수의 결과값을 담게 되는데, 이 값을 사용할 때는 await
키워드를 붙여주어야 합니다. 다음은 async let
을 활용한 예시 코드입니다.
func connectUser(to server: String) async { // fetchUserID 와 fetchUsername 을 동시에 실행 async let userID = fetchUserID(from: server) async let username = fetchUsername(from: server) // 위 두 함수의 결과를 기다렸다가 인사말을 만듦 let greeting = await "Hello \(username), user ID \(userID)" // 인사말을 출력 print(greeting) }
swift
connectUser
라는 비동기 함수 내에서 async let
을 사용해 fetchUserID
와 fetchUsername
두 비동기 함수를 동시에 실행하고 있어요. async let
으로 선언된 userID
와 username
은 각각의 비동기 함수 결과값을 담게 되는데요, 이후 await
키워드를 사용해 두 결과값을 모두 받아온 뒤 인사말을 만들어 출력하고 있습니다. 이렇게 async let
을 사용하면 서로 의존성이 없는 비동기 작업들을 동시에 실행시켜 효율성을 높일 수 있어요.
Task
로 비동기 함수를 동기 코드에서 실행하기
Task
를 사용하면 동기 코드 내에서 비동기 함수를 기다리지 않고 실행할 수 있어요. 아래는 Task
를 활용하는 코드 예시입니다.
Task { // connectUser 비동기 함수를 실행하고 끝나길 기다리지 않음 await connectUser(to: "primary") } // "Hello Guest, user ID 97" 출력
swift
위 코드는 Task
를 사용해 비동기 함수를 실행하는 방법을 보여주고 있어요. Task
블록 내에서는 await
키워드를 사용해 connectUser
비동기 함수를 호출하고 있는데요, Task
블록 밖의 코드는 이 비동기 함수가 완료되길 기다리지 않고 바로 다음 코드를 실행해요. 따라서 connectUser
함수의 실행 결과는 나중에 출력될 거예요.
Task Group 으로 동시성 코드 구조화하기
Task group 을 사용하면 관련있는 비동기 작업들을 구조화할 수 있어요. 다음은 withTaskGroup
을 활용하는 예시 코드입니다.
let userIDs = await withTaskGroup(of: Int.self) { group in // 여러 서버에 대해 반복하며 for server in ["primary", "secondary", "development"] { // 각 서버에 대해 fetchUserID 태스크를 추가 group.addTask { return await fetchUserID(from: server) } } // 결과를 담을 배열 생성 var results: [Int] = [] // 태스크 결과를 반복하며 결과 배열에 추가 for await result in group { results.append(result) } // 결과 배열 반환 return results }
swift
withTaskGroup
을 사용해 여러 비동기 작업을 구조화하는 방법을 보여주고 있어요. withTaskGroup
블록 내에서는 group
에 대해 addTask
메서드를 호출하며 각 서버에 대한 fetchUserID
태스크를 추가하고 있어요. 그리고 group
의 결과를 for await
루프를 사용해 하나씩 받아오며 results
배열에 추가한 후 최종 결과 배열을 반환하고 있습니다.
Actor 로 안전한 동시성 보장하기
Actor 는 클래스와 유사하지만, 동일한 actor 의 인스턴스에 대해 여러 비동기 함수가 동시에 안전하게 접근할 수 있도록 보장해줍니다. 아래는 actor
를 선언하는 예시 코드예요.
actor ServerConnection { // 기본 서버를 "primary" 로 설정 var server: String = "primary" // 활성 사용자 배열 (외부에서 직접 접근 불가) private var activeUsers: [Int] = [] // 비동기 메서드 connect() 선언 func connect() async -> Int { // 서버로부터 userID 를 가져옴 let userID = await fetchUserID(from: server) // ... 서버와 통신하는 코드 ... // 활성 사용자 배열에 userID 추가 activeUsers.append(userID) // userID 반환 return userID } }
swift
이 코드는 actor
를 사용해 동시성을 안전하게 관리하는 방법을 보여주고 있어요. ServerConnection
이라는 actor
를 정의하고 있는데요, 내부에는 기본 서버를 나타내는 server
속성과 활성 사용자 ID 를 담는 activeUsers
배열, 그리고 connect()
비동기 메서드가 선언되어 있어요.
connect()
메서드는 server
에서 fetchUserID
함수를 호출해 사용자 ID 를 가져오고, 가져온 ID 를 activeUsers
배열에 추가한 후 반환하도록 되어 있는데요, 여기서 주목할 점은 activeUsers
배열이 actor
외부에서는 접근할 수 없도록 private
으로 선언되어 있다는 것입니다. 이렇게 함으로써 여러 비동기 태스크에서 동시에 activeUsers
배열에 접근하는 일이 생겨도 데이터의 안전성을 보장할 수 있게 됩니다.
Actor 의 메서드나 속성에 접근할 때는 해당 코드 앞에 await
를 붙여 이미 실행 중인 다른 코드가 끝날 때까지 기다려야 할 수 있음을 나타냅니다. 다음은 ServerConnection
actor 를 사용하는 예시입니다.
// ServerConnection 의 인스턴스 생성 let server = ServerConnection() // connect() 메서드 호출하고 결과 기다림 let userID = await server.connect()
swift
이상으로 Swift 의 동시성 프로그래밍 기능에 대해 살펴보았습니다. async
와 await
를 사용해 비동기 코드를 깔끔하게 작성하고, task group
과 actor
를 활용해 안전하고 구조화된 동시성 코드를 만들 수 있습니다. Swift 의 동시성 프로그래밍 기능은 정말 강력하기 때문에 잘 활용할 수 있는 능력을 꼭 갖춰야합니다.