[Swift / Lazy] 지연 저장 프로퍼티, 너 누구니?

박준혁 - Niro·2023년 11월 12일
3
post-thumbnail

💭 Lazy 그동안 왜 썼지?

무수히 많은 질문을 만들어 놓은 lazy 사태에 대해 많은 고민을 해보았습니다...

lazy 를 사용하면 해당 프로퍼티를 사용할 때 초기화하는 특성을 갖고 있어 메모리를 효율적으로 사용할 수있고 성능 저하의 문제를 없앨 수 있다 라고 단순히 알고 있었습니다

그럼 고민하지 말고 모든 프로퍼티에 대해 lazy 를 선언해주면 되는거 아닌가?

라는 생각으로 인해 lazy 를 남발하는 코드가 생겨났습니다..

또한 SOPT 3차 세미나 중 사용한 코드인데 likeButton 은 'ViewController 가 로드 될 때 부터 사용할 요소인데 굳이 왜 lazy 를 선언해야만 하나...?' 라는 궁금점과 함께 lazy 를 다시 알아보고 싶었습니다!


🚥 Lazy, 너 누구니?

💡 지연 저장된 프로퍼티 ( lazy stored property ) 는 처음 사용될 때까지 초기값은 계산되지 않은 프로퍼티 입니다.

지연 저장된 프로퍼티는 선언 전에 lazy 수정자를 붙여 나타냅니다

라고 정의가 되어 있습니다. 쉽게 말을 하자면!

변수의 값을 필요할 때까지 초기화하지 않고, 실제로 사용될 떄 초기화되도록 하는 프로그래밍 기법입니다.

Lazy 예시

class SomeClass {
    lazy var expensiveObject: ExpensiveClass = {
        print("Creating expensiveObject")
        return ExpensiveClass()
    }()
}

let obj = SomeClass()
print("SomeClass 인스턴스 생성됨")

// expensiveObject에 처음 접근하는 시점
obj.expensiveObject.doSomething()

리소스를 많이 사용하는 초기화 작업을 담당하는 expensivedObejct 프로퍼티가 있다고 가정해봅시다!

SomeClass 의 인스턴스가 만들어질때 초기화 될텐데 만약 자주 사용하지 않을 무거운 프로퍼티까지 초기화 하는 것은 비효율적이겠죠?

위의 코드와 같이 lazy 키워드를 사용하므로써 obj 라는 SomeClass 의 인스턴스가 만들어졌지만 print 구문이 출력되지 않은 것을 볼 수 있습니다.

해당 프로퍼티가 초기화 되는 시점은 접근할 때 (마지막줄) 초기화가 되겠죠?


💡 Lazy 의 강력한 이점!

앞서 본 예시처럼 lazy 변수는 필요에 따라 효율적으로 자원을 관리할 수 있어 무거운 작업이지만 자주 사용하지 않는 특정 상황에서 성능 향상에 도움을 줄 수 있습니다.

그럼 또 다른 이점은 무엇이 있을까요?

Stored property 의 값으로 초기화

class UserProfile {
    var username: String
    var email: String
    
    var profileURL: String = "http://example.com/users/\(username)?email=\(email)"
    
    init(username: String, email: String) {
        self.username = username
        self.email = email
    }
}

다음과 같이 저장 프로퍼티인 usernameemail 이 있습니다.

두개의 저장 프로퍼티를 사용해서 profileURL 변수를 초기화를 시키는데 만약 usernameemail 이 아직 설정되지 않은 상태에서 profileURL 을 출력하게 되면 어떠한 일이 벌어질까요?

정확히 출력되는 상황도 존재하겠지만 빈 문자열이 되거나 임시로 지정한 값으로 설정되어 원하는 동작을 못하게 됩니다.

lazy var profileURL: String = {
    return "http://example.com/users/\(username)?email=\(email)"
}()

원하는 동작을 수행할 수 있게 profileURLlazy 로 선언하여 해당 프로퍼티를 초기화 시키지 않습니다.

사용을 할때 초기화가 되고 usernameemail 의 값을 사용하여 더 안전하게 만들 수 있습니다!

Closure 내에서 self 사용

class MusicPlayer {
    var playlist: [String] = ["Song 1", "Song 2", "Song 3"]
    var currentTrackIndex = 0
    
    lazy var currentTrack: String = {
        return self.playlist[self.currentTrackIndex]
    }()
    
    func play() {
        print("Playing: \(currentTrack)")
    }
    
    func nextTrack() {
        currentTrackIndex += 1
        if currentTrackIndex >= playlist.count {
            currentTrackIndex = 0
        }
    }
}

플레이리스트를 관리하고 현재 트랙을 재생하는 간단한 예시 입니다

