devlog of ShinJe Kim

[Kotlin] 데이터 클래스와 클래스 위임

|

데이터 클래스와 클래스 위임

코틀린의 data 클래스는 equals, hashCode, toString을 자동으로 생성해줍니다. data 클래스에 대해 알아보기 이전에 기존의 방식을 살펴본다음, 코틀린의 data 클래스에는 어떤 이점이 있는지 알아보겠습니다.

자바 플랫폼에서는 클래스가 equals, hashCode, toString 등의 메소드를 구현해야 합니다. IDE에서 이러한 메소드를 자동으로 생성해주기는 하지만 반복되는 코드는 여전히 남아있습니다. 코틀린에서는 이러한 문제를 해결하기 위해 toString, equals, hashCode 메소드 구현을 보이지 않는 곳에서 자동으로 생성해줍니다.

class Client(val name: String, val postalCode: Int)

toString() : 문자열 표현

기본적으로 제공되는 객체의 문자열 표현은 ‘Client@5e9f23b4’와 같은 방식입니다. 이를 조금 더 유용하게 사용하기 위해서 toString 메소드를 오버라이드하여 사용할 수 있습니다.

class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

// 결과
>>> val client1 = Client("김모모", 1234)
>>> println(client1)
Client(name=김모모, postalCode=4122)

equals() : 객체의 동등성

코틀린에서는 객체의 동등성을 비교하는 방식으로 == 연산자와 equals 메소드를 사용합니다. 코틀린에서의 == 연산자와 자바에서의 == 연산자는 다릅니다.

코틀린에서의 == 연산자는 참조 동일성을 검사하지 않고 객체의 동등성을 검사합니다. 따라서 == 연산을 사용하더라도 내부적으로 equals를 호출하는 방식으로 컴파일됩니다. 기존의 자바에서의 == 연산자는 코틀린에서 === 연산자로 사용됩니다. 그리고 코틀린의 is 검사는 자바의 instanceOf와도 같습니다.

class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?): Boolean {
        if(other == null || other !is Client)
            return false
        return name == other.name && 
            postalCode == other.postalCode
    }
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

위의 예시에서 Anyjava.lang.Object에 대응하는 클래스로, 코틀린의 모든 클래스의 최상위 클래스입니다. 위의 예시에서 Any?는 널이 될 수 있는 타입이므로 other는 null일 수 있습니다.

hashCode() : 해시 컨테이너

자바에서는 equals를 오버라이드 할 때 반드시 hashCode도 함께 오버라이드 해야합니다. 아래의 예시를 통해 그 이유를 살펴보겠습니다.

>>> val processed = hashSetOf(Client("김모모", 4122))
>>> println(processed.contains(Client("김모모", 4122)))
false

위의 예시에서 Client의 프로퍼티는 모두 일치합니다. 따라서 결과값은 true가 나와야 할 것 같습니다. 예상과 다르게 false가 나온 이유는 Client 클래스가 hashCode 메소드를 정의하지 않았기 때문입니다. JVM 언어에서는 hashCode가 지켜야 하는 제약이 있습니다.

equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다.

예시코드에서 Client 클래스는 이 제약을 지키지 않고 있습니다. processed의 집합은 HashSet입니다. HashSet은 원소를 비교할 때 비용을 줄이기 위해 먼저 객체의 hashCode를 비교하고, hashCode가 같은 경우에만 실제 값을 비교합니다.

위의 두 Client 인스턴스는 hashCode가 다르기 때문에 두 번째 인스턴스가 집합 안에 들어있지 않다고 판단합니다. 즉, hashCode가 다를 때 equals가 반환하는 값은 판단 결과에 영향을 끼치지 못합니다. 따라서 원소 객체들이 hashCode에 대한 규칙을 지키지 않는 경우 HashSet은 제대로 작동할 수 없습니다. 이러한 문제를 해결하기 위해서는 아래와 같이 Client가 hashCode를 구현해야합니다.

class Client(val name: String, val postalCode: Int) {
    ...
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

data class (데이터 클래스)

지금까지 살펴본 toString, hashCode, equals를 번거롭게 직접 생성하거나 IDE를 통해 생성할 필요 없이, 자동으로 생성해주는 클래스가 data 클래스입니다. 아래와 같이 class 앞에 data 변경자를 선언하기만 하면 됩니다.

data class Client(val nam: String, val postalCode: Int)

data 클래스는 아래와 같은 메소드를 생성해줍니다.

