이전 글에서 왜 Kotlin에서는 멤버변수가 프로퍼티라고 불리는지 알아보았다.
프로퍼티는 getter와 setter 메서드가 자동으로 생성되기 때문에 직접 작성해줄 필요가 없다. 하지만 만약 직접 작성하려면 아래와 같이 작성해줄 수 있다.
// getter, setter 자동 생성
class Person {
var name: String = "John"
}
// getter, setter 직접 작성
class Person {
var name: String = "John"
get() = field
set(value) {
field = value
}
}
두번째 Person class처럼 getter, setter를 직접 작성하면 IDE에서 redundant(불필요한 코드라는 뜻) 경고가 나타나며 삭제할 것을 권장할 것이다. 즉 첫번째 Person과 두번째 Person은 완전히 같은 코드이다.
(덧붙이자면, 이렇게 getter와 setter를 직접 작성하는 것을 커스텀 게터, 커스텀 세터라고 한다. 값을 get, set하면서 특정 연산을 수행해야 할 때 사용한다. 위의 두번째 Person class는 특정 연산을 수행하는 것이 아니고 단순히 값만 get, set하는 것이기 때문에 redundant라는 경고가 나타나는 것이다.)
여기서 field라는 이름의 식별자가 갑자기 튀어나왔다. field는 Kotlin의 Backing Field(백킹 필드)를 참조하는 식별자이다. 이 field라는 특별한 식별자를 통해 백킹 필드에 접근할 수 있다. field 식별자는 게터와 세터 내에서만 사용 가능하다.
Kotlin에서 필드는 메모리에서 프로퍼티의 값을 저장(참조형이라면 주소값 저장)하는 용도로만 사용되며, 직접적으로 선언이 불가능하다. 백킹 필드는 프로퍼티의 값을 저장하기 위한 숨겨진 필드이다.
Kotlin Properties 공식문서에 따르면, 프로퍼티가 적어도 하나의 접근자에 대해 기본 구현을 사용하거나, 커스텀 게터 혹은 세터가 field 식별자를 통해 참조하는 경우 생성된다고 한다. 이 말의 뜻은 아래 코드를 참조하자.
// 하나의 접근자에 대해 기본 구현 사용
val name: String = "John" // getter에 대한 기본 구현 사용
var age: Int = 10 // getter, setter에 대한 기본 구현 사용
// 커스텀 게터 혹은 세터에서 field 식별자 사용
val name: String = "John"
get() = field
var name: String = "Alice"
get() {
println("Getter 호출됨")
return field // 백킹 필드 사용
}
set(value) {
println("Setter 호출됨")
field = value // 백킹 필드 사용
}
// 백킹 필드 생성 X (기본 구현 X, field 식별자 사용 X)
class Person {
val age: Int
get() = 20
var name: String
get() = "default"
set(value) {
println("set name")
}
}
class Person {
var name: String
get() = name
set(value) {
name = value
}
}
fun main() {
val person = Person()
person.name = "Bob" // StackOverflowError 발생
println(person.name) // StackOverflowError 발생
}
만약 이렇게 백킹 필드를 사용하지 않고 get() = name으로 코드를 작성한다면, 프로퍼티의 get()이 다시 호출되는 것과 같으므로 무한 재귀 호출에 빠져서 스택 오버플로우 에러(StackOverflowError)가 발생한다. 그래서 백킹 필드를 이용해 필드에만 접근해야 한다. set()도 마찬가지로 set(value) { name = value }로 작성하지 않도록 주의해야 한다.
다행히도 Android Studio에서는 Recursive property accessor 경고를 표시해준다.
추가적으로, 백킹 필드를 사용하고 싶지 않다면 임시로 사용할 프로퍼티를 선언하여 게터나 세터에서 사용 가능하다. 이렇게 선언한 프로퍼티는 백킹 프로퍼티(Backing property)라고 부른다.
class Test {
private var _table: Map<String, Int>? = null // 백킹 프로퍼티
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")
}
}
인터페이스에서는 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수도 있다.
interface User {
var nickname: String
get() = "default"
set(value) {
println("set nickname")
}
}
하지만 게터와 세터는 백킹 필드를 참조할 수 없다. 즉 field 식별자를 사용할 수 없다. 백킹 필드가 있다면 인터페이스에 상태를 추가하는 셈인데 인터페이스는 상태를 저장할 수 없기 때문이다.