프로퍼티(Property)
property는 value를 특정 클래스, 구조체, 열거형과 연결하는 역할을 합니다.
말은 어렵지만 클래스, 구조체, 열거형에서 값을 쓸 수 있도록 해주는게 property라는거에요.
var hello: String = "Hello"
이 코드도 property를 나타낸건데 hello는 property 이름, String은 타입, "Hello"는 값입니다.
"Hello"를 쓰기 위해서는 hello를 이용해야 하죠.
Property에는 크게 4가지가 존재합니다.
- 저장 프로퍼티(Stored Property)
- 지연 저장 프로퍼티(Lazy Stored Property)
- 연산 프로퍼티(Computed Property)
- 프로퍼티 옵저버(Property Observers)
- 타입 프로퍼티(Type Property)
이번 포스팅에서는 저장 프로퍼티 ~ 연산 프로퍼티를 알아보겠습니다.
저장 프로퍼티(Stored Property)
가장 단순한 프로퍼티로 클래스, 구조체에서 값을 저장하기 위해 선언되는 상수나 변수입니다.
클래스와 구조체에서만 사용 가능하다는 게 특징이에요.
struct Person {
let name: String = "none"
var age: Int = 0
}
여기에서 name과 age가 모두 저장 프로퍼티입니다.
구조체와 클래스 차이점 - Initializer
구조체와 클래스의 저장 프로퍼티는 동작에 약간의 차이가 있습니다.
구조체에서는 저장 프로퍼티를 모두 포함하는 initializer를 자동으로 생성합니다.
struct Person {
var name: String
var age: Int
}
let person = Person(name: "유정주", age: 25)
struct에 initializer가 없어도 자동으로 생성해주기 때문에 에러가 발생하지 않아요.
하지만 클래스에서는 저장 프로퍼티에
직접 기본값을 설정하거나 initializer를 생성해줘야 합니다.
동일한 코드를 클래스로 바꾼 코드인데요.
생성자가 없다는 에러가 발생합니다.
class Person {
var name: String = ""
var age: Int = 0
}
그래서 클래스에서는 이렇게 기본값을 설정하거나
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
이렇게 직접 initializer를 구현해줘야 해요.
그렇지 않으면 프로퍼티 초기값을 할당할 수 없기 때문에 클래스 인스턴스 생성이 불가능합니다.
구조체와 클래스 차이점 - let/var
프로퍼티의 let/var는 구조체와 클래스에서 서로 다르게 동작합니다.
어떤 점이 다른지 예시를 통해 알아봅시다.
구조체에서의 let/var
먼저 구조체에서의 let/var 동작을 알아보겠습니다.
struct Human {
let name: String = "Human"
var age: Int = 10
}
var human1: Human = Human()
let human2: Human = Human()
Human 구조체 안에는 let, var 저장 프로퍼티가 하나씩 있고,
구조체 인스턴스도 let, var 하나씩 생성했습니다.
var로 선언한 human1은 상수인 name은 변경이 안 되고 변수인 age는 변경이 됩니다.
그럼 let으로 선언한 human2에서도 똑같은 결과일까요?
human1과는 달리 변수인 age도 변경이 안 되는걸 볼 수 있습니다.
그 이유는 구조체는 인스턴스 자체가 상수이기 때문입니다.
human1은 변수이기 때문에 내부 프로퍼티의 변경이 허용 되고,
human2는 상수이기 때문에 인스턴스의 어떠한 값 변경도 금지됩니다.
그래서 human2의 프로퍼티가 변수여도 인스턴스 자체가 상수이기 때문에 변경이 안 되는거죠.
물론 인스턴스에 다른 인스턴스를 넣는 것도 안 됩니다.
클래서에서의 let/var
클래스에서는 구조체와 다르게 동작합니다.
아래의 Human 클래스는 위의 구조체를 클래스로만 바꾼거에요.
var로 선언한 human1은 구조체 인스턴스와 똑같이 동작합니다.
하지만 human2는 구조체와 달리 age의 변경이 가능합니다.
왜 그럴까요?
클래스는 참조타입이기 때문입니다.
스택 공간에는 인스턴스가 저장되는 힙 영역의 주소가 저장되고,
힙에는 인스턴스의 프로퍼티가 저장이 되죠.
여기에서 let의 영향을 받는 것은 스택에 저장되는 힙 영역 주소값입니다.
그래서 인스턴스의 프로퍼티들은 let의 영향을 받지 않는 것이죠!
대신 let의 영향을 받는 human2에 다른 인스턴스를 넣으려고 하면 에러가 발생합니다.
구조체, 클래스에서 let/var의 동작 차이와 이유를 아시겠죠?
지연 저장 프로퍼티(Lazy Stored Property)
지연 저장 프로퍼티는 호출이 된 후 초기화가 되는 저장 프로퍼티입니다.
저장 프로퍼티에 lazy 키워드를 사용하면 지연 저장 프로퍼티로 사용할 수 있습니다.
lazy 프로퍼티는 개별적으로 초기화가 되기 때문에 반드시 변수 var로 선언 돼야 합니다.
그래서 lazy 키워드를 let에 붙이면 에러가 발생해요.
lazy 프로퍼티는 인스턴스 생성 당시 "값이 없음"으로 초기화가 됩니다.
그리고 lazy 프로퍼티가 호출이 되면 그 때 원하는 값으로 초기화가 되는거에요.
따라서 값이 없음 -> 원하는 값으로 변경이 돼야 하므로 var만 사용이 가능한 것입니다.
이제 지연 저장 프로퍼티의 예시를 봅시다.
다음과 같이 House를 저장 프로퍼티로 갖는 Human 클래스가 있다고 합시다.
class House {
var address: String = ""
var name: String = ""
init() {
print("Create House!")
}
}
class Human {
var name: String = "Human"
var house: House = House()
}
Human 인스턴스를 생성하면 House 인스턴스도 함께 생성이 됩니다.
지연 저장 프로퍼티일 때는 어떨까요?
class Human {
var name: String = "Human"
lazy var house: House = House()
}
lazy 키워드를 사용해 house를 지연 저장 프로퍼티로 만들어보았습니다.
그러면 human을 생성해도 당장은 House 인스턴스가 생성이 안 됩니다.
Human 인스턴스가 생성이 되었는데도 House의 init 함수가 호출이 안 된 것을 볼 수 있어요.
lazy 키워드를 사용하면 그 프로퍼티가 사용될 때 초기화가 됩니다.
house에 접근해야 Create House! 가 출력이 되는거죠.
그렇다면 왜 지연 저장 프로퍼티를 사용할까요?
class Database {
var name: String = "House Data"
lazy var houses: [House] = Array(repeating: House(), count: 100_000)
}
var database1: Database = Database()
var database2: Database = Database()
var database3: Database = Database()
var database4: Database = Database()
var database5: Database = Database()
위와 같이 사용할지 아닐지 모르는 10만개짜리 배열인 houses 프로퍼티가 있다고 합시다.
이 프로퍼티는 database 4, 5에서만 사용을 할거에요.
그렇다면 이 houses는 1, 2, 3에서는 불필요한 데이터겠죠?
lazy를 쓸 경우 1, 2, 3에서는 houses 프로퍼티가 초기화가 안 되므로
30만개의 House 크기만큼 메모리 절약이 되는 것이죠.
전연 변수/상수는 Lazily
전역 변수/상수는 항상 lazily하게 동작한다고 합니다.
대신 지연 저장 프로퍼티와는 달리 lazy 키워드를 붙이지 않아도 됩니다.
전역에 선언하기만 하면 자동으로 lazily하게 동작을 하는거죠.
연산 프로퍼티
연산 프로퍼티는 실제로 값을 저장하는 프로퍼티가 아니라,
저장 프로퍼티의 값을 읽어 연산을 실행해서 반환하는 프로퍼티입니다.
연산 프로퍼티는 항상 값이 바뀌기 때문에 반드시 var로 선언해야 하고
구조체, 클래스, 열거형에서 사용할 수 있습니다.
연산 프로퍼티 예시
struct Human {
var name: String
var age: Int
var koreanAge: Int {
get {
return age + 1
}
set(newAge) {
self.age = newAge - 1
}
}
}
get은 getter, set은 setter라고 부르는데요.
getter를 이용해 koreanAge에서 반환하는 값을 설정하고,
setter를 이용해 koreanAge에 값을 설정할 때 다른 저장 프로퍼티에 값을 설정할 수 있습니다.
var human: Human = Human(age: 10)
print("human age: \(human.age) / koreanAge: \(human.koreanAge)")
//human age: 10 / koreanAge: 11
human.koreanAge = 20
print("human age: \(human.age) / koreanAge: \(human.koreanAge)")
//human age: 19 / koreanAge: 20
그래서 age에 10을 설정하면 koreanAge = 10 + 1이 되서 koreanAge는 11이 되는거고,
koreanAge에 20을 설정하면 age = 20 - 1이 되서 19가 되는 것입니다.
getter
getter는 setter 없이 단독으로 사용할 수 있습니다.
이런 연산 프로퍼티는 get-only라고 해요.
setter가 없기 때문에 koreanAge에는 값을 대입할 수 없습니다.
이런 get-only는
이렇게 get 키워드를 생략할 수 있습니다.
후행 클로저의 특성으로 인해
var koreanAge: Int { age + 1 }
이렇게까지 생략이 가능해요 ㅎ
setter
하지만 setter는 단독으로 사용할 수 없습니다.
만약 setter만 작성하면 getter가 없다고 에러가 납니다..
setter는 매개변수 이름을 생략할 수 있습니다.
struct Human {
...
var koreanAge: Int {
get {
return age + 1
}
set {
self.age = newValue - 1
}
}
}
매개변수 이름을 생략하면 newValue로 자동으로 설정이 되서 사용할 수 있습니다.
자기자신은 getter/setter에서 사용 불가능
연산 프로퍼티의 정의에서 봤듯이 다른 저장 프로퍼티를 이용해서 특정 동작을 하는건데요.
그렇기 때문에 getter와 setter에서 자기 자신을 사용하면 안 됩니다.
만약 자기 자신을 이용하는 코드를 작성하면 warning이 뜨고,
getter를 사용하면 getter가 무한 호출 되고,
setter를 사용하면 setter가 무한 호출됩니다.
getter의 경우 return self이니 다시 getter가 호출되고 또 self를 부르니 getter가 또 호출되고... 이래서 무한 호출이 되는거고
settet는 계속해서 자기 자신에게 set을 하니 무한으로 setter가 호출되는 것입니다.
그래서 getter/setter에서는 자기 자신을 사용하면 안 돼요!
프로퍼티 옵저버와 타입 프로퍼티는 다음 포스팅에서 다루도록 하겠습니다.
감사합니다!
2편 보러가기
참고
https://docs.swift.org/swift-book/LanguageGuide/Properties.html
https://jinshine.github.io/2018/05/22/Swift/6.%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0(Property)/
https://babbab2.tistory.com/118
https://babbab2.tistory.com/119?category=828998
https://babbab2.tistory.com/120?category=828998
https://babbab2.tistory.com/121?category=828998
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.