1. 코틀린 프로퍼티의 기본 개념
- 코틀린에서 프로퍼티(Property)는 객체의 상태를 나타내는 핵심 요소입니다.
- 자바에서 프로퍼티란 필드와 게터/세터 메서드를 결합한 개념으로, 보다 간결하고 표현력 있는 코드를 작성할 수 있게 해줍니다.
- 코틀린은 프로퍼티를 언어 기본 기능으로 제공합니다.
- 코틀린은 프로퍼티를 선언할 때 크게 두 가지 유형으로 구분합니다.
var
: 가변(mutable) 프로퍼티로, 값을 읽고 쓸 수 있습니다.val
: 읽기 전용(read-only) 프로퍼티로, 초기화 후에는 값을 변경할 수 없습니다.
1.1 Java와 비교해서 이해하기
- 프로퍼티는 빈의 상채를 나타내는 중요한 요소입니다.
- 자바에서는 프로퍼티를 필드와 게터/세터 메서드로 구현합니다.
- 코틀린에서는 프로퍼티를 선언할 때
var
또는val
키워드를 사용하여 간결하게 표현합니다. - val 프로퍼티를 선언한느 경우, 이는 자바에서 private final 필드와 public getter 메서드와 동등합니다.
- var 프로퍼티를 선언하는 경우, 이는 자바에서 private 필드와 public getter/setter 메서드와 동등합니다.
- 즉 코틀린의 프로퍼티는 자바의 필드와 게터/세터 메서드를 결합한 형태로, 더 간결하고 직관적인 문법을 제공합니다.
1.2 기본 프로퍼티 선언 및 사용
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
var state: String? = null
var zip: String = "123456"
}
- 코틀린에서는 클래스의 프로퍼티를 위와 같이 선언합니다.
fun copyAddress(address: Address): Address {
val result = Address() // 코틀린에는 'new' 키워드가 없습니다
result.name = address.name // 접근자(accessor)가 호출됩니다
result.street = address.street
// ...
return result
}
- 프로퍼티를 사용할 때는 단순히 이름으로 참조하면 됩니다.
2. 게터와 세터 (Getters and Setters)
- 코틀린 프로퍼티의 전체 문법은 다음과 같습니다:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
- 초기화 식, 게터, 세터는 모두 선택사항입니다.
- 타입은 초기화 식이나 게터의 반환 타입에서 추론될 수 있다면 생략 가능합니다.
var initialized = 1 // Int 타입으로 추론됨, 기본 게터와 세터가 제공됨
2.1 이름 규칙
- 이름이 is로 시작하는 프로퍼티의 게터는 get이 붙지 않고 원래 이름을 그대로 사용합니다.
- 세터는 is를 set으로 바꾼 이름을 사용합니다.
예시
class Person(
val name: String,
var isStudent: Boolean
)
fun main() {
val person = Person("John", true)
println(person.name) // "John"
println(person.isStudent) // true
person.isStudent = false // 세터 호출
println(person.isStudent) // false
}
println(person.name)
에서 프로퍼티 이름을 직접 사용하면 코틀린이 자동으로 게터를 호출합니다.person.isStudent = false
에서 프로퍼티 이름을 직접 사용하면 코틀린이 자동으로 세터를 호출합니다.
2.2 커스텀 접근자 (Custom Accessors)
- 코틀린에서는 프로퍼티에 대한 커스텀 접근자를 정의할 수 있습니다.
- 커스텀 게터를 정의하면, 프로퍼티에 접근할 때마다 해당 코드가 실행됩니다.
- 어떤 프로퍼티가 같은 객체 안의 다른 프로퍼티에서 계산된 결과인 경우 커스텀 접근자를 사용합니다.
예시
class Rectangle(val width: Int, val height: Int) {
val area: Int // 게터의 반환 타입에서 추론 가능하므로 타입 생략 가능
get() = this.width * this.height
}
val area get() = this.width * this.height
- 타입이 추론 가능한 경우 더 간결하게 작성할 수 있습니다
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // 문자열을 파싱하여 다른 프로퍼티에 값을 할당
}
- 커스텀 세터도 정의할 수 있으며, 프로퍼티에 값을 할당할 때마다 호출됩니다:
- 관례적으로 세터의 파라미터 이름은
value
를 사용하지만, 원하는 다른 이름을 선택할 수도 있습니다.
2.3 접근자 가시성 변경 및 애노테이션
- 접근자의 가시성은 기본적으로 프로퍼티의 가시성과 동일합니다.
- 접근자의 가시성을 변경하거나 애노테이션을 추가하려면 본문 없이 접근자를 정의할 수 있습니다.
예시
var setterVisibility: String = "abc"
private set // 세터가 private으로 설정되며 기본 구현을 유지합니다
var setterWithAnnotation: Any? = null
@Inject set // 세터에 Inject 애노테이션 적용
3. 백킹 필드와 백킹 프로퍼티
3.1 백킹 필드 (Backing Fields)
- Backing field는 프로퍼티의 값을 메모리에 저장하기 위한 특별한 필드입니다.
- 코틀린에서는 직접적으로 필드를 선언할 수 없고, 대신 필요할 때 자동으로 생성됩니다.
- 접근자 내에서
field
식별자를 사용하여 백킹 필드를 참조할 수 있습니다.- field 식별자: 사용자 정의 getter와 setter 내에서만 사용 가능한 특별한 식별자입니다.
- setter 내에서 프로퍼티 이름을 직접 사용하면 무한 재귀가 발생하기 때문에 field 식별자를 사용합니다.
- 프로퍼티가 다른 프로퍼티나 계산된 값에만 의존할 경우 backing field가 필요하지 않습니다.
예시
var counter = 0 // 초기화 식은 백킹 필드에 직접 할당됩니다
set(value) {
if (value >= 0)
field = value
// counter = value // 오류: 스택 오버플로우! 실제 이름 'counter'를 사용하면 세터가 재귀적으로 호출됩니다
}
field
식별자는 프로퍼티의 접근자 내에서만 사용할 수 있습니다.- 백킹 필드는 프로퍼티가 최소한 하나의 접근자에 대해 기본 구현을 사용하거나, 커스텀 접근자가
field
식별자를 통해 참조할 경우에만 생성됩니다. - field는 실제 값을 저장하는 backing field를 참조합니다.
백킹 필드가 생성되지 않는 예시
val isEmpty: Boolean
get() = this.size == 0
- 이 프로퍼티는 다른 프로퍼티의 값에 기반하여 계산되므로 자체 값을 저장할 필요가 없습니다.
3.2 백킹 프로퍼티 (Backing Properties)
- 암시적 백킹 필드로는 충분하지 않은 경우, 백킹 프로퍼티를 사용할 수 있습니다:
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // 타입 파라미터가 추론됩니다
}
return _table ?: throw AssertionError("Set to null by another thread")
}
- 이 패턴은 지연 초기화나 스레드 안전성이 필요한, 보다 복잡한 초기화 로직을 구현할 때 유용합니다.
팁
JVM에서 기본 게터와 세터를 가진 private 프로퍼티에 대한 접근은 함수 호출 오버헤드를 피하도록 최적화됩니다.