[Kotlin in Action] 18. 인터페이스의 프로퍼티 구현, 뒷받침하는 필드(field 식별자), 접근자의 가시성 변경

반응형
728x90
반응형

인터페이스의 프로퍼티 구현

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

interface InterfaceUser {
    val nickname: String // 추상 프로퍼티
}

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

인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게터 등의 정보가 들어있지 않다. 사실 인터페이스는 아무 상태도 포함할 수 없으므로 상태를 저장할 필요가 있다면 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야한다.

 

 

예시

PrivateUser
  • 별명을 저장만 한다.
  • 주 생성자 안에 프로퍼티를 직접 선언하는 간결한 구문을 사용한다.
  • User의 추상 프로퍼티를 구현하고 있으므로 override 표시를 한다.
interface User { val nickname: String } // 추상 프로퍼티
class PrivateUser(override val nickname: String) : User

 

SubscribingUser
  • 이메일을 함께 저장한다.
  • 뒷받침필드에 저장하지 않고 매번 이메일 주소에서 별명을 계산해 반환한다.
  • 매번 호출할 때마다 substringBefore을 호출해 계산한다.
class SubscribingUser(val email: String) : InterfaceUser {
    override val nickname: String
        get() = email.substringBefore('@') // 커스텀 게터
}

 

facebookUser
  • 페이스북 계정의 ID를 저장한다.
  • 객체를 초기화하는 단계에 한번만 호출하여 뒷받침 필드에 저장한다.
  • getFacebookName()는 페이스북에 접속해서 인증을 거친 후 원하는 데이터를 가져와야하기 때문에 비용이 많이 들 수 있다.
  • 그래서 객체를 초기화하는 단계에 한번만 getFacebookName()을 호출하게 설계했다.
class FacebookUser(val accountId: Int) : InterfaceUser {
    override val nickname = getFacebookName(accountId) // 프로퍼티 초기화 식
}

 

main()
fun main() {
    println(PrivateUser("test@naver.com").nickname) // test@naver.com
    println(SubscribingUser("test@naver.com").nickname) // test
}

 

 

인터페이스의 getter, setter가 있는 프로퍼티

인터페이스는 추상 프로퍼티 뿐만 아니라 게터와 세터가 있는 프로퍼티를 선언할 수 있다. 그런 게터와 세터는 뒷받침하는 필드를 참조할 수 없다.

interface PropertyUser {
    val email: String
    val nickname: String
        get() = email.substringBefore('@') // 프로퍼티에 뒷받침하는 필드가 없다. 대신 매번 결과를 계산해 돌려준다.
}
  • email : 추상 프로퍼티
    • 하위클래스는 추상 프로퍼티인 email을 반드시 오버라이딩해야 한다.
  • nickname : 커스텀 게터가 있는 프로퍼티
    • 반면 nickname은 오버라이드 하지 않고 상속할 수 있다.

인터페이스에 선언된 프로퍼티와 달리 클래스에 구현된 프로퍼티는 뒷받침하는 필드를 원하는대로 사용할 수 있다.

 

 

뒷받침하는 필드에 접근

어떤 값은 저장하되, 그 값을 변경하거나 읽을때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만드는 방법을 살펴보자. 값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야한다.

프로퍼티에 저장된 값의 변경 이력을 로그에 남기려는 경우를 생각해보자. 그런 경우 변경 가능한 프로퍼티를 정의하되, 세터에서 프로퍼티 값을 바꿀때마다 약간의 코드를 추가로 실행해야한다.

class FieldUser(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            // 추가로직 print
            println("""
               Address was changed for $name:
                "$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
            field = value // 뒷받침하는 필드 값 변경
        }
}

 

1) field 식별자

  • 1) 게터 : field 값을 읽을수만 있다.
  • 2) 세터 : field 값을 읽거나 쓸 수 있다.

변경 가능 프로퍼티의 게터와 세터 중 한쪽만 직접 정의해도 된다.

컴파일러는 디폴트 접근자 구현을 사용하건 직접 게터나 세터를 정의하건 관계없이 게터나 세터에서 field를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다.

다만 field를 사용하지 않는 커스텀 접근자 구현을 정의한다면 뒷받침하는 필드는 존재하지 않는다.

 

main()
fun main() {
    val user = FieldUser("Alice")
    // 이 구문은 내부적으로 address 의 set()을 호출한다.
    user.address = "address1" // 프로퍼티 값을 변경한다.
}

 

결과
Address was changed for Alice:
   "unspecified" -> "address1".

 

 

 

접근자의 가시성

get, set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.

접근자의 가시성을 변경하는 방법을 살펴보자.

 

  • 비공개 세터가 있는 프로퍼티 선언하기
class LengthCounter {
    var counter: Int = 0
        private set // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 있다.
    fun addWord(word: String) {
        counter += word.length // 자신에게 추가된 모든 단어의 길이를 합산한다. (default: public)
    }
}

외부코드에서 단어 길이의 합을 마음대로 바꾸지 못하게 이 클래스 내부에서만 길이를 변경하게 만들고싶다.

그래서 기본 가시성을 가진 게터를 컴파일러가 생성하게 내버려두는 대신 세터의 가시성을 private으로 지정한다.

 

main()
fun main() {
    val lengthCounter = LengthCounter()
    lengthCounter.addWord("Hi!")
    println(lengthCounter.counter) // 3
}

 

 

반응형

Designed by JB FACTORY