본문 바로가기
콩코롱시의 밤

[Swift concurrency] 콩코롱시의 밤 1일차

by 차코.. 2025. 6. 20.

Intro.

콩코롱시의 밤

 

함께 사이드 프로젝트를 진행하는 iOS 팀원분들과, Swift 6을 도입하기에 앞서 스터디를 진행하기로 했습니다. 스터디는 팀원분들과 당일 해당되는 분량의 발표를 진행하고, 청취를 원하는 분들을 자유롭게 초대해 '보이는 라디오'처럼 (!!!) 했는데요. 이렇게 하니 약간의 책임감이 생겨 발표 자료도 열심히 준비하게 되고, 다른 분들과 지식을 나눌 수 있어 개념을 이해하는 범위가 넓어졌습니다. 그래서 스터디를 진행하며 만들었던 발표 자료를 블로그에도 기록해두려 합니다.

 

 

1일차 주제: 동시성 프로그래밍의 필요성, Task 톺아보기

 


동시성 프로그래밍, 왜 필요할까요?

 

안녕하새요.

 

누군가가 동시성 프로그래밍이 왜 필요할까요? 라고 묻는다면 뭐라고 답해야 할까요?

 

 

컴퓨터의 안에는 여러 개의 주방 안에서 요리를 하는 요리사들이 존재하는데요.

여기에서 주방을 코어, 요리사를 스레드라고 생각해 볼 수 있습니다.

 

(멀티 스레드 환경에서)

 

코어와 쓰레드

 

 

하지만 여러 개의 스레드가 존재할지라도, 앱이 만약에 실행될 경우, 주로 사용되는 스레드는 하나인데요.

 

그것는 바로 메인 스레드입니다.

 

 

여기에서, 만약 nn개의 사진을 서버로부터 불러오는 것처럼

 

하나의 작업이 아래처럼 오랜 시간이 걸린다면 어떨까요?

 

 

 

 

그렇담 앱이 버벅이게 될 것입니다. (스크롤이 느려지는 등)

 

 

그것은 또 왜일까요. 여기에서 디스플레이에서의 “hz” 개념을 생각해 볼 수 있겠습니다.

 

가져와봤어요

 

 

예를 들어, 내 노트북의 화면이 60hz를 가지고 있다고 가정하면, 1초에 60번 화면을 바꾼다는 뜻입니다.

 

인간의 눈은 매우 빠르게 변화하는 것을 연속적이라고 착각하는데요,

그렇기에 움직임을 나타내기 위해서 일종의 트릭을 쓰고 있는 것입니다.

 

 

그림으로 나타내면 위와 같아요

 

이 화면을 그리는 작업은 아시다시피 메인 스레드, 즉 위에서 설명하던 앱에서 주로 사용하는 스레드에서 발생해요.

 

 

 

 

그렇기에 위처럼 메인 스레드에서 짧은 작업들이 진행되면

화면을 다시 그리는 것에 아무런 영향을 미치지 않지만,

 

한 작업당 많은 시간이 소요된다면 화면을 다시 그리는 작업을 제대로 실행할 수 없겠죠.

 

 

 

 

 

그래서, 여러 개의 스레드에서 작업들을 나눠서 맡아야 하는데요.

이것이 동시성 프로그래밍이 필요한 이유입니다.

 

 

 

Swift Concurrency가 등장하면서, 이러한 동시성 프로그래밍을 더욱 효율적으로 처리하게 되었어요.

 

예를 들어서, 비동기 작업의 대표적인 예시인 네트워킹 통신을 생각해보면,

  • 서버로 요청을 보내고 → (기다림) → 응답을 받아 처리함

으로 중간에 기다리는 시간이 존재하는데요.

 

 

이렇게 (기다림)의 시간 동안에, 해당 스레드에게 다른 이를 시킬 수 있다라는 것이 Concurrency의 핵심입니다.

 

 

+) Instruments에서 동시에 실행 중인 활성 태스크들을 확인할 수 잇다 하네여

 

 

 

동시성과 관련해서 이전에 정리해뒀던 링크를 첨부합니다.

 

클로저와 self 키워드 복습하기

 

함수를 사용하다보면 'self' 키워드를 붙여야 하는 경우가 있고, 붙이지 않아도 동작하는 경우가 있습니다.

  • 꼭 붙여야 하는 경우를 명시적 self 참조라고 합니다.
  • 붙이지 않아도 동작하는 경우를 암시적 self 참조라고 합니다. (self가 생략된 것)

 

이를 클로저에서 살펴볼까요?

 

class Chef {
    var ingredient = "사과"

    func cook() {
        // 비동기 큐처럼 나중에 실행됨 → @escaping
        prepareLater {
            print("1️⃣ Escaping 클로저 실행 중: \(self.ingredient)") // 명시적 self
        }

        // 함수 안에서 바로 실행됨 → non-escaping
        prepareNow {
            print("2️⃣ Non-escaping 클로저 실행 중: \(ingredient)") // 암시적 self
        }

        // GCD는 본질적으로 @escaping → 나중에 실행됨
        DispatchQueue.global().async {
            print("3️⃣ GCD 클로저 실행 중: \(self.ingredient)") // 명시적 self
        }
     }
}

 

 