  • 인스턴스 간 비교를 위한 equals
  • HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
  • 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어주는 toString

equals와 hashCode는 주 생성자에 나열된 모든 프로퍼티를 고려하여 만들어집니다. 생성된 equals 메소드는 모든 프로퍼티 값의 동등성을 확인하고, hashCode 메소드는 모든 프로퍼티의 해시 값을 바탕으로 계산한 해시 값을 반환합니다. 이 떄 주 생성자 밖에 정의된 프로퍼티는 equals나 hashCode를 계산할 때 고려의 대상이 아닙니다.

이 떄 data 클래스의 프로퍼티가 꼭 val일 필요는 없습니다. 하지만 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어 데이터 클래스를 불변(immutable) 클래스로 만드는 것이 권장됩니다. 특히 HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우에는 불변성이 필수적입니다.

코틀린 컴파일러는 데이터 클래스의 인스턴스를 더 쉽게 불변 객체로 활용할 수 있게 메소드를 제공하는데, 이것이 copy 메소드 입니다. copy 메소드는 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해줍니다. 원본 객체를 메모리상에서 직접 바꾸는 대신 복사본을 만드는 것이 더 낫습니다. 복사본은 원본과 다른 생명주기를 가지며, 복사본의 프로퍼티 값을 바꾸거나 복사본을 제거해도 프로그램에서 원본을 참조하는 다른 부분에 전혀 영향을 주지 않기 때문입니다.

위에서 예시로 나왔던 Client의 copy를 구현하면 아래와 같습니다.

class Client(val name: String, val postalCode: Int) {
    ...
    fun copy(name: String = this.name,
            postalCode: Int = this.postalCode) = 
        Client(name, postalCode)
}

아래는 copy 메소드를 사용하는 방법입니다.

>>> val lee = Client("김모모", 4122)
>>> println(lee.copy(postalCode = 4000))
Client(name=김모모, postalCode=4000)

클래스 위임

코틀린에서는 모든 클래스가 기본적으로 final이며, open 변경자로 열어둔 클래스만 상속할 수 있습니다. 하지만 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야할 때가 있습니다. 이 때 일반적으로 사용하는 것이 데코레이터(decorator) 패턴입니다.

데코레이터 패턴이란 상속을 허용하지 않는 기존 클래스 대신 사용할 수 있는 새로운 클래스(데코레이터)를 만들되, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것입니다. 이 때 새로 정의해야하는 기능은 데코레이터의 메소드에 새로 정의하고 기존 기능이 그대로 필요한 부분은 데코레이터의 메소드가 기존 클래스의 메소드에게 요청을 전달(forwarding)합니다.

이러한 접근 방식의 단점은 준비 코드가 상당히 많이 필요하다는 것입니다. 코틀린에서는 이점을 개선하여 보일러플레이트 코드 없이 by 키워드라는 것을 통해 간단하게 클래스 위임을 구현할 수 있습니다. 인터페이스를 구현할 때 by 키워드를 사용하여 해당 인터페이스에 대한 구현을 다른 객체에 위임중이라는 사실을 명시할 수 있습니다.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BasImpl(10)
    Derived(b).print()
}

Derived 클래스에 있는 by 구문은 b가 Derived 클래스의 오브젝트에 내부적으로 저장되고, 컴파일러가 b로 전달하는 Base의 모든 메소드를 생성함을 의미합니다.

다음으로는 위임으로 구현된 인터페이스의 멤버를 오버라이딩 해보겠습니다.

interface Base {
    fun printMessage()
    fun printMessageLine()
}

class BaseImpl(val x: Int) : Base {
    override fun printMessage() { print(x) }
    override fun printMessageLine() { print(x) }
}

class Derived(b: Base) : Base by b {
    override fun printMessage() { print("abc")}
}

fun main() {
    val b = BaseImpl(10)
    Derived(b).printMessage()
    Derived(b).printMessageLine()
}

여기서 주의해야 할 것이 있습니다. 이 방법으로 오버라이딩 된 멤버는 위임된 객체의 멤버에서 호출되는 것이 아닙니다. 해당 멤버는 자체적으로 구현된 인터페이스 멤버에만 접근할 수 있습니다.

interface Base {
    val message: String
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override val message = "BaseImpl: x = $x"
    override fun print() { print(message) }
}

class Derived(b: Base) : Base by b {
    // This property is not accessed from b's implementation of 'print'
    override val message = "Message of Derived"
}

