본문 바로가기
2023년 이전/kotlin

kotlin - 생성자

by JeongUPark 2020. 2. 21.
반응형

[출처 -  Kotlin In Action] [아래 내용들은 Kotlin In Action을 공부하면서 스스로 정리한 내용입니다] 

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

 

클래스 초기화 

다음 클래스 선언을 보면

class User(val nickname: String)

중괄호( {} )가 없고 괄호( () ) 만 있습니다. 그리고 괄호 안에 val을 통해 String을 선언하고 있습니다. 위 처럼 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자(primary constructor)라고 부릅니다. 이 주 생성자는 생성자 파라미터를 지정하고, 그 생성자 파라미터에 의해 초기화 되는 프로퍼티를 정의하는 목적을 가집니다. 그럼 중괄호를 사용하여 위의 선언을 다시 해보겠습니다.

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

위 코드에서는 constructor와 init라는 새로운 키워드를 볼 수 있습니다.

constructor는 주생성자나 부 생성자 정의를 시작할 때 사용됩니다.

init는 초기화 블록을 시작할 때 사용 됩니다. 초기화 블록은 클래스 객체가 만들어 질때 실행될 초기화 코드가 들어 갑니다. 초기화 블록은 여러번 선언할 수 잇습니다.

constructor는 제한 적이기 떄문에 별도의 코드를 포함 할 수 없기 때문에 init가 필요 합니다.

그리고 위에서 _nickname으로 생성자 파라미터를 지정한 이유는 프로퍼티와 구분하기 위해서입니다. 보통 자바에서는 

this.nickname = nickname처럼 사용 합니다. (코틀린에서도 this를 사용할 수 있습니다.)

 

그리고 위의 User 클래스를 다음과 같이 고칠 수도 있습니다.

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

nickname 프로퍼티를 초기화하는 _nickname을 nickname 선언에서 대입할 수 있기 때문에 init 블록은 필요가 없고, 

별다른 애노테이션이나 가시성 변경자가 없기 때문에 constructor도 생략 할 수 있습니다. (보통은 constructor는 잘 안 쓰는 것 같습니다.)

또한 클래스 초기화에서 가장먼저본 코드인 

class User(val nickname: String)

을 통하여 User를 인스턴스화 할때 nickname에 값을 대입하는 방법이 가장 간단합니다. ( 사실 위의 세가지 방법의 User 클래슨느 모두 같다고 할 수 있습니다.)

또한  다음과 같이

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

디폴트 값을 가지는 생성자 파라미터가 있다면 이 클래스를 인스턴스 할때 다음과 같이

fun main(args: Array<String>) {
    val alice = User("Alice")
    println(alice.isSubscribed)
    val bob = User("Bob", false)
    println(bob.isSubscribed)
    val carol = User("Carol", isSubscribed = false)
    println(carol.isSubscribed)
}

Boolean 부분은 사용하지 않거나 (이 경우 디폴트값 사용),

모든 파라미터를 적용하거나,

생성자 파라미터 중 일부 이름을 지정할 수 도 있습니다.(이 부분은 여기서 확인 하시면 이해하는데 더 도움이 될 것 같습니다.)

 

클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출 해야 합니다. 글로만 하면 이해가 안되니 코드로 확인 해 보겠습니다.

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

위의 코드애서 googleUser라는 클래스가 User 클래스를 상속 받고 있습니다. 그리고 User 클래스의 주 생성자에 nickname이라는 생성자 프로퍼티가 있습니다. 그러므로 하위 클래스인 googleUser는 기반 클래스인 User 클래스를 상속 받을 때 생성자를 호출 합니다. 

예를 들어보면

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

fun main(){
    val jeongu = googleUser("jeongu")
      println(jeongu.nickname)
}

이렇게 하면 결과는 jeongu가 나타나며 위에서 jeongu.nickname에서 nickname은 User 클래스의 nickname을 말하게 됩니다. 만일 googleUser에 똑같은 nickname 파라미터를 만든다면 error가 뜨면서 override를 선언하라고 뜹니다.

 

