logo

Tech

후기

회고

Study

Java
Kotlin

코틀린의 생성자 및 constructor와 init

avatar

초코칩

2025년 04월 16일 17:20

공유하기
클립보드로 복사
thumbnail

이번 글에서는 코틀린에서 생성자를 정의하는 방법에 대해 알아보고, constructorinit 키워드의 차이점에 대해 알아봅니다.

코틀린에서 생성자를 정의하는 방법은 주로 주 생성자와 부 생성자를 사용하는 두 가지 방식이 있습니다.

주 생성자

constructor 키워드

주 생성자는 클래스 선언 헤더에 직접 정의되며, 클래스 이름 옆에 constructor 키워드와 함께 파라미터를 지정합니다.

class Person constructor(name: String, age: Int) {
    val name: String = name
    val age: Int = age
}

물론 constructor는 생략 가능합니다.

class Person(name: String, age: Int) {
    val name: String = name
    val age: Int = age
}

주 생성자의 파라미터를 val 또는 var로 선언하면 자동으로 클래스 프로퍼티로 정의됩니다.

프로퍼티는 필드의 역할도, 접근자 메서드의 역할도 할 수 있다 라고 이해할 수 있습니다.

class Person(val name: String, val age: Int)

위 코드는 클래스 본문 없이도 nameage를 프로퍼티로 자동 생성합니다.

init 키워드

주 생성자에서 추가 초기화 로직을 정의하려면 init 블록을 사용할 수 있습니다.

class Person(val name: String, val age: Int) {
    init {
        println("Person created: $name, $age")
    }
}

data class는 주 생성자를 필수로 요구하며, 주 생성자의 파라미터는 반드시 val 또는 var로 선언해야 합니다.

부 생성자

부 생성자는 클래스 본문 안에 constructor 키워드를 사용해 정의합니다. 부 생성자는 주 생성자가 없거나, 추가적인 생성 로직이 필요할 때 사용됩니다.

class Person {
    val name: String
    val age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

주 생성자가 있는 경우, 부 생성자는 주 생성자를 호출해야 합니다(위임 호출). Java에서와 마찬가지로 : this()를 호출할 수 있습니다.

class Person(val name: String) {
    var age: Int = 0

    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

여러 개의 부 생성자를 아래 처럼 정의할 수 있습니다.

class Person(val name: String) {
    var age: Int = 0
    var email: String = ""

    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }

    constructor(name: String, age: Int, email: String) : this(name, age) {
        this.email = email
    }
}

Private 생성자

생성자를 외부에서 호출하지 못하도록 private으로 설정할 수 있습니다. 이는 주로 싱글톤 패턴이나 팩토리 메서드와 함께 사용됩니다.

class Singleton private constructor() {
    companion object {
        val instance: Singleton by lazy { Singleton() }
    }
}

constructor vs init

constructorinit 키워드는 서로 유사한 기능을 갖고 있습니다. 어떤 차이가 있는지 예제를 통해 비교해 봅시다.

open class Parent(arg: Unit = println("Parent 주 생성자")) {
    private val a = println("Parent a 프로퍼티")

    init {
        println("Parent 첫 번째 init")
    }

    init {
        println("Parent 두 번째 init")
    }

    private val b = println("Parent b 프로퍼티")
}
class Child : Parent {
    val a = println("Child a 프로퍼티")

    init {
        println("Child 첫 번째 init")
    }

    constructor(arg: Unit = println("Child 첫 번째 생성자의 매개변수")) : super() {
        println("Child 첫 번째 생성자")
    }

    val b = println("Child b 프로퍼티")

    constructor(arg: Int, arg2: Unit = println("Child 두 번째 생성자의 매개변수")) : this() {
        println("Child 두 번째 생성자")
    }

    init {
        println("Child 두 번째 init")
    }
}
fun main() {
    Child(1)
}

위 예제에서 Child(1)을 호출하면 다음과 같은 순서대로 코드가 실행됩니다.

도식화해서 호출 순서를 살펴보면 아래와 같습니다.

특이하게 살펴볼 점이 있습니다.

  • 생성자의 디폴트 매개변수는 해당 생성자가 호출되는 시점에 가장 먼저 평가됩니다.
  • 프로퍼티와 init 블록은 선언된 순서대로 실행됩니다.
  • 부모 클래스의 초기화가 자식 클래스보다 항상 먼저 실행됩니다.

정리

배운 점은 아래와 같습니다.

  • init은 생성자에 들어가야할 로직이 복잡한 경우에 사용하는 것을 추천한다.
  • init 블록은 주 생성자 실행 후, 모든 생성자 로직에 공통으로 넣어야할 경우에만 쓴다.
  • 단순 프로퍼티로 이루어진 생성자일 경우, constructor 키워드는 필요없다.
  • constructor는 부 생성자를 정의할 때 사용되며, 특정한 조건이나 파라미터에 따라 다른 방식으로 객체를 생성하고자 할 때 유용하다.
  • init 블록과 프로퍼티 초기화는 선언된 순서대로 실행되므로, 코드 작성 순서가 곧 실행 순서다.
  • 상위 클래스에 정의된init 블록은 하위 클래스가 super()를 통해 호출될 수 있으므로, open class일 경우 주의해서 사용한다.

Ref