fun main() {
    val b = BaseImpl(10)
    val derived = Derived(b)
    derived.print()
    println(derived.message)
}

참고 문헌

[Kotlin] Object 키워드, 식(Expressions), 선언(Declarations)

|

Object 키워드

object 키워드를 선언하는 상황은 아래와 같습니다.

  • 객체 선언(object declaration): 싱글턴을 정의하는 방법 중 하나
  • 동반 객체(companion object): 인스턴스 메소드는 아니지만 어떤 클래스와 관련 있는 메소드, 그리고 팩토리 메소드를 담을 떄 쓰임. 동반 객체 메소드에 접근할 때는 동반 객체가 포함된 클래스의 이름을 사용할 수 있음.
  • 객체 식은 자바의 무명 내부 클래스(anonymous inner class) 대신 쓰임.

때로는 하위 클래스를 새로 선언하지 않고 일부 클래스를 약간 수정한 객체를 만들어야 할 때가 있습니다. 코틀린은 이러한 상황을 객체 표현식 및 객체 선언으로 처리합니다.

Object Expressions(Object 표현식)

특정 타입으로 상속되는 익명 클래스의 객체를 생성하기 위해서는 아래와 같은 코드를 작성하게됩니다.

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }

    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})

만약 supertype에 생성자가 있다면 적절한 생성자 파라미터가 전달됩니다. supertype이 많다면 콜론(:) 뒤에 콤마(,)로 구분되어 표현됩니다.

open class A(x: Int){
    public open val y: Int = x
}

interface B { /*...*/}

val ab: A = object : A(1). B {
    override val y = 15
}

만약 supertype이 없는 그냥 객체 자체를 원한다면 아래와 같이 표현할 수 있습니다.

fun foo() {
    val adHoc = object{
        var x: Int = 0
        var y: Int = 0
    }
    print(adHoc.x + adHoc.y)
}

익명 객체는 지역(local) 혹은 프라이빗(private) 일때만 타입으로 사용될 수 있습니다. 만약 익명 객체를 public 함수의 리턴 타입으로 사용하거나 public 프로퍼티의 타입으로 사용한다면, 해당 함수/프로퍼티의 실제 타입은 익명 객체의 수퍼타입으로 선언된 타입이며 수퍼타입을 선언하지 않았을 경우에는 Any입니다. 익명 객체에 추가된 멤버는 접근할 수 없습니다.

class C {
    // private 함수: 리턴 타입은 익명 객체의 타입입니다.
    private fun foo() = object {
        val x: String = "x"
    }

    // public 함수: 리턴 타입은 Any입니다.
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x // 제대로 동작함.
        val x2 = publicFoo().x // 에러: Unresolved reference 'x'.
    }
}

객체 표현식의 코드는 해당 영역 안(enclosing scope)의 변수에 접근할 수 있습니다.

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ...
}

Object declarations(객체 선언)

코틀린에서는 싱글턴(singleton)을 쉽게 선언할 수 있도록 지원해줍니다.

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

위의 예시와 같은 코드를 객체 선언(object declarations)이라고 하며, 항상 object 키워드 뒤에 이름이 있습니다. 변수 선언과 마찬가지로 객체 선언은 표현식이 아니며 대입문(assignment statement)의 우변(right side)에 쓰일 수 없습니다.

객체 선언의 초기화는 스레드에 안전합니다. 객체를 참조하려면 해당 객체의 이름을 바로 사용하면 됩니다.

DataProviderManager.registerDataProvider(...)

이러한 객체는 supertype을 가질 수 있습니다.

object DefaultListener : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ... }
    override fun mouseEntered(e: MouseEvent) { ... }
}

주의: 객체 선언은 지역적으로 수행될 수 없으며(함수 내에 중첩될 수는 있습니다), 다른 객체 선언이나 내부 클래스가 아닌 곳에 중첩될 수 있습니다.

Companion Objects(동반 객체)

자바에서 static은 객체를 생성하지 않아도 전체적으로 사용할 수 있게 하는 것을 의미합니다. 코틀린에서는 static이 없는 대신 companion 객체를 만들면 인터페이스로 만들거나 상속받도록 할 수 있다. 클래스 내부의 객체 선언은 아래와 같이 companion 키워드로 표현할 수 있습니다.

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

아래와 같이 클래스 이름을 qualifier로 사용하여 간단하게 동반 객체의 멤버를 호출할 수 있습니다.

