[SwiftUI] Struct, Class, Actor

Junyoung Park·2022년 8월 29일
0

SwiftUI

목록 보기
61/136
post-thumbnail

Swift: Struct vs Class vs Actor, Value vs Reference Types, Stack vs Heap | Swift Concurrency #8

Struct, Class, Actor

Value VS Reference

  • 값 타입:
  1. Struct, Enum, String, Int
  2. 스택 저장
  3. 참조 타입보다 빠른 속도
  4. 스레드 세이프
  5. 값 타입을 할당하거나 전달할 때 새로운 복사 데이터가 생성됨
  • 참조 타입:
  1. Class, Function, Actor
  2. 힙 저장
  3. 값 타입보다 느린 속도. 하지만 동기화 보장 (특정 참조 포인터에서 해당 참조 타입의 값을 바꿀 경우 다른 참조 포인터에서도 동기화 보장)
  4. 스레드 세이프 X
  5. 참조 타입을 할당하거나 전달할 때 해당 참조 타입을 가리키는 참조값이 생성됨(포인터)
  • 스택:
  1. 값 타입 저장
  2. 스택 할당 변수는 메모리에 직접 저장되므로 스택 내 메모리 접근은 매우 빠른 속도
  3. 각 스레드마다 스택, 레지스터를 가짐
  • 힙:
  1. 참조 타입 저장
  2. 스레드 모두 동일한 값을 공유

Struct, Class, Actor 관련 코드

struct CustomStruct {
    var title: String
    // Default Initializer from Swift
}

struct CustomStruct2 {
    var title: String
    
    func makeNewStruct(_ title: String) -> CustomStruct2 {
        return CustomStruct2(title: title)
    }
    
    mutating func updateTitle(_ title: String) {
        self.title = title
    }
}

struct CustomStruct3 {
    private var title: String
    
    init(title: String) {
        self.title = title
    }
    
    func getTitle() -> String {
        return title
    }
    
    mutating func setTitle(_ title: String) {
        self.title = title
    }
}

class CustomClass {
    var title: String
    init(title: String) {
        self.title = title
    }
    func updateTitle(_ title: String) {
        self.title = title
    }
}

class CustomClass2 {
    var title: String
    init(title: String) {
        self.title = title
        print("\(title) is initiated")
    }
    
    deinit {
        print("\(title) is deinitiated")
    }
}

actor CustomActor {
    var title: String
    // to be updated
    
    init(title: String) {
        self.title = title
    }
    
    func updateTitle(_ title: String) {
        self.title = title
    }
}
extension StructClassActorBootCamp {
    private func runTest() {
        actorTest()
    }
    
    private func structTest1() {
        let objectA = CustomStruct(title: "Starting Title")
        print("ObjectA : \(objectA.title)")
        print("Pass the VALUE of objectA to objectB")
        var objectB = objectA
        print("ObjectB : \(objectB.title)")
        // ObjectA : Starting Title
        // ObjectB : Starting Title
        objectB.title = "Second Title"
        // let title -> var title, let objectB -> var objectB. complier changes it
        print("ObjectB title changed")
        print("ObjectA : \(objectA.title)")
        print("ObjectB : \(objectB.title)")
        // ObjectA : Starting Title
        // ObjectB : Second Title -> ObjectA <-> ObjectB not referencing themselves. (: Value Type)
    }
    private func classTest1() {
        let objectA = CustomClass(title: "Starting Title")
        print("ObjectA : \(objectA.title)")
        print("Pass the REFERENCE of objectA to objectB")
        let objectB = objectA
        print("ObjectB : \(objectB.title)")
        // ObjectA : Starting Title
        // ObjectB : Starting Title
        objectB.title = "Second Title"
        // let title -> var title. complier changes it: not 'let' objectB.
        print("ObjectB title changed")
        print("ObjectA : \(objectA.title)")
        print("ObjectB : \(objectB.title)")
        // ObjectA : Second Title
        // ObjectB : Second Title -> ObjectA === ObjectB referencing same place (: Reference Type)
        print(objectA === objectB)
        // true
    }
}
  • 값 자체를 복사하는 구조체 → 값 전달 또는 할당 시 새로운 구조체 생성
  • 주소값을 복사하는 클래스 → 새로운 변수에 할당 경우 새로운 '포인터' 생성
  • 구조체 내 변수 값을 바꿀 경우 → mutating 키워드를 통해 self를 바꾼다는 것을 명시해야 함
  • 클래스 내 변수 값을 바꿀 경우 → mutating 키워드가 없음. 즉 그대로 변경 가능하다는 뜻.

    특정 텍스트 인스턴스를 let, 상수로 받은 뒤 인스턴스의 변수 로 선언한 저장 프로퍼티 값을 변경 가능한 까닭: let이라는 상수는 곧 해당 클래스를 가리키고 있는 포인터가 변하지 않는다는 것을 의미할 뿐, 내부의 값은 (변수로 선언되었다면) 주소값을 통해 접근, 변경 가능하기 때문

