2014년 6월 13일 금요일

Swift - 프로퍼티(Properties)

클래스와 구조체에 공통되는 요소로, 프로퍼티는 OOP에서 멤버변수(Member Variables) 혹은 속성(Attributes)이라고도 불리우는 개념이다. 쉽게 말해 클래스나 구조체 안에 선언되어서 사용하는 '소속된 변수'이다.

참고로 Enum의 경우에도 프로퍼티 개념이 존재하는데 대체로 struct나 class에 한정된 개념이 많다.

이전 클래스 훑어보기 글에서 사용된 BaseDate 예제를 약간 고쳐봤다.
class BaseDate {
    var year = 0
    var month: Int = 0
    var day = Int(0)
    var comment: String? 
    var name: String {
        return "\(self.year)-\(self.month)-\(self.day)"
    }
    
    init() { }
    
    init(y: Int, m: Int, d: Int) {
        self.year = y
        self.month = m
        self.day = d
    }
    
    func print() {
        println(self.name)
    }
}
위 코드에서 굵게 표시한 부분이 프로퍼티 선언부이다. 상수나 변수 선언하는 것과 동일한 문법으로 그저 클래스나 구조체 내부에 정의하면 그것이 프로퍼티다.

이런 기본형 프로퍼티를 스위프트에서는 저장형 프로퍼티(Stored Properties)라고 부른다. (참고로 name 부분도 프로퍼티지만 일부러 굵게 처리하지 않았는데 getter와 setter 항목에서 별도로 설명하기 위함이다)

클래스나 구조체의 메소드(멤버 함수)에서 프로퍼티를 참조 할 때는 self.라는 키워드를 붙여서 프로퍼티와 지역 변수를 구분할 수 있다. 상식 수준의 이야기다. 물론 중복되는 이름을 구분하기 위한 용도가 아니라면 대체로 self 를 붙이지 않아도 관계는 없다.

읽기쓰기(read-write)와 읽기전용(read-only)

프로퍼티를 엑세스 관점에서 보면 크게 두 분류로 나누어진다.
  • 클래스나 구조체 내부나 외부에서 마음껏 읽고 쓸 수 있는 프로퍼티(read-write)
  • 읽기만 가능한 프로퍼티(read-only)
이건 정말 단순하게 구분된다. 상수나 변수 선언하는 것과 동일하게, var로 선언되면 읽기쓰기가 자유로운 프로퍼티가 되고, let으로 선언되면 읽기 전용 프로퍼티가 된다.

다만 var로 선언된 프로퍼티라도 getter 기능만이 제공되면 읽기 전용으로 사용된다. 이 부분은 아래에서 설명한다.

나중에 생성되는 프로퍼티(Lazy Stored Properties)

대충 번역하면 게으른 프로퍼티라고 읽혀질지도 모르겠는데, 게으르다기 보다 필요할때 제 할 일을 하는 프로퍼티라고 보는 편이 좋다.
class SomeClass {
    ...
    lazy var date = BaseDate()
    ...
}

var some = SomeClass()
some.date.print()
위 코드에서 lazy로 선언된 date 프로퍼티는 SomeClass가 인스턴스화 될 때 생성될 것 같지만, 실제로는 가장 마지막의 date를 액세스 하는 순간에 생성된다. 만약 이 프로퍼티의 인스턴스가 메모리를 많이 먹는다면 이런 식으로 필요할 때 생성되도록(lazy) 선언해 두면 메모리 관리 효율이 좋아질 것이다.

조금 더 자세한 글은 아래 링크를 참고하자:
[관련글] Lazy Stored Properties 좀 더 살펴보기​

Getter와 Setter

프로퍼티를 그냥 변수 처럼 사용 할 수도 있지만, 프로퍼티에 데이터를 넣거나 프로퍼티를 다른 곳에서 참조 할 때 특별한 일을 하게 만들 수도 있다.
class DoublingClass {
    var a = 0
    var b = 0
    var data: Int {
        get {
            return a + b
        }
        set(value) {
            self.a = value / 2
            self.b = value / 2
        }
    }
    
