Kotlin

[Kotlin] 주 생성자 & 부 생성자 & 초기화 블록

sh1mj1 2024. 1. 6. 20:48

 

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

 

이미지 출처          https://commons.wikimedia.org/wiki/File:Kotlin_Icon.svg

자바에서는 생성자를 하나 이상 선언할 수 있다.코틀린에서도 비슷하지만 바뀐 점들이 있다. 

코틀린에서는 primary constructor(주 생성자), secondary constructor(부 생성자)를 구분하며, initializer block(초기화 블록)을 통해 초기화 로직을 추가할 수 있다.

주 생성자 & 초기화 블록

코틀린에서 가장 일반적으로 사용하는 주 생성자 초기화를 보이기 전에, 가장 복잡한 형태로 초기화하는 방식부터 보자.

 

프로퍼티 `nickname`  을 가지고 있는 `User` 클래스

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

    init { // initializer block(초기화 블록)
        nickname = _nickname
    }
}
  • 주 생성자
    • 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
  •  `constructor`
    • 주 생성자나 부 생성자 정의를 시작할 때 사용한다
  • `init`
    • 초기화 블록을 시작한다
    • 초기화 블록에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어간다.
    • 주 생성자와 함께 사용된다.

`_nickname: String` 에서 생성자 파라미터를 명시하고 있다.

여기서 맨 앞의 `_` 는 프로퍼티(`nickname`)와 생성자 파라미터`(_nickname`)를 구분해준다.

 

물론 자바에서 흔히 쓰는 방식처럼 생성자 파라미터와 프로퍼티의 이름을 같게 하고 프로퍼티에 `this` 를 써서 모호성을 없애도 된다. 

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

 

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

그래서 초기화 코드를 초기화 블록에 넣을 필요가 없다.

또한 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자가 없다면 `constructor` 를 생략해도 된다.

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

 

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

class User(val nickname: String)

이렇게 지금까지 살펴본 `User` 선언은 모두 같다.

당연히 마지막 선언이 가장 간결하다.

 

이렇게 주 생성자는 생성자 파라미터를 지정하고, 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적으로 쓰인다.

 

  • 생성자 파라미터에도 디폴트 값을 정의할 수 있다.
class User(val nickname: String, val isSubscribed: Boolean = true)

@Test
fun userTest() {
    val sh1mj1 = User("심지") // 클래스의 인스턴스를 만들 때는 new 없이 생성자를 직접 호출
    assert(sh1mj1.nickname == "심지")
    assertTrue(sh1mj1.isSubscribed)

    val sh1mj12 = User("심지2", false)
    assertFalse(sh1mj12.isSubscribed)

    val sh1mj13 = User("심지3", isSubscribed = false) // 생성자 인자에 이름 지정
    assertFalse(sh1mj13.isSubscribed)
}

모든 생성자 파라미터에 디폴트 값 지정하면?

코틀린에서 모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어준다.

그 생성자는 디폴트 값을 이용해 클래스를 초기화한다.

자바 라이브러리 중에 파라미터가 없는 생성자를 통해 객체를 생성해야만 하는 경우가 있는데, 코틀린이 제공하는 파라미터 없는 생성자는 그런 라이브러리와의 통합을 쉽게 해준다.

기반(상위) 클래스를 확장할 때 주 생성자

클래스에 기반 클래스가 있을 때, 주 생성자에서 기반 클래스의 생성자를 호출해야 할 때도 있다.

기반 클래스를 초기화하려면 기반 클래스 이름 뒤 괄호에 생성자 인자를 넘긴다.

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

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

 

클래스를 정의할 때 생성자를 정의하지 않으면 자동으로 인자가 없는 디폴트 생성자를 만들어준다.

open class Button // 인자가 없는 디폴트 생성자가 생성됨

class RadioButton: Button()

`Button` 을 상속한 하위 클래스 `RadioButton` 은 반드시 `Button` 클래스의 생성자를 호출해야 한다.

그래서 기반 클래스를 확장할 때는 기반 클래스 이름 뒤에 꼭 괄호가 들어간다. (`: Button()`)

반면에 인터페이스를 구현할 때는 인터페이스 이름 뒤에 아무 괄호도 없다.

비공개 생성자

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

class Secretive private constructor(val name: String)

이렇게 되면 `Secretive` 의 유일한 주 생성자는 비공개이다.

그래서 외부에서 `Secretive` 를 인스턴스화할 수 없다.

 

유틸리티 함수를 담아두는 역할만을 하는 클래스는 인스턴스화할 필요가 없고, 싱글턴 클래스는 미리 정한 팩토리 메서드 등의 생성 방법으로만 객체를 생성해야 한다.

자바에서는 private 생성자를 정의해서 클래스를 다른 곳에서 인스턴스화하지 못하게 막는다.(자바에서 아무 생성자도 정의하지 않으면 기본 공개 생성자가 자동으로 생김)

반면에 코틀린은 정적 유틸리티 함수 대신 최상위 함수를 사용할 수 있고, 싱글턴을 사용하고 싶다면 `object` 키워드를 사용해서 객체를 선언하면 된다.

 

실제로 대부분의 경우 간단한 주 생성자 문법만으로도 충분하다.

당연히 다양한 생성자를 정의해야 하는 경우도 있다.

부 생성자

코틀린에서는 생성자가 여럿 있는 경우가 자바보다 훨씬 더 적다.

자바에서 오버로딩한 생성자가 필요한 상황 중 대부분은 코틀린의 디폴트 파라미터 값과 이름 붙인 argument 문법을 사용하여 해결할 수 있다.

 

하지만 프레임워크 클래스를 확장할 때, 여러 방법으로 인스턴스를 초기화할 수 있게 다양한 생성자를 지원해야 하는 경우도 있다. 

그렇다면 어떻게 여러 생성자를 만들 수 있을까?

이 때 부 생성자를 사용하면 된다.

 

생성자가 2개인 `View` 클래스가 있다고 하자.

open class View {
    // 부 생성자들
    constructor(context: Context) {/* ...*/}
    constructor(context: Context, attrs: AttributeSet) {/* ...*/ }
}

이 클래스는 주 생성자를 선언하지 않고 부 생성자만 2가지 선언했다. 

`constructor` 키워드로 시작하는 코드 블록이 부 생성자이다.

 

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

class MyButton : View {
    constructor(context: Context) : super(context){ /* ...*/ }
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { /* ...*/ }
}

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

 

상위 클래스 생성자에게 객체 생성을 위임하고 있다.

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

class ThisButton: View{
    constructor(context: Context): this(context, HashAttributeSet()){ /* ...*/ }
    constructor(context: Context, attrs: AttributeSet): super(context, attrs) { /* ...*/ }
}

같은 클래스의 다른 생성자에게 생성을 위임하고 있다.

 

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

즉, 위같은 그림에서 객체 생성을 위임하는 화살표를 따라가면 그 끝에는 상위 클래스 생성자를 호출하는 화살표가 있어야 한다는 뜻이다.