8.2.1 저장 프로퍼티
저장 프로퍼티(Stored Property)는 클래스 내에서 선언된 변수나 상수를 부르는 이름입니다. 일반 변수나 상수를 선언할 때 초기값을 할당할 수 있는 것처럼 저장 프로퍼티를 선언할 때에도 초기값을 할당할 수 있습니다. 하지만 반드시 선언하는 시점에서 초기값을 할당해야 하는 것은 아닙니다. 초기화 구문에서 초기값을 설정해도 됩니다. 구조체의 멤버와이즈 구문이 이같은 역할을 합니다.
하지만, 클래스에서 프로퍼티를 선언할 때 초기값을 함께 할당해 주지 않으면 신경쓸 것이 있습니다. 우선 프로퍼티 선언 시 초기값이 할당되지 않은 저장 프로퍼티는 반드시 옵셔널 타입으로 선언해 주어야 합니다. 스위프트에서는 클래스의 프로퍼티에 값이 비어 있으면 인스턴스를 생성할 때 무조건 nil 값으로 초기화하기 때문입니다.
옵셔널 타입으로 프로퍼티를 선언할 때에는 일반 옵셔널 타입과 묵시적 옵셔널 해제 타입 중에서 선택해서 정의할 수 있습니다. 묵시적 옵셔널 타입 해제 구문은 앞에서 잠깐 다룬 적이 있는 타입으로, 값을 사용할 시점에서는 절대 nil이 되지 않지만, 선언할 때에는 초기값을 할당해줄 수 없어서 옵셔널로 선언해야 하는 저장 프로퍼티에 사용됩니다. 묵시적 옵셔널 타입으로 지정해두면 이 값을 사용할 때 옵셔널 해제 처리할 필요 없이 일반 변수처럼 쓸 수 있기 때문에 편리합니다.
저장 프로퍼티를 선언할 때 초기값을 주지 않으면서도 옵셔널 타입으로 선언하지 않을 수 있는 방법이 있습니다. 바로 초기화 구문에서 프로퍼티의 값을 초기화해주는 겁니다. 어차피 클래스의 프로퍼티는 인스턴스를 생성할 때 초기화되기 때문에, 프로퍼티의 초기값은 인스턴스를 생성하기 전까지만 할당해 줄 수 있으면 문제가 되지 않습니다. 따라서 초기화 구문 내에서 프로퍼티의 값을 할당해 줄 수 있으면 이 프로퍼티의 타입은 옵셔널로 선언하지 않아도 됩니다.
반면, 구조체는 이같은 초기값으로부터 자유로워서, 초기값을 할당하지 않고 선언만 하더라도 프로퍼티의 타입을 옵셔널로 지정하지 않아도 됩니다. 멤버와이즈 초기화 구문이 제공되기 때문입니다. 멤버와이즈 초기화 구문은 인스턴스 생성 시 인자값을 받아 프로퍼티의 값을 초기화시켜주는 역할을 합니다. 이런 멤버와이즈 구문이 값의 초기화를 보장해 주기 때문에 옵셔널 타입으로 지정하지 않아도 됩니다. 예제를 봅시다.
class User {
var name: String
}
일반 타입의 문자열로 선언된 이 프로퍼티에 값이 할당되어 있지 않으므로 컴파일러는 오류를 발생시킵니다. 이를 해결하기 위해 세 개의 해결책이 있습니다.
// 첫 번째 해결책 - 초기화 구문을 작성하고, 그 안에서 초기값을 할당해 줍니다
class User {
var name: String
init() {
self.name = ""
}
}
init 메서드 내부에 작성된 구문은 인스턴스가 생성될 때 실행됩니다. 여기서 self라는 키워드에 주목합시다. 클래스에서 선언된 프로퍼티나 메서드는 self 키워드를 붙여서 구분합니다.
// 두 번째 해결책 - 프로퍼티를 옵셔널 타입으로 바꿔줍니다
class User {
var name: String?
}
(또는)
class User {
var name: String!
}
옵셔널 타입으로 프로퍼티를 선언할 경우, 초기화하지 않았더라도 시스템이 자동으로 초기화해 주므로 문제가 생기지 않습니다. 프로퍼티가 nil이 되지 않을 자신이 있다면 묵시적 옵셔널 해제 타입을 사용하는 것이 편리합니다.
// 세 번째 해결책 - 프로퍼티에 초기값을 할당해 줍니다
class User {
var name: String = ""
}
처음부터 빈 초기값을 입력하면 많은 문제로부터 벗어날 수 있습니다.
저장 프로퍼티의 분류
저장 프로퍼티는 다음 두 가지로 나눌 수 있습니다.
var 키워드로 정의되는 변수형 저장 프로퍼티(멤버 변수라고 부름)
let 키워드로 정의되는 상수형 저장 프로퍼티(멤버 상수라고 부름)
var 키워드로 정의한 멤버 변수는 값을 얼마든지 수정할 수 있는 반면, let 키워드로 정의한 멤버 상수는 최초에 할당된 값이 변경 없이 그대로 유지됩니다. 구조체에서의 저장 프로퍼티에 대해 살펴봅시다.
// 고정 길이 범위 구조체
struct FixedLengthRange {
var startValue: Int // 시작값
let length: Int // 값의 범위
}
// 가변 길이 범위 구조체
struct FlexibleLengthRange {
let startValue: Int // 시작값
var length: Int // 값의 범위
}
// 아래 구조체 인스턴스는 정수값 0,1,2를 의미합니다
var rangeOfFixedIntegers = FixedLengthRange(startValue: 0, length: 3)
// 아래처럼 시작값을 변경하면 객체 인스턴스는 정수값 4,5,6을 의미하게 됩니다
rangeOfFixedIntegers.startValue = 4
// 아래 구조체 인스턴스는 정수값 0,1,2를 의미합니다
var rangeOfFlexibleIntegers = FlexibleLengthRange(startValue:0, length: 3)
// 아래처럼 범위값을 변경하면 객체 인스턴스는 정수값 0,1,2,3,4를 의미하게 됩니다
rangeOfFlexibleIntegers.length = 5
주의해야 할 점은 구조체 인스턴스를 상수에 할당할 경우입니다. 인스턴스를 변수에 할당하면 구조체 내에서 변수로 정의한 저장 프로퍼티는 개발자가 원할 때 얼마든지 값을 수정할 수 있습니다.
// 변수에 할당된 구조체 인스턴스라면
var variablesOfInstance = FixedLengthRange(startValue: 3, length: 4)
// 아래와 같이 저장 프로퍼티를 수정할 수 있음
variableOfInstance.startValue = 0 // (O)
하지만 인스턴스를 상수에 할당하면 비록 구조체 내에서 저장 프로퍼티를 변수로 정의했더라도 값을 변경할 수 없습니다.
// 반면, 상수에 할당된 구조체 인스턴스라면
let constantsOfInstance = FixedLengthRange(startValue: 3, length: 4)
// 아래와 같이 저장 프로퍼티를 수정하려고 하면 오류가 발생함
constantsOfInstance.startValue = 0 // (X)
반면, 클래스는 클래스 인스턴스를 상수에 할당하더라도 클래스 내에서 변수로 선언한 저장 프로퍼티는 얼마든지 값을 수정할 수 있습니다.
이러한 차이는 구조체와 클래스의 값 전달 방식의 차이에서 비롯됩니다. 구조체는 값에 의한 전달 방식으로 인스턴스가 변수나 상수에 할당되고, 클래스는 참조에 의한 전달 방식으로 인스턴스의 레퍼런스가 변수나 상수에 할당되기 때문입니다. 따라서 구조체는 저장 프로퍼티의 값이 바뀌면 상수에 할당된 인스턴스 전체가 변경되고, 클래스는 저장 프로퍼티의 값이 바뀌더라도 상수에 할당된 인스턴스 레퍼런스는 변경되지 않습니다.
지연 저장 프로퍼티
일반적으로 저장 프로퍼티는 클래스 인스턴스가 처음 생성될 때 함께 초기화되지만, 저장 프로퍼티 정의 앞에 lazy라는 키워드가 붙으면 예외입니다. 키워드에서 짐작할 수 있듯이, 이 키워드는 저장 프로퍼티의 초기화를 지연시킵니다. 클래스 인스턴스가 생성되어 모든 저장 프로퍼티가 만들어지더라도 lazy 키워드가 붙은 프로퍼티는 선언만 될 뿐 초기화되지 않고 계속 대기하고 있다가 프로퍼티가 호출되는 순간 초기화됩니다.
class OnCreate {
init() {
print("OnCreate!!")
}
}
class LazyTest {
var base = 0
lazy var late = OnCreate()
init() {
print("Lazy Test")
}
}
LazyTest 클래스 내부에는 late라는 저장 프로퍼티를 선언하면서 여기에 lazy 키워드를 붙여 지연 저장 프로퍼티로 만들었습니다. 이 프로퍼티의 초기값은 OnCreate 클래스의 인스턴스입니다. 따라서 프로퍼티가 초기화될 때 OnCreate클래스의 인스턴스가 만들어질 겁니다. LazyTest 클래스 역시 초기화될 때 "Lazy Test"라는 구문을 출력합니다.
두 개의 클래스가 정의되었다면 이제 호출해보겠습니다
let lz = LazyTest()
// "Lazy Test"
인스턴스가 초기화되면서 내부에 정의된 초기화 블록도 함께 실행되며 출력 구문이 표시됩니다. 하지만 아직 OnCreate 클래스의 출력 구문은 보이지 않습니다. late 프로퍼티가 초기화되지 않았다는 뜻입니다.
lz.late
// "OnCreate!!"
late 프로퍼티를 호출하야 이제서야 구문이 출력됩니다. 즉 지연 저장 프로퍼티에 대입된 인스턴스는 프로퍼티가 처음 호출되는 시점에서 생성된다는 것을 알 수 있습니다. 처음으로 호출이 발생할 때 값을 평가하여 초기화되며, 이후 두 번째 호출부터는 처음 초기화된 값을 그대로 사용할 뿐 다시 초기화되지는 않습니다.
클로저를 이용한 저장 프로퍼티 초기화
저장 프로퍼티 중 일부는 연산이나 로직 처리를 통해 얻어진 값을 이용하여 초기화해야 하는 경우가 있습니다. 스위프트에서는 클로저를 사용하여 필요한 로직을 실행한 후 반환되는 값을 이용하여 저장 프로퍼티를 초기화할 수 있도록 지원합니다.
let/var 프로퍼티명: 타입 = {
정의 내용
return 반환값
}
이렇게 정의된 클로저 구문은 클래스나 구조체의 인스턴스가 생성될 때 함께 실행되어 초기값을 반환하고, 이후로는 해당 인스턴스 내에서 재실행되지 않습니다. 비슷한 구문 형식이지만 연산 프로퍼티가 참조될 때마다 매번 재평가된 값을 반환하는 것과 결정적으로 다릅니다. 예제를 보겠습니다.
class PropertyInit {
// 저장 프로퍼티 - 인스턴스 생성 시 최초 한 번만 실행
var value01: String! = {
print("value01 execute")
return "value01"
}()
// 저장 프로퍼티 - 인스턴스 생성 시 최초 한 번만 실행
let value02: String! = {
print("value02 execute")
return "value02"
}()
}
let s = PropertyInit()
// 실행 결과
value01 execute
value02 execute
단순히 클래스의 인스턴스를 생성했을 뿐인데, 실행 결과에 두 개의 메시지가 출력된 것을 볼 수 있습니다. 각각 value01, value02 프로퍼티의 초기값을 대신하는 클로저 구문입니다. 이들 프로퍼티를 참조해 봅시다.
s.value01
s.value02
// 실행 결과 없음
저장 프로퍼티를 단순히 참조만 하면 아무런 새로운 로그도 출력되지 않습니다. 저장 프로퍼티에 정의된 클로저 구문이 더 이상 재실행되지 않기 때문입니다.
저장 프로퍼티는 클래스 인스턴스가 생성될 때 자동으로 값을 평가하므로 메모리 자원의 낭비로 이어질 수 있습니다. 이때에는 앞에서 배운 lazy를 이용하면 됩니다. 클래스 인스턴스가 생성될 때 실행되는 것이 아니라 실제로 값을 참조하는 시점에 실행되고, 처음 한 번 실행되면 다시 값을 평가하지 않습니다.
lazy var 프로퍼티명: 타입 = {
정의 내용
return 반환값
}
PropertyInit에 이 구문을 추가하여 확인해 봅시다.
class PropertyInit {
...중략...
// 프로퍼티 참조 시 한번만 실행
lazy var value03: String! = {
print("value03 execute")
return "value03"
}()
}
let s1 = PropertyInit()
// 실행 결과
value01 execute
value02 execute
s1.value03
// 실행 결과
value03 execute
콘솔에 value03에 대한 로그 메시지가 출력되었습니다. 초기화 클로저가 실행된 것입니다. 한 번 더 value03을 참조해 보겠습니다.
s1.value03
// 실행 결과 없음
두 번째 참조에서는 아무런 메시지도 출력되지 않습니다. 이처럼 lazy 키워드를 붙여서 정의한 저장 프로퍼티를 클로저 구문으로 초기화하면 최초 한 번만 로직이 실행되고 참조되는 시점에 초기화되어 메모리 낭비를 줄일 수 있습니다.
Last updated