Field 필드 / Property 프로퍼티
자바에서는 필드와 접근자 메서드를 묶어 프로퍼티라고 지칭한다. 코틀린에서는 필드에 대한 기본 접근자 메서드를 자동으로 만들어주기 때문에 필드 대신 프로퍼티라는 말을 사용한다.
Backing Field
- 프로퍼티의 값을 메모리에 저장하기 위한 필드
- 대부분의 프로퍼티에는 backing field 가 존재하지만 원한다면 프로퍼티 값을 메모리에 저장하지 않고 바로바로 계산하도록도 할 수 있다
- 프로퍼티를 선언해줄 때 다음 조건을 만족시킨다면 (메모리에 저장할 필요가 있어지기 때문에) 자동으로 backing field 가 생김
- 적어도 하나의 접근자가 기본으로 구현되는 접근자를 사용하는 경우
- 커스텀 접근자가 field 키워드를 통해 backing field 를 참조하는 경우
// 기본 접근자 사용
val counter = 0 // 커스텀 get() 구현 x
// field 키워드 사용
var counter = 0 // the initializer assigns the backing field directly
set(value) {
if (value >= 0)
field = value
// counter = value // ERROR StackOverflow: Using actual name 'counter' would make setter recursive
}
- backing field 가 없는 경우 예시
val isEmpty: Boolean
get() = this.size == 0
Backing Properties 를 그럼 언제 쓸까?
- backing field 는 getter를 통해 반환되는 값이 항상 프로퍼티의 타입과 같아야한다는 제약이 있다.
- 예를 들어 만약에 내부적으로는 mutableList 이지만 외부적으로 반환할 때는 immutableList 를 반환하고 싶다면? backing property 를 사용하자. 내부적으로는 mutableList 를 저장하고 있고 커스텀 getter 에 immutable 를 반환하도록 하면 가능하다.
- 즉 backing field 를 사용함에 제약이 걸린다면 backing property 를 사용하여 따로 값을 저장하도록 한다.
- backing property 는 언더스코어 _를 사용한다
private var _table: Map<String, Int>? = null // backing property
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // Type parameters are inferred
}
return _table ?: throw AssertionError("Set to null by another thread")
}
lateinit
프로퍼티가 non-null이지만 dependency injection 등의 이유로 초기화가 늦어지는 경우 null 타입으로 선언하기는 부담스럽다면 (널 체크가 따라오니까) 사용한다. var 프로퍼티에만 사용가능하며 초기화 전에 접근된다면 특화된 에러를 따로 던져준다.
위임 프로퍼티
class Foo {
var p: Type by Delegate() // 여기서 Delegate() 인스턴스를 위임 객체 (도우미) 라 함
}
// 컴파일 버젼
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
}
프로퍼티의 초기화를 다른 객체에게 위임하여 초기화하는 것을 위임 프로퍼티라고 한다. 위임 프로퍼티는 실제로 컴파일이 되었을 때는 위임객체의 getter, setter (예시에서는 Delegate() 의 getter, setter) 를 호출한다. 그렇기 때문에 위임 객체 (Delegate) 는 getValue 와 setValue 를 지원해주어야한다.
위임 프로퍼티 이용한 초기화 지연 by lazy()
초기화 지연에 사용하는 lazy가 왜 좋은지 backing property 와 비교해보도록 하자
1. backing property 사용
class Person (val name: String) {
private var _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this)
}
return _emails!!
}
}
val p = Person("kkw")
p.emails // 최초로 emails 를 읽을 때 단 한번만 이메일을 가져옴
이메일이라는 프로퍼티를 초기화하는건 IO 작업이기 때문에 오래걸린다. 따라서 Person 를 처음 인스턴스화 했을 때 아직까진 emails 프로퍼티는 null 상태일 수 밖에 없다. null 로 두면 null check 가 항상 따라다니기 때문에 불편함이 많은데 이를 backing property 로 우선 해결보도록 하자.
방법은 프로퍼티 자체는 non-null 로 선언해두고 getter 에 이메일 초기화 로직을 넣어두는 것이다. 이때 backing property 는 null 로 두어 처음에는 null 이었다가 최초 초기화 이후에는 항상 email 를 지니고 있도록 한다.
- backing property 는 thread-safe 하지 않다는 문제점 존재
- backing property 를 사용하면 getter 에 초기화하는 로직을 사용하게 된다.
2. 위임 프로퍼티 사용
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
- lazy 는 코틀린 관례에 맞는 시그니처의 getValue 메서드가 들어있는 개체를 반환
- lazy 를 by와 함께 사용하여 위임 프로퍼티 만들 수 있음
- 오직 한번만 초기화됨을 보장하며 thread safe 함
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue) // computed! hello
println(lazyValue) // hello
}
lazy를 사용하게 되면 좋은 점은 한번의 초기화를 보장함이다. by lazy 를 통해 값을 초기화한 이후에는 값 초기화 로직 없이 바로 값을 반환하도록 한다.
데이터베이스 example
object Users: IdTable() { // 데이터베이스 테이블
val name = varchar("name", length=50).index() // 칼럼
val age = integer("age")
}
class User(id: EntityID): Entity(id) {
var name: String by Users.name
var age: Int By Users.age
// age 초기화가 function 필요하다면 var age: Int By lazy { loadAge(...) }
}
데이터베이스에서 User 객체가 호출될 때 공통적으로 데이터베이스에서 일어나야될 로직을 위임 프로퍼티를 통해 사용 가능하다. (왜냐하면 위임 프로퍼티의 setter, getter 가 불려지기 때문) 따라서 위임 프로퍼티에 공통적으로 처리해야될 로직을 넣는다면 위임받은 객체에서 따로 처리할 필요없이 편하게 사용가능하다.
참고
https://colour-my-memories-blue.tistory.com/6
https://kotlinlang.org/docs/properties.html#delegated-properties
'🔍Kotlin' 카테고리의 다른 글
[코틀린인액션] 9. 제네릭스 (2) | 2024.03.31 |
---|---|
[코틀린인액션] 고차함수: 파라미터와 반환 값으로 람다 사용 (3) | 2024.01.07 |