lazy var에 대해 알아보자!

lazy var에 대해서 알아보자

코드 베이스로 UI를 개발할 때, UICollection/TableViewCell을 상속받은 커스텀 셀 클래스 내부에 클로저를 통해서 초기화 하는 서브 뷰 프로퍼티들을 선언했다. 이 때 서브 뷰 프로퍼티들에 lazy 키워드를 사용하는 것이 좋을까 의문점이 생겨서 정리해 보려고 한다.

1. 우선 lazy var는…

lazy var 프로퍼티는 ‘‘최초로 접근할 때 딱 한 번 초기화 되는 프로퍼티’‘이다. 즉, 어떤 클래스(or 구조체) 내부에 lazy var 프로퍼티가 있다면, 인스턴스가 생성되는 시점에 프로퍼티 초기값을 설정해주지 않아도 된다. 나만 궁금했는지는 모르겠지만, 클로저로 초기화하는 lazy var 프로퍼티는 한 번 접근 후에 또 다시 접근하면 클로저가 실행되지 않는다.

2. lazy var는 언제 쓰는게 좋을까?

공식 문서를 찾아보면 아래와 같이 나와있다.

Lazy properties are useful when the initial value for a property is 
dependent on outside factors whose values are not known until after 
an instance’s initialization is complete. Lazy properties are also 
useful when the initial value for a property requires complex or 
computationally expensive setup that should not be performed unless 
or until it is needed.

lazy var 프로퍼티는 인스턴스가 생성된 후 외부 요인에 의해 초기화될 경우, 복잡하거나 계산 비용이 많이 드는 설정을 필요로 하는 경우에 사용하면 좋다. 지금 딱 떠오르는 건 테이블 생성이나 파일 읽기 등이 있다.

3. 그렇다면 코드로 UI를 만들 때, 서브 뷰 프로퍼티들을 lazy로 선언하는 것이 더 좋을까?

서브 뷰 프로퍼티를 클로저로 초기화하는 let , 클로저로 초기화 하는 lazy var 중 무엇으로 만들것인가? 디버깅 결과, 둘의 차이는 인스턴스 생성 전에 초기화 되는지, 프로퍼티에 접근할 때 초기화 되는지 여부이다.

만약 서브뷰가 항상 보여져야 한다면 lazy를 사용했을 때 이점이 적을 것이고, 특정 상황에서만 보여져야 한다면 lazy를 사용했을 때 이점이 있을 수 있다. 뷰 컨트롤러를 생성할 때, 해당 서브뷰를 만들지 않고 생성이 가능하므로 메모리와 성능 측면에서 좋을 것 이다.

4. lazy var를 사용할 때, self에 대해서 caturing을 해줘야 하는가

추가로, 클로저를 통해 초기화 되는 lazy 변수에서는 @noescape가 자동적으로 적용되기 때문에 self에 대한 순환 참조가 생기지 않는다. 변수 타입이 클로저인 경우와 분리해야한다. 변수 타입이 클로저인 경우에는 self에 대해서 순환 참조가 발생하므로 값을 캡처해야 한다.

// 클로저를 통해 초기화 되는 지연 변수
class DownLoadAppCell: UICollectionViewCell {
    lazy var labelStackView: UIStackView = {
        let stackView = UIStackView(arrangedSubviews: [appNameLabel, appSimpleCommentLabel])
        stackView.axis = .vertical
        stackView.distribution = .fill
        stackView.arrangedSubviews.forEach { $0.autoLayout }
        stackView.backgroundColor = UIColor.green
        return stackView
    }()
   ...
}
// 애플 공식문서에 나오는 코드의 일부분이다.
class HTMLElement {
    // 변수 타입이 클로저인 경우
    let name: String
    let text: String?
    
    lazy var asHTML: Void -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
...  
      deinit {
        print("\(name) is being deinitialized")
    }
}

순환 참조가 정말 안생기는지 궁금해서 실험 해봤다.

class Apple {
    private var count: Int = 10
    lazy var counting: String = {
        return "the number of apple is \(self.count)"
    }()
    deinit {
        print("delete apple")
    }
}

var apple: Apple? = Apple()
apple?.counting
apple = nil // delete apple
class Lemon {
    private var count: Int = 5
    lazy var counting: () -> String = {
        return "the number of lemon is \(self.count)"
    }
    deinit {
        print("delete lemon")
    }
}

var lemon: Lemon? = Lemon()
lemon?.counting() 
lemon = nil // 소멸자가 호출되지 않는다.

참고자료

애플공식문서-Properties

https://www.bobthedeveloper.io/blog/swift-lazy-initialization-with-closures

https://outofbedlam.github.io/swift/2016/03/04/Lazy/

https://stackoverflow.com/questions/47367454/swift-lazy-var-vs-let-when-creating-views-programmatically-saving-memory

http://seorenn.blogspot.com/2015/02/swift-lazy-stored-properties.html

https://medium.com/@abhimuralidharan/lazy-var-in-ios-swift-96c75cb8a13a

https://medium.com/@hossamghareeb/lazy-properties-in-ios-39cda14ec14f