Kotlin 을 이용한 Spring Boot 스터디 진행 중 Kotlin 기초를 진행한 후 Java를 Kotlin 으로 변환하는 과정에서 자바와 다른 생성자를 만드는 과정에서 Kotlin 만의 생성자 생성 방식이 햇갈릴 수 있다는 생각이 들어 내용을 정리하게 되었습니다.
Kotlin 의 Class와 Interface의 경우 Java의 Class, Interface와 약간의 차이가 있습니다.
대표적인 부분이 Kotlin 은 기본적으로 대부분의 선언이 public final 이라는 부분에서 생기고, 중첩 클래스의 경우 기본적으로 내부 클래스가 아닌 점 등
기본적으로 Kotlin 에서는 Java에서 진행하던 번잡스러운 과정이 없기 때문에 발생하는 차이라고 생각됩니다.
이러한 부분에 대해 Kotlin 에서 생성자를 다루는 방법을 정리하고 또 Java로 Decompile 하면 어떤 형태가 되는지 정리해보고자 합니다.
1. Kotlin의 Primary Constructor(주 생성자)
1.0.0.1. 파라미터 지정 및 프로퍼티 정의를 한번에 하는 생성자
<code />
class Member(
private val birth: String,
var age: Int,
var name: String
)
Kotlin 에서는 위와 같이 클래스 이름 옆에 괄호로 둘러싸인 코드를 Primary Constuctor 주생성자 라고 부릅니다.
주 생성자는 주로 클래스를 초기화 할때 주로 사용하는 간략한 생성자를 이야기 하며, 클래스 본문이 아닌 밖에서 정의합니다.
주 생성자는 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화 되는 프로퍼티를 정의하는 두 가지 기능을 할 수 있습니다.
따라서 위 코드에서는 Member
생성자 파라미터로 받은 값을 바탕으로 동시에 그에 상응하는 프로퍼티 또한 한번에 정의합니다.
Kotlin 에서 프로퍼티는 간단히 말하면 Java의 맴버 변수와 Getter/Setter를 포함한 개념입니다.
위 Kotlin 코드를 Java로 Decompile 하게 되면 아래의 코드를 확인 할 수 있습니다.
<code />
public final class Member {
private final String birth;
private int age;
@NotNull
private String name;
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}
public Member(@NotNull String birth, int age, @NotNull String name) {
Intrinsics.checkNotNullParameter(birth, "birth");
Intrinsics.checkNotNullParameter(name, "name");
super();
this.birth = birth;
this.age = age;
this.name = name;
}
}
1.0.0.2. 직접 초기화를 하는 생성자
위와 같이 단순하게 파라미터를 지정하고 초기화 하는 경우가 아닌 직접 초기화를 해야 하는 경우가 발생하기도 합니다. 단순하게 주 생성자 부분을 사용한다면 별도의 코드를 포함할 수 없기 때문에 Kotlin 에서는 이를 지원하는 init 키워드를 활용한 초기화 블록을 제공합니다.
<code />
class Member(
birth: String,
age: Int,
name: String
) {
private val birth: String
var age: Int
private set
var name: String = name
init {
// birth 유효성 검사
this.birth = birth
this.age = age
}
}
위와 같이 작성하게 되면 Member
객체를 생성하는 시점에 출생일의 유효성 검사를 init 블록에서 진행함으로 유효한 출생일이 들어왔는지 확인이 가능합니다.
유효성 검사를 위한 확인 뿐 아니라 age
와 같이 내부에서만 그 값을 변경할 수 있는 경우 Setter를 private으로 하는 프로퍼티에 값을 직접 초기화 하는 방식으로도 사용이 가능합니다.
유효성 검사 및 프로퍼티에 추가 작업이 필요한 경우가 아니라면 name
과 같이 init 블록을 거치지 않고 프로퍼티에서 직접 초기화를 해주는 방법도 존재합니다.
위 코드를 Java로 Decompile 하면 다음과 같습니다.
<code />
public final class Member {
private final String birth;
private int age;
@NotNull
private String name;
// 이전 코드와 다르게 setter를 private 으로 처리하였기에
// 내부에서만 값을 바꿀 수 있도록 setter를 생성하지 않음
public final int getAge() {
return this.age;
}
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}
public Member(@NotNull String birth, int age, @NotNull String name) {
Intrinsics.checkNotNullParameter(birth, "birth");
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.birth = birth;
this.age = age;
}
}
1.0.1. Private Primary Constructor
만약 객체의 생성을 제한 해야 하는 경우에는 생성자를 private으로 만들게 될 수 있습니다.
이 경우에는 주 생성자에 constructor
키워드를 통해 생성자를 감출 수 있습니다.
<code />
class Member private constructor(
birth: String,
age: Int,
name: String
) {
private val birth: String
var age: Int
private set
var name: String = name
init {
// birth 유효성 검사
this.birth = birth
this.age = age
}
}
2. Kotlin 의 Secondary Constructor( 부 생성자 )
객체 생성시 생성자가 여러가지 필요한 경우가 있습니다. 이런 경우에는 위에서 살펴본 constructor
키워드를 클래스 블록 내에서 사용하여 직접 부 생성자를 만들어 줄 수 있습니다.
<code />
class Member private constructor(
private val birth: String,
var name: String
) {
var age : Int? = null
private set
// 부 생성자
constructor(birth: String, age: Int, name: String): this(birth, name) {
this.age = age
}
}
constructor
키워드를 이용하면 위 코드와 같이 부 생성자를 만들 수 있습니다.
특이한 부분은 : this()
부분인데 Kotlin 에서는 주 생성자가 존재한다면 부 생성자는 반드시 주 생성자에게 생성을 위임해야하기 때문에 이 부분을 위임하고 있는 코드라고 보시면 될 것 같습니다 :)
하지만 위 코드의 예시처럼 사용하는 경우는 개인적 경험에서는 거의 없었습니다. Java의 경우는 생성자가 여럿 존재하는 경우가 많이 발생하지만, Kotlin 은 디폴트 값을 제공할 수 있기 때문에 이를 사용하면 부 생성자가 필요한 경우가 거의 없었습니다.
<code />
class Member(
private val birth: String = "2000.01.01",
age: Int,
var name: String = "익명의 회원"
) {
var age : Int = age
private set
}
따라서 위와 같이 작성하면 age
만 받는 생성자, birth
, age
를 받는 생성자, age
, name
을 받는 생성자, birth
, age
, name
을 받는 생성자 총 4가지 생성자가 있는 것과 같은 효과로 사용할 수 있습니다.
또한 kotlin 에서는 객체 생성 과정에서 초기화 할 파라미터를 지정할 수 있어 디폴트 값을 잘 이용하면 builder 패턴과 유사한 효과 또한 얻을 수 있습니다.
<code />
fun main() {
val memberA = Member(age = 20)
val memberB = Member(birth = "1999.06.26", age = 25)
val memberC = Member(age = 24, name = "JS")
}
참고 자료
- Kotlin in Action