본문 바로가기
iOS/Swift

[구조체&클래스] 속성 - 저장 속성, 지연(Lazy) 저장 속성, 저장 타입 속성, 속성감시자

by 나리._. 2021. 9. 10.

- 저장 속성(Stored Properties)

값이 저장되는 일반적인 속성(변수)을 저장 속성이라고 함

struct Bird {
    var name: String
    var weight: Double
    
    init(name: String, weight: Double) {    // 기본값이 없으면, 생성자를 통해 값을 반드시 속성 값을 초기화해야함
        self.name = name
        self.weight = weight
    }
    
    func fly() {
        print("날아갑니다.")
    }
}



var aBird = Bird(name: "참새1", weight: 0.2)

aBird.name // 참새1
aBird.weight = 0.3

var bBird = Bird(name: "참새2", weight: 0.3)

 - 저장 속성은 구조체와 클래스에서 동일하다.

 - let(상수) 또는 var(변수)로 선언 가능 (만약 저장 속성을 let으로 선언하면 값을 바꿀 수 없음)

 - 저장 속성(변수)은 각 속성 자체가 고유의 메모리 공간을 가짐 ⭐️

 - 초기화 이전에 값을 가지고 있거나, 생성자 메서드를 통해 값을 반드시 초기화해야만 함.

 

 

- 지연(Lazy) 저장 속성

struct Bird1 {
    var name: String
    lazy var weight: Double = 0.2 // 지연 저장 속성
    
    init(name: String) {
        self.name = name
        //self.weight = 0.2
    }
    
    func fly() {
        print("날아갑니다.")
    }
}


var aBird1 = Bird1(name: "새")   // weight 속성 초기화 안됨
aBird1.weight  // <============ 해당 변수에 접근하는 이 시점에 초기화됨 (메모리 공간이 생기고 숫자가 저장됨)

 - 위의 weight이라는 지연 저장 속성초기화 시점에 메모리 공간이 생기는 것이 아님

 - 예를 들어, 인스턴스가 생기고 난 후, aBird.weight 이렇게 접근하는 순간! 메모리 공간을 만들고 숫자를 저장하게 됨

 

이 코드의 생성자에서 self.weight = 0.2를 주석 처리한 이유는?

 

실제로 코드로 돌렸을 때 self.weight = 0.2 이런 식의 설정이 제대로 동작을 할 수도 있지만

그렇게 된다면 lazy로 선언하는 게 의미가 없기 때문!

lazy의 의도대로 선언했다면 속성 선언 시 기본값을 설정해야 하고, 생성자 내에서는 값을 설정하지 않아야 한다!

 

- 지연(lazy) 저장 속성 ===> 구조체, 클래스 동일

- 지연 저장 속성은 "해당 저장 속성"의 초기화를 지연시키는 것.   

===> 즉, 인스턴스가 초기화되는 시점에 해당 속성이 값을 갖고 초기화되는 것이 아니라(메모리에 공간과 값을 갖는 것이 아니라),

 해당 속성(변수)에 접근하는 순간에 (해당 저장 속성만) 개별적으로 초기화됨

- 생성자에서 초기화를 시키지 않기 때문에 "선언 시점에 기본값을 저장"해야 함

- 값을 넣거나, 표현식(함수 실행문)을 넣을 수 있음(모든 형태의 표현식)

- 함수 호출 코드, 계산 코드, 클로저 코드 등도 모두 가능  ===> 저장하려는 속성과 "리턴형"만 일치하면 됨

 - 따라서, 상수로의 선언은 안되고 변수(var)로의 선언만 가능 ➡︎  lazy var만 가능(lazy let 불가능)

 

 

 

그렇다면, 지연 저장 속성을 사용하는 이유는?

class AView {
    var a: Int
    
    // 1) 메모리를 많이 차지할때
    lazy var view = UIImageView()     // 객체를 생성하는 형태
    
    // 2) 다른 속성을 이용해야할때(다른 저장 속성에 의존해야만 할때)
    lazy var b: Int = {
        return a * 10
    }()
    
    init(num: Int) {
        self.a = num
    }
}

 1) 메모리 공간을 많이 차지하는 이미지 등의 속성에 저장할 때

 (반드시 메모리에 다 올릴 필요가 없으므로) 지연 저장 속성으로 선언한다. (메모리 낭비 막기 위함)

 

 2) 다른 속성들을 이용해야 할 때

  초기화 시점에 모든 속성들이 동시에 메모리 공간에 저장되므로 어떤 한 가지 속성이 다른 속성에 접근할 수가 없다.

  그렇지만, 지연 저장 속성을 이용하는 경우 지연으로 저장된 속성은 먼저 초기화된 속성에 접근할 수 있게 된다.

 

 

- 저장 타입 속성

class Dog {
    
    // static(고정적인/고정된)이라는 키워드를 추가한 저장 속성 - 저장 타입속성
    static var species: String = "Dog"
    
    var name: String
    var weight: Double
    
    init(name: String, weight: Double) {
        self.name = name
        self.weight = weight
    }

}


let dog = Dog(name: "초코", weight: 15.0)
dog.name
dog.weight


//dog.s         // =====> 인스턴스에서 .(점)을 찍어도 속성으로 보이지 않음


Dog.species    // ====> 반드시 타입(형식)의 이름으로 접근해야함
class Circle {
    
