프로퍼티
프로퍼티 (Properties)
값을 저장하거나 계산해서 내놓는 것 — 프로퍼티는 스위프트 타입의 한 부분입니다. 인스턴스에 속한 값과 타입 자체에 속한 값을 모두 다루는 방법을 이 챕터에서 차근차근 살펴볼게요.
프로퍼티(property) 는 값을 특정 클래스(class), 구조체(structure), 열거형(enumeration)과 연결해 줘요. 프로퍼티는 크게 두 종류로 나뉘는데, 값을 그대로 저장하는 저장 프로퍼티(stored property) 와 값을 저장하지 않고 계산해서 내놓는 계산 프로퍼티(computed property) 가 있어요. 계산 프로퍼티는 클래스·구조체·열거형 모두가 가질 수 있지만, 저장 프로퍼티는 클래스와 구조체만 가질 수 있어요.
저장 프로퍼티와 계산 프로퍼티는 보통 특정 타입의 인스턴스에 붙어요. 그런데 프로퍼티가 타입 자체에 붙는 경우도 있는데, 이걸 타입 프로퍼티(type property) 라고 불러요.
그리고 프로퍼티 값이 바뀌는 순간을 감시하는 프로퍼티 옵저버(property observer) 도 정의할 수 있어요. 직접 정의한 저장 프로퍼티뿐 아니라, 서브클래스가 슈퍼클래스에서 상속받은 프로퍼티에도 옵저버를 붙일 수 있죠. 또 여러 프로퍼티의 getter와 setter에서 반복되는 코드를 재사용하려면 프로퍼티 래퍼(property wrapper) 를 쓸 수 있어요.
본문
저장 프로퍼티 (Stored Properties)
가장 단순한 형태의 저장 프로퍼티는, 특정 클래스나 구조체의 인스턴스에 한 부분으로 저장되는 상수 또는 변수예요.
저장 프로퍼티는 var 키워드로 선언되는 변수 저장 프로퍼티(variable stored property) 와 let 키워드로 선언되는 상수 저장 프로퍼티(constant stored property) 로 나눌 수 있어요.
저장 프로퍼티는 정의할 때 기본값을 줄 수도 있고, 초기화 과정에서 초기값을 설정하거나 바꿀 수도 있어요. 상수 저장 프로퍼티조차 초기화 중에는 값을 설정할 수 있는데, 이 내용은 초기화(Initialization) 챕터에서 더 자세히 다뤄요.
아래 예시는 생성된 뒤에는 범위의 길이를 바꿀 수 없는 정수 범위를 나타내는 FixedLengthRange 구조체를 정의해요.
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// the range represents integer values 0, 1, and 2
rangeOfThreeItems.firstValue = 6
// the range now represents integer values 6, 7, and 8
FixedLengthRange의 인스턴스는 firstValue라는 변수 저장 프로퍼티와 length라는 상수 저장 프로퍼티를 가져요.
위 예시에서 length는 새 범위가 만들어질 때 초기화되고, 상수 프로퍼티이기 때문에 이후에는 바꿀 수 없어요.
상수 구조체 인스턴스의 저장 프로퍼티 (Stored Properties of Constant Structure Instances)
구조체의 인스턴스를 만들어서 상수에 할당하면, 그 인스턴스의 프로퍼티는 변수 프로퍼티로 선언됐더라도 수정할 수 없어요.
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even though firstValue is a variable property
rangeOfFourItems가 let 키워드로 상수로 선언됐기 때문에, firstValue가 변수 프로퍼티더라도 이 값을 바꿀 수는 없어요.
이 동작은 구조체가 값 타입(value type) 이기 때문에 생기는 일이에요. 값 타입의 인스턴스가 상수로 표시되면, 그 안의 모든 프로퍼티도 상수가 돼요.
클래스는 참조 타입(reference type) 이라서 상황이 달라요. 참조 타입의 인스턴스를 상수에 할당해도, 그 인스턴스의 변수 프로퍼티는 여전히 바꿀 수 있어요.
지연 저장 프로퍼티 (Lazy Stored Properties)
지연 저장 프로퍼티(lazy stored property) 는 처음 사용되기 전까지는 초기값을 계산하지 않는 프로퍼티예요.
선언 앞에 lazy 수식어를 붙이면 지연 저장 프로퍼티가 돼요.
Note: 지연 프로퍼티는 반드시
var키워드로 변수로 선언해야 해요. 그 이유는 초기값이 인스턴스 초기화가 끝난 뒤에야 얻어질 수 있기 때문이에요. 상수 프로퍼티는 초기화가 끝나기 전에 반드시 값을 가져야 하므로, 지연으로 선언할 수 없어요.
지연 프로퍼티는 초기값이 인스턴스 초기화가 끝난 뒤에야 알 수 있는 외부 요인에 의존할 때 유용해요. 또 초기값을 구하는 데 복잡하거나 비용이 많이 드는 계산이 필요해서, 꼭 필요할 때까지 수행하지 않으려는 경우에도 유용하죠.
아래 예시는 복잡한 클래스의 불필요한 초기화를 피하기 위해 지연 저장 프로퍼티를 사용해요.
이 예시는 DataImporter와 DataManager라는 두 개의 클래스를 정의하는데, 둘 다 전체 모습은 생략돼 있어요.
class DataImporter {
/*
DataImporter is a class to import data from an external file.
The class is assumed to take a nontrivial amount of time to initialize.
*/
var filename = "data.txt"
// the DataImporter class would provide data importing functionality here
}
class DataManager {
lazy var importer = DataImporter()
var data: [String] = []
// the DataManager class would provide data management functionality here
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// the DataImporter instance for the importer property hasn't yet been created
DataManager 클래스에는 data라는 저장 프로퍼티가 있는데, 비어 있는 새 String 배열로 초기화돼요.
나머지 기능은 생략됐지만, 이 DataManager 클래스의 목적은 이 String 데이터 배열을 관리하고 접근을 제공하는 거예요.
DataManager의 기능 중 하나는 파일에서 데이터를 가져오는 것이에요.
이 기능은 DataImporter 클래스가 제공하는데, 이 클래스는 초기화에 적지 않은 시간이 걸린다고 가정해요.
DataImporter 인스턴스가 초기화될 때 파일을 열고 내용을 메모리로 읽어 들여야 하기 때문일 수 있죠.
DataManager 인스턴스는 파일에서 데이터를 가져오지 않고도 데이터를 관리할 수 있기 때문에, DataManager가 만들어질 때 DataImporter 인스턴스를 만들지 않아요.
대신, DataImporter 인스턴스는 처음 사용될 때 만들어지는 편이 더 합리적이에요.
lazy 수식어로 표시됐기 때문에, importer 프로퍼티의 DataImporter 인스턴스는 importer 프로퍼티가 처음 접근될 때 — 예를 들어 filename 프로퍼티를 조회할 때 — 비로소 생성돼요.
print(manager.importer.filename)
// the DataImporter instance for the importer property has now been created
// Prints "data.txt".
Note:
lazy수식어가 붙은 프로퍼티를 여러 스레드가 동시에 접근하고 아직 초기화되지 않은 상태라면, 이 프로퍼티가 단 한 번만 초기화된다는 보장은 없어요.
저장 프로퍼티와 인스턴스 변수 (Stored Properties and Instance Variables)
Objective-C를 경험해 보셨다면, 클래스 인스턴스에 값과 참조를 저장하는 방법이 두 가지 있다는 걸 알고 계실 거예요. 프로퍼티 외에도, 프로퍼티에 저장된 값의 백킹 스토어(backing store)로 인스턴스 변수(instance variable)를 쓸 수 있었죠.
스위프트는 이 개념들을 하나의 프로퍼티 선언으로 통합해요. 스위프트 프로퍼티에는 대응하는 인스턴스 변수가 없고, 프로퍼티의 백킹 스토어에 직접 접근하지도 않아요. 이 방식은 서로 다른 맥락에서 값에 접근하는 방식에 대한 혼란을 피하고, 프로퍼티 선언을 하나의 명확한 문장으로 단순화해 줘요. 이름, 타입, 메모리 관리 특성을 포함한 프로퍼티의 모든 정보가 타입 정의의 한 곳에 정의되죠.
계산 프로퍼티 (Computed Properties)
저장 프로퍼티에 더해, 클래스·구조체·열거형은 계산 프로퍼티(computed property) 를 정의할 수 있어요. 계산 프로퍼티는 실제로 값을 저장하지는 않고, 대신 getter와 선택적인 setter를 제공해서 다른 프로퍼티와 값을 간접적으로 가져오고 설정해요.
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
// initialSquareCenter is at (5.0, 5.0)
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Prints "square.origin is now at (10.0, 10.0)".
이 예시는 기하 도형을 다루는 세 가지 구조체를 정의해요.
Point는 점의 x좌표와 y좌표를 담아요.Size는width와height를 담아요.Rect는 원점(origin)과 크기(size)로 사각형을 정의해요.
Rect 구조체는 또한 center라는 계산 프로퍼티도 제공해요.
Rect의 현재 중심 위치는 항상 origin과 size로부터 구할 수 있기 때문에, 중심점을 명시적인 Point 값으로 저장할 필요가 없어요.
대신 Rect는 center라는 계산 변수에 사용자 지정 getter와 setter를 정의해서, 사각형의 center를 마치 실제 저장 프로퍼티인 것처럼 다룰 수 있게 해 줘요.
위 예시는 square라는 새 Rect 변수를 만들어요.
square 변수는 원점 (0, 0)과 너비·높이 10으로 초기화돼요. 이 사각형은 아래 그림의 연한 초록색 사각형으로 표현돼요.
그다음 square 변수의 center 프로퍼티를 점 표기법(dot syntax, square.center)으로 접근하면 center의 getter가 호출돼서 현재 프로퍼티 값을 가져와요.
getter는 기존 값을 돌려주는 대신, 사각형의 중심을 나타내는 새 Point를 실제로 계산해서 돌려줘요. 위에서 본 것처럼 getter는 중심점 (5, 5)를 올바르게 돌려줘요.
그다음 center 프로퍼티를 새 값 (15, 15)로 설정하면 사각형이 위쪽 오른쪽으로 이동해요. 아래 그림의 진한 초록색 사각형이 새 위치예요.
center 프로퍼티를 설정하면 center의 setter가 호출되어 저장된 origin 프로퍼티의 x와 y 값을 수정하고 사각형을 새 위치로 옮겨요.
셋터 축약 선언 (Shorthand Setter Declaration)
계산 프로퍼티의 setter가 설정할 새 값에 이름을 정해 주지 않으면, 기본 이름인 newValue가 사용돼요.
이 축약 표기를 활용한 Rect 구조체의 다른 버전을 볼게요.
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
겟터 축약 선언 (Shorthand Getter Declaration)
getter의 본문 전체가 단일 표현식이라면, getter는 그 표현식을 암시적으로 반환해요.
이 축약 표기와 setter 축약 표기를 함께 활용한 Rect 구조체의 또 다른 버전을 볼게요.
struct CompactRect {
var origin = Point()
var size = Size()
var center: Point {
get {
Point(x: origin.x + (size.width / 2),
y: origin.y + (size.height / 2))
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
getter에서 return을 생략하는 것은 함수에서 return을 생략하는 것과 같은 규칙을 따라요. 이 내용은 함수(Functions) 챕터에서 다뤄요.
읽기 전용 계산 프로퍼티 (Read-Only Computed Properties)
getter는 있지만 setter는 없는 계산 프로퍼티를 읽기 전용 계산 프로퍼티(read-only computed property) 라고 해요. 읽기 전용 계산 프로퍼티는 항상 값을 반환하며 점 표기법으로 접근할 수 있지만, 다른 값으로 설정할 수는 없어요.
Note: 계산 프로퍼티(읽기 전용 계산 프로퍼티 포함)는 값이 고정되어 있지 않기 때문에 반드시
var키워드로 변수 프로퍼티로 선언해야 해요.let키워드는 상수 프로퍼티에만 사용해요 — 인스턴스 초기화의 일부로 값이 설정되면 이후에는 바꿀 수 없다는 뜻이죠.
읽기 전용 계산 프로퍼티는 get 키워드와 그 중괄호를 제거해서 선언을 더 간단하게 만들 수 있어요.
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// Prints "the volume of fourByFiveByTwo is 40.0".
이 예시는 width, height, depth 프로퍼티를 가진 3차원 직사각형 상자를 나타내는 Cuboid라는 새 구조체를 정의해요.
이 구조체는 또한 volume이라는 읽기 전용 계산 프로퍼티가 있는데, 큐보이드의 현재 부피를 계산해서 돌려줘요.
volume을 설정 가능하게 만드는 것은 말이 안 돼요. 왜냐하면 특정 volume 값에 width, height, depth 중 어떤 값을 써야 할지 모호하기 때문이죠.
그럼에도 Cuboid가 읽기 전용 계산 프로퍼티를 제공해서 외부 사용자가 현재 계산된 부피를 알아낼 수 있게 하는 건 유용해요.
프로퍼티 옵저버 (Property Observers)
프로퍼티 옵저버는 프로퍼티 값의 변화를 관찰하고 그에 반응해요. 프로퍼티 옵저버는 프로퍼티 값이 설정될 때마다 호출되는데, 새 값이 현재 값과 같아도 호출돼요.
프로퍼티 옵저버는 다음 위치에서 추가할 수 있어요.
- 직접 정의하는 저장 프로퍼티
- 상속받은 저장 프로퍼티
- 상속받은 계산 프로퍼티
상속받은 프로퍼티의 경우, 서브클래스에서 그 프로퍼티를 오버라이드해서 옵저버를 추가해요. 직접 정의하는 계산 프로퍼티의 경우에는 옵저버를 만들려고 하지 말고, 프로퍼티의 setter를 사용해서 값 변화를 관찰하고 반응해요. 프로퍼티 오버라이드에 대한 내용은 상속(Inheritance) 챕터에서 다뤄요.
여러분은 이 옵저버 중 하나 또는 둘 다를 프로퍼티에 정의할 수 있어요.
willSet은 값이 저장되기 직전에 호출돼요.didSet은 새 값이 저장된 직후에 호출돼요.
willSet 옵저버를 구현하면 새 프로퍼티 값이 상수 파라미터로 전달돼요.
willSet 구현의 일부로 이 파라미터에 이름을 지정할 수 있어요. 파라미터 이름과 괄호를 구현 안에 적지 않으면, 파라미터는 기본 이름인 newValue로 제공돼요.
마찬가지로 didSet 옵저버를 구현하면 이전 프로퍼티 값을 담은 상수 파라미터가 전달돼요.
파라미터에 이름을 붙이거나 기본 이름인 oldValue를 사용할 수 있어요.
자신의 didSet 옵저버 안에서 프로퍼티에 값을 할당하면, 할당한 새 값이 방금 설정된 값을 대체해요.
Note: 슈퍼클래스 프로퍼티의
willSet과didSet옵저버는, 서브클래스 이니셜라이저에서 프로퍼티가 설정될 때 — 슈퍼클래스 이니셜라이저가 호출된 뒤에 — 호출돼요. 클래스가 이니셜라이저 본문에서 자신의 프로퍼티를 설정할 때는 호출되지 않아요.이니셜라이저 위임에 대한 자세한 내용은 초기화(Initialization) 챕터를 참고하세요.
willSet과 didSet이 실제로 어떻게 동작하는지 예시를 볼게요.
아래 예시는 사람이 걸으면서 누적한 총 걸음 수를 추적하는 StepCounter라는 새 클래스를 정의해요.
만보기(pedometer)나 다른 걸음 수 측정 기기의 입력 데이터와 함께 사용해서, 일상 활동 중 운동량을 추적할 수 있겠죠.
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps
StepCounter 클래스는 Int 타입의 totalSteps 프로퍼티를 선언해요.
이것은 willSet과 didSet 옵저버를 가진 저장 프로퍼티예요.
totalSteps의 willSet과 didSet 옵저버는 프로퍼티에 새 값이 할당될 때마다 호출돼요.
새 값이 현재 값과 같아도 호출된다는 점을 기억하세요.
이 예시의 willSet 옵저버는 곧 설정될 새 값에 newTotalSteps라는 사용자 지정 파라미터 이름을 사용해요.
이 예시에서는 곧 설정될 값을 단순히 출력해요.
didSet 옵저버는 totalSteps 값이 갱신된 뒤에 호출돼요.
totalSteps의 새 값을 이전 값과 비교하고, 총 걸음 수가 늘었으면 얼마나 많은 새 걸음이 추가됐는지 나타내는 메시지를 출력해요.
didSet 옵저버는 이전 값에 사용자 지정 파라미터 이름을 제공하지 않아서 기본 이름인 oldValue를 사용해요.
Note: 옵저버가 있는 프로퍼티를 함수에 in-out 파라미터로 전달하면,
willSet과didSet옵저버는 항상 호출돼요. 그 이유는 in-out 파라미터의 copy-in copy-out 메모리 모델 때문이에요 — 함수가 끝날 때 값이 항상 프로퍼티에 다시 쓰여지기 때문이죠. in-out 파라미터의 동작에 대한 자세한 설명은 선언(Declarations) 챕터를 참고하세요.
프로퍼티 래퍼 (Property Wrappers)
프로퍼티 래퍼는 프로퍼티를 어떻게 저장하는지 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 한 겹의 분리를 더해 줘요. 예를 들어, 스레드 안전성 검사를 제공하거나 기반 데이터를 데이터베이스에 저장하는 프로퍼티가 여러 개 있다면, 그 코드를 매 프로퍼티마다 작성해야 해요. 프로퍼티 래퍼를 사용하면 관리 코드를 래퍼를 정의할 때 한 번만 작성하고, 그 관리 코드를 여러 프로퍼티에 적용해서 재사용할 수 있어요.
프로퍼티 래퍼를 정의하려면 wrappedValue 프로퍼티를 정의하는 구조체, 열거형, 또는 클래스를 만들면 돼요.
아래 코드에서 TwelveOrLess 구조체는 자신이 감싸는(wrap) 값이 항상 12 이하의 숫자를 담도록 보장해요.
더 큰 숫자를 저장하라고 요청하면 대신 12를 저장해요.
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
setter는 새 값이 12 이하가 되도록 보장하고, getter는 저장된 값을 반환해요.
Note: 위 예시의
number선언은 변수를private로 표시해요. 이렇게 하면number가TwelveOrLess구현 안에서만 사용되도록 보장되죠. 다른 곳에 작성된 코드는wrappedValue의 getter와 setter를 통해 값에 접근하고,number를 직접 사용할 수 없어요.private에 대한 자세한 내용은 접근 제어(Access Control) 챕터를 참고하세요.
래퍼를 프로퍼티에 적용하려면 프로퍼티 앞에 속성(attribute)으로 래퍼의 이름을 쓰면 돼요.
다음은 TwelveOrLess 프로퍼티 래퍼를 사용해 사각형의 크기가 항상 12 이하가 되도록 보장하는 구조체예요.
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}
var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0".
rectangle.height = 10
print(rectangle.height)
// Prints "10".
rectangle.height = 24
print(rectangle.height)
// Prints "12".
height와 width 프로퍼티의 초기값은 TwelveOrLess의 정의에서 오는데, 거기서 TwelveOrLess.number를 0으로 설정해요.
TwelveOrLess의 setter는 10을 유효한 값으로 취급하므로 rectangle.height에 10을 저장하는 것은 적힌 대로 진행돼요.
하지만 24는 TwelveOrLess가 허용하는 것보다 크므로, 24를 저장하려 하면 rectangle.height가 대신 허용된 최대값인 12로 설정돼요.
프로퍼티에 래퍼를 적용하면, 컴파일러가 래퍼를 위한 저장 공간을 제공하는 코드와 래퍼를 통해 프로퍼티에 접근하는 코드를 합성해요.
(프로퍼티 래퍼가 감싼 값을 저장하는 책임을 지므로, 그 부분은 합성되는 코드가 없어요.)
특별한 속성 문법을 활용하지 않고도 프로퍼티 래퍼의 동작을 사용하는 코드를 직접 작성할 수도 있어요.
예를 들어, 앞선 코드 목록의 SmallRectangle 버전처럼, 속성으로 @TwelveOrLess를 쓰는 대신 프로퍼티를 TwelveOrLess 구조체로 명시적으로 감싸는 버전이 있어요.
struct SmallRectangle {
private var _height = TwelveOrLess()
private var _width = TwelveOrLess()
var height: Int {
get { return _height.wrappedValue }
set { _height.wrappedValue = newValue }
}
var width: Int {
get { return _width.wrappedValue }
set { _width.wrappedValue = newValue }
}
}
_height와 _width 프로퍼티는 프로퍼티 래퍼(TwelveOrLess)의 인스턴스를 저장해요.
height와 width의 getter와 setter는 wrappedValue 프로퍼티에 대한 접근을 감싸요.
래핑된 프로퍼티의 초기값 설정 (Setting Initial Values for Wrapped Properties)
위 예시의 코드는 TwelveOrLess 정의에서 number에 초기값을 줌으로써 래핑된 프로퍼티의 초기값을 설정해요.
이 프로퍼티 래퍼를 사용하는 코드는 TwelveOrLess로 래핑된 프로퍼티에 다른 초기값을 지정할 수 없어요.
예를 들어, SmallRectangle의 정의는 height나 width에 초기값을 줄 수 없죠.
초기값 설정이나 다른 사용자 지정을 지원하려면 프로퍼티 래퍼가 이니셜라이저를 추가해야 해요.
다음은 래핑된 값과 최대값을 설정하는 이니셜라이저를 정의하는 TwelveOrLess의 확장 버전인 SmallNumber예요.
@propertyWrapper
struct SmallNumber {
private var maximum: Int
private var number: Int
var wrappedValue: Int {
get { return number }
set { number = min(newValue, maximum) }
}
init() {
maximum = 12
number = 0
}
init(wrappedValue: Int) {
maximum = 12
number = min(wrappedValue, maximum)
}
init(wrappedValue: Int, maximum: Int) {
self.maximum = maximum
number = min(wrappedValue, maximum)
}
}
SmallNumber의 정의는 init(), init(wrappedValue:), init(wrappedValue:maximum:) 세 개의 이니셜라이저를 포함하는데, 아래 예시들이 이 이니셜라이저를 사용해서 래핑된 값과 최대값을 설정해요.
초기화와 이니셜라이저 문법에 대한 자세한 내용은 초기화(Initialization) 챕터를 참고하세요.
프로퍼티에 래퍼를 적용하고 초기값을 지정하지 않으면, 스위프트는 init() 이니셜라이저를 사용해 래퍼를 설정해요. 예를 들어:
struct ZeroRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0".
height와 width를 감싸는 SmallNumber 인스턴스는 SmallNumber()를 호출해서 생성돼요.
그 이니셜라이저 안의 코드는 기본값인 0과 12를 사용해서 초기 래핑 값과 초기 최대값을 설정해요.
프로퍼티 래퍼가 여전히 모든 초기값을 제공하죠 — 앞서 SmallRectangle에서 TwelveOrLess를 쓴 예시와 같아요.
그 예시와 달리 SmallNumber는 프로퍼티를 선언하는 일부로 그 초기값을 작성하는 것도 지원해요.
프로퍼티에 초기값을 지정하면, 스위프트는 init(wrappedValue:) 이니셜라이저를 사용해 래퍼를 설정해요. 예를 들어:
struct UnitRectangle {
@SmallNumber var height: Int = 1
@SmallNumber var width: Int = 1
}
var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1".
래퍼가 있는 프로퍼티에 = 1이라고 쓰면, 이것은 init(wrappedValue:) 이니셜라이저에 대한 호출로 변환돼요.
height와 width를 감싸는 SmallNumber 인스턴스는 SmallNumber(wrappedValue: 1)을 호출해서 생성돼요.
이 이니셜라이저는 여기에 지정된 래핑 값을 사용하고, 기본 최대값 12를 사용해요.
사용자 지정 속성 뒤 괄호 안에 인자를 쓰면, 스위프트는 그 인자를 받는 이니셜라이저를 사용해 래퍼를 설정해요.
예를 들어 초기값과 최대값을 제공하면, 스위프트는 init(wrappedValue:maximum:) 이니셜라이저를 사용해요.
struct NarrowRectangle {
@SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
@SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}
var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// Prints "2 3".
narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4".
height를 감싸는 SmallNumber 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 5)를 호출해서 생성되고, width를 감싸는 인스턴스는 SmallNumber(wrappedValue: 3, maximum: 4)를 호출해서 생성돼요.
프로퍼티 래퍼에 인자를 포함함으로써, 래퍼 안에서 초기 상태를 설정하거나 래퍼가 생성될 때 다른 옵션을 전달할 수 있어요. 이 문법이 프로퍼티 래퍼를 사용하는 가장 일반적인 방법이에요. 속성에 필요한 인자를 얼마든지 제공할 수 있고, 그 인자들은 이니셜라이저에 전달돼요.
프로퍼티 래퍼 인자를 포함할 때, 할당(assignment)을 사용해 초기값을 지정할 수도 있어요.
스위프트는 그 할당을 wrappedValue 인자처럼 취급하고, 포함된 인자를 받는 이니셜라이저를 사용해요. 예를 들어:
struct MixedRectangle {
@SmallNumber var height: Int = 1
@SmallNumber(maximum: 9) var width: Int = 2
}
var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// Prints "1".
mixedRectangle.height = 20
print(mixedRectangle.height)
// Prints "12".
height를 감싸는 SmallNumber 인스턴스는 SmallNumber(wrappedValue: 1)을 호출해서 생성되는데, 이것은 기본 최대값 12를 사용해요.
width를 감싸는 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 9)를 호출해서 생성돼요.
프로퍼티 래퍼에서 값 투영하기 (Projecting a Value From a Property Wrapper)
래핑된 값에 더해, 프로퍼티 래퍼는 투영된 값(projected value) 을 정의해서 추가 기능을 노출할 수 있어요.
예를 들어 데이터베이스 접근을 관리하는 프로퍼티 래퍼는 투영된 값에 flushDatabaseConnection() 메서드를 노출할 수 있죠.
투영된 값의 이름은 래핑된 값과 같지만, 달러 기호($)로 시작해요.
코드에서 $로 시작하는 프로퍼티를 정의할 수 없기 때문에, 투영된 값은 여러분이 정의한 프로퍼티와 절대 충돌하지 않아요.
위 SmallNumber 예시에서, 너무 큰 숫자로 프로퍼티를 설정하려고 하면 프로퍼티 래퍼가 저장 전에 숫자를 조정해요.
아래 코드는 SmallNumber 구조체에 projectedValue 프로퍼티를 추가해서, 프로퍼티 래퍼가 새 값을 저장하기 전에 조정했는지 여부를 추적해요.
@propertyWrapper
struct SmallNumber {
private var number: Int
private(set) var projectedValue: Bool
var wrappedValue: Int {
get { return number }
set {
if newValue > 12 {
number = 12
projectedValue = true
} else {
number = newValue
projectedValue = false
}
}
}
init() {
self.number = 0
self.projectedValue = false
}
}
struct SomeStructure {
@SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()
someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false".
someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true".
someStructure.$someNumber라고 쓰면 래퍼의 투영된 값에 접근해요.
4 같은 작은 숫자를 저장한 뒤에는 someStructure.$someNumber의 값이 false예요.
하지만 55 같은 너무 큰 숫자를 저장하려고 하면 투영된 값은 true가 돼요.
프로퍼티 래퍼는 투영된 값으로 어떤 타입의 값이든 반환할 수 있어요.
이 예시에서 프로퍼티 래퍼는 숫자가 조정됐는지 여부라는 한 가지 정보만 노출하므로, 그 Boolean 값을 투영된 값으로 노출해요.
더 많은 정보를 노출해야 하는 래퍼는 어떤 다른 타입의 인스턴스를 반환하거나, self를 반환해서 래퍼 자신의 인스턴스를 투영된 값으로 노출할 수 있어요.
타입의 일부인 코드 — 예를 들어 프로퍼티 getter나 인스턴스 메서드 — 에서 투영된 값에 접근할 때는 다른 프로퍼티에 접근하는 것처럼 프로퍼티 이름 앞의 self.를 생략할 수 있어요.
다음 예시의 코드는 height와 width를 감싼 래퍼의 투영된 값을 $height와 $width로 참조해요.
enum Size {
case small, large
}
struct SizedRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
mutating func resize(to size: Size) -> Bool {
switch size {
case .small:
height = 10
width = 20
case .large:
height = 100
width = 100
}
return $height || $width
}
}
프로퍼티 래퍼 문법은 getter와 setter가 있는 프로퍼티를 위한 문법적 설탕(syntactic sugar)일 뿐이므로, height와 width에 접근하는 것은 다른 프로퍼티에 접근하는 것과 동일하게 동작해요.
예를 들어, resize(to:)의 코드는 프로퍼티 래퍼를 사용해서 height와 width에 접근해요.
resize(to: .large)를 호출하면 .large의 switch case가 사각형의 height와 width를 100으로 설정해요.
래퍼는 이 프로퍼티들의 값이 12보다 커지지 않게 막고, 값이 조정됐다는 사실을 기록하기 위해 투영된 값을 true로 설정해요.
resize(to:)의 끝에서 return 문은 $height와 $width를 확인해서 프로퍼티 래퍼가 height나 width 중 하나를 조정했는지 판단해요.
전역 변수와 지역 변수 (Global and Local Variables)
위에서 설명한 프로퍼티 계산과 관찰 기능은 전역 변수(global variable) 와 지역 변수(local variable) 에도 동일하게 적용돼요. 전역 변수는 어떤 함수, 메서드, 클로저, 타입 문맥 밖에서 정의된 변수예요. 지역 변수는 함수, 메서드, 클로저 문맥 안에서 정의된 변수죠.
이전 챕터들에서 만나온 전역 변수와 지역 변수는 모두 저장 변수(stored variable) 였어요. 저장 변수는 저장 프로퍼티처럼 특정 타입의 값을 위한 저장 공간을 제공하고, 그 값을 설정하고 가져올 수 있게 해 줘요.
하지만 전역 또는 지역 스코프에서 계산 변수(computed variable) 를 정의하고 저장 변수에 옵저버를 정의할 수도 있어요. 계산 변수는 값을 저장하는 대신 계산해요. 작성 방식은 계산 프로퍼티와 같아요.
Note: 전역 상수와 변수는 항상 지연되어(lazily) 계산돼요. 지연 저장 프로퍼티와 비슷한 방식이죠. 지연 저장 프로퍼티와 달리, 전역 상수와 변수는
lazy수식어로 표시할 필요가 없어요.지역 상수와 변수는 절대 지연 계산되지 않아요.
지역 저장 변수에는 프로퍼티 래퍼를 적용할 수 있지만, 전역 변수나 계산 변수에는 적용할 수 없어요.
예를 들어 아래 코드에서 myNumber는 SmallNumber를 프로퍼티 래퍼로 사용해요.
func someFunction() {
@SmallNumber var myNumber: Int = 0
myNumber = 10
// now myNumber is 10
myNumber = 24
// now myNumber is 12
}
프로퍼티에 SmallNumber를 적용했을 때처럼, myNumber의 값을 10으로 설정하는 것은 유효해요.
프로퍼티 래퍼가 12보다 높은 값을 허용하지 않기 때문에, 24 대신 myNumber를 12로 설정해요.
타입 프로퍼티 (Type Properties)
인스턴스 프로퍼티는 특정 타입의 인스턴스에 속하는 프로퍼티예요. 그 타입의 새 인스턴스를 만들 때마다, 각 인스턴스는 다른 인스턴스와 분리된 자신만의 프로퍼티 값 집합을 가져요.
타입 자체에 속하는 프로퍼티 — 특정 타입의 어떤 인스턴스 하나에 속하지 않는 프로퍼티 — 도 정의할 수 있어요. 이런 프로퍼티는 그 타입의 인스턴스를 몇 개나 만들든 항상 단 하나의 복사본만 존재해요. 이런 종류의 프로퍼티를 타입 프로퍼티(type property) 라고 해요.
타입 프로퍼티는 특정 타입의 모든 인스턴스에 공통인 값을 정의할 때 유용해요. 예를 들어 모든 인스턴스가 사용할 수 있는 상수 프로퍼티(C의 정적 상수(static constant)처럼)나, 그 타입의 모든 인스턴스에 전역인 값을 저장하는 변수 프로퍼티(C의 정적 변수(static variable)처럼) 같은 것들이요.
저장 타입 프로퍼티는 변수 또는 상수일 수 있어요. 계산 타입 프로퍼티는 계산 인스턴스 프로퍼티와 같은 방식으로 항상 변수 프로퍼티로 선언돼요.
Note: 저장 인스턴스 프로퍼티와 달리, 저장 타입 프로퍼티에는 반드시 기본값을 줘야 해요. 타입 자체에는 초기화 시점에 저장 타입 프로퍼티에 값을 할당할 수 있는 이니셜라이저가 없기 때문이에요.
저장 타입 프로퍼티는 처음 접근할 때 지연 초기화돼요. 여러 스레드가 동시에 접근해도 단 한 번만 초기화된다는 보장이 있고,
lazy수식어로 표시할 필요도 없어요.
타입 프로퍼티 문법 (Type Property Syntax)
C와 Objective-C에서는 타입과 연관된 정적 상수·변수를 전역 정적 변수로 정의해요. 하지만 스위프트에서는 타입 프로퍼티를 타입 정의의 일부로, 타입의 바깥 중괄호 안에 작성해요. 각 타입 프로퍼티는 그것이 지원하는 타입에 명시적으로 스코프가 지정되죠.
타입 프로퍼티는 static 키워드로 정의해요.
클래스 타입의 경우 계산 타입 프로퍼티에 class 키워드를 대신 사용해서 서브클래스가 슈퍼클래스의 구현을 오버라이드하도록 할 수 있어요.
아래 예시는 저장 타입 프로퍼티와 계산 타입 프로퍼티의 문법을 보여줘요.
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
}
}
Note: 위의 계산 타입 프로퍼티 예시들은 읽기 전용 계산 타입 프로퍼티지만, 계산 인스턴스 프로퍼티와 같은 문법으로 읽기-쓰기(read-write) 계산 타입 프로퍼티도 정의할 수 있어요.
타입 프로퍼티 조회와 설정 (Querying and Setting Type Properties)
타입 프로퍼티는 인스턴스 프로퍼티처럼 점 표기법으로 조회하고 설정해요. 하지만 타입 프로퍼티는 인스턴스가 아니라 타입 위에서 조회하고 설정해요. 예를 들어:
print(SomeStructure.storedTypeProperty)
// Prints "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// Prints "Another value."
print(SomeEnumeration.computedTypeProperty)
// Prints "6".
print(SomeClass.computedTypeProperty)
// Prints "27".
뒤따르는 예시들은 여러 오디오 채널의 오디오 레벨 미터(audio level meter)를 모델링하는 구조체의 일부로 두 개의 저장 타입 프로퍼티를 사용해요.
각 채널은 0부터 10까지(양 끝 포함)의 정수 오디오 레벨을 가져요.
아래 그림은 이 두 오디오 채널을 어떻게 결합해서 스테레오 오디오 레벨 미터를 모델링하는지 보여줘요.
채널의 오디오 레벨이 0이면 그 채널의 불이 하나도 켜지지 않아요. 오디오 레벨이 10이면 그 채널의 불이 모두 켜지죠.
이 그림에서 왼쪽 채널의 현재 레벨은 9이고, 오른쪽 채널의 현재 레벨은 7이에요.
위에서 설명한 오디오 채널은 AudioChannel 구조체의 인스턴스로 표현돼요.
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// cap the new audio level to the threshold level
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// store this as the new overall maximum input level
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
AudioChannel 구조체는 기능을 지원하기 위해 두 개의 저장 타입 프로퍼티를 정의해요.
첫 번째 thresholdLevel은 오디오 레벨이 취할 수 있는 최대 임계값을 정의해요. 모든 AudioChannel 인스턴스에 대해 상수값 10이에요.
10보다 높은 값을 가진 오디오 신호가 들어오면, 이 임계값으로 캡(제한)돼요(아래에서 설명하는 대로요).
두 번째 타입 프로퍼티는 maxInputLevelForAllChannels라는 변수 저장 프로퍼티예요.
이것은 어떤 AudioChannel 인스턴스든 그동안 받은 최대 입력값을 추적해요. 초기값은 0으로 시작해요.
AudioChannel 구조체는 또한 currentLevel이라는 저장 인스턴스 프로퍼티를 정의하는데, 이것은 0부터 10까지의 눈금에서 채널의 현재 오디오 레벨을 나타내요.
currentLevel 프로퍼티에는 currentLevel이 설정될 때마다 값이 적절한지 확인하는 didSet 프로퍼티 옵저버가 있어요.
이 옵저버는 두 가지 검사를 수행해요.
currentLevel의 새 값이 허용된thresholdLevel보다 크면, 프로퍼티 옵저버가currentLevel을thresholdLevel로 캡해요.- (캡 이후의)
currentLevel의 새 값이 어떤AudioChannel인스턴스든 그동안 받은 어떤 값보다 높으면, 프로퍼티 옵저버가 새currentLevel값을maxInputLevelForAllChannels타입 프로퍼티에 저장해요.
Note: 이 두 검사 중 첫 번째에서
didSet옵저버는currentLevel을 다른 값으로 설정해요. 하지만 이것이 옵저버를 다시 호출하게 만들지는 않아요.
AudioChannel 구조체를 사용해서 스테레오 사운드 시스템의 오디오 레벨을 나타내는 leftChannel과 rightChannel이라는 두 개의 새 오디오 채널을 만들 수 있어요.
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
왼쪽 채널의 currentLevel을 7로 설정하면, maxInputLevelForAllChannels 타입 프로퍼티가 7과 같게 갱신되는 걸 볼 수 있어요.
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// Prints "7".
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "7".
오른쪽 채널의 currentLevel을 11로 설정하려고 하면, 오른쪽 채널의 currentLevel 프로퍼티가 최대값 10으로 캡되고, maxInputLevelForAllChannels 타입 프로퍼티가 10과 같게 갱신되는 걸 볼 수 있어요.
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// Prints "10".
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "10".