Swift에서 Property는 크게 5가지로 나뉜다.
- Stored Properties(저장 프로퍼티)
- Computed Properties(연산 프로퍼티)
- Property Observers(프로퍼티 감시자)
- Property Wrappers(프로퍼티 래퍼)
- Type Properties(타입 프로퍼티)
Stored Properties
가장 간단한 형태의 Stored Properties는 class 혹은 structure의 instance에만 관련된 값을 저장한다.
variable Stored Property(변수 저장 프로퍼티)는 var 키워드를 사용하며,
constant Stored Property(상수 저장 프로퍼티)는 let 키워드를 사용한다.
struct SomeStruct {
var variableProperty: Int
let constantProperty: Int
}
class SomeClass {
var variableProperty: Int
let constantProperty: Int
init() {
self.variableProperty = 10
self.constantProperty = 10
}
}
structure는 "값타입"이기 때문에, let키워드로 인스턴스를 생성할 경우
변수 저장 프로퍼티일지라도 값 변경이 불가능하다.
let someStruct = SomeStruct(variableProperty: 10, constantProperty: 10)
someStruct.variableProperty = 10 // compile error
반면, class의 경우는 "참조타입"이기 때문에, let키워드로 인스턴스를 생성하여도 변수 저장 프로퍼티는 변경이 가능하다.
class의 인스턴스를 let으로 생성한다는 것은 참조를 바꾸지 못한다는 것이지, 변수의 값 변경에 영향을 미치진 않는다.
let someClass = SomeClass()
someClass.variableProperty = 20 // 가능
Lazy Stored Properties
Lazy Stored Proeperties(지연 저장 프로퍼티)란 호출이 이루어져야 값의 초기화가 이루어지는 Stored Properties이다.
Lazy property는 var 키워드를 사용해야만 하는데,
이는 let키워드는 인스턴스가 완전히 생성되기 전에 초기화가 이루어지므로 Lazy Property에 맞지 않기 때문이다.
기본적으로 프로퍼티들은 인스턴스화가 되면 초기화가 완료가 되어야 하는데, 해당 역할을 initializer가 담당한다.
하지만, 지연 저장 프로퍼티는 initializer가 아닌 첫 호출시점에 초기화가 이루어지기 때문에, 초깃값을 가지고 있어야 한다.
class SomeClass {
var variableProperty: Int
let constantProperty: Int
lazy var lazyProperty: Int = 10
init() {
self.variableProperty = 10
self.constantProperty = 10
}
}
지연 저장 프로퍼티는 외부 요인에 의해 초깃값이 결정되는 경우 유용하게 사용할 수 있다.
또한, 첫 호출 시점에 초기화가 이루어지기에 원하는 시점에 메모리에 올릴 수 있기 때문에,
불필요한 공간낭비 혹은 성능낭비를 줄일 수 있다.
추가적으로 멀티 쓰레딩 환경에서 지연 저장 프로퍼티는 여러 번 초기화될 수 있기 때문에 조심해야 한다.
자세한 내용은 해당 포스팅을 참고바란다.
Computed Properties
Computed Property(연산 프로퍼티)는 class, structure, enumeration에 정의될 수 있으며, 실제 값을 저장하는 프로퍼티가 아니다.
인스턴스 내/외부의 값을 계산하여 리턴해주는 getter와 은닉화된 내부 프로퍼티들을 간접적으로 설정하는 setter 역할을 할 수 있다.
setter는 Optional이기 때문에 생략이 가능하다.
Computed Property 역시 var 키워드를 사용해야만 한다.
class SomeClass {
var variableProperty: Int
let constantProperty: Int
private lazy var lazyProperty: Int = 10
var computedProperty: Int {
get {
return lazyProperty * 2
}
set(multipleValue) {
lazyProperty = multipleValue / 2
}
}
init() {
self.variableProperty = 10
self.constantProperty = 10
}
}
let someClass = SomeClass()
// getter에서 lazyProperty 호출됨에 따라 초기화 이루어짐.
print(someClass.computedProperty) // 20
someClass.computedProperty = 50
setter의 경우 새로운 값이 이름을 생략할 수 있는데, 해당 경우 default name인 newValue가 사용된다.
getter의 경우는 내부 코드가 한 줄이고, 타입이 Computed property의 타입과 같다면 return 키워드를 생략할 수 있다.
var computedProperty: Int {
get {
lazyProperty * 2
}
set{
lazyProperty = newValue / 2
}
}
앞서 말했다 싶이, setter는 생략이 가능한데 해당 경우에는 read-only가 되며, get키워드도 생략이 가능하다.
var readOnlyProperty: Int {
get {
return lazyProperty * 2
}
}
// Read-Only이기에 get 생략가능
var readOnlyProperty: Int {
return lazyProperty * 2
}
// 한줄 + 타입같으므로 return 생략가능
var readOnlyProperty: Int {
lazyProperty * 2
}
굳이 메서드를 두고 연산 프로퍼티를 사용하는 이유는 메서드의 경우에는 get, set 역할을 하는 2개의 메서드를 구현해야 한다.
코드가 분산되기에 코드의 가독성이 떨어질 우려가 있지만, Computed Property의 경우에는 하나의 프로퍼티로 해결이 가능하기 때문에 가독성 측면에서 장점이 있다.
하지만, Computed Property는 Read-Only는 가능하지만, Write-Only는 불가능하기에 둘 간의 장단점을 고려하여 사용하자.
Property Observers
Property Observer(프로퍼티 감시자)는 특정 프로퍼티를 감시하고 있다가 값이 변경되면 적절한 작업을 취할 수 있다.
Property Observer를 추가할 수 있는 3가지 경우가 있다.
- 저장 프로퍼티
- 상속받은 저장 프로퍼티를 Override(재정의)
- 상속받은 연산 프로퍼티를 Override(재정의)
Property Observer는 값의 변경 직전에 호출되는 willSet 메서드와
값의 변경 직후에 호출되는 didSet 메서드가 존재한다.
두 메서드는 파라미터를 가지고 있는데,
willSet의 경우에는 변경될 값이고, didSet의 경우에는 변경 이전의 값이다.
Computed Property의 set 메서드와 마찬가지로 파라미터는 생략이 가능한데,
해당 경우 willSet은 newValue로, didSet은 oldValue로 자동 지정이 된다.
class SomeClass {
var variableProperty: Int { // 저장 프로퍼티에 감시자 추가
willSet {
print("\(variableProperty)에서 \(newValue)로 변경될 것.")
}
didSet {
print("\(oldValue)에서 \(variableProperty)로 변경완료.")
}
}
}
상속받은 저장 프로퍼티를 재정의 하는 경우에는 감시자만 추가할 뿐이지, 기존의 저장 프로퍼티를 변경하는 것이 아니다.
따라서 기존의 저장 프로퍼티의 기능은 온전히 사용이 가능하다.
추가적으로 Lazy Stored Property에는 사용이 불가능하다.
class SubClass: SomeClass {
override var variableProperty: Int {
willSet {
print("\(variableProperty)에서 \(newValue)로 변경될 것.")
}
didSet {
print("\(oldValue)에서 \(variableProperty)로 변경완료.")
}
}
}
상속받지 않은 연산 프로퍼티의 경우, 값의 변경을 setter에서 감지하고 반응할 수 있기 때문에,
필요가 없을 뿐더러 불가능하다.
상속받은 연산 프로퍼티를 재정의 하는 경우는 저장 프로퍼티와 마찬가지로 기존의 연산 프로퍼티의 기능은 온전히 사용이 가능하다.
class SubClass: SomeClass {
override var computedProperty: Int {
willSet {
print("\(computedProperty)에서 \(newValue)로 변경될 것.")
}
didSet {
print("\(oldValue)에서 \(computedProperty)로 변경완료.")
}
}
}
Property Observer는 값의 변경을 관찰하고 반응하기 때문에,
값의 변경이 이루어지지 않는 Constant Stored Property와 Read-Only Computed Property에는 추가할 수 없다.
추가적으로 Property Observer가 추가된 프로퍼티를 함수의 in-out 파라미터로 전달하면
항상 willSet, didSet이 호출된다.
이는 copy-in-copy-out 메모리 모델 때문인데, 함수 내부에서 값이 변경되든 말든 함수 종료시점에 항상 값을 다시 쓰기 때문이다.
또한, willSet과 didSet은 값이 변경된 Thread에서 호출되기 때문에,
UI Update작업을 진행할 경우에 유의해야 한다.
추가적으로 set, get메서드는 값이 변경되거나 값을 읽은 Thread에서 호출되고,
Computed Property에 Property Observer를 추가한 경우, setter와 같은 Thread에서 willSet과 didSet이 호출된다.
class SomeClass {
var variableProperty: Int = 20 {
willSet {
print("willSet : \(Thread.current)")
}
didSet {
print("didSet : \(Thread.current)")
}
}
var computedProperty: Int {
get {
print("getter : \(Thread.current)")
return variableProperty * 2
}
set{
print("setter : \(Thread.current)")
variableProperty = newValue / 2
}
}
}
let someClass = SomeClass()
//Main Thread
print(someClass.computedProperty) // getter
someClass.computedProperty = 20 // setter
print("----- Global Queue -----")
//Other Thread
DispatchQueue.global().async {
print(someClass.computedProperty) // getter
someClass.computedProperty = 20 // setter
}
/* Prints:
getter : <_NSMainThread: 0x600003eac040>{number = 1, name = main}
40
setter : <_NSMainThread: 0x600003eac040>{number = 1, name = main}
willSet : <_NSMainThread: 0x600003eac040>{number = 1, name = main}
didSet : <_NSMainThread: 0x600003eac040>{number = 1, name = main}
----- Global Queue -----
getter : <NSThread: 0x600003ea2a40>{number = 5, name = (null)}
20
setter : <NSThread: 0x600003ea2a40>{number = 5, name = (null)}
willSet : <NSThread: 0x600003ea2a40>{number = 5, name = (null)}
didSet : <NSThread: 0x600003ea2a40>{number = 5, name = (null)}
*/
추가적으로 getter, setter, willSet, didSet모두 sync로 동작하였다.
따라서, Main Thread에서 동작할 경우 오래 걸리는 작업의 경우 UI Update가 버벅일 수 있다.
Reference
'iOS > Swift' 카테고리의 다른 글
[WWDC] Understanding Swift Performance(1) - Dimensions of Performance (0) | 2023.08.06 |
---|---|
[Swift] Properties(2) - Wrappers, Type (0) | 2023.01.18 |
[Swift] Memory Leak(2) - Closure의 [weak self] (0) | 2022.11.26 |
[Swift] Memory Leak(1) - 약한 참조, 강한 참조, unowned 참조 (0) | 2022.11.25 |
[Swift] Initializer(3) - required init?(coder:) (1) | 2022.11.10 |