    // (저장) 타입 속성 (값이 항상 있어야 함)
    static let pi: Double = 3.14
    static var count: Int = 0   // 인스턴스를 (전체적으로)몇개를 찍어내는지 확인
    
    // 저장 속성
    var radius: Double     // 반지름
    
    // 생성자
    init(radius: Double) {
        self.radius = radius
        Circle.count += 1
    }
    
}


var circle1 = Circle(radius: 2)   // 인스턴스를 +1 개 찍어냈다.
Circle.count // 1

var circle2 = Circle(radius: 3)   // 인스턴스를 +1 개 찍어냈다.
Circle.count // 2

 - 일반 저장 속성은 인스턴스를 생성할 때, 생성자에서 모든 속성을 초기화를 완료하고 해당 저장 속성은 각 인스턴스가 가진 고유한 값이다.

 하지만, 저장 타입(형식) 속성은 생성자가 따로 없고 타입 자체(유형 그 자체)에 속한 속성이기 때문에 항상 기본값이 필요하다 (생략할 수 없음)

 - 저장 타입 속성은 기본적으로 지연 속성 성격이다.(속성에 처음 접근하는 순간에 초기화됨)

   (참고: 여러 스레드에서 동시에 액세스 하는 경우에도 한 번만 초기화되도록 보장됨. Thread-Safe)

 - let 또는 var 둘 다 선언 가능

 - 타입 속성은 특정 인스턴스에 속한 속성이 아니기 때문에 인스턴스 이름으로는 접근 불가

 - 상속 시 재정의 불가

 

그렇다면, 어떤 경우에 타입 속성을 선언해야 하나?

➡︎ 모든 인스턴스가 동일하게 가져야 하는 속성이거나(해당 타입의 보편적인 속성), 모든 인스턴스가 공유해야 하는 성격에 가까운 경우!

 

 

- 속성 감시자

기본적으로 "속성 감시자"라는 이름이지만, 성격은 메서드 ➞ 저장 속성 감시

class Profile {
     
    // 일반 저장 속성
    var name: String = "이름"
    
    var statusMessage: String = "기본 상태메세지" { // 저장 속성
        willSet(message) { // 저장 속성이 변하는 시점을 관찰하는 메서드
            print("메세지가 \(statusMessage)에서 \(message)로 변경될 예정입니다.")
        }
    }
    
}


let p = Profile()

p.name
p.name = "전지현"

p.statusMessage
p.statusMessage = "행복해"

p.statusMessage = "우울해"

코드 실행 결과

저장 속성이 변하는 시점을 관찰, 저장 속성이 변하는 순간 ===> 딸려있는 메서드가 호출됨

속성 감시자는 새 값이 속성의 현재 값과 동일하더라도 속성 값이 설정될 때마다 호출됨

 

 

속성 감시자의 2가지 종류 - willSet / didSet

1) willSet은 값이 저장되기 직전에 호출됨

2) didSet은 새 값이 저장된 직후에 호출됨

class Profile1 {
    
    // 일반 저장 속성
    var name: String = "이름"
    
        
//    var statusMessage: String {
//        willSet(message) {  // 바뀔 값이 파라미터로 전달
//            print("메세지가 \(statusMessage)에서 \(message)로 변경될 예정입니다.")
//            print("상태메세지 업데이트 준비")
//        }
//        didSet(message) {   // 바뀌기 전의 과거값이 파라미터로 전달
//            print("메세지가 \(message)에서 \(statusMessage)로 이미 변경되었습니다.")
//            print("상태메세지 업데이트 완료")
//        }
//    }

	// 파라미터의 생략 - oldValue / newValue (애플의 약속!)
	var statusMessage = "기본 상태메세지" {
        willSet {
            print("메세지가 \(statusMessage)에서 \(newValue)로 변경될 예정입니다.")
            print("상태메세지 업데이트 준비")
        }
        didSet {
            print("메세지가 \(oldValue)에서 \(statusMessage)로 이미 변경되었습니다.")
            print("상태메세지 업데이트 완료")
        }
    }

    
    init(message: String) {
        self.statusMessage = message
    }
    
}

let profile1 = Profile1(message: "기본 상태메세지")  // 초기화시, willSet/didSet이 호출되지는 않음
profile1.statusMessage = "기분 good"

코드 실행 결과

 - 클래스, 구조체, (열거형) 동일하게 적용

 - 일반적으로는 willSet 또는 didSet 중에서 한 가지만 구현한다. (실제 프로젝트에서는 didSet을 많이 사용)

 

속성 감시자가 왜 필요할까?

변수 변하면, 변경 내용을 반영하고 싶을 때(업데이트) 유용하기 때문!

어떤 속성이 변하는 시점을 알아차리도록 시점에 제약을 만드는 코드를 짜기는 어렵다 ==> 그래서 실제 앱을 만들고 활용할 때 좋은 수단

 

 

속성 감시자를 추가 가능한 경우 ⭐️

 - 1) 저장 속성 (원래, 상속한 경우 둘 다 가능)

 - 2) 계산 속성 (상속해서 재정의하는 경우에만 가능) (단순 메서드 추가)

 

 - 계산 속성의 경우, 속성 관찰자를 만드는 대신 계산 속성의 set블록에서 값 변경을 관찰할 수 있기 때문에

   (재정의(상속)이 아닌 본래의 계산 속성에는 추가 불가)

 - let(상수) 속성에는 당연히 추가 안됨 (값이 변하지 않으므로, 관찰할 필요가 없기 때문)

 - 지연 저장 속성에 안됨