-
[코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.3 단순한 변수 이상 인 프로퍼티)코틀린 공부/코틀린 2023. 12. 30. 13:13
4.3 단순한 변수 이상 인 프로퍼티
4.3.1 최상위 프로퍼티
클래스나 함수와 마찬가지로 최상위 수준의 프로퍼티를 정의할 수 도 있다. 이런 경우 프로퍼티는 전역 변수나 상수와 비슷한 역할을 한다.
val prefix = "Hello, " // 최상위 불변 프로퍼티 fun main(){ val name = readLine() ?: return println("$prefix$name") }
이런 프로퍼티에 최상위 가시성(public / internal / private)을 지정할 수 있다.
4.3.2 늦은 초기화
클래스를 인스턴스화할 때 프로퍼티를 초기화해야 한다는 요구 사항이 불필요하게 엄격할 때가 있었다.
그러나 어떤 프로퍼티는 클래스 인스턴스가 생성된 뒤에, 그리고 사용되는 시점보다 이전에 초기화되야 할 수도 있다.
예를 들어서 단위 테스트를 준비하는 코드나 의존 관계 주입에 의해 대입돼야 하는 프로퍼티가 이런 종류에 속한다.
이런 경우 생성자에게는 초기화되지 않은 상태라는 사실을 의미하는 디폴트 값에 대입하고 실제 값을 필요할 때 대입할 수도 있다.
class Content { var text: String? = null fun loadFile(file: File) { text = file.readText() } } fun getContentSize(content: Content) = content.text?.length ?: 0
이 예제의 단점은 실제 값이 항상 사용 전에 초기화디므로 절대 널이 될 수 없는 값이라는 사실을 알고 있음에도 늘 널 가능성을 처리해야 한다는 점이다.
코틀린에서는 이런 패턴을 지원하는 lateinit 키워드를 제공한다.
class Content { lateinit var text: String fun loadFile(file: File) { text = file.readText() } } fun getContentSize(content: Content) = content.text.length
lateinit 표시가 붙은 프로퍼티는 값을 읽으려고 시도할 때 프로그램이 프로퍼티가 초기화됐는지 검사해서 초기화되지 않은 경우 UninitializedPropertyAccessException을 던진다는 한 가지 차이를 제외하면 일반 프로퍼티와 같다. 이 특성 때문에 !! 연산자와 비슷하다.
프로퍼티를 lateinit으로 만들기 위한 조건 :
- var (O)
- 널이 아닌 타입 (X)
- Int나 Boolean 같은 원시 값이 아닌 타입 (X)
- lateinit을 정의하면서 초기화 식을 지정해 값을 바로 대입할 수 없다. (X)
코틀린 1.2부터 최상위 프로퍼티와 지역 변수에서 늦은 초기화를 사용할 수 있게 됐다.
lateinit var text: String fun readText(){ text = readLine()!! } fun main() { readText() println(text) }
다른 개선으로는 lateinit 값을 읽기 전에 lateinit 프로퍼티가 설정됐는지를 알아보는 기능이 있다. (이거는 10장에서)
4.3.3 커스텀 접근자 사용하기
코틀린에서 프로퍼티는 변수와 함수 동작을 한 선언안에 조합할 수 있는 기능이 있다. 이런 기능은 커스텀 접근자(custom accessor)를 통해 이뤄진다. 커스텀 접근자는 프로퍼티 값을 읽거나 쓸 때 호출되는 특별한 함수다.
class PersonB(val firstName: String, val lastName: String) { val fullName: String get(): String { return "$firstName $lastName" } }
게터는 프로퍼티 정의 끝에 붙이며 기본적으로 이름 대신 get이라는 키워드가 붙은 함수처럼 보인다. 하지만 get 사용 없이 자동으로 게터를 호출한다.
val personB = PersonB("John", "Doe") println(personB.fullName)
함수와 비슷하게 접근자에도 식이 본문인 형태로 사용할 수도 있다.
val myName: String get() = "$firstName $lastName"
- getter 는 파라미터가 없다. 반면 게터의 반환 타입은 프로퍼티의 타입과 같아야 한다.
- 그리고 프로퍼티와 게터 정의에서 프로퍼티의 타입을 생략하고 타입 추론에 의존해도 된다.
fullName에는 뒷받침하는 필드가 없기 때문에 클래스 인스턴스에서 전혀 메모리를 차지하지 않는다.
즉, 기본적으로 fullName은 프로퍼티 형태인 함수와 같다.
뒷받침하는 필드와 관련한 규칙은 다음과 같다.
- 명시적으로 field를 사용하는 디폴트 접근자나 커스텀 접근다
- 불변 프로퍼티의 접근자는 읽기 접근자 하나뿐이므로 앞 예제에서 fullName은 직접 뒷받침하는 필드인 field를 참조하지 않는다.
프로퍼티가 어떤 저장된 값을 사용하지만 프로퍼티에 대한 접근을 커스텀화해야 할 경우, 뒷받침하는 필드에 접근할 수 있다면 유용하다.
예를 들어 프로퍼티를 읽을 때마다 로그를 남기고 싶다면 다음과 같이 할 수 있다.
class PersonB(val firstName: String, val lastName: String, age: Int) { val age = age get() { println("access age info") return field } }
커스텀 게터가 있는 프로퍼티는 약간의 문법적인 차이에도 불구하고 파라미터가 없는 함수처럼 동작하므로, 어떤 경우 함수를 사용하고 어떤 경우 프로퍼티를 사용할지 의문??
> 공식 코틀린 코딩 관습은 값을 계산하는 과정에서
- 예외가 발생할 여지가 없거나
- 값을 계산 하는 비용이 충분히 싸거나
- 값을 캐시해 두거나
- 클래스 인스턴스의 상태가 바뀌기 전에 여러번 프로퍼티를 읽거나
- 함수를 호출해도 항상 똑같은 결과를 내는 경우
프로퍼티 사용 권장.
var로 정의하면 게터, 세터 두가지 접근자가 사용 가능하다.
class PersonC(val firstName: String, val lastName: String) { var age: Int? = null set(value) { if(value != null && value <=0){ throw IllegalArgumentException("Invalid age: $value") } field = value } }
val personC = PersonC("John", "Doe") personC.age = 20 // 커스텀 세터를 호출 println(personC.age) // 20 커스텀 게터를 호출
프로퍼티를 초기화 하면 값을 바로 뒷받침하는 필드에 쓰기 때문에 프로퍼티 초기화는 세터를 호출하지 않는다.
가변 프로퍼티에는 두가지 접근자(getter, setter)가 있으므로 두 접근자를 모두 커스텀화 하고 두 접근자가 모두 다 field 키워드를 통해 뒷받침하는 필드를 사용하지 않는 경우를 제외하면 항상 뒷받침하는 필드가 생긴다.
class PersonD(var firstName: String, var lastName: String) { var fullName:String get() = "$firstName, $lastName" set(value) { val names = value.split(" ") if(names.size != 2){ throw IllegalArgumentException("Invalid full name: '$value'") } firstName = names[0] lastName = names[1] } }
프로퍼티 접근자에 별도로가시성 변경자를 붙일 수도 있다. 외부에서 프로퍼티의 값을 변경하지 못하게 해서 객체가 불변인 것처럼 여겨지게 하고 싶을 때 이런 방식을 사용할 수 있다.
class PersonE(name: String) { var lastChanged: Date? = null private set // Person 클래스 밖에서는 변경할 수 없다 var name: String = name set(value) { lastChanged = Date() field = value } }
4.2.4 지연 계산 프로퍼티와 위임
lazy 프로퍼티는 프로퍼티를 처음 읽을 때까지 그 값에 대한 계산을 미뤄두고 싶을 때 사용한다.
val textA by lazy { File("data.txt").readText() }
fun main(){ while(true){ when (val command = readLine() ?: return){ "print data" -> println(textA) "exit" -> return } } }
main() 함수에서 사용자가 적절한 명령으로 프로퍼티 값을 읽기 전까지, 프로그램은 lazy 프로퍼티 값을 계산하지 않는다.
코틀린이 기본으로 제공하는 몇 가지 위임 객체가 있다.
- 지연 계산을 활성화하는 lazy 외에도,
- 프로퍼티를 읽거나 쓸 때마나 리스너에게 통지해주는 위임이나
- 프로퍼티 값을 필드에 저장하는 대신 맵에 저장하는 위임 등이 기본으로 제공된다.
lateinit 프로퍼티와 다르게 lazy 프로퍼티는 불변 프로퍼티가 아니다. lazy 프로퍼티는 일단 초기화된 다음에는 변경되지 않는다.
//Type 'Lazy<String>' has no method 'setValue(Nothing?, KProperty<*>, String)' and thus it cannot serve as a delegate for var (read-write property) var textA by lazy { File("data.txt").readText() }
lazy 프로퍼티는 스레드 안전(thread-safe)하다. 즉, 다중 스레드 환경에서도 값을 한 스레드 안에서만 계산하기 때문에 lazy 프로퍼티에 접근하려는 모든 스레드는 궁극적으로 같은 값을 얻게 된다.
코틀린 1.1부터는 지역 변수에도 위임을 쓸 수 있게 됐다.
fun longComputation(): Int { return 10 }
fun test4_4_1(name: String) { val data by lazy { longComputation() } val myName = name.firstOrNull() ?: return pr
위임 프로퍼티에 대해서는 스마트 캐스트를 사용할 수 없다.
fun test4_4_2(name: String) { val data by lazy { readLine() } if(data != null){ // Smart cast to 'String' is impossible, because 'data' is a property that has open or custom getter println("Length: ${data.length}") } }
'코틀린 공부 > 코틀린' 카테고리의 다른 글
[코틀린 완벽 가이드] 5.1 코틀린을 활용한 함수형 프로그래밍 (0) 2024.01.03 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.4 객체) (1) 2023.12.30 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.2 널 가능성) (0) 2023.12.29 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.1 클래스 정의하기) (0) 2023.12.29 [코틀린 완벽 가이드] 3장 함수 정의하기 (3.5 예외 처리) (0) 2023.12.29