[!note] 주말에 공부했던 부분이지만 한 번에 설명하기엔 머리에 들어오지 않아서, 다시 한 번 정리해보자.
스위프트의 클래스 프로퍼티는 저장 프로퍼티, 연산 프로퍼티로 나뉜다. 저장 프로퍼티는 상수, 변수에 담기는 값이다.
반면 연산 프로퍼티는 프로퍼티에 값을 설정하거나 가져오는 시점에서 어떤 계산이나 로직에 따라 처리된 값이다. 연산 프로퍼티는 게터를 생성하고, 선택적으로 세터 메서드를 생성하며, 연산을 수행할 코드가 포함된다. 예를 들어, BankAccount 클래스에 은행 수수료를 뺀 현재 잔액을 담는 프로퍼티가 추가로 필요하다고 해보자. 저장 프로퍼티를 이용하는 대신 값에 대한 요청이 있을 때마다 계산되는 연산 프로퍼티를 이용하는 게 더 좋겠다.
class BankAccount { var accountBalnce: Float = 0 var accountNumber: Int = 0 let fees: Float = 25.00 var balanceLessFees: Float // 이게 연산 프로퍼티. get { return accountBalance - fees // 이게 게터 메소드. } } init(number: Int, balance: Float) { accountNumber = number // 여기서 할당을 해주고, 후에 number에 숫자를 부여. accountBalance = balance } } - 위의 코드에서는 현재의 잔액에서 수수료를 빼는 연산 프로퍼티를 반환하는 게터를 추가했다. 선택 사항인 세터 역시 거의 같은 방법으로 선언할 수 있다.
var balanceLessFees: Float { get { return accountBalance - fees } set(newBalance) { accountBalance = newBalance - fees } } - 새롭게 선언한 세터는 부동소수점 값을 매개변수로 받아서 수수료를 뺀 결과를 프로퍼티에 할당한다. 점 표시법을 이용하여 접근하는 저장 프로퍼티처럼, 연산 프로퍼티도 같은 방법으로 접근할 수 있다. 다음은 현재의 잔액에서 수수료를 뺀 값을 얻는 코드와 새로운 값을 설정하는 코드다.
var balance1 = account1.balanceLessFees account1.balanceLessFees = 12123.12 지연 저장 프로퍼티
프로퍼티를 초기화하는 여러 방법이 있는데, 가장 기본적인 방법은 다음과 같이 직접 할당하는 것이다.
var myProperty = 10 - 다른 방법으로는 초기화 작업에서 프로퍼티에 값을 할당하는 것이다.
class MyClass { let title: String init(title: String) { self.title = title } } - 조금 더 복잡한 방법으로는 클로저를 이용하여 프로퍼티를 초기화할 수도 있다.
class MyClass { var myProperty: String = { var result = resourceIntensiveTask() result = processData(data: result) return result }() } [!note] 잠깐만, 클로저는 뭐지? 조금 더 복잡하다면서 이건 아예 뭔 소린지 모르겠는데ㅋㅋㅋㅋㅋㅋㅋ [[0805-2 클로저]]
복잡한 클로저의 경우는 초기화 작업이 리소스와 시간을 많이 사용하게 될 수 있다. 클로저를 이용하여 선언하면 해당 프로퍼티가 코드 내에서 실제로 사용되는지와는 상관없이 클래스의 인스턴스가 생성될 때마다 초기화 작업이 수행될 것이다.
예를 들어, 데이터베이스로부터 데이터를 가져오거나 사용자로부터 사용자 입력을 얻게 될 때, 실행 프로세스의 후반부 단계까지 프로퍼티에 값이 할당되었는지 모르게 되는 상황이 생길 수 있다. 이러한 상황에서의 훨씬 더 효율적인 방법은, 프로퍼티를 최초로 접근할 때만 초기화 작업을 하는 것이다. 다행히도 이 작업은 다음과 같이 lazy로 프로퍼티를 선언하면 된다.
class MyClass { lazy var myProperty: String = { var result = resourceIntensiveTask() ressult = processData(data: result) return result }() } - 프로퍼티를 lazy로 선언하면 프로퍼티가 최초로 접근할 때만 초기화된다. 따라서 리소스를 많이 사용하는 작업은 관련 프로퍼티에 lazy를 사용해 해당 프로퍼티가 사용될 때까지 리소스 집약적인 활동울 미룰 수 있다.
- 지연 프로퍼티는 반드시 변수(var)로 선언되어야 한다.
느낌은 온다, 하지만 이해가 잘 가지 않는다. 코드를 한 번만 더 뜯어보자.
class MyClass { var myProperty: String = { var result = resourceIntensiveTask() result = processData(data: result) return result }() } { var result = resourceIntensiveTask() result = processData(data: result) return result } - 이 부분은 클로저(closure)다. 이렇게 생긴 블럭은 함수처럼 작동한다. 실행하면 결과값이 나오는 하나의 코드 덩어리이다.
- {...}() -> 마지막에 ()이 붙은 이유는? -> 클로저를 만들었으면 이제 그걸 실행해야지? ()는 이 클로저를 지금 바로 실행하라는 뜻이다.
- 즉, 클로저를 만든 다음 바로 실행해서 그 결과값을 myProperty에 저장하는 것.
let tmpClosure = { return "Hello" } let result = tmpClosure() // "Hello" - 여기서 tmpClosure 따로 만들기 귀찮으니까, 그냥
let result = { return "Hello" }() - 이렇게 {...}() 와 같이 클로저 선언과 실행을 동시에 해버린다.
결론적으로 이건 뭐냐?
class MyClass { var myProperty: String = { // 클로저(익명 함수) var result = resourceIntensiveTask() result = processData(data: result) return result }() // 이 클로저를 즉시 실행하고, 결과값을 myProperty에 저장! } [!note] 거의 이해가 잘 된다. 다만 한가지 짚고 가야 할 게 보인다. 스위프트 처음 공부하면서부터 헷갈렸던 건데, 도대체 result가 몇 번을 똑같은 게 나오냐는거다. GPT한테 상담을 요청했다.
먼저 구조만 보자
var result = resourceIntensiveTask() result = processData(data: result) return result - 이걸 순서대로 무슨 일이 일어나는지 짚어보자.
- var result = resourceIntensiveTask()
- resourceIntensiveTask()는 무거운 작업을 하는 임의의 함수다. (파일 읽기, API 호출 등)
- 이 함수가 리턴한 값을 result에 담는다.
var result = "RawData"
2. result = processData(dat: result) - 아까 리턴된 값인 result를 processData 함수에 넣는다. - 이 함수가 리턴된 값을 같은 이름의 return 변수에 **덮어씌운다** ```swift result = processData(data: "RawData") - return result
- 마지막으로 그 가공된 데이터를 반환한다.
왜 헷갈렸나?
- result라는 같은 이름의 변수가 계속 나오니까 어지러웠다.
- 할당 연산자 = 에 대해 기존 인식이 관여한다고 느낀다.
- 실제로는 하나의 변수에 단계적으로 값을 계속 바꾸는 과정을 표현한 것이다.
- 순차적으로, 악보를 읽으며 연주하듯 코드를 읽어나가자.
마지막으로 한 개만 더 점검하자.
lazy를 사용하면 프로퍼티가 최초로 접근될 때만 초기화된다는 말의 의미를 깊이 이해하고 싶다.
- lazy 프로퍼티는 단순히 "나중에 초기화된다"가 아니라, 언제, 왜, 어떻게 초기화되는지를 알면 성능 최적화와 구조 설계에서 훨씬 유리해진다.
- 결국, "인스턴스를 만들 때는 아무 일도 안 일어나다가, 그 값을 처음 읽으려고 하는 순간에야 비로소 클로저가 실행된다는 뜻이다."
class MyClass { lazy var text: String = { print("클로저 실행됨!") return "Hello World" }() } let obj = MyClass() // 아직 아무 일도 안 일어남. print(obj.text) // 이 순간 "클로저 실행됨!" 출력, 값이 할당됨. print(obj.text) // 이번엔 실행되지 않음. 이미 초기화된 값이기 때문. - 출력
클로저 실행됨! Hello World Hello World - 인스턴스를 만든다고 값이 만들어지는 게 아니다.
- MyClass() 했을 때, text 프로퍼티에는 아무 일도 일어나지 않는다.
- lazy가 붙은 프로퍼티는 초기화를 미룬다.
- 최초 '접근'이란?
- obj.text를 읽거나, 출력하거나, 조건문에 쓰거나, 값을 변경하려 할 때
- 이때 내부적으로 클로저가 한 번만 실행되어 값이 설정된다.
- 한 번만 실행된다.
- 클로저는 딱 한 번만 실행된다.
- 이후에는 이미 저장된 값이 계속 재사용된다.
실제 사용 시나리오
- 리소스 낭비 방지
lazy var image = loadHighResolutionImage() // 접근하지 않으면 이미지도 로딩 안됨 -> 메모리 아낌.
2. 상황 따라 값 바뀌게 만들 수 있음 ```swift var usernameInput: String = "" lazy var greeting: String = { return "Welcome, \(usernameInput)" }() - 사용자가 이름 입력 하지 않으면 greeting은 생성되지 않는다.
- 입력한 다음 greeting을 접근하면 그 시점의 usernameInput 기준으로 생성된다.
메모리적 관점
- 일반 프로퍼티는 인스턴스를 생성할 때 메모리와 CPU 사용이 무조건 발생한다.
- lazy 프로퍼티는, 해당 프로퍼티를 절대 사용 안 하면, 함수 자체도 실행 안된다. -> 메모리 절약
- 필요할 때만 계산해서 저장한다. -> 계산 결과를 메모리에 유지한다.
- 그래서 lazy는 let을 쓸 수 없다. -> 값이 나중에 초기화되기 때문에 var만 가능하다.
요약
lazy는 리소스를 아끼고, 필요한 순간에만 계산을 수행하는 똑똑한 프로퍼티 초기화 방식이다. 한 번만 실행되고, 그 결과는 이후에도 계속 재사용된다.