val instance = MyClass.create()

동반 객체의 이름은 생략될 수 있으며 이 때에는 Companion이라는 명칭이 대신 사용됩니다.

class MyClass {
    companion object { }
}

val x = MyClass.Companion

다른 이름의 qualifier가 아닌, 해당 클래스 자체적으로 사용되는 클래스 이름은 이름 지정 여부에 관계없이 동반 객체가 참조하게 됩니다.

class MyClass1 {
    companion object Named { }
}

val x = MyClass1

class MyClass2 {
    companion object { }
}

val y = MyClass2

동반 객체의 멤버가 다른 언어에서는 static 멤버처럼 보이지만, 런타임시에는 실제 객체의 인스턴스 멤버이며 인터페이스를 구현하는 등의 역할을 할 수 있습니다.

interface Factory<T> {
    fun create(): T
}

class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

val f: Factory<MyClass> = MyClass

하지만 JVM에서는 @JvmStatic 어노테이션을 사용한다면 실제 static 메소드와 필드로 생성된 동반 객체의 멤버를 가질 수 있습니다. 이는 Java interoperability에 자세하게 나와있습니다.

객체 표현식과 객체 선언의 차이점(Semantic difference between object expressions and declarations)

객체 표현식과 객체 선언 사이에는 중요한 차이가 있습니다.

  • 객체 표현식은 사용되는 곳에서 즉시 실행되고 초기화됩니다.
  • 처음으로 접근할 때에는 객체 선언이 느리게 초기화됩니다.
  • 동반 객체는 해당 클래스가 로드될 때 초기화되어 자바의 static 초기화 프로그램의 의미와 일치합니다.

참고 문헌

[TIL] 2019-09-02

|

Today I Learned

  • 코틀린의 장점 3가지.
    • 상호운용성: ios개발의 경우 옵씨와 swift 상호 운용이 어려움. 이런 것들을 호환하게 해주는게 브릿지라고 함.
    • 안전성: 컴파일러가 타입을 자동으로 추론해주어서 타입 정보를 직접 지정할 필요가 없다. 또한, 실행 시점에 오류를 발생시키는 대신 컴파일 시점 검사를 통해 오류를 방지해준다. 마지막으로 null 값으로 인한 오류(NullPointerException)와 class cast로 인한 오류(ClassCastException)을 방지해준다.
    • 간결성: 적은 양의 코드로 가독성 있게 많은 것을 표현할 수 있다.
  • 코틀린에서 부 생성자를 사용하는 이유
    • 자바와의 상호운용성을 위해
    • ‘정적 팩토리 메소드’는 이를 개선하기 위해 나온 것. 팩토리는 객체를 생성하는 역할을 지칭하는 용어이다(디자인 패턴에서는 또 다른 의미임). 팩토리 메소드를 쓰는 이유는, 일반적인 생성자는 이름이 고정되어 있는데 이는 가독성이 좋지 않다. 따라서 셍성자를 매핑해서 가독성을 높이기 위해 팩토리 메소드와 부 생성자를 사용한다. (이펙티브 자바 1장에 팩토리 메소드 나옴)
    • 생성자는 아래와 같은 한계가 있기 때문에 부생성자를 남발하는 것은 좋지 않다.
      • 이름을 가질 수 없다.
      • 시그니처 제약이 있다(이름을 가질 수 없기 때문에 오러지 파라미터로만 시그니처를 다르게 하여 만들어야 한다)
      • 반드시 새로운 인스턴스가 생성된다.
  • 코틀린 인터페이스와 자바 인터페이스의 차이?
    • 강한 결합과 느슨한 결합
    • 강한 결합이란? 정적 바인딩. 주체적인 타입을 이용하는 것. 장점은 컴파일 최적화가 가능하다. 성능적으로 우수하다(인라인 호출). 하지만 느슨한 결합은 코드를 수정하지 않고 확장하는데에 유리하다. 단점은 동적 바인딩을 하기 때문에 해당하는 객체가 어떤 타입인지에 대해 최적화가 불가능하다.

[Kotlin] 프로퍼티와 필드(Properties and Fields)

|

프로퍼티와 필드(Properties and Fields)

프로퍼티(properties)

코틀린에서는 두 가지 방법으로 프로퍼티를 선언할 수 있습니다. 변경이 가능한(mutable) 변수는 var로 선언합니다. 반면 읽을 수만 있고 값을 변경할 수 없는(immutable) 변수는 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 and Setters)

