깨알 개념/Kotlin

[Kotlin] 프로퍼티(Property)

interfacer_han 2024. 1. 17. 11:20

#1 필드와 프로퍼티

#1-1 프로퍼티의 개념

// Java
String name = "steve"; // Field

System.out.println("my name is" + name);
name = "kevin";

---

// Kotlin
var name : String = "steve" // Property

println("my name is" + name)
name = "kevin"

코틀린에는 필드의 역할을 프로퍼티(속성)가 수행한다. 필드는 값 그리고 그 값에 접근하는 레퍼런스의 단순한 구성이다. 프로퍼티는 여기에 그 값을 다루는 메소드 즉, Getter와 Setter를 결합한 개념이다. 하지만, 위 코드에서 자바의 필드 name과 코틀린의 프로퍼티 name은 서로 다른 게 없어보인다. 이는 프로퍼티가 보유한 Getter와 Setter가 생략되어있기 때문이다. 생략된 부분을 명시적으로 밝혀쓰면 아래과 같다.
 

#1-2 명시적 get() 및 set()

// Java
String name = "steve"; // Field

System.out.println("my name is" + name);
name = "kevin";

---

// Kotlin
var name : String = "steve" // Property
    get() = field
    set(value) {
        field = value
    }

println("my name is" + name) // println("my name is" + name.get())
name = "kevin" // name.set("kevin")

먼저, field라는 이름의 키워드부터 설명한다. 이 field는 우리가 아는 전통적인 의미의 필드가 아니며, Backing Field라는 이름으로 불린다. 백킹 필드는 자체적으로 존재할 수 없고, 오직 프로퍼티의 값을 저장하기 위해 존재하는, 프로퍼티에 종속된 저장소다. value는 field처럼 코틀린의 키워드는 아니고 그냥 매개변수다. v 라든지 argument 라든지로 고쳐써도 된다.

코틀린에서 변수 name에 값을 참조하거나 재할당할 때 각각 Getter와 Setter의 역할을 수행하는 name.get()과 name.set()라는 메소드가 자동으로 호출된다. 그러나, name.get()이나 name.set() 메소드를 아래 쪽 코드에 붙어 있는 주석들처럼 명시적으로 쓸 수 있는 건 아니다. Getter와 Setter의 동작은 그저 프로퍼티명(레퍼런스)를 호출하거나 프로퍼티명에 대입할 때 알아서 수행될 뿐이다.
 
get() 또는 set()을 프로그래머의 입맛대로 커스텀하지 않는 이상 get()과 set()을 명시적으로 볼 일은 없다. 다음 코드에서 한번 get()과 set()을 입맛대로 커스텀해보겠다.
 

#1-3 Getter와 Setter 커스텀하기, var 프로퍼티와 val 프로퍼티의 차이

class PropertyPractice1 {
    var name : String = "steve"
        get() {
            return field[0].uppercase() + field.substring(1) // 이름 첫 글자를 대문자로
        }
        set(value) {
            field = value.replace(" ", "") // 모든 공백 제거
        }
}

fun main() {
    val instance = PropertyPractice1()

    println("my name is " + instance.name)
    instance.name = "k e v i n"
    println("my name is " + instance.name)
}

/* 실행 결과:
 * my name is Steve
 * my name is Kevin
 */

PropertyPractice1.name과 같은 var 프로퍼티는 get()과 set() 둘 다 보유하고 있다. 반면, val 프로퍼티는 get()만을 보유한다. 따라서, 만약 위의 코드에서 name 프로퍼티가 val이었다면 set() 부분은 존재할 수 없다.
 

#2 private 프로퍼티에 접근하기

class PropertyPractice2 {
    private var name : String = "steve"
        get() {
            return field[0].uppercase() + field.substring(1) // 이름 첫 글자를 대문자로
        }
        set(value) {
            field = value.replace(" ", "") // 모든 공백 제거
        }

    val nameData : String
        get() = name
}

fun main() {
    val instance = PropertyPractice2()

    // ↓ Error
    // println("my name is " + instance.name)
    
    // ↓ 정상 작동
    println("my name is " + instance.nameData)
}

/* 실행 결과:
 * my name is Steve
 */

자바에선 Getter와 Setter에 public 접근제어자를 붙여서, private한 필드를 참조하거나 재설정할 때 쓰곤 했다. 코틀린에서도 프로퍼티 외부의 메소드, 예를들어 getName()을 만들어 사용할 수 있다. 이 때, 코틀린에서는 메소드가 아닌 프로퍼티를 사용할 수도 있다. 바로, 다른 public 프로퍼티의 get()이 private한 자신의 get()을 반환하게 만들면 된다.
 

#3 백킹 필드(Backing Field)의 부재(Absence) 이해하기

var name : String = "steve" // error
    get() {
        return ""
    }
    set(value) {
        return
    }

#1-2에 있는 PropertyPractice1 클래스의 name 프로퍼티를 위와 같이 고치면, 무슨 일이 일어날까? 바로, "Initializer is not allowed here because this property has no backing field"라는 에러를 뱉는다. 해석하면, "이 프로퍼티에는 백킹 필드가 없으므로 초기화가 허용되지 않습니다"다. get()과 set() 어느 쪽에도 field 키워드가 쓰이지 않을 때, 초기화 코드를 써넣으면 이 에러가 난다. 위 코드대로라면 name에 무슨 값을 할당(Setter)하든, 언제 참조(Getter)하든 "steve"라는 초기화 값에는 절대 도달할 수 없기 때문에, 초기화를 하는 의미가 없다. 그래서 메모리 절약을 위해 백킹 필드도 생성되지 않는 것이다. #1-2에서 백킹 필드는 자체적으로 존재할 수 없고, 오직 프로퍼티의 값을 저장하기 위해 존재하는, 프로퍼티에 종속된 저장소라고 했다. 저장소가 없으니 프로퍼티에 값을 넣을 수 없다. 그래서 에러가 난다.