    init() {}
}
위 예제에서 data 프로퍼티는 Int 타입이지만 set과 get 이라는 기능을 추가로 정의하고 있다. 여기서 get은 이 data프로퍼티를 참고 할 때 실행되는 코드이고, set은 이 data에 데이터를 넣으려고 할 때 작동하는 코드이다.

실제로 테스트 해 보면 이런 식으로 동작한다.
var d = DoublingClass()
d.a             // 0
d.b             // 0
d.data = 10     // a=5, b=5
d.a             // 5
d.b             // 5
d.a = 6
d.b = 10
d.data          // 16
굵게 표시한 부분이 위에서 getter와 setter를 정의한 data를 다루는 부분이다. .data에 값을 대입하니 자동으로 a와 b 프로퍼티에 해당 값을 2로 나눈 값이 할당된다. 반대로 .data의 값을 읽을 때는 a와 b 프로퍼티의 내용을 더한 값을 돌려준다.

스위프트의 프로퍼티는 이렇게 setter와 getter라는 특징을 부여 할 수 있다.

축약형 setter와 getter

앞의 예제에서 data의 setter를 약간 변형해보자.
    var data: Int {
        get {
            return a + b
        }
        set {
            self.a = newValue / 2
            self.b = newValue / 2
        }
    }
앞서 set(value) 라는 정의가 그냥 set으로 바뀌었다. 그리고 set 코드 내부에서는 newValue 라는 정의되지도 않은 심볼이 쓰이고 있다.

이 코드는 축약형 setter 정의(ShortHand Setter Declaration)를 활용한 것으로, setter의 매개변수가 하나이기 때문에 생략시키고 이를 newValue 라는 이름으로 쓸 수 있도록 미리 스위프트가 준비해 주는 것을 이용한 것이다.

이런 축약형은 getter에도 존재하는데 좀 다른 제한이 있다. read-only로 정의하려는 경우에만 사용이 가능하다. 아래 코드는 data 프로퍼티의 setter를 없애고 read-only getter를 정의하는 코드이다.
    var data: Int {
        return a + b
    }
get이라는 이름이 아예 사라졌다. 그리고 프로퍼티 선언 뒤에 바로 중괄호가 시작되고 getter의 내용이 들어간다.

이렇게 만든 .data 프로퍼티는 이제 read-only 로만 동작하게 된다. 하지만 .data의 참조 결과는 이전과 동일하다.

추신. 읽기 전용으로 getter만 있는 프로퍼티라도 let으로는 선언하지 못 한다. setter나 getter가 필요하다면 무조건 var로 선언해야 한다.

Setter와 Getter에 관한 참고사항

기존 Objective-C의 경우 Setter와 Getter는 저장형 프로퍼티(Stored Properties) 형태이면서 동시에 Setter와 Getter를 구현 할 수 있는 형태였다. 이는 프로퍼티와 멤버 변수를 synthesize 를 이용해 연결해서 구현하는 형태였기 때문이다.

하지만 스위프트에서 Setter나 Getter를 선언하면 연산형 프로퍼티(Computed Properties)로만 정의가 가능하다. 이 말은 프로퍼티에 직접 데이터를 저장하는 것이 불가능하다는 의미이다. 즉 아래와 같은 식의 코드는 에러가 발생한다.
class SomeClass {
    var data: Int {
        get {
            return data          // ERROR
        }
        set {
            data = newValue      // ERROR
        }
    }
}
위 코드의 경우 EXC_BAD_ACCESS 오류가 발생한다. 혹시나 Objective-C를 이용하던 개발자라면 이 차이점을 알아둬야 한다. (사실 당연하게 생각해야 한다. 저런 행위는 무한 recursive call 을 유발시킬 수도 있으니 말이다)

