코틀린의 생성자 및 constructor와 init

초코칩
2025년 04월 16일 17:20

이번 글에서는 코틀린에서 생성자를 정의하는 방법에 대해 알아보고, constructor
와 init
키워드의 차이점에 대해 알아봅니다.
코틀린에서 생성자를 정의하는 방법은 주로 주 생성자와 부 생성자를 사용하는 두 가지 방식이 있습니다.
주 생성자
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)
위 코드는 클래스 본문 없이도 name
과 age
를 프로퍼티로 자동 생성합니다.
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
constructor
와 init
키워드는 서로 유사한 기능을 갖고 있습니다. 어떤 차이가 있는지 예제를 통해 비교해 봅시다.
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
일 경우 주의해서 사용한다.