클로저에는 두 가지 종류가 있쬬

  • escaping 클로저
    • 클로저가 끝난 이후에도 외부의 변수 등에 저장되어, 나중에 실행될 수 있음
    • 클로저가 함수의 생명주기보다 더 오래 살아남는 경우
      • 이 때, 함수 스코프를 벗어나기 때문에 반드시 heap에 저장됨
  • non-escaping 클로저
    • 클로저가 함수 안에서만 실행되고, 함수가 끝나기 전에 반드시 실행되어 사라짐

 

 

escaping 클로저에서는 명시적 self 참조를 쓰고, non-escaping에서는 암시적 self 참조를 쓰는데요.

 

그 이유는, 클로저가 포함된 함수보다 생명주기가 반드시 짧은 non-escaping 클로저는

순환 참조의 문제가 없기 때문에 self를 생략해도 되지만,

 

escaping 클로저는 함수가 끝난 이후에도 self에 대한 참조를 유지해서

순환 참조를 발생할 수도 있기 때문에, 명시적으로 self를 적어주는 것입니닷.

 

 


Task가 무엇인가요?

Task {
    // 여기에서 비동기 작업 진행
}

 

Task는 동시성 프로그래밍에서, 비동기적인 일처리를 할 수 있는 하나의 일 단위예요.

 

 

Task(priority: TaskPriority?, operation: sending ( ) async -> Success)
Task(priority: TaskPriority?, operation: sending ( ) async throws -> Success)

 

Task의 operation 파라미터에 해야 할 일을 클로저로 전달하면,

 

바로 비동기 일처리가 시작되어요.

 

 

task.cancel( )

 

Task는 변수에 담아 관리할 수도 있고,

 

await task.value // 작업의 성공의 결과값에 접근
await task.result // 작업의 결과를 Result 타입으로 반환

 

 

Success / Failure를 리턴해요.

 

 

또한, 현재 실행 중인 컨텍스트의 메타 데이터를 그대로 상속해 사용하는데요.

 

여기에서 말하는 메타 데이터는

  • 지역 변수
  • 실행 액터(Execution Actor): 어떤 동시성 환경에서 실행되는지 (@MainActor 등)
  • 우선 순위: .high, .medium, .low 등

 

를 의미해요.

 

 


Task의 특징이 무엇인가요?

 

Task는 위에서 설명한대로, 비동기적인 일처리를 위한 기본 단위입니다.

 

키워드: 독립적인 실행 환경

 

각각의 Task 안에는 독립적인 실행 환경(self-contained)을 가지는데요.

 

이 안에는

  • 지역 변수
  • 실행 액터
  • 우선 순위

 

와 같은 정보가 포함되어요.

 

 

키워드: 내부는 sequential

 

또한, Task 안에 작성한 코드는 sequential해서, 위에서 아래로 순차적으로 실행돼요.

 

 

키워드: 비동기 함수는 Task 안에서만

 

비동기 함수는 Task 안에서만 호출하도록 제한하는데요.

 

예를 들어, fetchData()라는 비동기 함수가 있을 때, 꼭 Task라는 블록을 열어야만 호출이 가능합니다.

 

Task {
    await fetchData()
}

 

 

그 이유는, 컴파일러가 비동기 함수 안에서 중단점이 생길 수 있다라는 것을 인식하고

안전하게 관리하기 위함이에요.

 

 

키워드: 중단점, 흐름 제어

 

여기에서 GCD와의 차이점은 중단점을 활용해, 흐름 제어가 가능하다는 점입니다.

 

  • GCD에서는 queue에 작업 블록을 넣고 실행하기 때문에, 중간에 멈췄다가 다시 이어서 실행하는 흐름 제어가 불가능합니다.
  • 그러나 Task는 await 키워드를 통해 중단(suspend) → 재개(resume) 할 수 있는 구조예요.

 

키워드: 직접적인 우선 순위 지정

 

또한, 우선 순위를 지정할 수 있는데요.

 

Task(priority: .high) {
    await fetchData()
}

 

이 때 GCD의 DispatchQueue는 FIFO 방식이고, queue에 우선 순위를 부여하는 방식이기 때문에

실제 실행 순서를 보장하지는 않아요.

 

그치만 Task는 우선 순위를 지정해서, 우선 처리하고 싶은 일을 먼저 처리할 수 있습니다.

 

 

키워드: data race 방지

 

Task는 다른 스레드들과 병렬로 실행되기 때문에, Task 안에서 쓰는 값들이 thread-safe 해야 해요.

 

이를 위해서 data race 문제를 방지하는 Sendable 프로토콜이 있고, 이후 자세히 설명합니다.