본문 바로가기

Kotlin/Basic

[Kotlin] 코틀린 프로퍼티와 필드(Properties and Fields)

🥅  들어가며

N2T로 업로드 했던 글들을 하나씩 다시 보고 있는데, 티스토리에서 전부 코드 블럭이 뭉개져서 업로드 되는 것을 발견했다..

천천히 하나하나 전부 티스토리 에디터로 다시 수정작업을 진행할 예정이다.(ㅠㅠ)

 

이번 글의 목표는 다음과 같다.

프로퍼티와 필드(백킹 필드)에 대한 내용 이해하기

그럼 긴말 없이 바로 시작해 보자.

 

 

 

✍🏼 프로퍼티(Properties)


코틀린에서는 두가지 방법으로 프로퍼티를 선언할 수 있다.

변경이 가능한 (= Mutable) 변수로 var

값을 읽기만 가능(read only)하고, 변경할 수 는 없는( = Immutable) 변수로 val

class Address{
    var name : String = "Park Sang Hyun"
    var street: String = "Seoul somewhere"
    var city: String = "Seoul"
    var state: String? = null
    var zip: String = "123456"
}

프로퍼티를 사용할 때에는 이름만 명시하면 된다.

fun copyAddress(address : Address): Address { 		
    val result = Address() 		
    result.name = address.name // 접근자 호출 		
    result.street = address.street 		
    ... 		
    return result 
 }

여기까지는 어디서든 흔하게 알 수 있는 내용이다.

그렇다면, 프로퍼티의 심화 내용인 게터와 세터(getters and setters)를 보자.

 

🤲🏼 게터와 세터(Getters and Setters)


사실, 코틀린에서 게터와 세터를 사용할 일은 크게 많지 않다.(아직 내 견문이 짧아서 그럴 수도 있음)

하지만, 안드로이드 ViewModel을 사용하며, ViewModel의 Private field의 커스텀 getter를 사용하는 일이 최근에 잦아져서 이참에 정리해 둔다.

 

프로퍼티를 선언하는 full syntax는 다음과 같다. 프로퍼티란 필드와 accessor 메서드를 자동으로 생성해 주는 문법을 의미한다.

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

getter와 setter를 사용하는 것은 사용자의 선택에 따른 옵션이다.

 

만약 아래와 같이 초기화 값이나 getter/setter의 리턴 타입으로 프로퍼티의 타입을 유추할 수 있으면 프로퍼티의 타입은 생각해도 된다.

var allByDefault: Int? //❗️에러: 명시적인 초기화를 해주어야만 함. 디폴트 getter/setter가 포함 
var initialized = 1 // Int 타입이며, default getter, setter를 가진다.

 

값을 변경할 수 없는 프로퍼티 선언하는 방식은 변경 가능한 프로퍼티를 선언하는 방식과 미세하게 다르다.

첫번 째로, 변경할 수 없는 프로퍼티 선언은 val키워드로 해야한다.

val simple: Int? // Int 타입이며 디폴트 getter를 가짐, 생성자에서 초기화해주어야 한다. 
val inferredType = 1 // Int 타입이며 디폴트 getter를 가진다.

 

프로퍼티에 커스텀 접근자를 지정할 수도 있다.

getter는 프로퍼티에 접근할 때 마다 사용한다. 다음은 커스텀 getter의 예시이다.

val isEmpty: Boolean
	get() = this.size == 0

탁 이것만 보면 이해가 잘 안갈 수 있다. 실제로 처음봤을때 이게 뭐지? 싶었음

다음 코드를 통해 사용방법을 이해해 보자.

class square(val height:Int, val width:Int){
    val isSqure: Boolean
    	get() = height == width 
}  
    
fun main() {
	println(square(8,8).isSqure) 
} 

// >> true

만약 반환타입을 지정한다면, 게터의 반환타입은 반드시 프로퍼티의 타입과 같아야 한다.

