[Kotlin] - property

코틀린 완벽 가이드를 공부하며 작성한 글입니다.
혼자 공부하고 정리한 내용이며, 틀린 부분은 지적해주시면 감사드리겠습니다 😀

최상위 프로퍼티

클래스나 함수와 마찬가지로 최상위 수준에 프로퍼티를 정의할 수 있다.

val prefix = "Hello, "

fun main() {
    val name = readlnOrNull()  ?: return
    println("$prefix $name")
}

만약 읽어온 값에 null이 들어오면 프로그램을 종료하고, 그렇지 않으면 환영 메시지를 출력해주는 코드이다.

이런 최상위 프로퍼티에는 가시성(public/internal/private)을 지정할 수 있다.

package example.util

val prefix = "Hello, "
import example.util.prefix

fun main() {
    val name = readlnOrNull()  ?: return
    println("$prefix $name")
}

또한, 위 코드와 같이 다른 파일에 있는 최상위 프로퍼티를 임포트할 수도 있다.

늦은 초기화

특정 프로퍼티는 클래스 인스턴스가 생성된 뒤, 프로퍼티가 사용되기 전 시점에 초기화해야할 때가 있다. 이런 경우 생성자에서는 초기화되지 않은 상태라는 사실을 의미하는 디폴트 값을 대입하고, 실제 값을 필요할 때 대입할 수 있다.

class Content {
    var text: String? = null

    fun loadFile(file: File) {
        text = file.readText()
    }
}

fun getContentSize(content: Content) = content.text?.length ?: 0

위 코드는 실제 파일을 가져와 값을 읽은 다음 text 변수에 저장하는 코드이다. text에는 항상 값이 들어오게 되지만, 첫 값을 어쩔 수 없이 null로 지정해 이를 사용하는 함수에서는 꼭 null에 대한 처리를 해줘야 한다.

즉, 실제 값이 항상 사용 전에 초기화되므로, 절대 null이 될 수 없는 값임을 알면서도, 늘 null 가능성을 처리해야 한다는 것이다. 이런 상황에서 lateinit을 적용하면 더 유연한 설계가 가능해진다.

class Content {
    lateinit var text: String

    fun loadFile(file: File) {
        text = file.readText()
    }
}

fun getContentSize(content: Content) = content.text.length

프로퍼티를 lateinit으로 만들기 위해서는 몇 가지 조건을 만족해야 한다.

  1. 프로퍼티가 변경될 수 있는 지점이 여러 곳일 수 있으므로 가변(var)으로 정의
  2. 프로퍼티의 타입은 null이 아니고, Int, Boolean 같은 원시 타입이 아니어야 함
  3. lateinit를 정의하면서 초기화 식을 지정해 바로 값을 대입할 수 없다.
    • lateinit을 사용하는 의미가 없기 때문

커스텀 접근자 사용하기

java에서 밥먹듯이 사용하던 getter, setterkotlin에서도 동일하게 적용할 수 있다.

Getter

class Person(val firstName: String, val familyName: String) {
    val fullName
        // 식이 본문인 형태로도 작성 가능
        // get() = "$firstName $familyName"
        get(): String {
            return "$firstName $familyName"
        }

}

fun main() {
    val p = Person("Matin", "Kim")
    println(p.fullName)
}

java처럼 p.getFullName()으로 가져오는 것이 아닌, 프로퍼티에 접근해 값을 가져오는 과정을 get()으로 정의한다. 즉, p.fullName으로 접근하면 자동으로 get() 함수를 호출하게 되는 것이다.

만약 변수가 호출될 때마다 로깅을 하고 싶다면 아래와 같이 작성하면 된다.

class Person(val firstName: String, val familyName: String, age: Int) {
    val age = age
        get() {
            println("age = $age")
            return field
        }
    val fullName
        get() = "$firstName $familyName"

}

위와 같이 return feild를 사용할 경우 age의 값을 그대로 반환한다는 뜻이다.