extension StructClassActorBootCamp {
    private func structTest2() {
        var struct1 = CustomStruct(title: "Title1")
        print("Struct1: \(struct1.title)")
        struct1.title = "Title2"
        print("Struct1: \(struct1.title)")
        // Struct1: Title1
        // Struct1: Title2
        var struct2 = CustomStruct2(title: "Title1")
        print("Struct2 : \(struct2.title)")
        struct2 = CustomStruct2(title: "Title2")
        // totally new struct assigned
        print("Struct2 : \(struct2.title)")
        // struct2.title : Title1
        // struct2.title : Title2
        struct2 = struct2.makeNewStruct("New Title")
        print("Struct2 : \(struct2.title)")
        // struct2.title : New Title
        // Totally new Structure
        struct2.updateTitle("Title3")
        // Mutating function
        print("Struct2 : \(struct2.title)")
        // struct2.title : Title3
    }
    
    private func structTest3() {
        var struct1 = CustomStruct3(title: "Title1")
        var title = struct1.getTitle()
        print(title)
        // Title1
        struct1.setTitle("Title2")
        title = struct1.getTitle()
        print(title)
        // Title2
        
        // get, set method to handle data inside struct
    }
}
extension StructClassActorBootCamp {
    private func classTest2() {
        let class1 = CustomClass(title: "Title1")
        print("Class1 : \(class1.title)")
        // Class1 : Title1
        class1.updateTitle("Title2")
        print("Class1 : \(class1.title)")
        // Class1 : Title2
        // Does not have to set "mutating" keyward inside Class
    }
}
  • 새로운 저장 프로퍼티를 담고 있는 구조체를 반환하는 방법: (1). mutating 키워드를 통해 변수로 선언한 저장 프로퍼티 값을 바꾸는 방법 (2). 새로운 구조체를 할당, 변수로 선언된 특정 값을 덮어 씌우는 방법

    mutating이 곧 inout으로 바꾸는 것과 같은 맥락인 것 같다!

extension StructClassActorBootCamp {
    private func actorTest() {
        Task {
            // Need to get async -> Call them inside 'Task' block
            var actor1 = CustomActor(title: "Actor1")
            await print("Actor1 title : \(actor1.title)")
            // Actor1 title : Actor1
            
            // actor1.title = "Actor2" -> Ban
            // Actor-isolated property 'title' can not be mutated from a non-isolated context
            await actor1.updateTitle("Actor2")
            await print("Actor1 title : \(actor1.title)")
            // Actor1 title : Actor2
        }
    }
}
  • actor 내부의 프로퍼티는 actor-isolated 프로퍼티이기 때문에 일반적인 방법으로는 변경 불가능 → actor 클래스 내부에서 선언한 함수를 통해 self 접근, 특정 프로퍼티 값을 변경 가능 → await를 통해 비동기 동작이 끝났는지 확인할 수 있고 이를 Task 내부에서 실행

