(Swift) Property

Swift의 Property

객체 인스턴스는 그 내부에 어떠한 상태를 저장하기 위한 저장공간을 가질 수 있는데, 객체의 외부에서는 기본적으로 이 상태를 저장하는 스토리지를 액세스할 수 없다. 따라서 객체를 디자인하는 시점에 외부에서 어떠한 상태값을 읽거나 쓰도록 하려면 객체 외부에서 액세스할 수 있도록 하는 API가 필요하다. Objective-C에서는 이러한 API를 “프로퍼티”라 정의하고 클래스 내 특정 멤버에 대해서 getter, setter를 정의해서 특정 멤버의 값을 읽거나 쓸 수 있게끔했다.

Swift에서는 인스턴스 멤버와 접근자 메소드라는 분리된 개념자체를 아예 프로퍼티라는 개념으로 통합했다. 프로퍼티는 크게 저장 프로퍼티(stored property)와 계산 프로퍼티(computed property)로 구분된다. 저장 프로퍼티에는 초기화 방법에 따라 느긋한 프로퍼티 개념이 추가될 수 있으며, 이 외에도 인스턴스 스코프가 아닌 타입 스코프에서 액세스하게되는 타입 프로퍼티가 있다.

저장 프로퍼티(Stored Property)

Objective-C에서 저장 프로퍼티를 만들 때는 내부적으로 해당 값이 저장될 스토리지 변수를 인스턴스 내에 선언하고, 두 개의 접근자 (getter, setter)를 만들어서 이 값을 외부에서 액세스하게끔 코딩했었다. 즉, Objective-C에서는 클래스내의 멤버 변수는 철저하게 가려지고 접근자 메소드에 의해서만 액세스가능고, 접근자를 만들어 주지 않으면 객체 외부에서는 해당 값에 아예 액세스할 수 없거나, 읽기만 하고 쓸 수가 없거나 혹은 반대로 쓰기만하고 읽을 수 없게 조정하는 것이 가능했다. 그리고 액세스 자체가 실제로는 메소드 호출이므로 원래 저장되는 값이 아닌 전처리된 값을 읽거나, 값을 쓸 때에도 적절히 변환해서 쓰게하는 것이 가능했다.1

하지만 가장 일반적인 동시에 많이 쓰이던 방법은 저장된 스토리지의 값을 그대로 외부에 전달해주거나, 외부에서 받은 값을 스토리지에 그대로 쓰는 방식이었다. 하지만 이러한 프로퍼티가 많으면 이런 코드를 여러 번 반복해서 써야하는 엄청나게 따분한 작업이 기다리고 있었기 때문에 Objective-C 는 @property , @synthesize 라는 컴파일러 디렉티브를 도입하여 유형별로 흔히 쓰이는 프로퍼티 접근자들을 자동으로 생성해주게끔 처리해주었다. 대략 다음과 같은 식으로 써서 입력해야 할 코드의 양을 줄여주도록 하였다.

@interface SomeClass: NSObject {
@property (strong, nonatomic) NSArray* itemList;
@end

@implementation SomeClass
@synthesize itemList;
...
@end

이 코드는 컴파일러에 의해서 다음과 같은 식으로 변경된다. 2

@interface SomeClass: NSObject
{ NSArray* _itemList; }
-(NSArray*) itemList;
-(void) setItemList:(NSArray*)itemList
...
@end

@implementation SomeClass
-(NSArray*)itemList { return _itemList; }
-(void) setItemList: (NSArray*) itemList {
  if (itemList != _itemList) {
    [_itemList release];
    _itemList = [itemList retain];
  }
}
  1. 내부적으로 프로퍼티 이름앞에 언더스코어가 붙은 backing storage 변수를 선언한다.
  2. 해당 프로퍼티이름과 동일한 getter 메소드를 생성한다.
  3. 해당 프로퍼티이름 앞에 set을 붙이고 프로퍼티 이름의 첫글자를 대문자로 바꾼 형식으로 setter 메소드를 정의한다. setter 메소드의 처리 방식은 프로퍼티 선언시 부여한 속성에 따라 달라질 수 있다.

Swift에서는 이 모든 작업을 뒤로 숨겨버리고, 그냥 멤버 변수/상수를 선언하는 것으로 모든 준비가 완료되게끔 한다.

