무수히 많은 질문을 만들어 놓은 lazy 사태에 대해 많은 고민을 해보았습니다...
lazy 를 사용하면 해당 프로퍼티를 사용할 때 초기화하는 특성을 갖고 있어 메모리를 효율적으로 사용할 수있고 성능 저하의 문제를 없앨 수 있다 라고 단순히 알고 있었습니다
그럼 고민하지 말고 모든 프로퍼티에 대해 lazy 를 선언해주면 되는거 아닌가?
라는 생각으로 인해 lazy 를 남발하는 코드가 생겨났습니다..
또한 SOPT 3차 세미나 중 사용한 코드인데 likeButton 은 'ViewController 가 로드 될 때 부터 사용할 요소인데 굳이 왜 lazy 를 선언해야만 하나...?' 라는 궁금점과 함께 lazy 를 다시 알아보고 싶었습니다!
💡 지연 저장된 프로퍼티 ( lazy stored property ) 는 처음 사용될 때까지 초기값은 계산되지 않은 프로퍼티 입니다.
지연 저장된 프로퍼티는 선언 전에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
변수는 필요에 따라 효율적으로 자원을 관리할 수 있어 무거운 작업이지만 자주 사용하지 않는 특정 상황에서 성능 향상에 도움을 줄 수 있습니다.
그럼 또 다른 이점은 무엇이 있을까요?
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
}
}
다음과 같이 저장 프로퍼티인 username
과 email
이 있습니다.
두개의 저장 프로퍼티를 사용해서 profileURL
변수를 초기화를 시키는데 만약 username
과 email
이 아직 설정되지 않은 상태에서 profileURL
을 출력하게 되면 어떠한 일이 벌어질까요?
정확히 출력되는 상황도 존재하겠지만 빈 문자열이 되거나 임시로 지정한 값으로 설정되어 원하는 동작을 못하게 됩니다.
lazy var profileURL: String = {
return "http://example.com/users/\(username)?email=\(email)"
}()
원하는 동작을 수행할 수 있게 profileURL
을 lazy
로 선언하여 해당 프로퍼티를 초기화 시키지 않습니다.
사용을 할때 초기화가 되고 username
과 email
의 값을 사용하여 더 안전하게 만들 수 있습니다!
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 인스턴스가 초기화 될 때 바로 계산 되어야합니다. 이 때 playlist
와 currentTrackIndex
가 아직 초기화되지 않았을 수 있어 self.playlist[self.currentTrackIndex]
코드는 실행될 수 없습니다.
위의 예시와 같이 lazy
키워드가 있다면 currentTrack
에 접근하는 순간 ( play() 메서드가 호출 될 때 ) playlist
와 currentTrackIndex
는 이미 초기화된 상태이므로 self
를 사용해서 안전하게 currentTrack
의 값을 계산할 수있습니다.
결론적으로, lazy
프로퍼티를 사용하면 초기화 과정에서 self
를 안전하게 사용할 수 있으며, 객체가 완전히 초기화된 후에만 프로퍼티의 값을 계산하게 되어 순서와 관련된 오류를 피할 수 있습니다.
맨 위에서 보여드렸던 예시와도 일맥상통한 얘기 입니다!
likeButton 의 동작을 위해 addTarget 메서드를 사용하여 likeButtonTap 메서드를 연결시킵니다.
만약 여기서 lazy 를 안 사용하게 되면 어떻게 될까요?
실험을 해본 결과
문법적 오류를 발생시키지는 않지만 주의를 주며 예상치 못한 결과를 가져올 수 있다는 문구를 보여줍니다.
self
대신에 UmageCollectionViewCell.self
를 사용하라는건데….
제가 생각하기에는 Xcode 는 항상 최적의 해결책을 제시해주는 것이 아니기 때문에 잘못된 해결책이라 생각됩니다..
도대체 무슨 경고 문구인지 이해가 안가 ChatGPT 한테 물어본 결과
근데… 저희는 ImageCollectionViewCell
인스턴스 의 메서드를 가리키는건데 ImageCollectionViewCell.self
는 타입 자체를 가리키는거니까 인스턴스화 시키지 않고 쓴다는 의미인데 사실 이해가 잘 가질 않았습니다..
워낙 Xcode 가 최적의 해결책을 보여주는 것이 아니니까.. 넘어가고…
결국 문법적인 오류가 발생하지는 않았지만 ImageCollectionViewCell
가 다 초기화 되고 나서 likeButtonTap
메서드를 사용하도록 lazy
키워드를 사용해주어야 한다는 점입니다!
사실
viewDidLoad()
메서드 안에서addTarget
해도 괜찮습니다!
lazy
프로퍼티의 초기화 클로저는 @noescape 속성을 가진다고 합니다.
클로저는 정의된 함수나 메서드가 리턴하기 전에 실행되고 완료되어야 한다는 것입니다. 이로 인해 컴파일러는 이 클로저가 메모리에서 유지될 필요가 없다는 것을 알고, 자동으로 클로저 내에서 self
를 강한 참조로 캡쳐하지 않습니다.
즉, 클로저 내에서 self
를 사용해도 강한 참조 순환을 걱정하지 않아도 됩니다. 초기화 클로저는 프로퍼티가 처음 접근될 때만 실행되며, 그 후에는 클로저가 메모리에서 해제되기 때문입니다.
Lazy
키워드를 사용함으로써 우리가 얻을 수 있는 것은
정도로 정리할 수 있을거 같습니다!
이렇게 lazy
를 알아보았는데도 아직 궁금점이 해결이 안되거 같습니다....
이렇게 장점만 있으면 더더욱.. 모든 프로퍼티에 lazy 를 사용하면 좋은거 아닌가...?
에 대해 많은 고민을 더 해보고 다음 글로 찾아오겠습니다!
사용법에 대한 다양한 예시와 발생 할 수 있는 문제, 그리고 코드까지 함께 읽으니 더욱 더 이해가 쉬웠던 거 같습니다! 좋은 글 감사합니다 :) 많이 배우게 되네요!!