Struct VS Class VS Actor

  • 구조체
  1. 값 타입
  2. 변경 가능(mutated)
  3. 스택 저장
  4. 상속 불가능
  5. 자료 구조에 적절한 모델(애플 프레임워크: 문자열, 정수 등 기본형 모두 구조체)
  6. 뷰 모델
  • 클래스
  1. 참조 타입(인스턴스)
  2. 힙 저장
  3. 상속 가능
  4. 특정 값의 변화 감지 가능 → ObservableObject 등 참조 방식을 통해 동일한 값의 현재 상태 감지 보장(뷰 모델, 매니저 클래스 등)
  • 액터
  1. 클래스와 동일, 스레드 세이프
  2. 참조 타입인 클래스의 값 참조, 변경 등을 여러 시점, 여러 곳에서 실행할 때 동기화 이슈가 생길 수 있기 때문에 스레드 안전 X → 액터는 await를 통해 동기화 보장 가능
  3. 예를 들어 싱글턴 패턴(특정 클래스의 접근 방법이 제한된 모델. 이니셜라이즈 또한 외부 접근 불가능 → 스레드 세이프 모델, URLSession.shared 등) 적절. 다양한 곳에서 접근 가능한 클래스, 동기화 자동으로 보장 (await)

    구조체는 클래스와 달리 상속이 불가능하다. 하지만 프로토콜 + 익스텐션 활용을 통해

ARC

  • Automatic Reference Counting(자동 참조 카운팅): 클래스 타입이 사용되는 횟수가 자동으로 카운트, 메모리 할당 여부를 결정하는 기준이 됨
  • ARC가 0이 될 때 해당 클래스는 자동으로 메모리에서 소멸(deinit을 통해 직접 확인 가능), 효율적 메모리 사용이 가능

ARC 관련 코드

class StrongClass1 {
    var title: String
    var strongClass2: StrongClass2?
    var weakClass: WeakClass?
    
    init(title: String, strongClass2: StrongClass2? = nil, weakClass: WeakClass? = nil) {
        self.title = title
        self.strongClass2 = strongClass2
        self.weakClass = weakClass
        print("\(title) is initiated")
    }
    
    deinit {
        print("\(title) is deinitiated")
    }
}

class StrongClass2 {
    var title: String
    var strongClass1: StrongClass1?
    
    init(title: String, strongClass1: StrongClass1? = nil) {
        self.title = title
        self.strongClass1 = strongClass1
        print("\(title) is initiated")
    }
    
    deinit {
        print("\(title) is deinitiated")
    }
}

class WeakClass {
    var title: String
    weak var strongClass1: StrongClass1?
    
    init(title: String, strongClass1: StrongClass1? = nil) {
        self.title = title
        self.strongClass1 = strongClass1
        print("\(title) is initiated")
    }
    
    deinit {
        print("\(title) is deinitiated")
    }
}
extension StructClassActorBootCamp {
    private func classTest3() {
        var class1: CustomClass2?
        class1 = CustomClass2(title: "New Class")
        // New Class is initiated
        // Reference Count : 1
        class1 = nil
        // Reference Count : 0 -> Deinit. Deallocated from Memory
        // New Class is deinitiated
    }
    private func classTest4() {
        var class1:StrongClass1? = StrongClass1(title: "Strong Class1")
        var class2:StrongClass2? = StrongClass2(title: "Strong Class2")
        // Strong Class1 is initiated
        // Strong Class2 is initiated
        class1?.strongClass2 = class2
        class2?.strongClass1 = class1
        class1?.strongClass2 = nil
        class2?.strongClass1 = nil
        class1 = nil
        class2 = nil
        // Strong Class1 is deinitiated
        // Strong Class2 is deinitiated
        
        class1 = StrongClass1(title: "Strong Class1")
        class2 = StrongClass2(title: "Strong Class2")
        // Strong Class1 is initiated
        // Strong Class2 is initiated
        class1?.strongClass2 = class2
        class2?.strongClass1 = class1
        class1 = nil
        class2 = nil
        // Still Strong reference cycle retained -> Memory leaking
    }
    