  1. var 로 선언한 경우 해당 멤버는 가변적이고 이는 멤버 이름 그대로 액세스하여 get/set 동작을 할 수 있다는 의미이다.
  2. let으로 선언한 경우 해당 멤버는 불변이며, 인스턴스의 초기화가 끝나면 내/외부에서 변경할 수 없다.
  3. 해당 멤버가 객체 인스턴스 외부에 노출될 것인지 아닌지 여부는 access control directive에 의해서 결정된다. 기본적으로 모든 멤버는 internal 범위로 정의되며 이는 모듈 내에서 읽고 쓸 수 있다는 의미이며, public, private, fileprivate 등을 지정해 줄 수 있다.
  4. public private(set)과 같이 set의 범위를 더 좁은 범위로 한정하는 것이 가능하며, 이를 이용하면 read-only 타입의 프로퍼티를 정의할 수 있게 한다.

Swift의 문법 상으로 겉보기에서 볼 때 저장 프로퍼티는 객체 인스턴스의 멤버에 그대로 접근하며 예외적으로 접근 범위 디렉티브에 의해서 가려지거나 하는 것과 완전히 동일하므로 이 부분은 상당히 깔끔하게 느껴지고, 접근자가 메소드로 처리됐었다는 사실 조차 필요없어 보인다.

하지만 왜 굳이 이런 설명들을 주저리 주저리 풀어놓았느냐? 왜냐면 이런 백그라운드가 없으면 이 다음에서 설명할 문법들이 상당히 뜬금없고 괴상하게 보이기 때문이다.

느긋한 프로퍼티

저장 프로퍼티는 멤버 그대로를 액세스할 수 있는 것처럼 보인다고 했다. 실제로 저장 프로퍼티를 선언해놓으면 이는 멤버 선언과 동일하며, Swift에서 모든 멤버는 초기화시에 초기값을 반드시 가져야 한다.

하지만 초기화에 비용이 많이 들면서 한편으로는 반드시 액세스될거라는 보장이 없는 프로퍼티에 대해서 초기화시에 해당 멤버값을 같이 초기화하는 것은 객체 자체의 초기화 비용을 증가시키는 부작용을 낳게 된다.

느긋한 프로퍼티3는 저장 프로퍼티의 일종으로 객체 초기화시에 반드시 초기화되지 않아도 되며, 최초로 액세스되는 시점에 초기화되는 프로퍼티이다. 보통 다음과 같은 문법으로 정의한다.

lazy var imageData = timeConsumingOperation()

느긋한 프로퍼티는 실제로는 객체 인스턴스가 초기화된 시점에 값이 정해지지 않았다가, 액세스가 처음 일어나는 부분에 선언문의 우변이 평가되면서 값이 변하게 되므로 항상 var를 통해 선언하게 된다.

느긋한 프로퍼티는 객체의 초기화 비용을 줄이기 위한 목적으로도 사용되지만, 흔히 객체가 완전히 초기화될 때까지는 값을 알 수 없는 조건에 의존할 때도 사용할 수 있다.

특히 클래스의 경우에 느긋한 프로퍼티를 정의하면서 우변에 클로저를 쓰는 경우가 많은데, 클로저 내부에서 self를 참조해야 하는 경우가 있다. 이 클로저는 클래스의 일부가 아니므로, self는 명시적으로 참조해야 하며, 클로저로부터 self로의 참조가 발생한다. 이는 순환 참조에 의한 메모리 누수의 원인이 되므로, 다음과 같은 패턴을 (거의 항상) 사용하게 된다.

lazy var imageData = { [unowned self] in .... }()

프로퍼티의 초기화

위에서도 언급했듯이 저장 프로퍼티의 문법은 표면상 객체 인스턴스 외부에서 객체의 멤버에 그대로 접근하는 것과 동일한 것으로 보인다고 했다. 따라서 느긋한 프로퍼티를 제외한 모든 저장프로퍼티는 객체의 초기화가 끝나기 이전에 초기값을 가져야 한다. 이 조건은 다음과 같다 4

