본문 바로가기
Language/Kotlin

[Kotlin IN ACTION] - 코틀린 생성자(2)

by Jman 2022. 7. 29.

부 생성자

생성자가 여럿 필요한 경우가 가끔 있다.

가장 일반적인 상황은 프레임워크 클래스를 확장해야 하는데 여러 가지 방법으로 인스턴스를 초기화 할 수 있게 다양한 생성자를 지원해야 하는 경우다.

 

예시

View 클래스가 있다. 그 클래스를 코틀린으로 코드를 작성하면

open class View {
    constructor(ctx : Context) {
        ...
    }

    construcotr(ctx : Context, attr : AttributeSet) {
        ....
    }
}

위 코드로 작성할 수 있다. 해당 클래스는 주 새성자를 선언하지 않고 부 생성자만 2개 선언했다.

부 생성자는 constructor 키워드로 시작한다.

 

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

class MyButton : View {
    constructor(ctx : Context) : super(ctx) {
        ...
    }

    constructor(ctx : Context, attr : AttributeSet) : super(ctx, attr) {
        ...
    }
}

 

여기서 두 부 생성자는 super() 키워드를 통해 자신에 대응하는 상위 클래스 생성자를 호출한다.

 

또한, 자바와 마찬가지로 생성자에서 this() 를 통해 클래스 자신의 다른 생성자를 호출할 수 있다.

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

    constructor(ctx : Context, attr : AttributSet) : super(ctx, arrt) {
        ...
    }
}

MyButton 클래스의 생성자 중 하나가 파라미터의 디폴트 값을 넘겨서 같은 클래스의 다른 생성자(this를 사용하는..)에게 생성을 위임한다.

 

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

 

부 생성자가 필요한 주된 이유는 자바 상호운용성이다.

하지만, 부 생성자가 필요한 다른 경우도 있다.

클래스 인스턴스를 생성할 때 파라미터 목록이 다른 생성 밥이 여럿 존재하는 경우, 부 생성자를 여럿 둘 수 밖에 없다.

 

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

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

interface User {
	val nickname : String
}

인터페이스에 있는 프로퍼티 선언엔 뒷받침하는 필드나 게터 등의 정보가 들어있지 않다.

따라서, 인터페이스는 아무 상태도 포함할 수 없으므로 상태를 저장할 필요가 있으며,

인터페이스를 구현한 하위 클래스에 상태 저장을 위한 프로퍼티 등을 만들어야 한다.

 

아래는 인터페이스를 구현하는 클래스의 각기 다른 방법이다.

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

// 커스텀 게터 (nickname은 매번 호출될 때마다 substringBefore를 호출해 계산)
class SubscribingUser(val email: String) : User{
    override val nickname: String
        get() = email.substringBefore("@")
}

// 프로퍼티 초기화 식(객체 초기화 시 계산한 데이터를 뒷받침하는 필드에 저장했다가 불러옴)
class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)
}

PrivateUser : 주 생성자 안에 프로퍼티를 직접 선언하는 간결한 구문 사용. 이 클래스 프로퍼티는 User 의 추상 프로퍼티를 구현하고 있으므로, override 를 표시해야 한다.

 

SubscribingUser :  커스텀 게터로 nickname 프로퍼티를 설정한다. 이 프로퍼티는 뒷받침하는 필드에 값을 저장하지 않고, 매번 이메일 주소에서 별명을 계산해서 반환한다.

 

FacebookUser : 초기화 식으로 nickname 값을 초기화 한다. 페이스북 사용자 ID를 받아서 그 사용자의 이름을 반환해주는 함수를 호출 해서 nickname 을 초기화 한다. (이 부분은 페이스북에서 접속해서 인증을 거친 후 원하는 데이터를 가져와야하기 때문에 비용이 많이 들 수 있다. 그래서 객체를 초기화하는 단계에 한 번만 getFacebookName 을 호출하게 설계한 것이다)

 

인터페이스 에는 추상 프로퍼티뿐 아니라, 게터와 세터가 있는 프로퍼티를 선언할 수 있다.

물론 그런 게터와 세터는 뒷받침하는 필드를 참조할 수 없다.

 

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

만약 위 인터페이스를 구현하는 하위 클래스는 추상 프로퍼티인 email 을 반드시 오버라이드해야 한다. 반면 nickname 은 오버라이드 하지 않고 상속할 수 있다.

그리고 역시나, 인터페이스에 선언된 프로퍼티와 달리 클래스에 구현된 프로퍼티 프로퍼티는 뒷받침하는 필드를 원하는대로 사용가능하다.

 

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

프로퍼티의 두 가지 유형이 있다.

  • 값을 저장하는 프로퍼티
  • 커스텀 접근자에서 매번 값을 계산하는 프로퍼티
// 세터에서 뒷받침하는 필드 접근하기

fun main(args : Array<String>) {
    val user = User("chrisAn")
    user.address = "서울 강남구 대치동" // 내부적으로 세터를 호출
}

class User(val name : String) {
    var address : String = "unspecified"
        set(value : String) {
            println("Address was changed for $name")
            println("$field" + " => " + "$value")
            field = value
        }
}

 

접근자의 가시성 변경

class LengthCounter {
    val counter : Inte = 0
        private set // 외부에 공개하고싶지 않을 때, 가시성을 private 으로 지정.


    fun addWord(word : String) {
        counter += word.length
    }
}