Class의 특징
상속 가능
클래스는 상속을 통해 다른 클래스의 속성과 메서드를 물려받을 수 있습니다. 이를 통해 코드 재사용성을 높이고, 기존 클래스를 확장하여 새로운 기능을 추가할 수 있습니다.
Reference Type
클래스는 참조 타입입니다. 이는 객체의 인스턴스를 변수에 할당하거나 함수에 전달할 때, 실제 객체가 아닌 객체의 참조(메모리 주소)를 전달한다는 의미입니다.
초기화 메서드 (Initializer)
클래스는 객체 생성 시 초기화를 위한 초기화 메서드 init 를 가질 수 있습니다. 이를 통해 객체 생성 시 필요한 초기 설정을 할 수 있습니다.
소멸자 (Deinitializer)
클래스는 객체가 메모리에서 해제될 때 호출되는 소멸자 deinit 를 가질 수 있습니다. 이를 통해 객체가 해제될 때 필요한 정리 작업을 수행할 수 있습니다.
프로퍼티와 메서드
클래스는 저장 프로퍼티, 연산 프로퍼티, 타입 프로퍼티를 가질 수 있으며, 메서드(인스턴스 메서드와 타입 메서드)를 정의할 수 있습니다.
Heap
클래스의 인스턴스를 생성하면 생성된 인스턴스는 heap 에 저장되며 주로 포인터를 이용하여 값을 참조합니다
value type은 stack, reference type은 heap에 저장됩니다.
ARC (Automatic Reference Counting)
Object-C와 달리 Swift는 ARC를 통해 클래스의 메모리 관리를 자동으로 수행합니다. ARC는 reference type에서만 사용되며 ARC는 객체에 대한 참조 카운트를 유지하여 더 이상 사용되지 않는 객체를 자동으로 메모리에서 해제합니다.
클래스의 객체는 참조 카운트를 가지며, 새로운 참조가 생길 때마다 카운트가 증가하고, 참조가 해제될 때마다 카운트가 감소합니다. 참조 카운트가 0이 되면 메모리에서 해제됩니다.
성능
swift의 컴파일러를 사용했을 때 클래스는 구조체에 비해 속도가 느리지만 동기화됩니다.
Not Thread Safe
기본적으로 Tread Safe하지 않습니다
Reference Type이란
참조 전달
클래스를 다른 객체나 함수에 전달할 때, 실제 객체가 아닌 객체의 참조(메모리 주소)를 전달합니다. 따라서 전달된 곳에서 객체를 수정하면 원본 객체도 변경되어 객체의 동일성을 유지하는데 유리합니다. 즉, 여러 변수나 상수에서 동일한 객체를 가리키도록 할 수 있습니다.
참조 카운트
클래스 객체는 참조 카운트를 가지며, 새로운 참조가 생길 때마다 카운트가 증가하고, 참조가 해제될 때마다 카운트가 감소합니다. 참조 카운트가 0이 되면 메모리에서 해제됩니다.
Reference Type 종류
참조 타입으로는 Class와 Function, Actor 이 존재합니다.
반대로 Value Type으로는 Struct, Enum, Tuple, struct String, struct Array (Set, Dictionary)가 존재합니다.
Class를 사용하는 경우
상속이 필요할 때
객체지향 프로그래밍의 중요한 개념인 상속을 통해 코드 재사용성을 높이고, 다형성을 구현할 때 클래스를 사용합니다. 예를 들어, 동물(Animal) 클래스를 상속받아 개(Dog), 고양이(Cat) 등의 하위 클래스를 만들 수 있습니다.
참조를 통해 데이터 공유가 필요할 때
- 객체가 여러 위치에서 참조되어 동일한 데이터를 유지하고, 그 데이터를 변경할 수 있어야 할 때 클래스를 사용합니다. 예를 들어, 게임에서 동일한 플레이어 객체를 여러 곳에서 참조하여 관리할 수 있습니다.
복잡한 데이터 구조를 다룰 때
- 클래스는 메서드와 프로퍼티를 포함한 다양한 기능을 가질 수 있으므로, 복잡한 데이터 구조나 다양한 동작을 가지는 객체를 만들 때 유용합니다. 예를 들어, 사용자 인터페이스(UI) 요소를 클래스로 정의하여 관리할 수 있습니다.
메모리 효율이 상대적으로 덜 중요한 경우
클래스는 힙 메모리를 사용하므로, 상대적으로 더 많은 메모리를 사용할 수 있습니다. 그러나 복잡한 객체 구조나 상태를 관리할 때는 이러한 메모리 사용이 불가피할 수 있습니다.
정리
- 상속: 클래스를 통해 부모 클래스의 특성과 메서드를 물려받을 수 있습니다.
- 참조 타입: 객체를 참조를 통해 전달하며, 참조를 통해 객체의 동일성을 유지할 수 있습니다.
- 초기화 및 소멸화 메서드: 초기화 메서드를 통해 객체를 초기화하고, 소멸자 메서드를 통해 객체가 해제될 때 정리 작업을 수행할 수 있습니다.
- 자동 참조 카운팅: ARC를 통해 메모리 관리를 자동으로 수행합니다.
class 프로퍼티 값 수정
class 에서 프로퍼티의 값을 바꾸려면 struct 와 다른 방식으로 작업이 이루어집니다, struct은 value type 이였지만 class는 reference type이므로 struct과는 달리 mutating을 사용하여 값을 수정하는 것은 불가능합니다.
mutating은 value type을 위한 것으로 현재 값을 새로운 값으로 바꾼 후 새로운 값의 새로운 객체를 만들어내는 것입니다.
class와 struct의 차이점
class는 struct와 다르게 reference type으로 값을 참조할 수 있습니다. 즉 struct에서 값을 수정하려면 새로운 struct을 만들어야 했지만 class는 메모리에 저장된 해당 값을 참조해서 수정하면 되기 때문에 새로운 class를 만들 필요가 없습니다. (mutating이 필요X)
구조체에서 프로퍼티 값 변경 시
struct MyClass {
var title: String
init(title: String) {
self.title = title
}
mutating func updateTitle(newTitle: String) {
title = newTitle
}
}
클래스에서 프로퍼티 값 변경 시
class MyClass {
var title: String
init(title: String) {
self.title = title
}
func updateTitle(newTitle: String) {
title = newTitle
}
}
클래스에서 프로퍼티 값 변경 시 발생할 수 있는 문제점
만약 여러 스레드에서 하나의 클래스 인스턴스의 프로퍼티 값을 변경하게 된다면 클래스는 reference type이므로 힙에 저장된 데이터를 참조하여 값을 수정하기 때문에 문제가 발생할 수 있습니다.
A라는 클래스의 인스턴스를 생성하였다고 했을 때 a와 b라는 인스턴스가 서로 다른 함수에서 동시에 데이터를 접근하게 된다면 변경하고 싶은 데이터로 변경을 못하는 상황이 발생할 수 있습니다.
class A {
let value = 0
}
let a = A() let b = A()
a.value = 1 b.value = 4
위에서 설명한 data race 또는 메모리 누수와 같은 문제점들을 발생시키지 않기 위해서는 비교적 신경을 덜 써도 되는 value type인 Struct를 사용하는 방법도 존재합니다. value type은 참조하는 것이 아닌 완전히 새로운 데이터를 만들어내기 때문입니다.
Class가 Struct보다 느린 이유
앞서 설명하기 전에 Swift는 멀티 스레드를 지원하는 것을 알고 있어야 합니다. 여기서 멀티 스레드란 하나의 프로세스 작업을 프로세스 안에서 여러 개의 스레드들이 하나의 프로세스 작업을 분산해서 해주는 것입니다.
위에 내용을 이해했다면 이제 아래 내용을 확인해봅시다.
프로세스는 하나의 힙으로 구성되어 있으며, 스레드는 레지스터와 스택으로 구성되어 있습니다.
왼쪽은 싱글 스레드로 하나의 프로세스에 하나의 스레드만 존재하고 있는 것으로 확인됩니다.
오른쪽은 멀티 스레드로 하나의 프로세스에 여러개의 스레드가 존재하고 있는 것으로 확인됩니다.
분석이 끝났다면 이제 이유를 알아봅시다, Class는 데이터를 Heap에 저장된다고 했습니다. 이때 Heap은 스레드에서 하나만 존재하며 만약 코드에서 여러개의 클래스 인스턴스가 존재한다고 하더라도 하나의 Heap에 동시에 접근할 수 없습니다.
이유
하지만 Struct은 데이터를 Stack에 저장합니다, 멸티 스레드 환경에서 여러개의 Stack이 존재할 수 있으므로 코드에서 여러개의 한 Class인스턴스를 접근하는 것보다 여러개의 한 Stack 인스턴스에 접근하는 것이 더 속도가 빠를 것입니다. 이게 바로 Struct이 Class보다 빠른 이유입니다.
추가설명
아래 사진처럼 Value Type(Struct)은 인스턴스의 프로퍼티 값을 수정하려면 새롭게 하나의 완전 다른 인스턴스가 생성되지만
Reference Type(Class)은 인스턴스의 프로퍼티 값을 수정하려면 해당 인스턴스의 프로퍼티 값을 저장하고 있는 메모리를 참조하여 수정합니다.
아래 사진에서 B-Doggo는 Heap에 저장되어 있으며 bDog와 aDog는 힙에 저장된 B-Doggo 인스턴스의 값을 포인터로 참조합니다.
클래스는 B-Doggo를 하나의 힙에서 동기적으로 돌아가며 사용하며 구조체는 B-Doggo와 A-Doggo가 각각 스택에 존재하여 동기적으로 코드가 수행되어도 괜찮습니다.
Weak Self
코드를 만들 때 [weak self] 를 클로저에서 사용하는 경우가 많습니다, 이것은 강한 참조를 방지하여 메모리 누수를 방지하기 위함인데 정확히 어떤 원리로 메모리 누수를 막게 되는 것인지를 알아보겠습니다.
예를 들어 다음과 같이 특정 URL로부터 데이터를 받아오는 코드가 존재할 수 있겠습니다, 이때 클래스에 존재하는 data에 url로부터 가져온 데이터를 저장을 하려고 합니다. 하지만 URL로부터 데이터를 가져오는데는 시간이 걸립니다, 1시간 또는 2시간이 걸린다고 가정했을 때 앱은 url로부터 데이터를 받아오기전까지 클래스를 Heap에 넣어 기다립니다. 이것은 메모리 누수로 이어지게 됩니다.
인스턴스가 Heap 메모리에 살아있기 때문에 ARC가 0이 아니므로 클래스가 Heap 메모리에서 없어지지 않습니다.
[weak self]를 사용하지 않는다면 getData 메서드를 통해서 데이터를 가져오는 작업을 할 때 CheckedContinuationViewModel 클래스가 반드시 Heap 메모리에 존재하는 것을 알고 있는 반면에
class CheckedContinuationViewModel: ObservableObject {
@Published var data: Data? = nil
...
func getHeartImage() {
manager.getData { data in
self?.data = data
}
}
}
[weak self]를 사용하게 된다면 getData 메서드를 통해서 데이터를 가져오는 작업을 할 때 CheckedContinuationViewModel 클래스가 Heap 메모리에 존재하지 않을 수 있다는 것을 의미합니다.
func getHeartImage() {
manager.getData { [weak self] data in
self?.data = data
}
}
weak self 없을 때와 있을 때 차이점 정리
[weak self] 없을 때: getData를 사용하여 받은 데이터를 담기 위해서는 클래스가 참조되는데 ARC가 증가합니다.
[weak self] 있을 때: getData를 사용하여 받은 데이터를 담기 위해서는 클래스가 참조되지만 ARC는 증가하지 않습니다.
ARC가 증가하지 않으므로 만일 데이터를 url로부터 가져오는 작업을 수행중이라도 ARC가 0개라면 메모리에서 없앱니다.
@StateObject, ObservableObject
@StateObject를 사용하기 위해서는 반드시 인스턴스는 ObservableObject 프로토콜을 준수하는 클래스여야 합니다.
ObservableObject를 준수하는 클래스는 @Published를 통해서 모든 프로젝트에서 해당 값을 사용할 수 있게 해줍니다.
@StateObject는 주로 ViewModel과 함께 많이 사용되는 프로퍼티 레퍼로 아래 코드처럼 선언되게 됩니다.
struct StructClassActor: View {
@StateObject private var viewModel = StructClassActorViewModel()
let isActive: Bool
init(isActive: Bool) {
self.isActive = isActive
print("View INIT")
}
var body: some View {
Text("Hello World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
.background()
.background(isActive ? Color.red : Color.blue)
}
}
여기서 알아야할 것은 View는 Struct 이기 때문에 뷰가 변경될 때마다 매번 다시 만들어지는데 부담이 없다는 사실입니다. (Struct은 Heap보다 훨씬 빠르기 때문에)
'SwiftUI' 카테고리의 다른 글
Actor (0) | 2024.05.21 |
---|---|
Value Type, Reference Type, Stack, Heap, Struct, Class 사용되는 상황들, @StateObject 가 클래스여야 하는 이유 (0) | 2024.05.20 |
Struct 구조체 (0) | 2024.05.18 |
Subscripts (0) | 2024.05.17 |
async let 으로 비동기 작업들을 동시에 수행하기 (1) (0) | 2024.05.17 |