Skils/Kotlin

[Kotlin] 프로퍼티(Property)

재한 2022. 9. 2. 16:12

👀학습목표

앞에서 배운 내용을 토대로는 프로퍼티는 어떤 클래스 인스턴스나 파일 퍼사드에 묶인 변수이며, 자바 필드와 비슷하다고 설명했다. 하지만 일반적으로 코틀린 프로퍼티는 일반 변수를 넘어서, 프로퍼티 값을 읽거나 쓰는 법을 제어할 수 있는 훨씬 더 다양한 기능을 제공한다.

이번 글에서는 단순하지 않은 프로퍼티의 의미에 대해서 다룰것이다.

 

👀학습하기 전에 알면 도움 되는 용어 정리

  • 필드(field) : 클래스 멤버 변수
  • 프로퍼티(Property) : 필드와 게터 세터를 한 번에 묶어서 부르는 단어

📕최상위 프로퍼티

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

이런 경우 프로퍼티는 전역 변수나 상수와 비슷한 역할을 한다.

import kotlin.*
val prefix="Hello ," //최상위 프로퍼티
fun main(){
    val name=readLine()?:return //엘비스 연산자
    println("$prefix $name")
}

이런 프로퍼티에 상위 가시성(public/internal/private)을 지정할 수 있다. 그리고 임포트 디렉티브에서 최상위 프로퍼티를 임포트 할 수 있다.

 

📕늦은 초기화

클래스를 인스턴스 화할 때 프로퍼티를 초기화해야 한다는 요구 사항이 불필요하게 엄격할 때가 있다. 어떤 프로퍼티는 클래스 인스턴스가 생성된 뒤에, 그러나 해당 프로퍼티가 사용되는 시점보다는 이전에 초기화돼야 할 수 있다.

이런 경우 생성자에서는 초기화되지 않은 상태라는 사실을 의미라는 디폴트 값[ex) null]을 대입하고 실제 값을 필요할 때 대입할 수도 있다.

import java.io.File

class Content{
    var text: String? = null
    fun loadFile(file : File){
        text=file.readText()
    }
}
fun  getContentSize(content : Content)= content.text?.length?: //널 가능성 체크

여기서 loadFile()은 다른 곳에서 호출되며 어떤 파일의 내용을 모두 문자열로 읽어온다고 가정하자.

이 예제의 단점은 실제 값이 항상 사용 전에 초기화되므로 절대 날이 될 수 없는 값이라는 사실을 알고 있음에도 불구하고 늘 널 가능성을 처리해야 한다는 점이다.

--> 코틀린은 이러한 불편한 점을 개선시키기 위해서 lateinit 키워드를 제공한다.

import java.io.File

class Content{
    lateinit var text: String
    fun loadFile(file : File){
        text=file.readText()
    }
}
fun  getContentSize(content : Content)= content.text.length?:0

lateinit 표시가 붙은 프로퍼티는 값을 읽으려고 시도할 때 프로그램이 프로퍼티가 초기화됐는지 검사해서 초기화되지 않은 경우 UninitializePropertyAccessException을 던진다는 한 가지 차이점을 제외하면 일반 프로퍼티와 같다.

 

💡프로퍼티를 lateinit으로 만들기 위한 조건

  1. 프로퍼티가 코드에서 변경될 수 있는 지점이 여러 곳일 수 있으므로 프로퍼티를 가변 프로퍼티(var)로 정의해야 한다.
  2. 프로퍼티의 타입은 날이 아닌 타입어아야 하고 Int나 Boolean 같은 원시 값을 표현하는 타입이 아니어야 한다.
    • 내부에서 lateinit 프로퍼티는 초기화되지 않은 상태를 표현하기 위해 null을 사용하는 널이 될 수 있는 값으로 표현되기 때문이다.
  3. lateinit 프로퍼티를 정의하면서 초기화 식을 지정해 값을 바로 대입할 수 없다.
    • 이런 대입을 허용하면 lateinit을 지정하는 의미가 없기 때문이다.
  4. 최상의 프로퍼티와 지역변수에서 늦은 초기화(lateinit)를 사용할 수 있다.

📕커스텀 접근자

변수와 함수의 동작을 선언 안에 조합할 수 있는 기능을 하며 프로퍼티 값을 읽거나 쓸 때 호출되는 특별한 함수

get과 set을 내가 임의로 커스터마이징 해서 사용.

class Person(val firstName : String, val familyName : String){
    val fullName: String
    get(): String{
        return "$firstName $familyName"
    }
}

게터는 프로퍼티 정의 끝에 붙으며 기본적으로 이름 대신 get이라는 키워드가 붙은 함수처럼 보인다. 하지만 이런 프로퍼티를 읽으면 자동으로 게터를 호출한다.

 

함수와 비슷하게 접근자에도 식이 본문인 형태를 사용할 수 있다.

val fullName: String
    get()="$firstName $familyName"

