티스토리 뷰
TCA 내부 코드를 살펴보면 기본적으로 struct로 구현되어 있는 것을 볼 수 있다.
struct로 구현함으로써 아래와 같은 장점을 가질 수 있다.
Struct 사용의 장점
- 불변성이 보장된다.
- 복사로 인한 상태 변화 추적이 용이하다.
- 스레드 간 공유 시 자동으로 복사되어 안전하다.
- 참조가 아닌 값으로 동작하므로 사이드 이펙트가 감소한다.
하지만 struct를 사용함으로써 스택 오버플로우와 같은 중요한 단점도 발생할 수 있다.
https://github.com/pointfreeco/swift-composable-architecture/discussions/3147
이미 해당 블로그 및 깃허브에서 TCA의 문제점인 스택 오버플로우가 발생할 수 있음을 암시한다.
스택 오버플로우 문제
스택 오버플로우는 지정된 스택 메모리 크기보다 더 많은 스택 메모리를 사용하게 되어 발생하는 오류를 말한다.
스택은 함수 호출, 지역 변수 저장 등에 사용되는 제한된 메모리 공간이다.
주요 발생 원인은 다음과 같다.
- 무한 재귀 호출
- 깊은 depth를 가진 struct 구조
- 큰 크기의 값 타입 데이터
그렇다면 TCA에서의 스택 오버플로우는 어떤 이유로 발생할까?
TCA에서 발생할 수 있는 가장 큰 원인은 State의 중첩 구조다.
struct AppState: Equatable {
var mainState: MainState
var homeState: HomeState
var profileState: ProfileState
// 각 State도 내부적으로 여러 하위 State를 포함할 수 있다
}
struct MainState: Equatable {
var subState1: SubState1
var subState2: SubState2
// 계속해서 중첩되는 구조
}
이러한 구조의 문제점은 아래와 같다.
- State가 struct로 구현되어 모든 데이터가 스택 메모리에 할당된다.
- 프로젝트가 커질수록 중첩된 State 구조가 깊어진다.
재귀적 액션으로 인한 문제가 있다.
struct Feature: Reducer {
struct State { ... }
enum Action {
case subFeatureAction(SubFeature.Action)
case update
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .update:
return .send(.subFeatureAction(.update)) // 재귀적 액션 발생
case .subFeatureAction:
// 하위 Reducer로 전파
return .send(.update)
}
}
}
}
- 상태 업데이트가 연쇄적으로 발생한다.
- 각 업데이트마다 State 전체가 복사될 수 있다.
그렇다면 어떻게 방지할 수 있을까?
Copy-on-Write는 Swift에서 값 타입의 성능을 최적화하기 위해 사용된다.
(Array, String, Dictionary 등의 기본 컬렉션 타입들도 내부적으로 이 패턴을 사용하고 있다.)
값 타입의 가장 큰 단점은 값이 복사될 때마다 전체 메모리가 복사된다는 점이다.
var array1 = [1, 2, 3, 4, 5] // 메모리 할당
var array2 = array1 // 전체 배열이 새로 복사됨
큰 데이터를 다룰 때 심각한 성능 저하를 일으킬 수 있다.
struct LargeStruct {
private var _data: Reference<[Int]> // 참조 타입으로 실제 데이터를 감싼다
var data: [Int] {
get { _data.value } // 읽기 시에는 참조를 통해 직접 접근
set {
// 이 참조를 유일하게 사용하는지 확인
if isKnownUniquelyReferenced(&_data) {
_data.value = newValue // 유일한 참조라면 직접 수정
} else {
_data = Reference(newValue) // 공유된 참조라면 새로운 복사본 생성
}
}
}
}
final class Reference<T> {
var value: T
init(_ value: T) {
self.value = value
}
}
실제 수정이 필요할 때까지 데이터 복사를 지연시키므로 메모리 효율성이 향상된다.
이를 통해 값 타입의 의미론적 안전성은 유지하면서도 참조 타입의 성능상 이점을 얻을 수 있다.
'[Swift] 이것저것' 카테고리의 다른 글
[SwiftData] @Model Macro 살펴보기 (3) | 2024.12.13 |
---|---|
[Swift Concurrency] TMDB API를 사용한 Task, TaskGroup, Async-let 성능 비교 (3) | 2024.12.03 |
[Swift Concurrency] Continuation 내부 코드 뜯어보기 (0) | 2024.11.21 |
[Swift Concurrency] Task 내부 코드 뜯어보기 (0) | 2024.11.21 |
[Swift] guard의 모든 것? (옵셔널 바인딩, 조건문) (0) | 2022.04.14 |
댓글