커스텀 게터가 있는 프로퍼티는 약간의 문법적인 차이에도 불구하고 파라미터가 없는 함수처럼 동작하므로, 어떤 경우 함수를 사용하고 어떤 경우 프로퍼티를 사용할지에 대한 의문이 떠오를 수 있다. 공식 코틀린 코딩 관습은 값을 계산하는 과정에서 예외가 발생할 여지가 없거나, 값을 계산하는 비용이 충분히 싸거나, 값을 캐시해 두거나, 클래스 인스턴스의 상태가 바뀌기 전에는 여러 번 프로퍼티를 읽거나, 함수를 호출해도 항상 똑같은 결과를 내는 경우에는 함수보다 프로퍼티를 사용하는 쪽을 권장한다.

 

커스텀 setter를 지정할 수도 있다.

setter는 프로퍼티에 값을 할당할 때 사용한다. 프로퍼티 setter의 파라미터는 단 하나이며, 타입은 프로퍼티 자체의 타입과 같아야한다. 보통 파라미터 타입을 미리 알 수 있기 때문에, 세터에서는 파라미터 타입을 생략한다.

관례적으로 setter파라미터의 이름은 value로 사용한다. 원하면 선호하는 다른 이름도 가능하지만, 관례를 따르는게 가독성이 좋지 않을까.

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

아래는 커스텀 setter의 예시이다.

