이펙티브 코틀린 Item 16: 프로퍼티는 동작이 아니라 상태를 나타내야 한다

woga·2023년 5월 13일
0

코틀린 공부

목록 보기
19/54
post-thumbnail

코틀린의 프로퍼티는 자바의 필드와 비슷해보이지만 사실 전혀 다른 개념이다

var name: String? = null

String name = null;

물론 둘 다 데이터를 저장한다는 점은 같다. 그치만 코틀린의 프로퍼티는 사용자 정의 게터/세터를 가질 수 있다

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

fiedl라는 식별자를 확인할 수 있는데 val을 사용해서 읽기 전용 프로퍼티를 만들 때는 field가 만들어지지 않는다.
이 필드를 우린 백킹 필드(backing field)로 부른다

var을 사용해서 만든 읽고 쓸 수 있는 프로퍼티는 게터와 세터를 정의할 수 있다. 이러한 프로퍼티를 파생 프로퍼티(derived property)라고 부르며 자주 사용된다.

이처럼 코틀린의 모든 프로퍼티는 디폴트로 캡슐화되어 있다. 만약 자바에서 Date를 활용해 객체에 날짜를 저장해서 많이 활용한 상황을 가정해보자
프로젝트에서 직렬화 문제 등으로 객체를 더 이상 이러한 타입으로 저장할 수 없게 됐는데, 이미 프로젝트 전체에서 이 프로퍼티를 많이 참조하고 있다면 어떻게 해야할까?
코틀린은 그저 데이터를 millis라는 별도의 프로퍼티로 옮기고, 이를 활용해서 date 프로퍼티에 데이터를 저장하지 않고 랩/언랩하도록 코드만 변경하기만 하면 된다.

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

프로퍼티는 필드가 필요없다 오히려 접근자를 나타낸다.
그래서 코틀린은 인터페이스에도 프로퍼티를 정의할 수 있다

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

...

프로퍼티를 함수 대신 사용할 수 있지만, 그렇다고 완전히 대체해서 사용하는 것은 좋지 않다.

관습적으로 프로퍼티 게터에 많은 계산량이 필요하지 않다고 생각한다. 그런데 알고리즘 적인 확장 프로퍼티 라든지.. 이런 오해를 불러일으킬수 있는 점을 예방하자.
이런 처리는 함수로 따로 구현해야 한다.

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

그럼 조금 더 프로퍼티 대신 함수를 사용하는 것이 좋은 경우를 구체적으로 정리해보자

  • 연산 비용이 높거나, 복잡도가 O(1)보다 큰 경우: 관습적으로 프로퍼티를 사용할 때 연산 비용이 많이 필요하다고 생각하지 않는다. 연산 비용이 많이 들어간다면 함수를 사용하는 것이 좋다. 그래야 사용자가 예측해서 캐싱 등을 고려할 수 있다.

  • 비즈니스 로직(애플리케이션 동작)을 포함하는 경우: 관습적으로 코드를 읽을 때 프로퍼티가 로깅, 리스너 통지, 바인드된 요소 변경과 같은 단순한 동작 이상을 하거라고 기대하지 않는다.

  • 결정적이지 않은 경우: 같은 동작을 연속적으로 두 번 했는데 다른 값이 나올 수 있다면, 함수를 사용하는 것이 좋다

  • 변환의 경우: 변환은 관습적으로 Int.toDouble()과 같은 변환 함수로 이루어진다. 따라서 이러한 변환을 프로퍼티로 만들면, 오해를 불러 일으킬 수 있다

  • 게터에서 프로퍼티의 상태 변경이 일어나야 하는 경우: 관습적으로 게터에서 프로퍼티의 상태 변화를 일으킨다고 생각하지 않는다. 따라서 게터에서 프로퍼티의 상태 변화를 일으킨다면, 함수를 사용하는 것이 좋다

선형복잡도를 가진다면 프로퍼티가 아니라 함수로 구현하자
표준 라이브러리에서도 다음과 같이 함수로 정의되어 있다

val s = (1..100).sum()

반대로 상태를 추출/설정할 때는 프로퍼티를 사용해야한다.
특별한 이유가 없다면 함수를 사용하면 안된다

// Don't do that

class UserIncorrect {
	private var name: String = ""
    
    fun getName() = name
    
    fun setName(name: String) {
    	this.name = name
    }
}

class UserCorrect {
	var name: String = ""
}

많은 사람은 경험적으로 프로퍼티는 상태 집합을 나타내고, 함수는 행동을 나타낸다 생각한다.

profile
와니와니와니와니 당근당근

0개의 댓글