관리 메뉴

나만을 위한 블로그

[이펙티브 코틀린] 아이템 16. 프로퍼티는 동작이 아닌 상태를 나타내야 한다 본문

책/Effective Kotlin

[이펙티브 코틀린] 아이템 16. 프로퍼티는 동작이 아닌 상태를 나타내야 한다

참깨빵위에참깨빵_ 2022. 7. 24. 22:27
728x90
반응형

코틀린의 프로퍼티는 자바의 필드와 비슷해 보이지만 서로 완전히 다른 개념이다.

 

var name: String? = null
    
String name = null

 

데이터를 저장한다는 건 같다. 하지만 프로퍼티엔 더 많은 기능이 있다. 기본적으로 프로퍼티는 사용자 정의 게터, 세터를 가질 수 있다.

 

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

 

field는 프로퍼티의 데이터를 저장해 두는 backing field에 대한 레퍼런스다. 이런 backing field는 게터, 세터의 디폴트 구현에 쓰이므로 따로 만들지 않아도 디폴트로 생성된다. val을 써서 읽기 전용 프로퍼티를 만들 때는 field가 만들어지지 않는다.

 

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

 

var을 써서 만든 읽고 쓸 수 있는 프로퍼티는 게터, 세터를 정의할 수 있다. 이런 프로퍼티를 파생 프로퍼티(derived property)라고 부르며 자주 쓰인다.

 

코틀린의 모든 프로퍼티는 디폴트로 캡슐화되어 있다. 자바 표준 라이브러리 Date를 써서 객체에 날짜를 저장해 활용한 상황을 가정한다. 프로젝트 진행 중 직렬화 문제 등으로 객체를 더 이상 이런 타입으로 저장할 수 없게 됐는데 이미 프로젝트 전체에서 이 프로퍼티를 많이 참조하고 있다면 어떻게 해야 할까?

코틀린은 데이터를 millis란 별도 프로퍼티로 옮기고 이를 활용해서 date 프로퍼티에 데이터를 저장하지 않고 wrap, unwrap하도록 코드를 바꾸기만 하면 된다.

 

var date: Date
    get() = Date(millis)
    set(value) {
        millis = value.time
    }

 

프로퍼티는 필드가 필요 없다. 프로퍼티는 개념적으로 접근자(val의 경우 게터, var의 경우 게터와 세터)를 나타낸다. 따라서 코틀린은 인터페이스에도 프로퍼티를 정의할 수 있다.

 

interface Person {
    val name: String
}

 

이렇게 쓰면 게터를 가질 거란 걸 나타낸다. 따라서 아래처럼 오버라이드할 수 있다.

 

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 프로퍼티는 모든 요소를 반복 처리하므로 알고리즘 동작을 나타낸다고 할 수 있다. 이런 프로퍼티는 여러 오해를 불러일으킬 수 있다. 큰 컬렉션의 경우 답을 찾을 때 많은 계산량이 필요하다. 하지만 관습적으로 이런 게터에 그런 계산량이 필요하다고 예상하진 않는다. 따라서 이런 처리는 프로퍼티가 아닌 함수로 구현해야 한다.

 

fun Tree<Int>.sum(): Int = when (this) {
    is Leaf -> value
    is Node -> left.sum() + right.sum()
}

 

원칙적으로 프로퍼티는 상태를 나타내거나 설정하는 목적으로만 쓰는 게 좋고 다른 로직 등을 포함하지 않아야 한다. 어떤 걸 프로퍼티로 해야 하는지 판단할 수 있는 간단한 질문이 있다. '이 프로퍼티를 함수로 정의할 경우 접두사로 get 또는 set을 붙일 것인가?' 아니라면 프로퍼티로 만드는 건 좋지 않다. 좀 더 구체적으로 프로퍼티 대신 함수를 쓰는 게 좋은 경우를 정리하면 아래와 같다.

 

  • 연산 비용이 높거나 복잡도가 O(1)보다 큰 경우
  • 비즈니스 로직(앱의 동작)을 포함하는 경우
  • 결정적이지 않은 경우
  • 변환의 경우
  • 게터에서 프로퍼티의 상태 변경이 일어나야 하는 경우

 

요소의 합계를 계산하려면 모든 요소를 더하는 반복 처리가 필요하다. 어떤 처리가 실질적으로 이뤄지므로 상태가 아닌 동작이다. 선형 복잡도를 갖기 때문에 함수로 정의하는 게 좋다.

반대로 상태를 추출 / 설정할 때는 프로퍼티를 써야 한다. 특별한 이유가 없다면 함수를 쓰면 안 된다. 많은 사람은 경험적으로 프로퍼티는 상태 집합을 나타내고 함수는 행동을 나타낸다고 생각한다.

반응형
Comments