기존의 프로젝트를 TCA로 리팩토링 하면서 알게된 개념들이 몇가지 있습니다. 오늘부터는 해당 개념들에 대한 포스팅을 통해 공부해보도록 하겠습니다.
TCA의 예제코드를 살펴보면 데이터 통신을 위한 Client 객체를 만드는 아래와 같은 코드가 있습니다. 데이터 통신을 다룰 struct를 정의하는 부분인데요. 각각 forecast와 search라는 클로저를 정의하고 있습니다. 여기보면 @Sendable이라는 attribute가 붙어 있는 것을 볼 수 있는데요.
@Sendable의 의미는 아래 클로저 (함수)를 concurrency, 즉 multi-threading 환경에서 안전하게 사용할 수 있다는 의미입니다. 즉 아래 클로저를 다른 thread에서 실행해도 race condition이 발생하지 않는다는 것이죠.
struct WeatherClient {
var forecast: @Sendable (GeocodingSearch.Result) async throws -> Forecast
var search: @Sendable (String) async throws -> GeocodingSearch
}
Race Condition에 대해서 간단하게 설명을 하자면 다수의 스레드가 하나의 자원에 접근할 때 발생하는 문제입니다. 아래 클래스를 보도록 하겠습니다. 전형적인 race condition의 예시에 많이 사용하는 클래스입니다.
class RaceClass {
var num = 0
func increaseNum() {
num += 1
}
}
스레드가 1개라면 increaseNum을 n번 실행했을 때 당연히 num의 값은 n일 것입니다.
하지만 다수의 스레드에서 실행한다면 어떨까요? increaseNum를 실행했을 때 스레드는 현재 num 값을 참조합니다. 그리고 해당 값에 1을 더해서 다시 num에 할당을 합니다.
예를 들어 1번 스레드가 num의 값 (0) 참조하는 사이에 2번 스레드가 이미 1을 더할 수도 있습니다. 하지만 1번 스레드는 그 사실을 모릅니다. 따라서 0에 1을 더해 1을 다시 할당합니다. 그렇게 되면 increaseNum 함수는 1번 스레드와 2번 스레드에서 총 2번 실행이 되었음에도 불구하고 2가 아닌 1이 됩니다.
이렇게 다수의 스레드가 하나의 자원에 접근해서 발생하는 문제를 Race Condition이라고 부릅니다.
위처럼 2개 이상의 서로 다른 스레드가 동시에 접근 가능한 데이터에 접근해서 기존 메모리에 다른 데이터를 할당하는 “쓰기” 동작을 실행하는 경우 문제가 발생합니다. 즉 Race Condition이 발생하는 데이터의 조건은 스레드 간의 공유된(Shared) 그리고 변경할 수 있는 (Mutable) 상태(State)이기 때문입니다. 이런 데이터를 Shared Mutable State라고 부릅니다.
변경할 필요가 없는 데이터는 변경을 하지 못하게 상수로 선언하면 됩니다.
그래도 데이터의 변경이 필요하다다면 struct를 사용합니다.
struct와 class의 대표적인 차이로 struct는 stack에 class는 heap에 저장된다는 것을 알고 계실텐데요. stack은 각각의 스레드가 별도로 가지고 있고 공유되지 않고 heap은 스레드 간의 공유가 가능한 공간입니다. 따라서 각각 value type (값 타입), reference type (참조 타입)이라고 불리기도 합니다.
따라서 stack에 저장되는 struct를 사용하면 각각의 스레드에 전달이 될 때 class처럼 공유되는 것이 아니라 복사를 해서 전달합니다. 따라서 shared 하지 않는 상태를 만드는 것이죠.
actor는 class와 동일하게 Shared 데이터이지만 다수의 스레드가 동시에 접근할 수 없는 타입입니다. actor에 대해서는 별도의 포스팅으로 소개합니다.
이번 포스팅에서는 @Sendable에서 시작해서 Race condition에 대해서 알아보고 마무리합니다. 다음 포스팅에서는 Sendable Protocol에 대해서 알아보도록 하겠습니다.