클래스 초기화: 주 생성자와 초기화 블록

클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자(primary constructor)라고 한다.

class User(val nickname: String)

이를 명시적인 선언으로 풀어보면

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

constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용한다. init 키워드는 초기화 블록을 실행한다.

초기화 블록은

  • 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어간다.
  • 주 생성자와 함께 사용된다.
  • 주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없으므로 초기화 블록을 사용한다.
  • 필요하다면 클래스 안에 여러 초기화 블록을 선언할 수 있다.

위 예제는 nickname 프로퍼티를 초기화하는 코드를 nickname 프로퍼티 선언에 포함시킬 수 있다.

class User constructor(_nickname: String) {
    val nickname = _nickname
}

주 생성자 파라미터 이름 앞에 val을 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 쓸 수 있다.

class User(val nickname: String)

함수 파라미터와 마찬가지로 디폴트 값을 정의할 수 있다.

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

클래스의 인스턴스를 만드려면 생성자를 직접 호출하면 된다.

>>> val hyun = User("현석")
>>> println(hyun.isSubscribed)
true
>>> val gye = User("계영", false)
>>> println(gye.isSubscribed)
false
>>> val hey = User("혜원", isSubscribed = false)
>>> println(hey.isSubscribed)
false

기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출할 필요가 있다. 기반 클래스를 초기화하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다.

open class User(val nickname: String)
class TwitterUser(nickname: String) : User(nickname)

별도의 생성자를 정의하지 않을 수 있다. 이 경우 컴파일러가 자동으로 아무런 인자가 없는 디폴트 생성자를 만든다.

open class Button

생성자가 아무 인자를 받지 않더라도 하위 클래스는 반드시 생성자를 호출해야 한다.

class RadioButton: Button()

이러한 규칙으로 인해 기반 클래스의 이름 뒤에는 꼭 괄호가 들어간다. 반면 인터페이스는 생성자가 없기 때문에 괄호를 사용하지 않는다. 따라서 괄호의 유무로 클래스인지 인터페이스인지 쉽게 판별할 수 있다.

어떤 클래스를 외부에서 인스턴스화하지 못하게 하려면 모든 생성자를 private로 만들면 된다.

class Secretive private constructor() {}

위 클래스는 유일한 주 생성자가 비공개이므로 외부에서 인스턴스화할 수 없다.

부 생성자: 상위 클래스를 다른 방식으로 초기화

일반적으로 코틀린은 생성자가 여럿 있는 경우가 자바보다 훨씬 적다. 자바와 달리 디폴트 파라미터 값과 이름 붙은 인자 문법을 제공하기 때문이다.
open class View {
    constructor(ctx: Context) {}
    constructor(ctx: Context, attr: AttributeSet) {}
}

이 클래스는 주 생성자를 선언하지 않고(클래스 헤더의 클래스 이름 뒤에 괄호가 없다) 부 생성자 2개를 선언한다. 부 생성자는 constructor 키워드로 시작하고 얼마든지 생성할 수 있다.

생성자에서 super를 이용해 상위 생성자에게 객체 생성을 위임할 수 있다.

class MyButton: View {
    constructor(ctx: Context): super(ctx) {}
    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {}
}

생성자에서 this()를 이용해 클래스 자신의 다른 생성자를 호출할 수 있다.

class MyButton: View {
    constructor(ctx: Context): super(ctx, MY_STYLE) {}
    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {}
}

클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다.

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

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

interface User {
    val nickname: String
}

User 인터페이스를 구현하는 클래스는 nickname의 값을 얻을 수 있는 방법을 제공해야 한다. 인터페이스는 상태를 포함할 수 없으므로 인터페이스를 구현하는 하위 클래스에서 상태 저장을 위한 프로퍼티를 만들어야 한다.

아래와 같은 방법으로 이 인터페이스를 구현할 수 있다.

class PrivateUser(override val nickname: String) : User //주 생성자에 있는 프로퍼티

class SubscribingUser(val email: String): User {
    override val nickname: String
        get() = email.substringBefore('@') //커스텀 게터
}

class FacebookUser(val accountId: Int): User {
    override val nickname = getFacebookName(accountId) //프로퍼티 초기화 식
}

커스텀 게터와 프로퍼티 초기화 식은 비슷하면서도 차이점이 존재한다. 프로퍼티 초기화 식은 객체 초기화 시 한번만 실행되며 커스텀 게터는 호출될 때마다 실행된다.

인터페이스는 게터와 세터가 있는 프로퍼티를 선언할 수 있다. 물론 뒷받침하는 필드를 참조할 수 없다.

interface User {
    val email: String
    val nickname: String
        get() = email.substringBefore('@')
}

게터와 세터에서 뒷받침하는 필드에 접근

프로퍼티의 저장된 값의 변경 이력을 로그에 남기려는 경우를 생각해보자.

class User(val name: String) {<br>    var address: String = "unspecified"<br>        set(value: String) {<br>            <em>println</em>("""<br>                Address was changed for $name:<br>                "$<strong>field</strong>" -> "$value".""".<em>trimIndent</em>()) //뒷받침하는 값 필드 읽기<br>            <strong>field </strong>= value //뒷받침하는 값 필드 변경하기<br>        }<br>}

코틀린에서 프로퍼티의 값을 바꿀 때는 user.address = "new value"를 사용한다. 이 구문은 내부적으로 address의 세터를 호출한다. 이 세터를 커스텀하여 추가 로직을 실행할 수 있다.

접근자의 본문에서는 field라는 식별자를 이용해 뒷받침하는 필드에 접근할 수 있다. 게터는 field 값을 읽을 수만 있고 세터는 읽고 쓸 수 있다.

접근자의 가시성 변경

접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다. 하지만 원한다면 가시성 변경자를 추가해서 가시성을 변경할 수 있다.

class LengthCounter {
    var counter: Int = 0
        private set //이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다
    fun addWord(word: String) {
        counter += word.length
    }
}

© 2021. All rights reserved.

Powered by Hydejack v9.1.6