Swift

❓lazy

_yunie 2024. 5. 20. 20:44

강의를 듣거나 다른 사람의 코드를 참고할 때 간혹 lazy라는 키워드를 볼 수 있었습니다. 들어본 적은 있지만 제대로 사용해 본 적이 없어 알아두면 좋을 것 같아 기록하게 되었습니다!

 

lazy

lazy를 해석하면 ‘게으른’, ‘느긋한’ 등의 의미를 지닙니다.

그리고 키워드로서의 lazy 또한 lazy로 프로퍼티 선언 시 lazy가 붙지 않은 다른 프로퍼티보다 지연된다는 점에서 이와 같은 특징을 보여줍니다.

결국 이는 lazy가 붙은 프로퍼티는 사용되기 전까지는 메모리에 올라가지 않음을 의미합니다.

이제 간단한 예시로 lazy 프로퍼티가 없을 때와 있을 때를 비교하며 lazy가 어떤 식으로 작동하는지 알아보겠습니다

import UIKit

class GPS {
    init() {
        print("GPS!")
    }
}

class iPhone {
    var gps = GPS()
    init() {
        print("iPhone!")
    }
}

let myiPhone = iPhone()

 

코드를 보면 GPS와 iPhone이라는 두 가지 클래스를 생성해주고 있고 모든 아이폰에는 GPS기능이 들어있음을 알 수 있습니다.

그리고 마지막 코드를 통해 myiPhone이라는 객체를 생성해 줍니다.

 

let myiPhone = iPhone()

 

그럼 아래와 같은 결과가 출력되는 것을 확인할 수 있습니다.

GPS!
iPhone!

 

하지만 실생활에서 GPS를 사용하는 경우가 하루에 몇 번이나 있나요?

배달을 시키기 위해 내 위치를 찾을 때, 모르는 길을 찾을 때 등 GPS 기능을 생각보다 자주 이용하지는 않습니다.

그럼에도 불구하고 계속해서 GPS 기능을 이용 중인 상태로 핸드폰을 사용하게 된다면 GPS는 쓸데없이 메모리를 차지하고 있게 됩니다🥺

 

그리고 이런 상황을 방지하기 위해 사용하는 것이 lazy입니다..!

이제 다음 코드를 볼까요?

import UIKit

class GPS {
    init() {
        print("GPS!")
    }
}

class iPhone {
    lazy var gps = GPS()
    init() {
        print("iPhone!")
    }
}

let myiPhone = iPhone()

 

방금 전 코드와 비교했을 때 gps 변수에 lazy라는 키워드를 하나 붙여준 점이 차이입니다.

그럼 결과는 어떻게 될까요?

iPhone!

 

이번에는 iPhone! 만 출력되는 것을 알 수 있습니다.

그리고 이 상태에서 myiPhone의 gps에 접근해 보겠습니다.

그럼 아까와 같이 GPS!가 나타납니다.

GPS!

 

lazy 키워드가 가진 특성대로 gps가 선언될 때에 컴파일러는 lazy라는 키워드를 보고 일단 해당 프로퍼티를 메모리에 올리지 않고 넘어갑니다. 그리고 방금처럼 lazy로 선언된 프로퍼티에 접근해주면 그때서야 메모리에 올라갑니다!

 

이제 조금 이해가 되실까요?

 

🧐 그럼 lazy 키워드는 언제 사용하는 게 좋을까요?

저도 lazy 키워드를 아직 사용해보지 않아서 다양한 예제들을 찾아봤는데요 :)

lazy는 프로퍼티의 초기화가 끝나기 전까지 외부 데이터에 의존적인 경우 유용하게 쓰인다고 합니다.

사실 글로만 보면 이해하기 어려웠는데 한 블로그에서 좋은 예시를 찾을 수 있었습니다.

import Foundation

class Child {
    var name: String
    var mother: Mother
    init(name: String, mother: Mother) {
        self.name = name
        self.mother = mother
    }
}

class Mother {
    var name: String
    lazy var child: Child = { return Child(name: "유니", mother: self)}()
    init(name: String) {
        self.name = name
    }
}

let mother: Mother = Mother(name: "옴맘마")

 

이번 코드에서 lazy는 Mother 클래스 내 child 프로퍼티에 사용되고 있는 것을 알 수 있습니다.

그리고 만약 Mother의 child가 생성된다면 유니라는 이름을 가진 아이를 가지도록 해주고 있습니다.

그런데 여기서 lazy를 없앤다면 어떻게 될까요?

error: lazy.playground:14:63: error: cannot find 'self' in scope; did you mean to use it in a type or extension context?
    var child: Child = { return Child(name: "유니", mother: self)}()

 

❗️바로 위와 같은 에러를 마주하게 됩니다.

이는 ‘Mother가 Child를 가진 상태 + Child도 Mother를 가지고 있어야 하는 상태’가 충돌(Mother가 Child에 의존)하는 상황이 무한정 반복되어 발생합니다.

