1편 보러가기
프로퍼티 옵저버(Property Observers)
프로퍼티 옵저버는 프로퍼티의 값이 변경됨을 감지하고 적절한 동작을 수행할 수 있도록 합니다.
프로퍼티의 값이 새로 할당될 때마다 호출되고 같은 값이 할당되도 호출이 됩니다.
프로퍼티 옵저버는 아래 세 가지 상황에서 사용할 수 있습니다.
- 저장 프로퍼티를 정의할 때 설정
- 저장 프로퍼티를 Overriding 할 때 설정
- 연산 프로퍼티를 Overriding 할 때 설정
저장 프로퍼티는
프로퍼티 옵저버에는 두 가지가 있습니다.
- willSet : 값이 할당되기 직전에 호출
- didSet : 값이 할당된 직후에 호출
하나씩 알아봅시다.
willSet
willSet은 프로퍼티에 값이 할당되기 직전에 호출됩니다.
struct Human {
var name: String = "none" {
willSet(newName) {
print("oldName: \(name) / newName: \(newName)")
}
}
}
var human: Human = Human()
human.name = "애플"
//oldName: none / newName: 애플
Human 구조체에 name 프로퍼티에 willSet 프로퍼티 옵저버를 작성했습니다.
name 프로퍼티에 값이 할당되기 직전에 willSet이 호출되어
oldName은 현재 name 값이, newName은 할당되는 값이 출력됩니다.
willSet {
print("oldName: \(name) / newName: \(newValue)")
}
willSet은 setter처럼 매개변수를 생략할 수 있으며,
생략할 경우 newValue로 접근할 수 있습니다.
human.name = "none"
//oldName: none / newName: none
값이 변할 때가 아니라 값이 할당될 때 willSet이 호출되는 것이므로
동일한 값을 할당해도 willSet이 호출됩니다.
헷갈리시면 안 돼요.
didSet
didSet은 파라미터 값이 할당된 직후에 호출됩니다.
willSet과 모든 것이 동일한데 호출되는 타이밍만 달라요.
struct Human {
var name: String = "none" {
didSet(oldName) {
print("oldName: \(oldName) / newName: \(name)")
}
}
}
var human: Human = Human()
human.name = "애플"
//oldName: none / newName: 애플
didSet은 할당된 직후 호출되므로,
여기에서는 newName이 name 프로퍼티 값입니다.
그래서 oldName에 할당 이전 값이 매개변수로 전달되고 newName으로 name 값이 출력됩니다.
didSet도 매개변수명을 생략할 수 있는데요.
didSet {
print("oldName: \(oldValue) / newName: \(name)")
}
생략하게 되면 oldValue로 접근이 가능합니다.
willSet & didSet
setter는 단독으로 사용할 수 없다는 제약이 있었던 것 기억 하시나요?
willSet과 didSet은 제약 없이
둘 다 사용할 수도 있고 둘 중 하나만 사용할 수도 있습니다.
둘 중 하나만 쓰는건 위에서 봤으니 둘이 같이 쓰는 것을 해봅시다.
var name: String = "none" {
willSet {
print("oldName: \(name) / newName: \(newValue)")
}
didSet {
print("oldName: \(oldValue) / newName: \(name)")
}
}
//[willSet] oldName: none / newName: 애플
//[didSet] oldName: none / newName: 애플
willSet과 didSet을 둘 다 사용했을 때는 willSet -> 값 할당 -> didSet 순서로 호출됩니다.
그래서 willSet에서는 name이 oldName이고, 값이 할당이 되어 didSet에서는 newName이 name이 되죠.
연산 프로퍼티에서의 프로퍼티 옵저버
연산 프로퍼티는 일반적으로 프로퍼티 옵저버를 사용할 수 없습니다.
var name: String {
get {
"nickname"
}
set {
self.nickname = newValue + "!"
}
}
이런 연산 프로퍼티에 willSet과 didSet을 추가하면
willSet과 didSet은 getter와 함께 사용할 수 없다고 컴파일 에러가 발생합니다.
연산 프로퍼티는 값을 할당하지 않는다고 했었죠?
willSet과 didSet은 프로퍼티에 값을 할당되기 전후에 호출이 되므로
값을 할당하지 않는 연산 프로퍼티에서는 사용할 수 없는 것이죠.
하지만 연산 프로퍼티에서 프로퍼티 옵저버를 사용할 수 있는 방법이 있는데요.
바로 프로퍼티를 상속할 때 입니다.
class Human {
var nickname: String = "none"
var name: String {
get {
print("[getter]")
return "name"
}
set {
print("[setter]")
self.nickname = newValue + "!"
}
}
}
class Siri: Human {
override var name: String {
willSet {
print("[willSet]")
}
didSet {
print("[didSet]")
}
}
}
Human 클래스에는 name이라는 연산 프로퍼티가 있습니다.
Siri에는 이 name 프로퍼티를 오버라이딩 합니다.
이때, 오버라이딩한 name 프로퍼티에는 프로퍼티 옵저버를 설정할 수 있다는 것입니다.
var siri: Siri = Siri()
siri.name = "애플"
Siri 클래스 인스턴스를 생성하여 값을 할당해보면
setter와 프로퍼티 옵저버 모두 호출되는 것을 볼 수 있습니다.
이렇게 프로퍼티 옵저버의 내용은 끝입니다.
마지막으로 타입 프로퍼티에 대해 알아봅시다.
타입 프로퍼티(Type Property)
타입 프로퍼티는 각각의 인스턴스가 아닌 타입 자체에 속하는 프로퍼티입니다.
클래스, 구조체, 열거형에서 사용되고,
저장 프로퍼티와 연산 프로퍼티의 var/let 앞에 static/class를 써서 선언할 수 있습니다.
(static을 붙이는 것과 class를 붙이는 것의 차이점은 아래에서 다룰게요.)
타입 프로퍼티는 자동으로 lazy로 작동하고
저장 타입 프로퍼티는 반드시 초기값을 설정해야 합니다.
var siri: Siri = Siri()
이런 예시라고 하면
지금까지는 "siri.name"처럼 인스턴스를 통해 프로퍼티를 사용했습니다.
인스턴스 프로퍼티는 인스턴스가 생성될 때 프로퍼티가 초기화되고 인스턴스마다 다른 값을 지닐 수 있습니다.
타입 프로퍼티는 siri라는 인스턴스에 속하는 프로퍼티가 아니라 Siri 클래스에 속하는 프로퍼티입니다.
인스턴스의 생성 여부와 상관 없이 타입 자체에 영향을 미치는 프로퍼티로,
모든 인스턴스가 공통으로 접근해서 사용할 수 있고 모든 인스턴스에서 값이 동일합니다.
예시를 통해 알아보도록 합시다.
Swift 문서의 예시를 가져왔어요.
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}
저장 프로퍼티나 연산 프로퍼티 앞에 static을 붙여 타입 프로퍼티를 선언할 수 있습니다.
연산 프로퍼티 앞에 class를 붙이면 오버라이딩 가능한 타입 프로퍼티를 선언할 수 있어요.
타입 프로퍼티는 타입을 이용해 접근할 수 있습니다.
위 코드의 SomeClass를 이용해 알아보면,
이렇게 SomeClass를 이용해 타입 프로퍼티에 접근할 수 있고
인스턴스를 이용해 접근할 수 없습니다.
이제 "인스턴스가 아닌 타입 자체에 속한다"라는 말이 이해가 가시나요?
타입 프로퍼티는 인스턴스가 생성될 때 초기화가 되는게 아니라
어디에서든 한 번이라도 타입 프로퍼티를 부르면 메모리에 올라가고
그 뒤로는 계속 메모리에 상주하면서 값을 공유하는 형태입니다.
인스턴스 생성과 타입 프로퍼티는 전혀 상관이 없기 때문에 initializer와 상관 없고
초기값을 설정할 수 없기 때문에 저장 프로퍼티에서는 반드시 초기값을 설정해야 하는 것이죠.
또한, 자동으로 lazy처럼 동작한다는 정의도 여기서 해결이 됩니다.
타입 프로퍼티를 한 번이라도 부르기 전에는 초기화가 안 되있다가
최초로 호출이 될 때 메모리에 올라가 초기화가 됩니다.
마지막으로 일반적인 lazy는 let이 불가능하고 반드시 var만 사용이 가능했습니다.
하지만 타입 프로퍼티는 let도 사용이 가능합니다.
왜냐하면 타입 프로퍼티는 실제 사용할 때 설정한 초기값으로 초기화가 되기 때문입니다.
일반적인 lazy는 처음에 값이 없음으로 초기화가 되었다가 사용이 될 때 값이 설정됩니다.
값이 없음 -> 원하는 값으로 변화가 생기기 때문에 let이 불가능했던 것인데,
타입 프로퍼티는 처음부터 원하는 값으로 초기화가 되기 때문에 let도 사용이 가능한 것이죠.
static과 class 차이
연산 타입 프로퍼티는 static과 class로 선언할 수 있습니다.
참고로 저장 타입 프로퍼티는 static으로만 사용 가능합니다.
저장 프로퍼티에 class를 사용하면 지원하지 않는다고 하네요 ㅎㅎ;
아무튼, static과 class는 오버라이딩 가능 여부의 차이가 있습니다.
클래스를 상속했을 때 static은 오버라이딩이 불가능하고 class는 오버라이딩이 가능합니다.
class ExtensionClass: SomeClass {
override class var overrideableComputedTypeProperty: Int {
return 200
}
}
print("ExtensionClass: \(ExtensionClass.overrideableComputedTypeProperty)")
//ExtensionClass: 200
SomeClass를 상속한 ExtensionClass에서 프로퍼티를 오버라이딩할 수 있는 것은
SomeClass에서 class로 선언한 연산 타입 프로퍼티뿐입니다.
static으로 선언한 저장 타입 프로퍼티나 연산 타입 프로퍼티는 오버라이딩이 불가능해요.
만약 오버라이딩을 하려고 하면
"Property does not override any property from its superclass"라는 에러가
발생하게 됩니다.
결론은 static은 오버라이딩 불가능하고
class는 연산 타입 프로퍼티에서만 사용 가능하면서 오버라이딩이 가능하기 때문에
연산 타입 프로퍼티의 오버라이딩이 필요하면 class를 이용하면 된다! 겠네요.
마무리
이렇게 프로퍼티에 대한 포스팅이 끝났습니다.
어렴풋이 추상적으로만 알고 있던 프로퍼티에 대해 정확히 알게 되었네요.
틀린 점, 이해가 안 가는 점은 댓글로 알려주시면 감사하겠습니다.
감사합니다!
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.