    private func classTest5() {
        var class1:StrongClass1? = StrongClass1(title: "Strong Class1")
        var class2:WeakClass? = WeakClass(title: "Weak Class")
        // Strong Class1 is initiated
        // Weak Class is initiated
        class1?.weakClass = class2
        class2?.strongClass1 = class1
        class1 = nil
        class2 = nil
        // Strong Class1 is deinitiated
        // Weak Class is deinitiated
        // strongClass1 inside class2(Weak Class instance) -> nil, then ARC -> 0, then class1 -> deinit
        // weak reference -> make its non-having strong reference less than strong reference
    }
}
  • 강한 참조 사이클이 일어날 수 있는 상황 → 약한 참조를 통해 '언젠가는 결국 nil이 될 참조'임을 미리 알려주기 때문에 메모리 누수에 대한 걱정을 덜 수 있음
  • 강한 참조라 할지라도 사용 전 적절한 nil 할당을 통한 카운팅을 줄이는 방법을 통해 ARC를 조정 가능, 강한 참조 및 약한 참조를 함께 씀으로써 고리를 끊을 수 있다는 것 역시 주의

MVVM

  • 뷰, 뷰 모델, 모델 디자인 패턴 → 구조체로 리렌더링하는 뷰, 클래스를 통해 참조하는 뷰 모델 및 모델에 적합한 디자인 패턴
  • 뷰: 이니셜라이저에 따라 새로운 뷰 구조체 계속해서 재생성 → COW(Copy On Write), 구조체 자체의 매우 빠른 속도로 인해 클래스 뷰 구현보다 매우 적합한 방법
  • 뷰 모델: @StateObject로 뷰에서 관찰하고 있는 ObservableObject는 뷰 리렌더링 도중에도 값을 계속해서 유지. @ObjservedObject로 관찰하고 있다면 Deinit
  • 모델: 여러 뷰, 여러 시점에서 해당 서비스 클래스를 사용하고 있다면 actor 클래스를 통해 동기화 보장 가능

MVVM 관련 코드

import SwiftUI

actor StructClassActorBootCampDataManager {
    
    init() {
        print("StructClassActorBootCampDataManager Init")
    }
    
    deinit {
        print("StructClassActorBootCampDataManager Deinit")
    }
    
    func getDataFromDatabase() {
        
    }
}

class StructClassActorBootCampViewModel: ObservableObject {
    @Published var title: String = ""
    let dataManager: StructClassActorBootCampDataManager
    
    init(dataManager: StructClassActorBootCampDataManager) {
        self.dataManager = dataManager
        print("StructClassActorBootCampViewModel Init")
    }
    
    deinit {
        print("StructClassActorBootCampViewModel Deinit")
    }
}

struct StructClassActorBootCamp: View {
    @StateObject private var viewModel = StructClassActorBootCampViewModel(dataManager: StructClassActorBootCampDataManager())
    // -> ObservableObject class: Init ...
    // Even if this entire struct View would be re-rendered, its StateObject would be same
    // @ObservedObject private var viewModel = StructClassActorBootCampViewModel()
    // -> ObservableObject class: Init and Deinit ...

    let isActive: Bool
    
    init(isActive: Bool) {
        self.isActive = isActive
        print("View Init, isActive : \(isActive)")
        /*
         View Init, isActive : true
         View Init, isActive : false
         View Init, isActive : true
         View Init, isActive : false
         ... -> Whenever isActive toggled, this View has been initiated.
         */
    }
    
    var body: some View {
        Text(viewModel.title)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .ignoresSafeArea()
            .background(isActive ? .red : .blue)
            .onAppear {
                runTest()
            }
    }
}

struct StructClassActorBootCampHomeView: View {
    @State private var isActive: Bool = false
    var body: some View {
        StructClassActorBootCamp(isActive: isActive)
            .onTapGesture {
                isActive.toggle()
            }
    }
}
  • HomeView의 자식 뷰인 StructClassActorBootCamp 뷰는 이니셜라이저에 필요한 파라미터 isActive 값이 바뀜에 따라 계속해서 재생성 → 뷰 리렌더링
  • 뷰가 구독하고 있는 StructClassActorBootCampViewModel ObservableObject@StateObject로 관찰되고 있기 때문에 뷰 렌더링이 일어난다 하더라도 초기 Init 이후 Deinit되지 않고 있음 → 기존에 참조한 값을 그대로 유지할 수 있음
profile
JUST DO IT

0개의 댓글