그리고 다음과 같이

open class Button

클래스를 정의할 때 별도로 생성자를 정의하지 않는 다면 컴파일러가 자동으로 아무일도 하지 않고 인자가 없는 디폴트 생성자를 만들어 줍니다. (코트린은 컴파일러가 정말 열일 하는 것 같습니다.)

하지만 이 Button 클래스를 상속하는 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 합니다.

class RadioButton : Button()

반면 인터페이스는 생성자가 없기 때문에 따로 인터페이스를 확장할 때는 괄호를 사용하지 않아도 됩니다.

interface View {
    fun getCurrentState(): State
    fun restoreState(state: State) {}
}
class Button : View {
    override fun getCurrentState(): State = ButtonState()

    override fun restoreState(state: State) { /*...*/ }

    class ButtonState : State { /*...*/ }
}

 

그리고 클래스 외부에서 클래스를 인스턴스화 하지 못하게 하고 싶다면 생성자를 private로 만들면 됩니다.

class Secretive private constructor()

이렇게 하면 클래스 안에는 주생성자 밖에 없고 주 생성자는 비공개이므로 외부에서 이 Secretive클래스를 인스턴스화 할 수 없습니다. 이런 비공개 생성자는 동반객체를 사용할 때 유용합니다. 또한 싱글턴 클래스를 만들대 코틀린은 간단하게 

object Singleton {}
val singleton = Singleton

이렇게 만들면 되지만 만일 생성자를 사용하여 무엇인가 설정하고 싶다만  비공개 생성자를 사용하여

class Singleton private constructor() {
    
    companion object {
        @Volatile private var instance: Singleton? = null
        
        @JvmStatic fun getInstance(): Singleton =
            instance ?: synchronized(this) {
                instance ?: Singleton().also {
                    instance = it
                }
            }
    }
}
val singleton = Singleton.getInstance()

이렇게 사용할 수 있습니다.

또한 유틸리티 함수 담아주는 클래스를 만들때도 비공개 생성자를 사용하여 인스턴스는 만들지 않지만 그 클래스의 유틸리티들을 가져다 쓸 수 있습니다.

 

부 생성자

코틀린에서는 디폴트 파라미터에 의해서 생성자가 여러개 필요한 경우가 적지만, 아예 없지는 않습니다. 그러므로 생성자가 여럿 필요할 경우 부 생성자들을 만들어서 여러가지 방법으로 인스턴스를 초기화 할 수 있도록 합니다.

다음 코드를 보겠습니다.

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

View 라는 class는 주 생성자를 선언하지 않고 안에 constructor로 2개의 부 생성자를 만들었습니다. 그리고 이 부 생성자들을 통하여 View라는 클래슬르 인스턴스화 할 수 있습니다. (부 생성자는 constructor로 시작합니다.)

그리고 이 클래스를 상속받은 똑같이 부 생성자를 정의할 수 있습니다.

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

    }
}

우선은 MyButton, MyButton2와 같이 주생성자를 생성하여 상속할 수 있고, MyButton3과 같이 constructor를 통하여 부 생성자들을 생성하여 상속할 수 있습니다. 이때 MyButton3의 부 생성자들에 super가 있는데 이는 기반 클래스 생성자를 호출 하는 것입니다.

그래서 super를 통하여 MyButton3의 ctx와 View의 ctx가 대응되고, MyButton3의 attr과 View의 attr이 대응됩니다. 

마지막으로 this를 통하여 클래스 안의 다른 생성자도 호출 할 수 있습니다.

open class ParentView{
    constructor(name: String){}
    constructor(name:String, child : Int){}
}
class childView : ParentView{
    constructor(name:String) : this(name, 3)
    constructor(name:String,child :Int) : super(name,child)
}

이렇게 this를 활용하여 childView 안에 있는 다른 부 생성자에게 생성을 위임 할 수 도 있습니다.

 

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

 

반응형