본문 바로가기

Kotlin

공식 문서로 배우는 코틀린 - 15. Properties

열다섯 번째, 프로퍼티입니다.

 

프로퍼티 선언

Kotlin 클래스에서 프로퍼티는 var 키워드를 사용하요 가변되게 선언하거나, val 키워드를 사용하여 읽기 전용으로 선언할 수 있습니다.

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 // 이름으로 접근시 접근자가 호출됩니다. 세부 내용은 아래 내용을 참고하세요.
    result.street = address.street
    // ...
    return result
}

 

게터(getters)와 세터(setters)

프로퍼티를 선언하는 전체 구문은 다음과 같습니다.

var <프로퍼티이름>[: <프로퍼티타입>] [= <프로퍼티초기화>]
    [<getter>]
    [<setter>]

 

초기화 부분, 게터, 세터는 선택 사항입니다. 아래 예와 같이, 초기화 부분이나 게터의 반환 타입을 통해 프로퍼티 타입이 추론되는 경우에는 프로퍼티 타입 부분도 선택 사항입니다.

var initialized = 1 // 타입 Int와 기본적인 게터, 세터를 가집니다.
// var allByDefault // 오류: 명시적인 초기화가 필요합니다.

 

읽기 전용 프로퍼티 선언의 전체 구문은 가변적인 프로퍼티에 비해서 두 가지 부분에서 다릅니다. 첫째, var 대신 val로 시작하며, 세터가 허용되지 않습니다.

val simple: Int? // Int 타입과 기본 게터를 가집니다. 반드시, 생성자에서 초기화 돼야 합니다.
val inferredType = 1 // Int 타입과 기본 케서를 가집니다.

 

프로퍼티를 위해 맞춤화된(custom) 접근자(accessor, 게터와 세터)를 정의할 수 있습니다. 맞춤화된 게터를 정의하면, 프로퍼티를 접근할 때마 매번 해당 게터가 호출됩니다(이 방법으로 고정된 값이 아닌 접근시 연산 결과에 따르는 계산된 프로퍼티를 구현할 수 있습니다). 다음은 맞춤화된 게터 예입니다.

class Rectangle(val width: Int, val height: Int) {
    val area: Int // 게터의 반환 타입을 통해 타입을 추론할 수 있어서 프로퍼티의 타입은 선택 사항입니다.
        get() = this.width * this.height
}

fun main() {
    val rectangle = Rectangle(3, 4)
    println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}

/* 결과
Width=3, height=4, area=12
*/

 

게터를 통해 타입을 추론할 수 있는 경우 프로퍼티의 타입을 생략할 수 있습니다.

val area get() = this.width * this.height

 

맞춤화된 세터를 정의하는 경우, 프로퍼티에  값을 할당할 때마가 해당 세터가 호출됩니다. 단, 프로퍼티를 초기화 하는 때는 호출되지 않습니다. 맞춤화된 세터는 다음과 같은 형태입니다.

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // 문자열을 파싱하고 다른 프로퍼티에 할당
    }

 

관례적으로, 세터의 매개변수 이름은 value를 사용하지만, (당연히) 원하는대로 지을 수 있습니다.

 

단지 접근자에 어노테이션을 추가하거나 가시성을 변경하고 기본 접근자를 그대로 유지하고 싶은 경우에는 접근자의 몸체 없이 정의하면 됩니다.

var setterVisibility: String = "abc"
    private set // 기본 구현을 갖는 private 세터

var setterWithAnnotation: Any? = null
    @Inject set // 어노테이션을 갖는 세터

 

몇가지 용어에 관하여
맞춤화(custom), 뒷받침하는(backing) 같은 용어들은 실제 현업에서는 커스텀 세터, 백킹 필드처럼 영어 발음 그대로 쓰는 경우도 꽤 많을 겁니다. 연재에서는 가능하면 많이 사용하는 한글 표현을 쓰려고 여러 모로 확인을 한 후에 사용하고 있으며, 한 번 사용하면 (문서이기 때문에) 일관되게 쓰려고 같은 단어를 쓰고 있습니다. 하지만, 꼭 이를 따를 필요는 없고, 자신의 환경이나 생각에 맞게 편하게 생각하고 사용하시면 되지 않을까 생각됩니다.

 

뒷받침하는 필드(Backing fields)

코틀린에서 필드는 오로지 해당 프로퍼티의 값을 메모리 상에 가지고 있는 프로퍼티의 한 부분으로서만 사용됩니다. 필드는 직접적으로 선언될 수 없습니다. 하지만, 프로퍼티가 뒷받침하는 필드가 필요한 경우에는 Kotlin은 이를 자동으로 제공합니다. 뒷받침하는 필드는 접근자에서 field 식별자를 통해 참조할 수 있습니다.