  1. 선언과 동시에 초기값을 대입해버려도 된다. (가장 권장되는 문법)
  2. 만약 let으로 선언하면서 초기값을 대입한 프로퍼티라 하더라도, init()(정확히는 designated initializer)내에서는 초기화가 종료되기 직전까지는 다른 값을 대입할 수 있다.
  3. 클래스의 경우, super.init()을 호출하기 이전에 부모클래스로부터 상속받지 않은 저장 프로퍼티는 초기값을 가진 상태여야 한다.

명확하게 초기값이 정해지는 경우라면 선언과 동시에 초기값을 할당해주는 것이 가장 좋다. 만약 초기화시 요구되는 파라미터와 관련있는 값이라면 init()내에서 초기값을 빼먹지 않고 대입해주어야 한다. (다행히 컴파일러가 이런 건 미리 알려준다) 또한 클래스의 경우에는 부모로부터 상속받은 멤버가 아니라면 super.init()을 호출하기 이전에 자신의 모든 새로운 저장 프로퍼티가 초기값을 갖는 상태여야 한다.

계산 프로퍼티

계산 프로퍼티는 주로 대응되는 스토리지 멤버가 없는 접근자이다. 예를 들어 문자열 타입의 firstName, lastName 이라는 저장 프로퍼티가 각각 있을 때, func getFullName() { return "\(firstName) \(lastName)" } 이같은 메소드를 만들 수 있다. 이 메소드는 실질적으로는 두 개의 멤버값에 의존하여 생성되는 결과를 리턴하기 때문에 fullName 이라는 읽기 전용 프로퍼티로 만들 수 있는 셈이다.

Objective-C에서는 접근자도 메소드이고, 이 언어는 메소드 호출 시 괄호를 필요로하지 않으므로 getFullName 이라는 메소드보다는 fullName 이라는 접근자로 호출하는 것이 표기상 아무런 차이가 없었고, 따라서 계산 프로퍼티라는 개념으로 발전했다. Swift의 계산 프로퍼티는 이 개념을 이어받은 것이라 보면 된다. 또한 더 발전하여 setter를 지원하기도 한다. 아래의 예는 위에서 언급한 내용으로 fullName이라는 계산 프로퍼티를 추가한 것이다.5

struct Person {
  let firstName: String
  let lastName: String
  var fullName: String { return "\(firstName) \(lastName)" }
...
}

계산 프로퍼티는 기본적으로 getter, setter 메소드를 하나로 합쳐놓은 문법이며, Objective-C에서 getter/setter를 각각 정의해놓던 것을 하나의 블럭으로 합쳐놓은 것이다. 조금 더 복잡한 예제를 통해 계산 프로퍼티에서 setter는 어떻게 작성하고 동작하는지 살펴보자.

아래의 예제에서는 CGPoint, CGSize, CGRect를 흉내낸 세 타입을 정의했다. 사각형 영역은 시작점의 x, y 좌표와 가로, 세로 크기로 정의될 수 있으므로 이를 표현하는 타입의 멤버는 시작점(Point)과 크기(Size)가 되는데, 시작점과 크기로부터 우리는 중심점의 좌표를 항상 계산해낼 수 있다. 그리고 getCenter()라는 메소드로 이를 표기하는 것보다, center 라는 프로퍼티의 형태로 접근할 수 있도록 했다.

흥미로운 부분은 어떤 사각형의 center 값을 변경할 수 있다면, center의 위치로부터 크기를 이용해서 시작점의 위치를 업데이트하는 것이 논리적으로 가능하다는 것이다. 그래서 아래 예제는 계산 프로퍼티 center에 대해 getter/setter를 모두 정의했다. 그리고 객체 외부에서 center는 프로퍼티이기 때문에 aRect.center = otherPoint와 같은 식으로 바로 대입해서 업데이트하는 것이 가능하다.

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
square.center = Point(x:15.0, y:15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// (10.0, 10.0)

계산 프로퍼티는 하나의 이름이므로 getter, setter를 동시에 정의하려면 { ... } 블럭 내에서 { get {}, set {} } 이렇게 두 개 블럭을 나눠서 정의한다. setter의 기본 인자는 newValue 인데, 필요시에는 set(newValueName) 과 같은 식으로 새 값의 이름을 정해줄 수도 있다.

프로퍼티 옵저버6

Objective-C의 프로퍼티는 실질적으로 항상 접근자에 의해서 액세스되며, 이는 메소드를 통해서 값을 주고 받았기 때문에 Swift의 개념으로는 모든 프로퍼티가 계산 프로퍼티였다.

계산 프로퍼티의 장점 중 하나는 어떤 프로퍼티가 외부에서 전해진 값으로 변경될 때, 내부적으로는 스토리지의 값을 바꾸는 메소드를 호출하는 것이기 때문에, 이 업데이트 작업 전/후로 임의의 다른 작업을 할 수 있다는 것이다.7 예를 들어 특정한 프로퍼티를 변경할 때, 이와 관련있는 다른 프로퍼티 값을 함께 변경해주는 것이 가능했다.

물론 Swift에서도 계산 프로퍼티를 쓸 수 있지만, 저장 프로퍼티에 대해서 이러한 동작을 추가해주려면 결국 Objective-C에서 처럼 backing storage를 따로 정의하고, setter 쪽에서 이 처리를 별도로 해야하는데, 이것은 무척이나 번거로운 일이 될 것이다.

Swift는 이러한 불편함을 덜기 위해서 프로퍼티 옵저버 문법을 추가하고 있다. 프로퍼티 옵저버 문법 역시, 프로퍼티는 실질적으로는 멤버에 대한 접근을 위한 메소드이다라는 점을 상기하자.

  1. 저장 프로퍼티에 대해서만 옵저버를 정의할 수 있다.
  2. 내부적으로는 진짜 setter가 호출되기 전과 후에 호출된다.
  3. 단, 느긋한 저장 프로퍼티에 대해서는 옵저버를 정의할 수 없다.
  4. 프로퍼티 옵저버는 변경전, 변경후 동작을 각각 정의할 수 있다.

이 때의 문법은 다음과 같다.

var propertyName: Type [ = initialValue] {
  willSet { ... }
  didSet { ... }
}

willSetdidSet 둘 중 하나만 정의해도 되고, 둘 다 정의할 수도 있다. 계산 프로퍼티와 달리 하나만 정의하더라도 블럭 안에 정의해야 한다. willSet 내부에서는 아직 프로퍼티 값이 업데이트 되기 이전이므로, 프로퍼티 이름으로 액세스하게 되는 값은 변경 이전의 값이며, 반대로 didSet 내부에서는 프로퍼티 값이 변경된 후이므로 프로퍼티 이름은 변경된 값을 액세스하게 된다. 그리고 이 때 상대적으로 변경전/변경후의 값은 각각 newValue, oldValue로 액세스된다.

참고로 인스턴스 초기화시에 init() 내에서 이미 정의된 초기값을 다른 값으로 대체하는 경우가 있는데, 이 때에는 프로퍼티 옵저버가 호출되지 않으니 참고할 것.

클래스 상속과 프로퍼티

프로퍼티는 멤버인 동시에 메소드이거나, 멤버처럼 보이는 메소드라고 했다. struct 타입의 경우에는 그다지 고려할 부분이 많지 않은데, 클래스의 경우에는 상속을 고려하면서 조금 골치아픈 부분이 나올 수 있다. 바로 오버라이딩이다. 프로퍼티는 실제로는 접근자 메소드를 꾸며주는 문법적 장식이므로, 클래스의 경우 부모 클래스의 프로퍼티를 상속받거나 오버라이드하는 것도 가능하다. 마지막으로 프로퍼티 오버라이딩에 대해서 조금 더 살펴보도록 하자.

서브 클래스의 프로퍼티

프로퍼티는 엄밀하게 객체 내부의 값에 액세스하기 위한 API이므로 메소드와 마찬가지로 상속받은 프로퍼티에 대해서는 getter와 setter를 오버라이딩하는 것이 가능하다. 그리고 이 때, 오버라이딩이 가능한 프로퍼티는 저장 프로퍼티, 계산 프로퍼티 여부를 따지지 않는다. 8 (단 저장 프로퍼티의 경우 let으로 선언한 경우에는 서브 클래스에서 부모 클래스의 프로퍼티를 변경할 수 없으므로 오버라이딩이 불가능하다.)

먼저 간단한 클래스를 하나 정의해보자. 저장 프로퍼티와 계산 프로퍼티 하나씩을 가지고 있다.

class Foo {
  var a: Int = 10
  var b: Int {
    get { return a * -2 }
    set { a = -newValue / 2 }
 }
}

let foo = Foo()
print(foo.b) // -20
foo.b = -60
print(foo.a) // 30

간단하다. 그럼 이번에는 위 Foo 를 상속받는 Bar 클래스를 만들어보자. 저장프로퍼티인 a를 오버라이딩했다. 이 때 getter/setter를 별도로 오버라이딩해서 제공하였으므로, Bar에서는 마치 저장 자체는 수퍼클래스에 위임한 계산 프로퍼티처럼 동작한다.

class Bar: Foo {
  override var a: Int {
    get { return super.a + 100 }
    set { super.a = newValue - 100 }
  }
}

let bar = Bar()
print(bar.a) // 110
print(bar.b) // -220
bar.a = 220
print(bar.b) // -440

, getter와 setter는 항상 같이 오버라이딩 해야하기 때문에 setter만 오버라이드 하고 싶을 때에도 getter에 대해 부모 클래스의 프로퍼티값을 리턴하도록 함께 오버라이딩 코드를 작성해야 한다.

위 코드에서 만약 b의 값을 변경했다면, bar.b.setter는 상속받은 동작에 의해서 bar.a.setter를 호출할 것이고, 그 다음은 위에서 정의한 대로, Foo.a.setter를 호출하여 값을 저장하게 된다.

프로퍼티 오버라이딩에서 한가지 주의할 점은 read-only인 프로퍼티를 상속받아서 적절한 setter를 정의하여 read-write 프로퍼티로 만드는 것은 괜찮지만, 반대로 read-write 프로퍼티를 상속받아서 read-only 프로퍼티로 만드는 것은 허용되지 않는 다는 점이다.

프로퍼티 옵저버의 오버라이딩

서브클래스에서는 상속받은 프로퍼티에 대해 오버라이딩하면서 옵저버를 추가할 수 있다. 이 때에는 부모 클래스에서 해당 프로퍼티가 저장 프로퍼티고 구현되었는지, 계산 프로퍼티로 구현되었는지에 상관없이 가능하다. 이 때 다음의 규칙을 주의하자.

  1. 부모 클래스에서 setter가 없는 프로퍼티는 set 자체가 불가능하므로 오버라이딩하여 옵저버를 추가할 수 없다.
  2. set을 오버라이딩하는 경우에는 이 지점에서 변경을 감지하게 되므로 옵저버를 오버라이딩하여 추가할 수 없다. (이 때 여전히 값 업데이트는 부모클래스를 통해서 하게 되므로, 부모클래스의 옵저버가 있다면 이는 동작한다.)
  3. willSet, didSet의 오버라이딩에서는 부모의 옵저버를 호출할 필요가 없다. 값의 변경은 부모 클래스의 접근자를 통해 일어날 것이므로 자식의 willSet -> 부모의 willSet -> 부모의 set -> 부모의 didSet -> 자식의 disSet 순으로 실행된다.

다시 위의 Foo, Bar 예제로 돌아가보자 이번에는 Foo.a에 옵저버를 설치했다.

class Foo {
  var a: Int = 10 {
    willSet { print("[Foo]: to be updated a to \(newValue)") }
    didSet { print("[Foo]: updated a from \(oldValue)") }
  }
  var b: Int {
    get { return a * -2 }
    set { a = -newValue / 2 }
  }
}

let foo = Foo()
print(foo.b) // -20
foo.b = -40 // a -> 20

/* output
-20
[Foo]: to be updated a to 20
[Foo]: updated a from 10
*/

다음은 Bar의 차례이다.

class Bar: Foo {
  override var b: Int {
    willSet { print("[Bar]: to be updated b to \(newValue)") }
    didSet { print("[Bar]: updated b from \(oldValue)") }
  }

  override var a: Int {
    willSet { print("[Bar]: to be updated a to \(newValue)") }
    didSet { print("[Bar]: updated a from \(oldValue)") }
  }

override init() { super.init() }
}

let bar = Bar()

print(bar.a) // 10
print(bar.b) // -20
bar.b = 220
print(bar.b) // 220
print(bar.a) // -110

/* ouput
10
-20
[Bar]: to be updated b to 220
[Bar]: to be updated a to -110
[Foo]: to be updated a to -110
[Foo]: updated a from 10
[Bar]: updated a from 10
[Bar]: updated b from -20
220
-110
*/

Barb를 업데이트하면 작동하는 절차는 다음과 같다.

  1. barbwillSet이 호출된다.
  2. 상속받은 동작에 의해 bara를 업데이트하도록 포워딩된다.
  3. barawillSet이 호출된다.
  4. 실제 스토리지는 Foo에 의해 정의되어 있으므로 super.a = newValue가 호출된다.
  5. FooawillSet이 호출된다.
  6. Fooa값이 변경된다.
  7. FooadidSet이 호출된다. Foo에서의 처리는 완료되었다.
  8. baradidSet이 호출된다.
  9. barbdidSet이 호출된다. 끝.

타입 프로퍼티

타입 프로퍼티는 어떤 타입의 인스턴스에 속하는 멤버가 아닌 타입/클래스 자체의 멤버가 되는 프로퍼티이다. 타입 프로퍼티를 정의하면 해당 타입의 인스턴스가 아주 많이 생성되더라도 오직 하나의 데이터를 참조하게 된다. 따라서 특정 타입의 모든 인스턴스가 전역적으로 가지는 값으로 활용하기에 좋다.

타입 프로퍼티는 인스턴스 프로퍼티와 동일한 방식으로 저장 프로퍼티 및 계산 프로퍼티로 정의할 수 있으며, static var, static let을 통해서 정의한다. (static let으로 정의하면 값을 변경할 수 없는 프로퍼티가 된다.)

struct someStruct {
static let someTypeReadOnlyProperty: Int = 42
}

이렇게 정의한 타입 프로퍼티는 인스턴스를 통해서 액세스하는 것이 아니라 타입 자체를 통해서 액세스해야 한다.

let x = someStruct()
print(x.someTypeReadOnlyProperty) // ERROR!!!
print(someStruct.someTypeReadOnlyProperty) // 42

또한, 타입 프로퍼티는 enum 타입에도 붙일 수 있다.

enum SomeEnum {
  static var storedProperty = "Some Value"
  static var computedProperty: Int { return 1 }
}

클래스의 타입 프로퍼티

클래스의 타입 프로퍼티는 클래스 프로퍼티로도 불리는데, static이 아닌 class를 사용해서 정의할 수 있다. 이렇게 구분하는데는 이유가 있는데 바로 상속과 Objective-C와의 연동 때문이다.

  1. static으로 정의한 타입 프로퍼티는 서브 클래스에서 오버라이딩 할 수 없다.
  2. 클래스에서는 저장 프로퍼티를 정의할 때는 static 키워드만이 허용된다. 클래스에서 static 키워드를 사용하면 저장 프로퍼티 및 계산 프로퍼티 모두를 정의할 수 있다.
  3. 대신에 class를 이용해서 정의한 계산 프로퍼티는 서브 클래스에서 오버라이딩 할 수 있다.

여기서 서브 클래싱과 관련하여 주의해야 할 부분이 있다. 특정 클래스를 서브 클래싱하면 부모 클래스의 저장 프로퍼티를 그대로 상속받게 된다. 이 때 서브 클래스에서 타입 프로퍼티의 값을 변경하면, 부모 클래스의 타입 프로퍼티값이 그대로 변경된다. (해당 프로퍼티의 스토리지는 부모 클래스에게 있으므로)

다시 말해 서브 클래스는 동시에 부모 클래스이기도 하기 때문에 (서브 클래스를 부모 클래스인것처럼 캐스팅하는 것은 언제나 가능하므로) 부모 클래스에서 정의된 타입 프로퍼티를 공유하는 것은 논리적으로 문제는 없다.

만약 서브 클래스에서 부모클래스의 타입 프로퍼티를 구분하여 사용하려는 경우에는 어떻게 할까? 이 경우에는 “부모 클래스와 자식 클래스간의 타입 프로퍼티 값에 특정한 관계”가 있는 경우에 다음과 같이 처리할 수 있을 것이다.

  1. 부모 클래스는 private한 저장 타입 프로퍼티를 가진다.
  2. 부모 클래스의 public한 계산 프로퍼티는 1.의 저장 프로퍼티를 포워딩한다.
  3. 자식 클래스는 2.의 계산 프로퍼티를 오버라이딩한다.

예를 들어 Person 클래스는 averageAge라는 타입 프로퍼티를 갖는데, 그 서브 클래스인 Student가 있다고 하자. 인류 전체의 평규나이와 학생의 평균나이는 분명 다를 것이기 때문에 이는 오버라이딩 가능해야 한다. 그리고 이 때 이 값이 변경 가능하다면 다음과 같이 처리할 수 있을 것이다.

class Person {
  private static var _averageAge: Int = 40
  class var averageAge: Int {
    get { return _averageAge }
    set { _averageAge = newValue }
  }
}

class Student {
  override class var averageAge: Int {
    get { return super.averageAge - 22 }
    set { super.averateAge = newValue + 22}
  }
}

print(Person.p) // 40
print(Student.p) // 18
Student.p = 19
print(Person.p) // 41
print(Student.p) // 19

상속 관계에 있는 두 클래스가 같은 이름의 다른 타입 프로퍼티를 가지는 것은 (인스턴스 프로퍼티와 마찬가지로) 불가능하다. 따라서 “특정한 관계로 엮여있어야 한다”는 조건은 있지만 위와 같은 구현을 통해서 부모클래스와 자식클래스에서의 타입 프로퍼티값이 다르게 표현되도록 하는 것은 가능하다.


  1. Objective-C에서는 메소드 호출을 “메시지를 보낸다”고 표현했다. 그리고 someObject.prop 으로 액세스하는 문법은 사실 [someObject prop] 으로, someObject.prop = 1; 과 같은 대입은 [someObject setProp: 1];의 형식으로 내부적으로 처리됐다. 
  2. Objective-C 2.0이 나오기 이전까지는 항상 ivar 들을 선언해주던 교재들도 많았다. 
  3. 사실 느긋한 저장 프로퍼티라고 불러야 명확할 거 같다. 
  4. The Swift Programming Language (Swift 3.0.1)의 Initialization장 참고 
  5. 이 경우, 읽기 전용 프로퍼티이므로 일반 메소드와 동일한 방식으로 작성된다. 
  6. 키-밸류 옵저빙과는 다르다. 키밸류 옵저빙은 특정한 프로퍼티의 업데이트가 발생하는 것을 외부의 다른 객체가 알아차리도록 통지해주는 매커니즘이다. 
  7. 실제로 ARC 도입 이전의 Objective-C의 프로퍼티들은 setter내에서 새 값의 retain count를 올리거나 새 값을 복사하거나, 기존 값을 release하는 동작을 앞/뒤로 수행해주었다. 
  8. Swift의 타입 구조 상, 프로퍼티의 이름과 타입에 대한 정보만 메타타입에 기록되며, 그것이 저장 프로퍼티인지 계산 프로퍼티인지 정보는 타입 자체가 알지 못한다. (타입입장에서보면 무조건 계산 프로퍼티이다.)