게터에는 파라미터가 없다. 반면 게터의 반환 타입은 프로퍼티의 타입과 같아야 한다.

class Person(val firstName : String, val familyName : String){
    val fullName: Any
        get(): String { //프로퍼티와 게터의 반환타입이 달라서 컴파일 오류가 발생한다.
            "$firstName $familyName"
        }
}

프로퍼티에 명시적으로 field를 사용하는 디폴트 접근자나 커스텀 접근자가 하나라도 있으면 (프로퍼티의 값이 저장되어 있는) 뒷받침하는 필드(backing field)가 생성된다.

불변 프로퍼티의 접근자는 읽기 접근자 하나뿐이므로 앞 예제에서 fullName은 직접 뒷받침하는 필드인 field를 참조하지 않는다는 사실을 쉽게 알 수 있다. 

 

뒷받침하는 필드 참조는 field라는 키워드를 사용하며 접근자의 본문 안에서만 유용하다.

프로퍼티에 뒷받침하는 필드가 없다면 필드를 초기화할 수 없다.

--> 초기화는 기본적으로 클래스를 인스턴스 화할 때 값을 뒷받침하는 필드에 직접 대입하기 때문이다.

계산에 의해 값을 돌려주는 프로퍼티의 경우 뒷받침하는 필드가 필요하지 않다.

[위 코드에서 fullName]

💡var로 정의하는 가변 프로퍼티에서의 두 가지 접근자

  • 게터(get) : 값을 읽어줌.
  • 세터(set) : 값을 설정해줌.
fun main(){
    val person=Person("Lee","Jaehan")
    person.age=24
    println(person.age)
}
class Person(val firstName : String, val familyName: String){
    var age: Int?=null
    set(value){
        if(value !=null && value<=0){
            throw java.lang.IllegalArgumentException("Invaild age : $value")
        }
        field=value
    }
}

프로퍼티 세터의 파라미터는 단 하나이며, 프로퍼티 자체의 타입과 같아야 한다.

파라미터의 타입을 미리 알 수 있기 때문에 세터에서는 파라미터 타입을 생략한다.

 

🛑프로퍼티를 초기화하면 값을 바로 뒷받침하는 필드에 쓰기 때문에 프로퍼티 초기화는 세터를 호출하지 않는다.

 

프로퍼티 접근자에 별도로 가시성 변경자를 붙일 수도 있다.

class Person(name:String) {
    var lastChange: Date? = null
        private set //Person 클래스 밖에서는 변경할 수 없다.
    var Name: String = name
        set(value) {
            lastChange = Date()
            field = value
        }
}

 

lateinit 프로퍼티의 경우 항상 자동으로 접근자가 생성되기 때문에 프로그래머가 직접 커스텀 접근자를 정의할 수 없다.

주생 성자 파라미터로 선언된 프로퍼티에 대한 접근자도 지원하지 않는다.

 

📕지연 계산 프로퍼티와 위임

💻lazy 프로퍼티

A라는 프로퍼티를 처음 읽을 때까지 그 값에 대한 계산을 미루고 싶을 때 사용

import java.io.*
val text by lazy{
    File("data.txt").readText()
}

fun main(){
    while(true){
        when(val command=readLine() ?: return){
            "print data"->println(text)
            "exit"->return
        }
    }
}
  • lazy 다음에 오는 블록 안에는 프로퍼티를 초기화하는 코드를 지정함.
  • main() 함수에서 사용자가 적절한 명령으로 프로퍼티 값을 읽기 전까지, 프로그램은 lazy 프로퍼티의 값을 계산하지 않는다.
  • 초기화가 된 이후 프로퍼티의 값은 필드에 저장되고, 그 이후로는 , 프로퍼티 값을 읽을 때마다 저장된 값을 읽게 된다.

정리

  • 최상위 프로퍼티는 전역 변수나 상수와 비슷한 역할을 한다.
    • 가시성을 지정할 수 있음.(Public/internal/private)
  • 늦은 초기화(lateinit)
    • 값을 읽으려고 시도할 때 프로그램이 프로퍼티가 초기화됐는지 검사한다.
  • lateinit으로 프로퍼티를 만들기 위한 조건
    • var로 정의되어야 함.
    • 날이 아닌 타입 이어야 하고 Int나 Boolean 같은 원시 값을 표현하는 타입이 아니어야 함.
    • 초기화 식을 지정해 값을 바로 대입할 수 없다.
  • 커스텀 접근자
    • 값을 읽거나 쓸 때 호출되는 특별한 함수 
    • val에는 값을 읽기 위한 게터(get)가 있다.
    • var에는 값을 읽기 위한 게터(get)와 값을 설정하기 위한 세터(setter)라는 두 가지 접근자가 있다.
      • 게터에는 파라미터가 없지만, 세터에는 단 하나의 파라미터를 가진다.
      • 둘 다 반환 타입은 프로퍼티의 타입과 같아야 한다.