var counter = 0 // 초기화 부분에서 직접적으로 뒷받침하는 필드에 값을 할당합니다.
    set(value) {
        if (value >= 0)
            field = value
            // counter = value // 스택 오버플로우 오류: 실제 이름 counter를 사용하면 세터를 재귀 호출하게 만듭니다.
    }

 

field 식별자는 프로퍼티의 접근자에서만 사용 가능합니다.

 

프로퍼티가 적어도 하나의 기본 접근자 구현을 사용하는 경우나 맞춤화된 접근자가 field 식별자로 프로퍼티를 참조하는 경우 뒷받침하는 필드는 (자동으로) 생성됩니다.

 

예를 들어, 다음과 같은 경우에는 뒷받침하는 필드가 없습니다.

val isEmpty: Boolean
    get() = this.size == 0

 

뒷받침하는 프로퍼티

만약에 묵시적인 뒷받침 필드 방식에 맞지 않는 뭔가를 해야하는 경우에는, 언제든지 뒷받침하는 프로퍼티라는 대안을 사용할 수 있습니다.

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 프로퍼티를 기본 게터, 세터로 접근 하는 경우 함수 호출 오버헤드를 피하기 위해 (게터, 세터를 만들지 않고 직접 조회하는 식으로) 최적화가 됩니다.

 

컴파일 시점 상수(compile-time constants)

읽기 전용 프로퍼티의 값이 컴파일 시점에 알려진 경우 const 수정자를 사용하여 컴파일 시점 상수(compile time constant)로 지정합니다. 이런 프로퍼티는 다음과 같은 요구 조건을 충족해야 합니다.

  • 최상위 수준 프로퍼티거나 객체 선언이나 동반 객체의 멤버여야 합니다.
  • 문자열이나 원시 타입으로 초기화 돼야 합니다.
  • 맞춤화된 게터가 아니어야 합니다.

컴파일러는 상수의 사용을 인라인화하여 해당 상수를 참조하는 부분을 실제 값을 교체합니다. 하지만, 필드는 제거되지 않기 때문에 리플렉션 사용에 응할 수 있습니다.

 

이러한 프로퍼티는 어노테이션에서도 사용할 수 있습니다.

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

 

늦은 초기화 프로퍼티와 변수

보통 프로퍼티는 널 가능하지 않은 타입으로 생성자에서 초기화되게 선언됩니다. 하지만, 때때로 이렇게 하기가 용이하지 않을 때가 있습니다. 예를 들어, 프로퍼티는 의존성 주입이나, 유닛 테스트의 설정 메소드에서 초기화 될 수 있습니다. 이런 경우에는 생성자에서 널이 아닌 초기화를 실행할 수가 없습니다. 하지만, 여전히 클래스 몸체에서 프로퍼티를 접근할 때 널 검사를 피하고 싶습니다.

 

이런 경우에는 lateinit 수정자를 사용할 수 있습니다.

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // dereference directly
    }
}

 

이 수정자는 클래스 몸체에 var 로 선언된 프로퍼티와 최상위 수준 프로퍼티, 그리고 지역 변수에  사용할 수 있습니다. 생성자에서 선언 된 프로퍼티나, 맞춤화된 게터 또는 세터를 가진 경우는 사용할 수 없습니다. 프로퍼티나 변수는 널 가능하지 않은 타입이어야하고, 원시 타입이 아니어야 합니다.

 

lateinit 프로퍼티가 초기화 되기 전에 접근하면 접근했다는 것과 아직 초기화 되지 않았다는 것을 명확히 식별하게 해주는 특별한 예외를 발생시킵니다.

 

lateinit var 이 초기화 됐는지 검사

lateinit var이 초기화 됐는지 확인하기 위해서는 해당 프로퍼티의 참조에 .isInitialized를 사용합니다.

if (foo::bar.isInitialized) {
    println(foo.bar)
}

 

이러한 검사는 같은 타입에 선언 됐거나, (중첩된 상황에서) 외부 타입 중 하나에 선언 됐거나, 같은 파일의 최상위 수준에 선언된, 어휘적으로 접근 가능한 프로퍼티들에만 가능합니다.

 

프라퍼티 오버라이딩

상속에서 프라퍼티 오버라이딩 부분을 참고하세요.

 

위임 프로퍼티(delegated properties)

대부분의 일반적인 종류의 프로퍼티는 뒷받침하는 필드로부터 값을 읽거나 거기에 쓰게 됩니다. 하지만, 맞춤화된 게터와 세터를 사용하면 프로퍼티의 어떠한 행동이라도 구현할 수 있습니다. 단순한 첫번째와 다양한 두 번째 사이 어딘가에 프로퍼티가 할 수 있는 공통적인 패턴이 있습니다. 예를 들면, 게으른(lazy) 값, 주어진 키로 맵에서 읽기, 데이터베이스 접근, 리스너에게 접근 알림 같은 것들입니다.

 

이러한 공통적인 행동은 (후에 살펴볼) 위임 프로퍼티를 사용하여 라이브러리처럼 구현될 수 있습니다.