프로퍼티를 선언하는 full syntax는 아래와 같습니다. 프로퍼티란 필드와 accessor 메소드를 자동으로 생성해주는 문법을 의미합니다.

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

getter와 setter를 사용하는 것은 옵션입니다. 만약 아래와 같이 초기화 값이나 getter/setter의 리턴 타입으로 프로퍼티의 타입을 유추할 수 있으면 프로퍼티의 타입은 생략해도 됩니다.

var allByDefault: Int? // 에러: 명시적인 초기화를 해주어야만 함. 디폴트 getter/setter가 포함됨.
var initialized = 1 // Int 타입이며, 디폴트 getter/setter를 가짐

변경할 수 없는 프로퍼티를 선언하는 방식은 변경 가능한 프로퍼티를 선언하는 방식과 다릅니다. 첫 번쨰로, 변경할 수 없는 프로퍼티 선언은 val 키워드로 해야합니다.

val simple: Int? // Int 타입이며 디폴트 getter를 가짐. 생성자에서 초기화해주어야만 한다.
val inferredType = 1 // Int 타입이며 디폴트 getter를 가짐.

프로퍼티에 커스텀 접근자를 지정할수도 있습니다. getter는 프로퍼티에 접근할때마다 사용합니다. 아래는 커스텀 getter의 예시입니다.

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

커스텀 setter를 지정할 수도 있습니다. setter는 프로퍼티에 값을 할당할때 사용합니다. 아래는 커스텀 setter의 예시입니다. 관례적으로 setter 파라미터의 이름은 value로 사용합니다. 원한다면 선호하는 다른 이름을 사용할수도 있습니다.

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(Vaule) // String을 parse하여 다른 프로퍼티에 값을 할당함.
    }

코틀린 1.1부터는 getter로부터 타입을 추론할 수 있다면 아래와 같이 타입 선언을 생략할 수 있게 되었습니다.

var isEmpty get() = this.size == 0 // Boolean 타입임을 추론할 수 있음.

접근자의 가시성이나 기본 구현을 변경할 필요가 없다면 아래와 같이 본문을 정의하지 않고 구현할 수도 있습니다.

var setterVisibility: String = "abc"
    private set // setter는 private이고 디폴트 구현이 되어있음.
var setterWithAnnotation: Any? = null
    @Inject set // Inject로 setter를 선언함.

Backing Fields(뒷받침하는 필드)

코틀린 클래스에서는 필드를 직접적으로 선언할 수 없습니다. 따라서 값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드(backing field)가 있어야 합니다. 접근자의 본문에서는 field 식별자를 이용하여 backing field에 접근할 수 있습니다. getter에서는 field값을 읽을수만 있고 setter에서는 field 값을 읽거나 쓸 수 있습니다.

var counter = 0 // 이 initializer는 backing field를 직접 할당함.
    set(value) {
        if(value >= 0) field = value
    }

여기서 사용된 field 식별자는 이처럼 프로퍼티의 접근자에서만 사용될 수 있습니다. backing field는 최소한 하나 이상의 접근자로 기본 구현을 사용하거나, 커스텀 접근자가 field 식별자를 이용해 backing field를 참조할 때 생성됩니다. 예를 들어 아래와 같은 예시에서는 backing field가 없습니다.

// 예시 1.
val isEmpty: Boolean
    get() = this.size == 0

// 예시 2.
var name: String // get, set
    get() {
        return "User"
    }

Backing Properties

만약 위에서 언급한 backing field의 scheme에 맞지 않는 작업을 하려고 한다면, backing field가 아닌 backing property가 됩니다.

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 프로퍼티와 디폴트 getter/setter에 대한 접근이 최적화되어있으므로, 이 경우에는 함수 호출 오버헤드가 발생하지 않습니다.

인터페이스에 선언된 프로퍼티 구현하기

코틀린에서는 아래와 같이 인터페이스에 추상 프로퍼티 선언을 넣을 수 있습니다.

interface User {
    val nickname: String
}

이는 User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 한다는 뜻입니다. 인터페이스는 상태를 포함할 수 없으므로 상태를 저장하기 위해서는 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야 합니다. 아래의 예시를 통해 세 가지 방법으로 인터페이스를 구현해보겠습니다.

// 1번. 주 생성자 안에 프로퍼티를 직접 선언함
class PrivateUser(override val nickname: String) : User

// 2번. 커스텀 게터로 프로퍼티를 설정함.
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}

