Kotlin

[Kotlin] 인터페이스의 프로퍼티 & 접근자에서 backing field 에 접근, 접근자의 가시성

sh1mj1 2024. 1. 9. 13:18

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

 

이미지 출처           https://commons.wikimedia.org/wiki/File:Kotlin_Icon.svg

 

인터페이스에 선언된 프로퍼티

코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.

interface User {
    val nickname: String
}

`User` 인터페이스를 구현하는 클래스는 `nickname` 의 값을 얻을 수 있는 방법을 제공해야 한다.

 

인터페이스에 있는 프로퍼티 선언에는 어떠한 상태를 포함할 수 없다.

즉, backing field 에 대한 정보가 들어있지 않다. 

프로퍼티 선언을 가진 인터페이스 구현

인터페이스를 구현하는 방법을 코드로 보자

class PrivateUser(override val nickname: String) : User

class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}

class FacebookUser(val accountId: Int) : User {
    override val nickname: String = getFacebookName(accountId))
}

fun getFacebookName(accountId: Int): String {
    // 페이스북 접속, 인증, 원하는 데이터를 리턴하는 코드라고 가정
    return "sh1mj1"
}

모두 `nickname` 프로퍼티를 `override` 하고 있다.

backing field 를 갖도록 하거나, getter 접근자를 구현해주고 있다.

  • `PrivateUser`
    • 주 생성자 안에 프로퍼티를 직접 선언하는 간결한 구문 사용
  • `SubscribingUser`
    • 커스텀 getter 로 추상 프로퍼티를 구현
    • backing field 에 값을 저장하지 않고 매번 `email` 에서 `nickname` 을 계산하여 리턴
  • `FacebookUser`
    • initializer(초기화 식)으로 `nickname` 을 초기화.
    • 객체를 초기화하는 단계에서 한번만 `getFacebookName` 을 호출하도록 설계함
      (계산한 데이터를 backing field 에 저장했다가 불러오는 방식). 

테스트 코드

@Test
fun testPrivateUser() {
    val privateUser = PrivateUser("sh1mj1")
    assert(privateUser.nickname == "sh1mj1")
}

@Test
fun testSubscribingUser() {
    val subscriber = SubscribingUser("sh1mj1@kotlinlang.org")
    assert(subscriber.nickname == "sh1mj1")
}

@Test
fun testFacebookUser() {
    val facebookUser = FacebookUser(9938)
    assert(facebookUser.nickname == "sh1mj1")
}

인터페이스에도 getter & setter 가 있는 프로퍼티 선언 가능

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

물론 그런 getter, setter 는 backing field 를 참조할 수 없다.

위에서 말했듯 인터페이스는 상태를 가질 수 없다!

interface User2 {
    val email: String
    val nickname: String
        get() = email.substringBefore('@')
}

이 인터페이스를 구현하는 클래스에서는 추상 프로퍼티인 `email` 을 반드시 `override` 해야 한다. 

반면에 `nickname` 은 `override` 하지 않아도 된다.

class SubscribingUser2(override val email: String) : User2

@Test
fun testSubscribingUser() {
    val subscribingUser2 = SubscribingUser2("sh1mj1@kotlinlang.org")
    assert(subscribingUser2.nickname == "sh1mj1")
}

getter 와 setter 에서 backing field 에 접근

위에서 프로퍼티는 값을 저장하는 프로퍼티 형식과 커스텀 접근자(getter,setter)에서 매번 값을 계산하는 프로퍼티 형식에 대해 살펴보았다.

이제는 두 형식을 조합해서 어떤 값을 저장하되, 그 값을 write 하거나 read 할 때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만들어보자.

값을 저장하는 동시에 로직을 실행하려면 접근자 안에서 프로퍼티의 backing field 에 접근할 수 있어야 한다.

 

프로퍼티에 대해 set 할 때, 즉 프로퍼티 값을 바꿀 때마다 프로퍼티에 저장된 값의 변경 사항을 출력하고자 한다.

class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println(
                """
                Address was changed for $name:
                "$field" -> "$value".
            """.trimIndent()
            )
            field = value
        }
}

위 코드에서처럼 접근자의 바디에서 `field` 라는 특별한 키워드를 통해 backing field 에 접근할 수 있다.

getter 에서는 `field` 값을 읽을 수만 있꼬, setter 에서는 `field` 값을 읽고, 쓸 수 있다.

 

테스트 코드

@Test
fun testUser() {
    val user = User("sh1mj1")
    user.address = "동대문구 서울"
}

// printed
/*
Address was changed for sh1mj1:
"unspecified" -> "동대문구 서울".
*/

 

`var` 프로퍼티의 getter 와 setter 중 하나만 직접 정의해도 된다. 

위 코드의 `address` 의 getter 는 필드 값을 그냥 리턴해주는 기본 getter 이다. 

 

컴파일러는 디폴트 접근자를 사용하던지, 혹은 직접 getter 나 setter 를 정의하던지에 관계없이 getter 나 setter 에서 field 를 사용하는 프로퍼티에 대해 backing field 를 생성해준다.

하지만 `field` 를 사용하지 않는 커스텀 접근자만을 사용하는 프로퍼티에 대해서는 backing field 는 생성되지 않는다. 

접근자의 가시성

접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다.

하지만 원한다면 get 이나 set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.

class LengthCounter {
    var counter: Int = 0
        private set

    fun addWord(word: String) {
        counter += word.length
    }
}

이렇게 하면 `counter` 는 이 클래스 내에서만 값을 변경할 수 있다.(set)

그리고 `counter` 의 가시성은 public 이므로 클래스 외부에서도 읽을 수는 있다(get). 

 

직접 테스트해보자.

@Test
fun testLengthCounter(){
    val lengthCounter = LengthCounter()
    lengthCounter.addWord("Hi!")
    assert(lengthCounter.counter == 3)
//  lengthCounter.counter = 3 // Cannot assign to 'counter': the setter is private in 'LengthCounter'
}

실제로 `counter` 를 테스트 코드에서 get 할 수 있지만, set 할 수 없는 것을 확인할 수 있다.

 

지금까지의 내용이 프로퍼티에 대해 모든 것은 아니다.

lateinit 변경자를 프로퍼티에 지정할 때, 또 delegated property(위임 프로퍼티) 등은 또 나중에 다루겠다.