[Kotlin] 생성자 정리

사명기·2020년 6월 22일
6

이번 글의 코드는 Github에 있습니다.


자바에 비해 코틀린에서는 간단하고, 다양하게 생성자를 정의할 수 있습니다. 그 방법들에 대해 자바와 비교하며 자세히 알아보도록 하겠습니다.

1. 주 생성자 (Primary Constructor)


아래와 같이 클래스 이름 옆에 괄호로 둘러싸인 코드를 주 생성자 라고 부릅니다. 주 생성자는 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 쓰입니다.
코틀린의 클래스는 하나의 주생성자와 다수의 부생성자를 가질 수 있습니다.

class User(val nickname: String)

이렇게 클래스 옆에 괄호를 붙여 생성자를 구현하는 여러가지 방법을 알아봅시다.

1.1 간략한 생성자 (선언과 초기화를 한번에!)

우리는 위의 User처럼 간단히 생성자를 정의하는 방법을 봤습니다. 하나의 예를 더 보겠습니다.

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

위는 nameage 프로퍼티를 가진 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프로퍼티는 필수로 필요하고 ageheight는 선택적으로 주입해 생성할 수 있다.
val person = Person("conas") ➡️ person = 이름:conas, 나이: 20, 키: 300
val person = Person("conas", height=150) ➡️ person = 이름:conas, 나이=20, 키=300



1.2 constructor 키워드 사용


앞에선 class name 옆에 괄호만 사용하는 방법을 공부했습니다. 이는 사실 class 이름 옆에 constructor 키워드를 생략한 것입니다. 만약 주 생성자가 어노테이션이나 접근 제어자(private 등)을 가지면 constructor 키워드를 생략할 수 없습니다.

이런 경우가 있을지 잘 모르겠지만 생성자를 극단적으로 private로 만드는 예를 보겠습니다.

class B private constructor(val name: String, val age: Int)

이 경우에는 constructor에 private라는 접근 제어자를 사용했기 때문에 constructor 키워드를 생략할 수 없습니다.

constructor 키워드는 클래스 본문 내에서 부 생성자를 선언할 때도 사용합니다. 부 생성자는 이후에 알아보도록 합니다.



1.3 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 블록을 통해 유효성 검사를 하고 초기화하도록 구현했습니다.




부 생성자 (Secondary Constructor)


부 생성자는 클래스 블록 내에 존재하는 생성자이며, 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 키워드로 시작하는 것들이 부 생성자입니다.

주석된 부분을 보면 컴파일 에러라고 나와있습니다. 그 이유는 주생성자가 존재한다면 부생성자는 무조건 주생성자에게 직간접적으로 생성을 위임해야 하기 때문입니다. 따라서 nameage를 파라미터로 가지는 생성자는 주생성자에게 this(name)을 통해 생성을 위임해야 합니다. 그리고 name, age, height를 가지는 생성자는 this(name, age) 에게 위임하고, 그다음 this(name,age)this(name)에게 위임하므로 결국 간접적으로 주생성자에게 생성을 위임한다고 할 수 있습니다.




2.1 부 생성자와 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

0개의 댓글