코틀린을 처음 접하면, 익숙한 field 라는 용어 대신 property 라는 용어를 접하게 됩니다. property 라는 용어는 지난 파이썬 Property까지 가는 길 에서 한번 다뤘던 개념과 사실상 같다고 보아도 무방합니다. property는 단순하게 이야기하면 클래스 내에 담겨있는 field, getter , setter 를 통칭하여 의미합니다. Java 에서는 이를 private 으로 필드를 숨기고, gettersetter 를 제공함으로서 property를 흉내냅니다.

public class SomeObject {

	private int someInteger;
	private String someString;

	public int getSomeInteger() {
		return this.someInteger;
	}

	public String getSomeString() {
		return this.someString;
	}

	public void setSomeInteger(int otherInteger) {
		this.someInteger = otherInteger;
	}

	public void setSomeString(String otherString) {
		this.someString = otherString;
	}
}

코틀린에서 변수는 기본적으로 valvar 를 통해 작성합니다. valvalue 를 의미하여 한 번 초기화 된 이후로는 read-only 입니다. Javafinal 키워드를 붙인 변수라고 생각할 수 있습니다. varvariable 을 의미하며, ‘변할 수 있는 값’, 즉 ‘변수’ 이라는 의미에 걸맞게 값을 수정할 수 있습니다. 그리고 이를 property 관점에서 살펴보면 var 는 순수 property , valread-only property 라고 표현합니다. 하지만 편의상 둘 다 묶어 property 라고 표현합니다.

class SomeObject {
	val someString: String = "string"
	var someInteger: Int = 123
}

property는 겉에서 보기에는 마치 public 으로 열려있는 field 처럼 보이지만, 내부적으로 gettersetter 메소드 호출을 통해 이루어집니다.(The syntax for reading and writing of properties is like for fields, but property reads and writes are (usually) translated to ‘getter’ and ‘setter’ method calls.) 그리고 이러한 getter, setter 를 묶어서 accessor, 즉 접근자라고 부릅니다.

하지만 property를 아무런 추가 작업 없이 순수하게 getter, setter 로만 사용한다면 큰 의미가 없겠죠. 뭔가 내가 원하는대로 작업을 할 수 있어야 합니다. property 는 다음과 같은 형태를 가지고 있습니다.

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

위에 보이는 형태에서 getter, setter 부분을 우리가 직접 정의할 수 있고, 이를 custom accessor 라고 부릅니다. 다음은 Age 라는 ‘나이’에 해당하는 클래스에 대한 예제입니다.

class Age {
    val value = 20
        get() = field + 1
}

fun main() {
    val age = Age()
    println(age.value) // 21
}

위 코드를 보면, Age 클래스 내부에 value에 대한 값을 20으로 정의하였습니다. 하지만 그 값을 얻어내는 데 있어 그 값을 1 늘려서 받도록 처리하였습니다. 그렇기에 age.value 는 20 대신 21이라는 값을 얻어냄을 확인할 수 있습니다. 여기서 주의해서 볼 점은, custom getter 를 정의할 때 field 라는 키워드를 사용한다는 점입니다.

propertyfieldaccessor를 묶어서 부른다는 점을 감안하면, field라는 값이 실제로 들어있는 값에 해당함을 직감할 수 있습니다. 엄밀하게는 공식 문서에서는 이를 backing field 라고 부르며, 다음과 같이 설명합니다.

In Kotlin, a field is only used as a part of a property to hold its value in memory. Fields cannot be declared directly. However, when a property needs a backing field, Kotlin provides it automatically. This backing field can be referenced in the accessors using the field identifier

다음은 setter에 대한 예제입니다.

class Age {
    var value = 20
        get() = field + 1
        set(value) {
            field = value - 2
        }
}

위 예제에서는 custom setter가 추가 된 것 이외에도, value의 타입이 var 로 바뀌었다는 점에 주목해야 합니다. 즉, val 은 앞서 언급했다시피 final에 해당하므로, setter가 동작할 수 없습니다. 한편 var는 변경가능하므로, setter를 정의할 수 있습니다.

또한 property 에 대해 한 가지 더 알아야 할 점은, “반드시 field를 가질 필요는 없다” 라는 것입니다. 위 Age 코드에서, ‘성인인가?‘에 대한 boolean 값을 메소드가 아닌 property로 저장할 필요가 있다고 가정해보겠습니다.

class Age {
    var value = 20
        get() = field + 1
        set(value) {
            field = value - 2
        }
    val isAdult = value > 20
}

fun main() {
    val age = Age()
    println(age.value) // 21
    println(age.isAdult) // true // #1

    age.value = 10

    println(age.isAdult) // ? #2
}

위 코드를 실행했을 때, #2 에 해당하는 print문은 어떤 결과물을 내놓을까요 ? 정답은 true 입니다. 왜냐하면, instance가 생성되는 시점에 isAdult가 초기화되고, 그 값은 변하지 않기 때문입니다.

#1 에 해당하는 코드 또한 true를 반환하다는 점도 유의해서 살펴보시기 바랍니다. isAdult가 참조하고 있는 대상이 value 이고, custom getter 를 통해 가져오기 때문에 true가 반환됩니다.

이 상황에서 우리는 isAdult가 ‘매 번 새로이 계산되는 것’ 을 원합니다. 이를 value의 custom setterisAdult를 초기화하는 것 대신 property의 특성을 활용하면 다음과 같이 작성할 수 있습니다.

class Age {
    var value = 20
        get() = field + 1
        set(value) {
            field = value - 2
        }
    val isAdult: Boolean
        get() = value > 20
}

isAdult 에는 아무런 값도 할당되어 있지 않지만, getter를 통해 매 번 새로운 값을 가져올 수 있고, val 이기 때문에 setter를 정의할 수 없으므로 isAdult를 사용함에 있어 전혀 문제가 되지 않습니다.

이러한 특성을 활용해 Backing Property 라는 개념도 존재합니다. Backing Property는 외부에 드러내는 값을 드러내고, 내부로 숨기고 싶은 값을 private 접근자와 함께 이름 앞에 _(언더스코어)를 붙여줍니다. 공식문서 참고