Swift 에서는 객체가 완전히 초기화되기 전까지는 self 를 사용할 수 없습니다!

아직 초기화 되지 않은 상태에서 객체의 메서드나 프로퍼티에 접근하게 되면 예상치 못한 오류가 발생할 수 있게 때문입니다

예를 들어 lazy 키워드가 없었다면 currentTrack 은 MusicPlayer 인스턴스가 초기화 될 때 바로 계산 되어야합니다. 이 때 playlistcurrentTrackIndex 가 아직 초기화되지 않았을 수 있어 self.playlist[self.currentTrackIndex] 코드는 실행될 수 없습니다.

위의 예시와 같이 lazy 키워드가 있다면 currentTrack 에 접근하는 순간 ( play() 메서드가 호출 될 때 ) playlistcurrentTrackIndex 는 이미 초기화된 상태이므로 self 를 사용해서 안전하게 currentTrack 의 값을 계산할 수있습니다.

결론적으로, lazy 프로퍼티를 사용하면 초기화 과정에서 self 를 안전하게 사용할 수 있으며, 객체가 완전히 초기화된 후에만 프로퍼티의 값을 계산하게 되어 순서와 관련된 오류를 피할 수 있습니다.

앞서 봤던 likeButton 에서도 마찬가지!

맨 위에서 보여드렸던 예시와도 일맥상통한 얘기 입니다!

likeButton 의 동작을 위해 addTarget 메서드를 사용하여 likeButtonTap 메서드를 연결시킵니다.

만약 여기서 lazy 를 안 사용하게 되면 어떻게 될까요?

실험을 해본 결과

문법적 오류를 발생시키지는 않지만 주의를 주며 예상치 못한 결과를 가져올 수 있다는 문구를 보여줍니다.

self 대신에 UmageCollectionViewCell.self 를 사용하라는건데….

제가 생각하기에는 Xcode 는 항상 최적의 해결책을 제시해주는 것이 아니기 때문에 잘못된 해결책이라 생각됩니다..

도대체 무슨 경고 문구인지 이해가 안가 ChatGPT 한테 물어본 결과

근데… 저희는 ImageCollectionViewCell 인스턴스 의 메서드를 가리키는건데 ImageCollectionViewCell.self 는 타입 자체를 가리키는거니까 인스턴스화 시키지 않고 쓴다는 의미인데 사실 이해가 잘 가질 않았습니다..

워낙 Xcode 가 최적의 해결책을 보여주는 것이 아니니까.. 넘어가고…

결국 문법적인 오류가 발생하지는 않았지만 ImageCollectionViewCell 가 다 초기화 되고 나서 likeButtonTap 메서드를 사용하도록 lazy 키워드를 사용해주어야 한다는 점입니다!

사실 viewDidLoad() 메서드 안에서 addTarget 해도 괜찮습니다!

@noescape 와 캡쳐

lazy 프로퍼티의 초기화 클로저는 @noescape 속성을 가진다고 합니다.

클로저는 정의된 함수나 메서드가 리턴하기 전에 실행되고 완료되어야 한다는 것입니다. 이로 인해 컴파일러는 이 클로저가 메모리에서 유지될 필요가 없다는 것을 알고, 자동으로 클로저 내에서 self 를 강한 참조로 캡쳐하지 않습니다.

즉, 클로저 내에서 self 를 사용해도 강한 참조 순환을 걱정하지 않아도 됩니다. 초기화 클로저는 프로퍼티가 처음 접근될 때만 실행되며, 그 후에는 클로저가 메모리에서 해제되기 때문입니다.


📌 결과적으로

Lazy 키워드를 사용함으로써 우리가 얻을 수 있는 것은

  1. 자원을 효율적으로!
  2. 순환 참조를 방지!
  3. 더욱 안전하게!

정도로 정리할 수 있을거 같습니다!

이렇게 lazy 를 알아보았는데도 아직 궁금점이 해결이 안되거 같습니다....

이렇게 장점만 있으면 더더욱.. 모든 프로퍼티에 lazy 를 사용하면 좋은거 아닌가...?

에 대해 많은 고민을 더 해보고 다음 글로 찾아오겠습니다!

profile
📱iOS Developer, 🍎 Apple Developer Academy @ POSTECH 1st, 💻 DO SOPT 33th iOS Part

1개의 댓글

comment-user-thumbnail
2023년 11월 12일

사용법에 대한 다양한 예시와 발생 할 수 있는 문제, 그리고 코드까지 함께 읽으니 더욱 더 이해가 쉬웠던 거 같습니다! 좋은 글 감사합니다 :) 많이 배우게 되네요!!

답글 달기