// 3번. 초기화 식으로 프로퍼티 값을 설정함.
class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)
}

여기서 2번 SubsribingUser와 3번 FacebookUser를 구현하는 방법의 차이에 주의해야합니다. SubscribingUser의 nickname 프로퍼티는 매번 호출될때마다 substringBefore를 호출하여 계산하는 커스텀 게터를 활용합니다. 이와 다르게 FacebookUser의 nickname 프로퍼티는 객체를 초기화할 때 계산한 데이터를 뒷받침하는 필드(backing fields)에 저장했다가 불러오는 방식을 활용합니다.

인터페이스에는 추상 프로퍼티뿐만 아니라 getter와 setter가 있는 프로퍼티를 선언할 수도 있습니다. 이 떄의 getter와 setter는 backing field를 참조할 수 없습니다. 왜냐하면 backing field가 있다면 인터페이스에 상태를 추가하는 셈인데, 인터페이스는 상태를 저장할 수 없기 때문입니다. 인터페이스에 선언된 프로퍼티와는 달리, 클래스에 구현된 프로퍼티는 backing field를 원하는 대로 사용할 수 있습니다.

참고 문헌

[Kotlin] 코틀린의 생성자(Constructor)

|

코틀린의 생성자(Constructor)

코틀린에서는 하나의 주(primary) 생성자와 여러 개의 부(secondary) 생성자를 사용할 수 있습니다. 주 생성자는 클래스의 헤더로써 클래스의 이름과 동일한 이름을 사용합니다. 만약 주 생성자에 어노테이션이나 가시성 변경자(visibility modifiers)가 없다면 생성자 키워드는 생략할 수 있습니다.

// constructor 키워드 사용
class Person constructor(firstName: String) {/*...*/}

// constructor 키워드 생략
class Person(firstName: String) {/*...*/}

보통 주 생성자는 클래스를 초기화할 떄 주로 사용하는 간략한 생성자로 클래스 본문 밖에서 정의하며 부 생성자는 클래스 본문 안에서 정의합니다. 또한 코틀린에서는 초기화 블록(initializer block)을 통해 초기화 로직을 추가할 수 있습니다.

주 생성자(primary constructor)

코틀린에서는 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자라고 합니다. 주 생성자는 두 가지 목적으로 쓰입니다. 하나는 생성자 파라미터를 지정하는 것이고, 다른 하나는 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 것입니다. 아래의 1번, 2번, 3번은 모두 같은 클래스의 생성자를 정의하는 것입니다.

// 1번
class User constructor(_nickname: String){
    val nickname: String
    init {
        nickname = _nickname
    }
}

// 2번
class User(_nickname: String){
    val nickname = _nickname
}

// 3번
class User(val nickname: String)

이 중 3번의 방식이 가장 간단합니다. 함수 파라미터처럼 생성자 파라미터에도 아래와 같이 디폴트 값을 정의할 수 있습니다.

class User(val nickname: String, val isSubscribed: Boolean = true)

클래스의 인스턴스를 만들떄에는 자바와는 달리 new 키워드 없이 생성자를 직접 호출하면 됩니다.

val hyun = User("유저")

부 생성자(secondary constructor)

위에서 언급했듯이 부 생성자는 클래스 본문 안에서 정의되는 생성자입니다. 아래와 같이 생성자가 2개인 View 클래스가 있다고 가정하겠습니다.

open class View {
    constructor(ctx: Context){ // 부 생성자
        // 코드
    }
    constructor(ctx: Context, attr: AttributeSet){ // 부 생성자
        // 코드
    }
}

이 클래스를 확장하면서 아래와 같이 두 가지 방식으로 부 생성자를 정의할 수 있습니다.

// 1번. super()를 통해 상위 클래스의 생성자를 호출함.
class MyButton : View {
    constructor(ctx: Context)
        : super(ctx){
            // ...
    }
    constructor(ctx: Context, attr: AttributeSet)
        : super(ctx, attr){

    }
}

// 2번. this()를 통해 자기 자신인 클래스의 다른 생성자를 호출함.
class MyButton : View {
    constructor(ctx: Context) : this(ctx, MY_STYLE){
        // ...
    }
    constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr){
        // ...
    }
}

클래스에 주 생성자가 없다면 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 합니다. 부 생성자가 필요한 이유는 자바와의 상호 운용성과 팩토리 메소드에서 필요하기 때문입니다.

참고 문헌