java를 공부한 사람에게 가장 헷갈리는 부분은 바로 age의 사용이다. Person() 생성자 내부에 있는 age는 클래스 내부 프로퍼티를 초기화할 때만 사용할 수 있다. 즉, 생성자에 val, var을 사용해 변수를 초기화하면 클래스 내부에 정의가 되는 변수이고, 그렇지 않을 경우 클래스 내부 프로퍼티에서만 접근이 가능하다.

class Test(val param1: Int, val param2: Int, param3: Int) {
    fun printParam1() {
        println("param1 : $param1")
    }
    fun printParam2() {
        println("param1 : $param1")
    }
   
    val param4 = param3
    fun printParam3() {
        // 함수 내부에서 param3에 접근 불가
        println("param1 : ${this.param4}")
    }
} 

위 코드와 같이 param1, param2는 클래스 변수이기에 내부 어디서든 접근이 가능하지만, param3은 단순히 생성자 변수기 때문에 클래스 내부 함수에서는 접근하지 못하고, 클래스 내부 프로퍼티를 정의하는 용도로만 사용이 가능하다.

Setter

프로퍼티의 Setter의 파라미터는 무조건 단 하나이며, value라는 네이밍을 많이 사용한다.

class Person(val firstName: String, val familyName: String, age: Int) {
    val fullName
        get() = "$firstName $familyName"

    var age = age
        set(value) {
            if (value != null && value <= 0)
                throw IllegalArgumentException("Invalid age: $value")
            field = value
        }

}

fun main() {
    val p = Person("Matin", "Kim", 50)
    println(p.fullName)
    p.age = -1
}

Getter와 동일하게 클래스 프로퍼티에 접근해 값을 수정하는 행위를 하면 set() 함수를 호출하게 된다. 위 코드를 기준으로 -1이라는 값이 set함수의 파라미터인 value에 들어가게 되는 것이다.

가시성 변경자

java에서의 Setter는 위험하기에 private로 지정하고, 의미있는 메소드 이름을 통해 수정하는 방식을 사용했다. 이처럼 get(), set() 함수에도 가시성을 추가할 수 있다.

class Person(val firstName: String, val familyName: String, age: Int) {
    var lastChanged: Date? = null
        private set
   
    var age = age
        set(value) {
            if (value != null && value <= 0)
                throw IllegalArgumentException("Invalid age: $value")
            lastChanged = Date()
            field = value
        }
}
fun main() {
    val p = Person("Matin", "Kim", 50)
    println(p.fullName)
    p.age = -1
    // 에러 발생 : Cannot assign to 'lastChanged': the setter is private in 'Person'
    p.lastChanged = Date()
}

위와 같이 private set으로 지정할 경우 클래스 내부에서만 접근(수정)이 가능하고, 외부에서 접근해서 수정할 경우 컴파일 에러가 발생한다.

지연 계산 프로퍼티와 위임

lateinit은 변수의 값을 나중에 지정해주기 위한 변경자였다면, by lazy는 어떤 프로퍼티를 처음 읽을 때까지 그 값에 대한 계산을 미뤄두는 것이다.

import java.io.File

val text by lazy {
   File("data.txt").readText()
}

fun printFile() {
   println(File("data.txt").readText())
}

fun write(content: String) {
   File("data.txt").writeText(content)
}

fun main() {
   while (true) { 
      print("input : ")
      when (val cmd = readlnOrNull() ?: return) {
         "p1" -> println(text)
         "p2" -> println(text)
         "w" -> write(readln())
         "e" -> return
      }
   }
}

w를 통해 값을 수정하기 전의 p1, p2는 동일한 값을 출력하지만, 값을 수정한 후 p1, p2를 출력하면 p2만 변경된 값으로 출력하는 것을 볼 수 있다.

input : p1
abc
input : p2
abc
input : w
abc123
input : p1
abc
input : p2
abc123
input : e

즉, by lazy로 정의할 경우 값을 읽기 전까지 lazy 프로퍼티의 값을 계산하지 않으며, 초기화가 된 이후 프로퍼티의 값은 필드에 저장된다. 그 이후로는 값을 읽을 때마다 저장된 값을 읽게 된다.

이러한 이유로 by lazy를 사용할 경우 한 번 저장된 값이 변하지 않는다.

태그:

카테고리:

업데이트:

댓글남기기