본문 바로가기
Kotlin

[Kotlin in Action] 4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언

by Nhahan 2025. 3. 15.

코틀린은 주 생성자와 부 생성자를 구분한다. 또한 코틀린에서는 초기화 블록을 통해 초기화 로직을 추가할 수 있다.

 

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

아래의 세 가지 User 선언은 모두 같다.

class User constructor(_nickname: String) { // 파라미터가 하나만 있는 주 생성자
    val nickname: String
    
    init { // 초기화 블록
        nickname = _nickname
    }
}

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

class User(nickname: String)

하지만 마지막 선언이 가장 간결하다.

 

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

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

 

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

open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }

 

클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 아무 일도 하지 않는 인자가 없는 디폴트 생성자를 만들어준다.

open class Button // 인자가 없는 디폴트 생성자가 만들어진다

위 Button 클래스의 생성자는 아무 인자도 받지 않지만, Button 클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다.

class RadioButton : Button()

 

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

class Secretive private constructor() {} // 이 클래스의 주 생성자는 비공개다

 

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

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

이 클래스를 확장하면서 똑같이 부 생성자를 정의할 수 있다.

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

 

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

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

 

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

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

interface user {
    val nickname: String
}

이 인터페이스를 구현하는 몇 가지 방법을 살펴보자. PrivateUser는 별명을 저장하기만 하고 SubscribingUser는 이메일을 함께 저장한다. FacebookUser는 페이스북 계정의 ID를 저장한다. 이 세 클래스는 각각 다른 방식으로 추상 프로퍼티 nickname을 구현한다.

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

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

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

>>> println(PrivateUser("test@kotlinlang.org").nickname)
test@kotlinlang.org
>>> println(SubscribingUser("test@kotlinlang.org").nickname)
test

 

인터페이스에는 추상 프로퍼티뿐 아니라 getter와 setter가 있는 프로퍼티를 선언할 수도 있다. 물론 그런 getter와 setter는 뒷받침하는 필드를 참조할 수 없다(인터페이스는 상태를 저장할 수 없으므로).

interface User {
    val email: String
    val nickname: String
        get() = email.substringbefore('@') // 프로퍼티에 뒷받침하는 필드가 없지만 대신 매번 결과를 계산해 돌려준다.
}

 

4.2.4 getter와 setter에서 뒷받침하는 필드에 접근

위에서는

1. 값을 저장하는 프로퍼티

2. 커스텀 접근자에서 매번 값을 계산하는 프로퍼티

두 가지 유형에 대해 살펴봤다. 이제는 이 두 유형을 조합해서 어떤 값을 저장하되 그 값을 변경하거나 읽을 때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만드는 방법을 살펴보자. 값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 한다.

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

>>> val user = User("Alice")
>>> user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
Address was changed for Alice:
"unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen"

이 코드에서는 커스텀 setter를 정의해서 추가 로직을 실행한다.

 

접근자의 본문에서는 field라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다. getter에서는 field 값을 읽을 수만 있고, 세터에서는 field 값을 읽거나 쓸 수 있다.

 

4.2.5 접근자의 가시성 변경

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

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

 

댓글