프로퍼티 상속

앞서 설명한 프로퍼티의 Setter와 Getter 덕분에 스위프트에는 프로퍼티 상속이 꽤 중요하다. 참고로 상속은 클래스만의 기능이기 때문에 프로퍼티 상속 또한 클래스의 프로퍼티만 가능하다.

스위프트의 프로퍼티는 setter와 getter 라는 특유의 함수와 비슷한 능력을 가질 수 있다. 그래서 프로퍼티 자체를 상속받아 이 setter와 getter를 오버라이드 하는 것도 가능하다.
class DdablingClass : DoublingClass {
    override var data: Int {
        return (a + b) * 2
    }
}

var dd = DdablingClass()
dd.a = 10
dd.b = 20
dd.data     // 60
더블링클래스를 상속받은 따블링클래스(-_-)를 만들었다. 그리고 data 프로퍼티를 override(상속 및 재구현)해서 getter를 재정의했다. 그리고 물론 결과는 오버라이드된 결과물이 나온다.

메소드 편에서도 이야기 하겠지만, 스위프트는 오버라이드를 하려는 것 앞에 반드시 override 키워드를 붙여야 한다.

프로퍼티의 변경 추적(Property Observers)

Objective-C의 경우 KVC 코딩을 이용해 옵저버를 붙이는 방식으로 프로퍼티의 변화를 추적해서 실시간으로 대응하는게 가능하다. 스위프트는 이런 기능을 비슷하게 이어받았지만 언어의 기능 일부로써 더 편하게 만들어버렸다. 바로 프로퍼티 옵저버 라는 기능이다.
class ObserverbingClass {
    var a: Int = 0 {
        willSet(value) {
            println(".a will change to \(value)")
        }
        didSet {
            println(".a did changed to \(self.a)")
        }
    }
}

var obj = ObserverbingClass()
obj.a = 10
setter나 getter와 비슷하게 이번에는 willSetdidSet 키워드가 등장했다. 이름에서 볼 수 있듯이 차례대로 해당 프로퍼티에 값을 대입하기 전에 호출되는 코드와 값의 대입이 끝나고 난 뒤에 호출되는 코드 두 가지이다.

실행 결과는 예측할 수 있다. 마지막 라인이 실행되면 우선 willSet() 에 정의된 코드가 실행되고 이 후 .a에 10이 할당된다. 그리고 이후 didSet의 코드가 호출된다.

다만 이 옵저버 기능을 이용하면 setter와 getter를 사용 할 수 없게 된다. 뭔가 제약이 심한 것 같은데, 살짝 생각해보면 이건 당연한 이야기다. setter와 getter는 값을 할당하거나 참조하는 코드를 개발자가 마음대로 넣을 수 있게 하는게 목적이다. 따라서 didSet이나 willSet에서 하는 일을 그냥 setter와 getter에서 구현해도 된다.

정적 프로퍼티

정적 프로퍼티라는건 클래스가 인스턴스화 되지 않아도 참조가 가능한 프로퍼티를 의미힌다. 사실 이 부분의 정확한 명칭은 타입 프로퍼티(Type Properties)가 되어야 하지만 일반적으로는 정적 멤버(Static Member)로 불리우기 때문에 그냥 제목을 이렇게 적었다.

C++이나 Objective-C나 기타 많은 언어에서 비슷한데, 구조체(struct)나 열거형(enum)에서 static 키워드를 이용한다.
struct HTTPResponseStruct {
    static var success = 200
    static var notFound = 404
    static var serverError = 500
}

var code1 = HTTPResponseStruct.success

enum HTTPResponseEnum {
    static var success = 200
    static var notFound = 404
    static var serverError = 500
}

var code2 = HTTPResponseEnum.notFound
특징은 인스턴로 만들지 않아도 멤버를 바로 엑세스 할 수 있다는 점이다. 그래서 특정한 구조체 등에 소속되는 매크로 상수와 비슷한 용도로 사용한다.