var stringRepressentation: String 		get = this.toString() 		set(value) { 				setDataFromString(Value)  // String을 parse하여 다른 프로퍼티에 값을 할당한다. 		}

이것또 딱 보면 애매하다. 하지만 다음 코드를 보자.

class User (val name: String){     var address: String = "unspecified"         set(value: String) {             println("Address was changed for $name: $field -> $value")             field = value // 안해주면 값 안바뀐다.         } }  fun main(){     val a = User("Park")     a.address = "seoul" }
💡
코틀린 1.1부터는 getter로 타입을 추론할 수 있다면 다음과 같이 선언을 생략할 수도 있게 되었다.
var isEmpty get() = this.size ==  0 // Boolean 타입임을 추론할 수 있음.

하지만, 코드는 혼자만 보는게 아닌 경우가 많다. 관례를 따라 저기 저 위처럼 작성하자.

 

💡
📌 용어정리

필드(field) : 클래스 내의 맴버변수

프로퍼티(property) : 필드와 게터 세터를 한데 묶어서 부르는 단어

 

🎢 Backing Fields(뒷바침하는 필드)


코틀린 클래스에서는 필드를 직접적으로 선언할 수 없다. 따라서 값을 저장하는 동시에 로직을 실행할 수 있게 하기위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드(backing field)가 있어야한다.

접근자의 본문에서는 field 식별자를 이용하여 backing field에 접근할 수 있다. getter에서는 field값을 읽을 수만 있고, setter에서는 field값을 읽거나 쓸 수 있다.

var counter = 0 // 이 initializer는 backing field를 직접 할당한다. 		set(value) { 				if(value >= 0) field = value 		}

여기서 사용된 field 식별자는 이처럼 프로퍼티의 접근자에서만 사용될 수 있다. backing field는 최소한 하나 이상의 접근자로 기본 구현을 사용하거나, 커스텀 접근자가 field 식별자를 이용해 backing field를 참조할 때 생성된다. 예를 들어 아래와 같은 예시에서는 backing field가 없다.

// example 1 val isEmpty: Boolean 		get() = this.size == 0  // example 2 var name: String // get, set 		get() { 				return "User" 		}

 

🎢 Backing Properties


만약 위에서 언급한 backing field의 scheme에 맞지 않는 작업을 하려고 한다면, backing field가 아닌 bakcing property가 된다.

private var _table: Map<String, Int>? = null public val table: Map<String, Int> 	get() { 		if (_table == null){ 				_table = HashMap() // 타입 파라미터가 유추된다. 		} 		return _table? : throw AssertionError("Set to null by another thread") 	}

JVM에서는 private 프로퍼티와 디폴트 getter/setter에 대한 접근이 최적화 되어있으므로, 이 경우에는 함수 호출 오버헤드가 발생하지 않는다.

 

📍 인터페이스에 선언된 프로퍼티 구현하기


코틀린에서는 아래와 같이 인터페이스에 추상 프로퍼티를 선언한다.

interface User{ 		val nickname: String }

이는 User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 한다는 뜻이다. 인터페이스는 상태를 포함할 수 없으므로 상태를 저장하기 위해서는 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야한다.

아래 예시를 통해 인터페이스를 구현하는 세가지 방법을 확인하자.

interface User{     val nickName:String }  // 1) 주 생성자 안에 프로퍼티를 직접 선언한다. class PrivateUser(override val nickname: String) : User  // 2) 커스텀 게터로 프로퍼티를 설정한다. class SubscribingUser (val email: String) : User{     override val nickname : String     	get() = email.substringBefore('@') }  // 3) 초기화 식으로 프로퍼티 값을 설정한다. class FacebookUser(val accountId:Int):User{     override val nickname = getFacebookName(accountId) }

2번 SubscribingUser와 3번 FacebookUser를 구현하는 방법의 차이에 주의하자.

SubscribingUser의 nickName프로퍼티는 매번 호출될때마다 substringBefore를 호출하여 계산하는 커스텀 게터를 활용한다.

FacebookUser의 nickName프로퍼티는 객체를 초기화할 때 계산한 데이터를 뒷받침하는 필드(backing fields)에 저장했다가 불러오는 방식이다.

 

인터페이스는 추상 프로퍼티 뿐만이 아니라 getter와 setter가 있는 프로퍼티를 선언할 수도 있다.

이 때의 getter와 setter는 backing field를 참조할 수 없다. 왜냐하면 backing field가 있다면 인터페이스에 상태를 추가하는 셈인데, 인터페이스는 상태를 저장할 수 없기 때문이다. 인터페이스에 선언된 프로퍼티와 달리, 클래스에 구현된 프로퍼티는 backing field를 원하는대로 사용할 수 있다.

 

아직도 getter와 setter가 어렵다.

실사용 에제를 몇개 첨부해서 나중에도 쉽게 이해할 수 있게 해야겠다.

 

getter 사용 예제


custom gettet 사용 시에도, 주의할 점은 있다.

var로 선언하는 프로퍼티의 경우, 반드시 초기화를 해줘야 한다.

val은 안해줘도 됨ㅎ

같은 코드지만, 선언 별로 작성한다. 먼저 var다.

class Person(val firstName: String, val familyName: String) {   var fullName: String = "well"     get(): String {       return "$firstName $familyName"     } }  fun main(){     val a = Person("SangHyun","Park")     println(a.fullName) } // >> SangHyun Park

이제 val다

class Person(val firstName: String, val familyName: String) {   var fullName: String = "well"     get(): String {       return "$firstName $familyName"     } }  fun main(){     val a = Person("SangHyun","Park")     println(a.fullName) }

여기서 초기화를 하면, 오류가 발생한다.

“Initializer is not allowed here because this property has no backing field” 즉, backing field가 없음으로 초기화를 할 수 없다는 것이다. 왜? val은 불변 프로퍼티이고 읽기 접근자하나 뿐이니깐!

 

'Kotlin > Basic' 카테고리의 다른 글

[Kotlin] Scope Functions 정리  (0) 2023.01.02
[Kotlin] 클래스 계층 정의  (0) 2022.12.31
[Kotlin basic] forEach  (0) 2022.12.31
[Kotlin Tip] 대소문자 변환  (0) 2022.12.31
[Kotlin Tip] 코틀린 변수 타입 출력하기  (0) 2022.12.31