안녕하세요.
이번에는 기초적이면서도 중요한 Struct와 Class의 공통점과 차이점에 대해 간략하게 정리해보려고 합니다.
# 1. Struct와 Class 공통점
- value를 저장할 프로퍼티를 정의할 수 있습니다.
- method를 정의할 수 있습니다.
- subscript syntax(.)으로 value에 접근이 가능합니다.
- 생성자(initializer)를 정의할 수 있습니다.
- 확장이 가능합니다.
- 프로토콜 채택이 가능합니다.
사실 공통점은 뻔한 얘기들이라... 다들 알고 계실 것 같아요ㅎㅎ
더욱 중요한 차이점에 대해 알아볼게요.
# 2. Struct와 Class 차이점
## 2.1 클래스는 상속이 가능합니다.
class Animal {
var age: Int = 0
}
class Person: Animal {
var name: String = ""
}
이런 식으로 Person class는 Animal class를 상속받을 수 있어요.
Struct는 상속이 불가능합니다.
## 2.2 Type casting으로 인해 클래스 인스턴스의 타입은 런타임 때 결정됩니다.
Class는 상속이 가능하니 upcasting, downcasting이 가능하기 때문에 런타임 때 타입이 결정됩니다.
그 반대로, Struct는 상속이라는 개념이 없기 때문에 컴파일 타임 때 타입을 알 수 있어요.
(Struct이더라도 existential type일 경우에는 런타임 때 타입이 결정됩니다!)
일반적으론 컴파일 타임 때 타입이 결정되는 것이 컴파일러가 최적화하는데 도움이 되니 성능 차이도 있겠죠?
## 2.3 Class가 메모리 해제되었을 때 deinitializer가 호출됩니다.
class Person {
var name: String = ""
deinit { ✅
print("deallocated!")
}
}
## 2.4 Struct는 기본 생성자를 제공해주지만, Class는 제공하지 않습니다.
struct User {
var name: String
var age: Int
}
var user = User(name: "Philip", age: 10)
위처럼 Struct는 별도의 생성자를 추가하지 않아도, 기본적 생성자를 제공해주고 있어요.
하지만 Class는 기본 생성자를 제공하지 않기 때문에 아래처럼 에러가 발생합니다.
class Person { // Class 'Person' has no initializers
var name: String // Stored property 'name' without initial value prevents synthesized initializers
var age: Int // Stored property 'age' without initial value prevents synthesized initializers
}
해결 방법은 Optional 처리를 하거나,
class Person {
var name: String?
var age: Int?
}
unwrapping하거나,
class Person {
var name: String!
var age: Int!
}
초기값을 넣어주거나,
class Person {
var name: String = "Philip"
var age: Int = 10
}
생성자를 넣어주는 방법이 있어요ㅎㅎ
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
## 2.5 Struct는 Value Type, Class는 Reference Type
설명하기에 앞서서 value type이 뭔지 reference type이 뭔지부터 알아야 해요.
value type은 value를 특정 변수에 할당하거나 함수 파라미터로 넘길 때 value 자체가 복사되어서 할당되는 타입을 의미합니다.
reference type은 값을 복사하는 것이 아닌 참조값(주소값)을 할당하는 타입을 의미합니다.
즉, Struct는 복사된 value가 할당되고 Class는 value의 참조값이 할당되는 것이죠.
예를 들어서 설명해볼게요.
Struct 먼저 살펴보면,
user2에게 user1을 할당하고 user2.age를 변경했을 때, user1.age에는 영향이 없죠??
struct User {
var name: String
var age: Int
}
var user1 = User(name: "Philip", age: 10)
var user2 = user1
user2.age = 20
print("user1.age = \(user1.age)") // user1.age = 10
print("user2.age = \(user2.age)") // user2.age = 20
Class도 살펴볼게요.
person2에게 person1을 할당하고 person2.age를 변경하면, person1.age도 변경이 되는 것을 볼 수 있어요.
class Person {
var name: String = "Philip"
var age: Int = 10
}
var person1 = Person()
var person2 = person1
person2.age = 20
print("person1.age = \(person1.age)") // person1.age = 20
print("person2.age = \(person2.age)") // person2.age = 20
person1, person2에는 참조값이 들어가 있기 때문에 같은 인스턴스를 바라보고 있기 때문이에요ㅎㅎ
이렇게 Class는 참조값으로 동일 인스턴스를 바라보기 때문에, reference counting이라는 개념도 존재하는 것이에요.
Swift ARC(Automatic Reference Counting) 관련해선 나중에 다뤄볼게요ㅎㅎ
## 2.6 Class는 === 연산자와 !== 연산자 사용이 가능합니다.
=== 연산자와 !== 연산자는 동일 인스턴스(즉, 참조값)를 참조하는지 검사하는 연산자입니다.
Class는 Reference Type이니 === 연산자 사용이 가능하구요,
if person1 === person2 {
// same reference
} else {
}
Struct에서 === 연산자를 사용하려면 아래처럼 에러가 발생합니다.
if user1 === user2 { // Argument type 'User' expected to be an instance of a class or class-constrained type
// same reference?
}
# 3. Struct, Class 중에 뭘 쓰지?
위에서 Struct와 Class의 공통점, 차이점을 알아봤으니
어떤 상황에서 어떤 걸 쓰면 좋은지에 대해서도 알아볼게요.
## 3.1 기본적으로는 Struct를 사용합시다.
Swift Struct는 프로토콜을 채택할 수 있으며,
이미 Swift 기본 타입(number, string, dictionary 등)은 Struct 타입이며,
Struct는 Value Type이라 변경사항이 앱의 다른 부분에 영향을 주지 않기 때문에 개발하는데 좀 더 유용할 수 있습니다.
## 3.2 Objective-C 하고 상호작용을 해야 할 경우에는 Class를 사용합니다.
데이터를 처리해야 하는 Objective-C API를 사용하거나 Objective-C 프레임워크에 정의된 클래스를 사용해야 할 경우에는 Class를 사용해야 합니다.
## 3.3 인스턴스를 공유해야 할 경우에는 Class를 사용합니다.
인스턴스의 변경사항이 그 인스턴스를 참조하고 있는 모든 부분에 영향을 줘야 할 경우 Class를 사용합니다.
(Classs는 Reference Type 이니까요!!)
## 3.4 Identifier가 포함된 데이터를 모델링할 때 Struct를 사용합니다.
struct PenPalRecord {
let myID: Int
var myNickname: String
var recommendedPenPalID: Int
}
var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)
위 코드처럼, 서버 응답값인 jsonResponse로부터 PenPalRecord 인스턴스로 변환할 때
Struct의 identifier(여기서는 myID겠죠?) 값은 서버에 저장되어 있고, 특히나 각 프로퍼티가 let으로 선언되어 있기 때문에 내부 코드가 ID를 변경할 수 없습니다.
내부적으로 identifier를 변경할 수 없고, 변경할 필요도 없을 경우에는 Struct를 사용합니다.
## 3.5 프로토콜 상속을 통해 구조를 설계하고, Struct가 그 프로토콜을 채택합시다.
Struct와 프로토콜 상속을 같이 사용하면 Class 상속과 같은 계층 구조를 만들 수 있습니다.
프로토콜을 사용하면 Class, Struct, Enumeration이 상속을 받을 수 있지만, 클래스는 클래스끼리만 상속받을 수 있습니다.
그래서 우선 프로토콜 상속을 사용하여 계층 구조를 만든 다음에, 그 프로토콜을 Struct에 적용시켜주세요.
## 참고
- https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html
- https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes
이번 글은 여기서 마무리.
'Swift' 카테고리의 다른 글
Automatic Reference Counting (ARC) (0) | 2022.04.10 |
---|---|
subscript (0) | 2022.04.03 |
FormatStyle (0) | 2022.03.24 |
PersonNameComponents를 사용해서 이름 파싱하기 (0) | 2022.03.23 |
[오픈소스] Flow (0) | 2022.03.16 |