티스토리 뷰

TCA 내부 코드를 살펴보면 기본적으로 struct로 구현되어 있는 것을 볼 수 있다.

struct로 구현함으로써 아래와 같은 장점을 가질 수 있다.

Struct 사용의 장점

  1. 불변성이 보장된다.
  2. 복사로 인한 상태 변화 추적이 용이하다.
  3. 스레드 간 공유 시 자동으로 복사되어 안전하다.
  4. 참조가 아닌 값으로 동작하므로 사이드 이펙트가 감소한다.

하지만 struct를 사용함으로써 스택 오버플로우와 같은 중요한 단점도 발생할 수 있다.

 

https://github.com/pointfreeco/swift-composable-architecture/discussions/3147

https://medium.com/@lot32nao/stack-overflow-due-to-memory-exhaustion-from-recursive-navigation-swiftui-tca-cbadc64aea89

 

Stack overflow due to memory exhaustion from recursive navigation SwiftUI / TCA

Stack overflow due to lack of memory

medium.com

 

이미 해당 블로그 및 깃허브에서 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
    }
}

 

 

 

실제 수정이 필요할 때까지 데이터 복사를 지연시키므로 메모리 효율성이 향상된다.

이를 통해 값 타입의 의미론적 안전성은 유지하면서도 참조 타입의 성능상 이점을 얻을 수 있다.

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함