본문 바로가기
Kotlin

[Effective Kotlin] 16 - 프로퍼티는 동작이 아니라 상태를 나타내야 한다.

by 매운돌 2023. 5. 13.

Kotlin의 프로퍼티는 Java의 필드와 비슷해 보이지만, 완전히 다른 개념입니다.

// Kotlin의 프로퍼티
var name: String? = null

// Java의 필드
String name = null

둘 다 데이터를 저장한다는 점은 같습니다. 하지만 프로퍼티에는 더 많은 기능이 있습니다. 일단 기본적으로 프로퍼티는 사용자 정의 setter와 getter를 가질 수 있습니다.

var name: String? = null
	get() = field?.toUpperCase()
    set(value) {
    	if (!value.isNullOrBlank()) {
        	field = value
        }
    }

위 코드에서 field란느 식별자를 확인할 수 있습니다. 이는 프로퍼티의 데이터를 저장해 두는 backing field에 대한 레퍼런스입니다. 이러한 backing field는 setter와 getter의 디폴트 구현에 사용되므로, 따로 만들지 않아도 디푤트로 생성됩니다.

(참고로, val을 사용해서 읽기 전용 프로퍼티를 만들 때는 field가 만들어지지 않습니다.)

 

val fullName: String
	get() = "$name $surname"

var을 사용해서 만든 읽고 쓸 수 있는 프로퍼티는 getter와 setter를 정의할 수 있습니다. 이러한 프로퍼티를 파생 프로퍼티(derived property)라고 부릅니다.

 

프로퍼티는 field가 필요 없습니다. 오히려 프로퍼티는 개념적으로 접근자(val의 경우 getter, var의 경우 getter와 setter)를 나타냅니다. 따라 kotlin은 인터페이스에도 프로퍼티를 정의할 수 있는 것입니다.

interface Person {
	val name: String
}

이렇게 코드를 작성하면, 이는 getter를 가질 거라는 것을 나타냅니다. 따라서 아래와 같이 오버라이드 할 수 있습니다.

open class Supercomputer {
	open val theAnswer: Long = 42
}

class AppleComputer : Supercomputer() {
	override val theAnswer: Long = 1_800_275_2273
}

마찬가지의 이유로 프로퍼티를 위임할 수도 있습니다.

val db: Database by lazy { connectToDb() }

 

프로퍼티는 본질적으로 함수이므로, 확장 프로퍼티를 만들 수도 있습니다.

val Context.preferences: SharedPreferences
	get() = PreferenceManager.getDefaultSharedPreferences(this)
    
val Context.inflater: LayoutInflater
	get() = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    
val Context.notificationManager: NotificationManager
	get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

위의 코드에서 확인할 수 있는것 처럼 프로퍼티는 필드가 아니라 접근자를 나타냅니다. 이처럼 프로퍼티는 함수 대신 사용할 수 있지만, 그렇다고 완전히 대체해서 사용하는 것은 좋지 않습니다.

// 이렇게 사용하지 마세요
val Tree<Int>.sum: Int
	get() = when (this) {
    	is Leaf -> value
        is Node -> left.sum + right.sum
    }

위의 코드에서 sum프로퍼티는 모든 요소를 반복 처리하므로, 알고리즘의 동작을 나타낸다고 할 수 있습니다. 이런 프로퍼티는 여러 가지 오해를 불러일으킬수 있습니다. 또한 큰 컬렉션의 경우 답을 찾을 때 많은 계산량이 필요합니다. 하지만 관습적으로 이런 getter에 그런 계산량이 필요하다고 예상하지 않습니다. 따라서 이러한 처리는 프로퍼티가 아니라 함수로 구현해야 합니다.

 

원칙적으로  프로퍼티는 상태를 나타내거나 설정하기 위한 목적으로 사용하는 것이 좋고, 다른 로직 등을 포함하지 않아야 합니다.

 

프로퍼티 대신 함수를 사용하는 것이 좋은 경우를 정리해 보면 다음과 같습니다.

  • 연산 비용이 높거나 복잡도가 O(1)보다 큰 경우
    - 사용자가 연산 비용을 예측하기 쉽고, 이를 기반으로 캐싱 등을 고려할 수 있기 때문입니다.
  • 비즈니스 로직(애플리케이션의 동작)을 포함하는 경우
    관습적으로 코드를 읽을 때, 프로퍼티가 로깅, 리스너 통지, 바이드된 요소 변경과 같은 단순한 동작 이상을 할거라 고 기대하지 않습니다.
  • 결정적이지 않은 경우
    같은 동작으로 연속적으로 두 번 했는데 다른 값이 나올 수 있다면, 함수를 사용하는 것이 좋습니다.
  • 변환의 경우
    -
    변환은 관습적으로 Int.toDouble()과 같은 변환 함수로 이루어집니다. 따라서 이러한 변환을 프로퍼틸로 만들면 오해를 불러 일으킬 수 있습니다.
  • getter에서 프로퍼티의 상태 변경이 일어나야 하는 경우
    관습적으로 getter에서 프로퍼티의 상태 변화를 일으킨다고 생각하지 않습니다. 따라서 게터에서 프로퍼티의 상태 변화를 일으킨다면 함수를 사용하는게 좋습니다.