그렇지만 본래의 코드처럼 child에 lazy키워드를 붙여주면 Mother의 인스턴스를 생성할 때, 자식이 없어도 Mother 인스턴스를 생성할 수 있습니다. 

그리고 Mother의 child 프로퍼티에 접근할 때, 유니라는 이름의 child가 생성되어 child의 이름과 mother의 이름 모두 가져올 수 있습니다.

print("\(mother.child.name)의 엄마는 \(mother.child.mother.name)")
// 유니의 엄마는 옴맘마

 

이처럼 lazy는 초기화 중인 프로퍼티가 외부 데이터에 의존하고 있을 때 발생하는 문제들을 해결하고 싶을 때 사용할 수 있습니다.

 

🧐 lazy의 장점은?

우리는 이제 lazy가 어떤 역할을 하는지 알고 있습니다.

그렇다면 lazy를 사용하면 어떤 점이 좋을까요?

lazy를 사용하면 인스턴스가 생성되기도 전에 self를 통해 인스턴스의 프로퍼티를 사용할 수 있습니다.

 

lazy var child: Child = { return Child(name: "유니", mother: self)}()

 

해당 코드에서 Mother의 객체가 생성되지도 않았는데 Child 정보에 mother를 self로 넣어주고 있는 것처럼 말이죠.

 

그리고 메모리를 보다 효율적으로 관리할 수 있게 됩니다.

앱은 복잡한 형태를 가지고 있을 때, 뷰가 로드되는 순간 모든 인스턴스를 생성해 메모리에 올릴 경우 과부하로 종료될 수 있습니다.

하지만 lazy를 이용해 정말 필요한 상황에서만 메모리에 올린다면 메모리의 부담을 덜어줄 수 있습니다.

 

이렇게만 보면 그냥 모든 변수에 lazy를 붙여줘도 되는 거 아니야??? 라는 생각이 들기도 합니다.

하지만 당연히! lazy도 단점을 가지고 있습니다.

 

🧐 lazy의 단점?

이 부분 또한 코드로 이해해봅시다!

만약 예시처럼 랜덤으로 1부터 10까지의 값을 가지는 lazy가 붙은 프로퍼티 num을 가진 클래스가 있는 상황일 때

import UIKit

class Example {
    lazy var num: Int = {
        print("num을 초기화합니다!")
        return Int.random(in: 1...10)
    }()
}

let ex = Example()

DispatchQueue.global().async {
    print("1번 스레드 : \(ex.num)")
}

DispatchQueue.global().async {
    print("2번 스레드 : \(ex.num)")
}

 

ex라는 이름의 인스턴스를 생성한 후 만약 두 개의 스레드에서 ex의 num에 대해 비동기적으로 접근한다면 어떻게 될까요?

 

우선 1번 스레드가 num에 접근하여 num을 초기화해 줍니다.

다음으로 2번 스레드가 num에 접근합니다.

그리고 1번 스레드의 초기화가 완료되기 전에 2번 스레드의 초기화 코드가 실행됩니다.

 

결국, 1번 스레드와 2번 스레드의 값이 다르게 출력되고 초기화 코드가 두 번 실행되는 결과가 나타납니다.

num을 초기화합니다!
num을 초기화합니다!
1번 스레드 : 2
2번 스레드 : 10

 

이처럼 초기화되지 않은 lazy 프로퍼티에 여러 곳에서 동시에 접근하게 되면 초기화가 여러 번 진행되는 문제가 발생할 수 있습니다.

즉, lazy는 thread safety를 보장하지 않는다는 단점이 있습니다.

*thread safety : 하나의 함수가 한 스레드로부터 호출돼 실행 중일 때, 다른 스레드에서 해당 함수를 호출하여 동시에 함께 실행하더라도 각 스레드에서의 수행결과가 올바르게 나오는 것

 

❗️lazy 사용할 때 주의할 점

프로퍼티가 초기화되기 전에는 값을 가지지 않다가 초기화가 되면 값을 가지기 때문에 lazy를 이용해 프로퍼티를 선언하고 싶다면 반드시 var 키워드를 사용해주세요!

 

📚 참고

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/
https://levenshtein.tistory.com/484
https://velog.io/@niro/Swift-Lazy-진짜-필요할-때만-씁시다
https://velog.io/@pccommen/Lazy를-쓰는-것이-성능에-미치는-영향
https://tdcian.tistory.com/302
https://dev-dmsgk.tistory.com/35
https://longlivedrgn-miro.tistory.com/41
https://medium.com/hackernoon/lazy-keyword-in-swift-the-very-basic-d51400f36fa4

'Swift' 카테고리의 다른 글

❓ Delegate와 DataSource의 차이  (1) 2024.06.10
❓ 타입 프로퍼티  (2) 2024.06.07
❓ 연산 프로퍼티  (0) 2024.06.05
❓ 저장 프로퍼티  (0) 2024.05.31
❓final  (0) 2024.05.27