"TDD, 클린 코드 with Kotlin 7기" 블랙잭 미션을 진행하다가 MutableList를 사용한 프로퍼티에서 backing-properties를 사용해 보라는 피드백을 받았다. 이번 기회에 코틀린에서 생소했던 개념인 Backing fields, Backing properties에 대해 알아보려고 한다.
field vs property
필드와 프로퍼티의 차이점은 무엇일까?
공식 문서에 따르면 필드란 프로퍼티의 일부로서 단지 메모리에 값을 저장하기 위한 용도로만 사용하는 것을 말한다.
In Kotlin, a field is only used as a part of a property to hold its value in memory.
자바에선 상태를 저장하기 위한 기본 개념이 필드이지만, 코틀린에서는 프로퍼티다. 코틀린의 프로퍼티란 필드와 접근자를 합친 것을 의미한다. 코틀린에서 프로퍼티를 선언하는 키워드는 val, var 2가지가 존재한다. val은 정적인 프로퍼티를 의미하며 게터가 자동으로 생성되고, var은 동적인 프로퍼티를 의미하며 게터와 세터가 자동으로 생성된다.
코틀린에서 필드는 직접적으로 선언할 수 없으며, 프로퍼티가 Backing-field가 필요할 때 자동으로 생성한다.
Fields cannot be declared directly.
However, when a property needs a backing field, Kotlin provides it automatically.
Backing fields
프로퍼티로도 충분한데, 코틀린에선 왜 뒷받침하는 필드(Backing fields)라는 개념이 나온 걸까?
이에 대한 해답으로 공식 문서에는 한가지 예시가 나온다. 편의상 Book 클래스를 임의로 만들었다.
class Book {
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
}
}
}
코틀린에서 프로퍼티에 값을 할당할 때는 내부적으로 세터를 호출한다. 다시 말해, book.counter = 2 을 호출하면 코틀린 내부에선 book.setCounter(2)을 호출한다. 이때 필요한 것이 바로 뒷받침하는 필드(Backing fields)이다. 뒷받침하는 필드가 없다면 값을 할당할 때 세터를 재귀적으로 호출함으로써 StackOverFlow 에러가 발생하기 때문이다.
class Book {
var counter = 0
set(value) { counter = value } // book.setCounter(value)를 재귀적으로 무한히 호출
}
프로퍼티 자기 자신인 counter 대신 코틀린에서 자동으로 제공하는 field라는 식별자를 사용하면 정상적으로 동작한다.
class Book {
var counter = 0
set(value) { field = value }
}
Backing properties
Backing properties는 뒷받침하는 프로퍼티라는 뜻이다. 변경 가능한 프로퍼티를 외부에서 변경이 불가능하도록 막고 싶을 때, Backing properties을 사용할 수 있다. 변경 가능한 값을 외부로부터 보호함으로써 유지보수성을 향상시킬 수 있다.
Backing properties를 사용하면 클래스 내 같은 상태를 의미하는 2개의 프로퍼티가 공존하게 된다. 이때 Mutable한 프로퍼티는 private 접근 제어자를 붙이고, Mutable하지 않은 프로퍼티는 외부에 공개한다.
공식 문서의 코딩 컨벤션에 따르면 Backing properties는 프로퍼티명 앞에 언더바(_)를 붙인다.
use an underscore as the prefix for the name of the private property
아래는 Backing properties를 사용한 예시이다. 외부에서 C.elementList에 값을 할당하려고 시도해도 반환 타입이 MutableList가 아닌 List이기 때문에 불가능하다.
class C {
private val _elementList = mutableListOf<Element>()
val elementList: List<Element>
get() = _elementList
}
Backing perperties는 객체 내부의 값을 외부로부터 보호한다는 점에서 방어적 복사와 동일한 목적을 가진다.