이번 글의 코드는 Github에 있습니다.
아래와 같이 클래스 이름 옆에 괄호로 둘러싸인 코드를 주 생성자 라고 부릅니다. 주 생성자는 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 쓰입니다.
코틀린의 클래스는 하나의 주생성자와 다수의 부생성자를 가질 수 있습니다.
class User(val nickname: String)
이렇게 클래스 옆에 괄호를 붙여 생성자를 구현하는 여러가지 방법을 알아봅시다.
우리는 위의 User처럼 간단히 생성자를 정의하는 방법을 봤습니다. 하나의 예를 더 보겠습니다.
class A(val name: String, val age: Int)
위는 name
과 age
프로퍼티를 가진 A 클래스입니다. 이렇게 정의하면 우리는 A의 인스턴스를 val a = A(conatuseus, 30)
이런식으로 생성할 수 있습니다. 클래스명 옆의 괄호가 프로퍼티의 선언과 초기화를 같이 해줍니다.(val/var
이 없다면 선언과 초기화는 직접 해야합니다)
자바 코드로 표현하면 아래와 같습니다.
public class JavaA {
private final String name;
private final int age;
public JavaA(String name, int age) {
this.name = name;
this.age = age;
}
//getter
}
✏️ 참고
생성자가 여러개 필요한 경우 추후에 알아볼constructor
를 사용할 수도 있지만 파라미터에default
값을 줘서 필요에 따라 생성하게 할 수도 있다.
Ex) class Person(val name: String, val age: Int=20, val height: Int=300)
이렇게 age와 height에 default 값을 주게 되면,name
프로퍼티는 필수로 필요하고age
와height
는 선택적으로 주입해 생성할 수 있다.
val person = Person("conas") ➡️ person = 이름:conas, 나이: 20, 키: 300
val person = Person("conas", height=150) ➡️ person = 이름:conas, 나이=20, 키=300
constructor
키워드 사용앞에선 class name 옆에 괄호만 사용하는 방법을 공부했습니다. 이는 사실 class 이름 옆에 constructor
키워드를 생략한 것입니다. 만약 주 생성자가 어노테이션이나 접근 제어자(private 등)을 가지면 constructor
키워드를 생략할 수 없습니다.
이런 경우가 있을지 잘 모르겠지만 생성자를 극단적으로 private로 만드는 예를 보겠습니다.
class B private constructor(val name: String, val age: Int)
이 경우에는 constructor에 private
라는 접근 제어자를 사용했기 때문에 constructor
키워드를 생략할 수 없습니다.
constructor
키워드는 클래스 본문 내에서 부 생성자를 선언할 때도 사용합니다. 부 생성자는 이후에 알아보도록 합니다.
init
사용해서 초기화public class JavaC {
private final String name;
public JavaC(String name) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Error");
}
this.name = name;
}
// getter
}
우리는 생성자의 인자로 들어온 값을 검증하고 주입하는 방식을 많이 사용합니다. 그럼 이제 코틀린에서는 어떻게 하는지 알아봅시다.
✏️ init 블록이란?
코틀린에서 주 생성자에 어떠한 코드도 추가될 수 없습니다. 따라서 초기화 시에 필요한 작업(유효성 검증 등)을 하기 위해 코틀린에서는init 블록
을 지원합니다.
init 블록
에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어갑니다. 초기화 블록을 주로 주 생성자와 함께 사용됩니다.
위 자바 코드를 코틀린으로 변경해보면 다음과 같습니다.
class C(nameParam: String) {
val name: String
init {
if (nameParam.isEmpty()) {
throw IllegalArgumentException("Error")
}
this.name = nameParam
}
}
1.1
에서 봤던 코드는 클래스명 옆에 생성자의 파라미터를 val
로 선언과 초기화를 같이 했습니다. 하지만 val/var
을 사용하지 않으면 생성시 프로퍼티 선언과 초기화 작업은 개발자의 몫 입니다.
따라서 클래스 내부에 name이라는 프로퍼티를 직접 작성했으며, init 블록
을 통해 유효성 검사를 하고 초기화하도록 구현했습니다.
부 생성자는 클래스 블록 내에 존재하는 생성자이며, constructor
키워드를 사용합니다. 주 생성자(Primary Constructor)에서는 constructor
키워드를 생략할 수 있었지만, 부 생성자는 constructor
키워드를 생략할 수 없습니다.
예를 보겠습니다.
class D(val name: String) {
var age: Int = 20
var height: Int = 500
// 컴파일 에러!
// constructor(name: String, age: Int) {
// this.age = age
// }
constructor(name: String, age: Int) : this(name) {
this.age = age
}
constructor(name: String, age: Int, height: Int) : this(name, age) {
this.height = height
}
}
D(val name: String)
이것이 주 생성자이고 클래스 블록 내의 constructor
키워드로 시작하는 것들이 부 생성자입니다.
주석된 부분을 보면 컴파일 에러라고 나와있습니다. 그 이유는 주생성자가 존재한다면 부생성자는 무조건 주생성자에게 직간접적으로 생성을 위임해야 하기 때문입니다. 따라서 name
과 age
를 파라미터로 가지는 생성자는 주생성자에게 this(name)
을 통해 생성을 위임해야 합니다. 그리고 name
, age
, height
를 가지는 생성자는 this(name, age)
에게 위임하고, 그다음 this(name,age)
는 this(name)
에게 위임하므로 결국 간접적으로 주생성자에게 생성을 위임한다고 할 수 있습니다.
init 블록
의 호출 순서만약 부 생성자 여러개와 init 블록
이 있으면 어떤 것이 먼저 선언될까요?
예제를 통해 알아봅시다.
class E {
var name: String
var age: Int = 1
var height: Int = 2
init {
println("call Init Block!")
}
constructor(name: String) {
this.name = name
println("call Name Constructor!")
}
constructor(name: String, age: Int) : this(name) {
this.age = age
println("call Name, Age Constructor!")
}
constructor(name: String, age: Int, height: Int) : this(name, age) {
this.height = height
println("call Name, Age, Height Constructor!")
}
}
이렇게 구현하고 val e = E("conas", 100, 200)
으로 객체를 생성한 결과는 다음과 같습니다.
이 결과를 보면 Init 블록
이 부생성자보다 먼저 실행되는 것을 확인할 수 있습니다.
✏️ 참고
만약 주생성자나 부생성자를 구현하지 않을 경우에는 코틀린이 자동으로 인자가 없는 생성자를 자동으로 생성해줍니다.
여기까지 코틀린의 주생성자와 부생성자에 대해 알아봤습니다. 잘못된 내용이나 궁금하신게 있으시면 댓글 달아주세요. 감사합니다.
참고 자료
- 책: Kotlin in Action