하지만 var로 선언한 이상 쓰기가 가능하다는 특징이 생긴다는 점을 염두에 두자. 위에서 HTTPResponseStruct에 정의된 정적 프로퍼티는 모두 외부에서 값을 어싸인 할 수 있다. 본의아니게 마치 싱글턴처럼 이용하는게 가능해진다. 어쨌든 외부 변경을 피하려면 let으로 선언하는 것을 잊지 말자.

그런데 정작 클래스(class) 에서는 정적 프로퍼티를 사용 할 수가 없다. 미래에 지원할지 모르겠지만 '아직 클래스에서 static을 사용 할 수 없다'는 오류메시지가 뜬다. 대신 클래스 내부에서는 class var 라는 독특한 타입을 명시해 주면 동일하게 사용이 가능하다. 이제(Swift 3) 클래스 에서도 static 선언이 가능해졌다.
class HTTPResponseClass {
    static var success: Int {
        return 200
    }
    static var notFound: Int {
        return 404
    }
    static var serverError: Int {
        return 500
    }
}

var code3 = HTTPResponseClass.serverError
결과적으로 클래스로도 동일하게 인스턴스(객체)를 만들지 않고도 참조가 가능하다.

정적 프로퍼티는 클래스를 인스턴스화(오브젝트 생성)하기 전에도 액세스 가능하다. 당연하게도 이 프로퍼티는 읽기 전용(read-only) 이다.

[관련글] Swift - 구조체(Structure) 훑어보기
[관련글] Swift - 클래스(Class) 훑어보기
[관련글] Swift - Lazy Stored Properties 좀 더 살펴보기

[돌아가기] 스위프트(Swift) 가이드

댓글 5개 :

Gonzales_eat :

class MyDate {
var year = 0

var name: String {
return "\(self.year)"
}
func print() { //error
print(self.name)
}
}

코드를 생략해서 에러나는 부분을 표시하였습니다. Argument passed to call that takes no arguments. 라고 표시됩니다. '인수(매개변수)를 가지지 않는 호출에 전달된 프로퍼티' 정도로 이해했습니다. 함수 print() 가 작동 안 하는 이유를 도저히 모르겠습니다.

gonzales_eat :

class DoublingClass {
var a = 0
var b = 0
var data:Int {
get {
return a + b
}
set (value) {
self.a = value*2
self.b = value*2
}
}

init() {}
}

class DdablingClass: DoublingClass {
override var data: Int {
get {
return (a+b)*2
}
}
}

위와같이 코드를 넣어 진행해봤습니다. getter의 생략형을 사용하지 않아서 read-write property일 것이라고 예측했습니다. 그러나 두 번째 class의 상속하는 과정에서 Cannot override mutable property with read-only property 'data' 라는 오류를 내는 것을 보면 getter가 read-only로 정의된 것 같습니다.

Seorenn :

1. 클래스에서 정의한 print 메소드 내에서 Swift의 내장 함수인 print()를 호출하려 하는데 아무래도 의도대로가 아니라 클래스 내에 정의된 print 메소드를 재귀호출하려 하는 것 같습니다. print 메소드의 이름을 다른 것으로 바꿔보시면 알 수 있으리라 생각됩니다.

2. read-write property 를 override 하는 경우이니 setter 역시 구현해 주어야 합니다. setter 가 없기 때문에 read-only 로 해석하게 됩니다. getter 생략형은 read-only computed property를 간단하게 구현할 수 있도록 제공하는 특수한 방법일 뿐입니다.

gonzales_eat :

xcode(8.3.3), swift(3.1)에서 class에서도 static property를 사용할 수 있네요. Seorenn님의 블로그 항상 감사하는 마음으로 읽고 있습니다. 진심으로 감사드립니다.

Seorenn :

지적해 주신 김에 내용을 슬쩍 업데이트 했습니다. ;-)