Kotlin - 클래스 생성자(Constructor)와 get/set 메소드 정의 방법

코틀린의 클래스를 정의하는 방법은 자바와 다른 부분이 많습니다. 자바는 멤버변수를 field라고 하지만 코틀린 프로퍼티라는 용어를 사용합니다. 자바는 생성자를 꼭 정의해야 하지만 코틀린은 생략이 가능합니다. 코틀린의 클래스 정의 방법을 알아보고 자바와의 차이점들에 대해서 알아보겠습니다.

클래스 및 생성자 정의

코틀린에서 클래스는 아래처럼 정의할 수 있습니다. 자바와는 다르게 클래스 이름 옆에 생성자를 정의할 수 있습니다.

class Person constructor(firstName: String) {
}

더 간단히 constructor를 생략할 수도 있습니다.

class Person(firstName: String) {
}

위 코드를 바이트코트로 컴파일 후, 다시 자바로 변환(decompile)해보았습니다. 자바코드를 보면 생성자 인자에 firstName이 입력된 것이 전부입니다.

public final class Person {
    public Person(@NotNull String firstName) {
    }
}

보신 것처럼 코틀린의 생성자에는 변수만 정의할 뿐, 코드는 넣을 수 없습니다. 코드를 넣으려면 init을 사용해야 합니다.

Kotlin의 빌드 산출물인 class를 Java로 decompile 하는 툴은 Android 앱(apk)을 decompile하는 방법에서 소개하는 jadx를 사용하였습니다.

init으로 클래스 초기화

init은 초기화 구문입니다. 초기화할 것이 있으면 init에 넣으면 됩니다. 아래 코드를 보시면, 클래스 안에 private var firstName, public var lastName, public val old를 선언하였습니다.(접근자를 생략하면 public으로 선언됩니다) 그리고 init구문에서 초기화를 하였습니다.

class Person(firstName: String) {
    private var firstName = ""
    var lastName = ""
    val old = 5
    init {
        this.firstName = firstName
        this.lastName = "Code"
    }
}

// 이 클래스는 코틀린에서 이렇게 사용할 수 있습니다.
fun main(args: Array<String>) {
    val person = Person("chacha")
    println(person.firstName)
    println(person.lastName)
    println(person.old)
    person.lastName = "chacha"
}

위의 바이트코드는 자바로 어떻게 변경이 될까요? 변환된 자바코드를 보면 모두 private으로 변수가 선언되었습니다.

대신, 자바에서 lastName가 private으로 선언된 대신에, get/set 메소드가 추가되었습니다. 코틀린의 var은 변경가능한 변수 선언을 의미하기 때문에 get/set 메소드가 모두 추가된 것입니다.

코틀린에서 old는 val로 선언했기 때문에 자바에서 final로 선언되었고, get 메소드만 생성되었습니다. val은 변경 불가능한 변수 선언을 의미하기 때문에 get 메소드만 추가되었습니다.

코틀린에서는 클래스의 멤버변수에 직접 접근하는 것처럼 보이지만, 자바에서 보면 모두 get/set 메소드를 사용하고 있습니다. 객체지향 관점에서 코틀린 코드를 보면 좀 이상할 수 있습니다. 하지만 코틀린은 자바의 field(변수)라는 개념이 없고 property(자바에서 변수와 메소드를 합친 정도)라는 개념을 사용합니다. 따라서, method없이 public field를 접근하지 않습니다.

public final class Person {
    private String firstName = "";
    @NotNull
    private String lastName = "";
    private final int old = 5;

    public Person(@NotNull String firstName) {
        Intrinsics.checkParameterIsNotNull(firstName, "firstName");
        this.firstName = firstName;
        this.lastName = "Code";
    }

    @NotNull
    public final String getLastName() {
        return this.lastName;
    }

    public final void setLastName(@NotNull String <set-?>) {
        Intrinsics.checkParameterIsNotNull(<set-?>, "<set-?>");
        this.lastName = <set-?>;
    }

    public final int getOld() {
        return this.old;
    }
}

// 위의 코드는 아래와 같은 자바코드로 변환됩니다.
public static final void main(@NotNull String[] args) {
    Person person = new Person("chacha");
    System.out.println(person.getFirstName());
    System.out.println(person.getLastName());
    System.out.println(person.getOld());
    person.setLastName("chacha");
}

정리하면, 다음과 같은 특징을 갖습니다.

  • 코틀린은 클래스를 정의할 때 생성자도 함께 정의할 수 있습니다. 하지만 변수만 할당하고 구현부는 추가할 수 없습니다.
  • 초기화의 구현부는 init에 추가할 수 있습니다.
  • 코틀린 클래스의 변수는 프로퍼티(property)라는 개념을 사용하며, 자바의 (필드 + get/set메소드)의 역할을 합니다
  • 프로퍼티를 선언하면 val/var 여부에 따라서 get/set 메소드를 생성해줍니다.
  • get/set 메소드의 이름은 앞에 get/set 을 붙이고 PascalCase로 변수 이름을 붙여줍니다.
  • 프로퍼티를 private으로 선언하면 get/set 메소드가 생성되지 않고, 객체 내부에서만 접근할 수 있습니다.

생성자 정의 및 초기화를 함께하기

위에서는 생성자에서 인수를 받고, 클래스 내부에 프로퍼티를 생성한 뒤에 init구문에서 초기화를 따로 하였습니다.

여기서 비효율적인 것은, 스트링 인자를 받고 그것을 클래스의 지역변수에 다시 할당을 해주는 과정에서 쓸데없이 코드가 많아진다는 점입니다. 코틀린은 이런 패턴을 적은 코드로 구현할 수 있도록 하였습니다.

아래 처럼 생성자에 var firstName: String로 선언하면 클래스 내부에 프로퍼티 firstName을 생성하고 인자로 받은 값을 초기화해줍니다. 위와 차이점은 var이 붙었다는 점입니다.

class Person(var firstName: String) {
}

// 이 클래스는 코틀린에서 이렇게 사용할 수 있습니다.
fun main(args: Array<String>) {
    val person = Person("chacha")
    println(person.firstName)
}

자바로 확인하면 더 명확합니다. private 필드로 firstName가 생성되고 생성자에서 초기화 되고 있습니다. 코틀린의 생성자에서 var을 붙여주었기 때문에 get/set 메소드가 자동으로 생성되었습니다.

public final class Person {
    @NotNull
    private String firstName;

    public Person(@NotNull String firstName) {
        Intrinsics.checkParameterIsNotNull(firstName, "firstName");
        this.firstName = firstName;
    }

    @NotNull
    public final String getFirstName() {
        return this.firstName;
    }

    public final void setFirstName(@NotNull String <set-?>) {
        Intrinsics.checkParameterIsNotNull(<set-?>, "<set-?>");
        this.firstName = <set-?>;
    }
}

// 위의 코틀린 코드는 아래와 같은 자바코드로 변환됩니다.
public static final void main(@NotNull String[] args) {
    System.out.println(new Person("chacha").getFirstName());
}

정리하면 다음과 같습니다.

  • 생성자 인자에 var/val을 붙여주면 클래스 내부에 프로퍼티를 생성하고 초기화를 합니다.
  • var은 get/set 메소드를 모두 생성하고, val은 get 메소드만 생성합니다.

두개 이상의 생성자 만들기

지금까지 아래와 같이 생성자는 1개만 정의하였습니다.

class Person constructor(var firstName: String) {
}

사실 이 코드는 이렇게 쓸 수 있습니다. constructor를 클래스 이름 옆에서, 클래스 아래로 내리고 costructor 내부에 init처럼 초기화 구문을 넣을 수 있습니다.

class Person {
    var firstName = ""
    constructor(firstName: String) {
        this.firstName = firstName
    }
}

하지만 var firstName: String처럼 프로퍼티 생성과 동시에 초기화하는 코드를 사용할 수는 없습니다. 이런 동시 생성 및 초기화 코드는 클래스 이름 옆에서 정의될 때만 사용할 수 있습니다.

여기서 constructor를 하나 더 만들수도 있습니다.

class Person {
    var firstName = ""
    var lastName = ""
    constructor(firstName: String) {
        this.firstName = firstName
    }
    constructor(firstName: String, lastName: String) {
        this.firstName = firstName
        this.lastName = lastName
    }
}

// 이렇게 객체를 생성할 수 있습니다.
fun main(args: Array<String>) {
  val person = Person("chacha")
  val person2 = Person("chacha", "code")
}

자바로 변환해서 보시면 아래처럼 생성자가 두개가 생성됩니다.

public final class Person {
    @NotNull
    private String firstName = "";
    @NotNull
    private String lastName = "";

    public Person(@NotNull String firstName) {
        this.firstName = firstName;
    }

    public Person(@NotNull String firstName, @NotNull String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @NotNull
    public final String getFirstName() {
        return this.firstName;
    }

    public final void setFirstName(@NotNull String <set-?>) {
        Intrinsics.checkParameterIsNotNull(<set-?>, "<set-?>");
        this.firstName = <set-?>;
    }

    @NotNull
    public final String getLastName() {
        return this.lastName;
    }

    public final void setLastName(@NotNull String <set-?>) {
        Intrinsics.checkParameterIsNotNull(<set-?>, "<set-?>");
        this.lastName = <set-?>;
    }
}

또, 위의 코드는 아래처럼 좀 더 간단히 쓸 수도 있습니다. 위에서 설명한 초기화 방법 2가지를 함께 사용한 케이스입니다.

class Person(var firstName: String) {
    var lastName = ""
    constructor(firstName: String, lastName: String) : this(firstName) {
        this.lastName = lastName
    }
}

fun main(args: Array<String>) {
    val person = Person("chacha")
    val person2 = Person("chacha", "code")
    println(person.firstName)
    println(person2.firstName)
    println(person2.lastName)
}

자세히 보시면 생성자가 두개 있습니다. 하나는 클래스 이름 옆에, 하나는 클래스 내부에 constructor로 두개의 인자를 받습니다.

위의 코드에서 주의해야 할 점은 : this(firstName)입니다. 클래스 이름 옆에 정의된 생성자를 호출하는 코드인데요. 클래스와 함께 정의된 생성자는 항상 호출되어야 합니다. 그 이유는 firstName라는 프로퍼티를 초기화하는데 사용되기 때문입니다. 위에서 말씀드린 것처럼 firstName은 클래스와 함께 선언되어 클래스 내부 프로퍼티가 생성되는데요. 클래스 내부에 정의된 생성자에서 클래스와 함께 정의된 생성자를 호출해주지 않으면 내부 프로퍼티가 생성되지 않기 때문입니다.

자바로 변환해서보면 위와 거의 똑같습니다. 하지만 두번째 생성자에서 첫번째 생성자를 호출해주는 this(firstName)코드가 있습니다.

public final class Person {
    @NotNull
    private String firstName;
    @NotNull
    private String lastName;

    public Person(@NotNull String firstName) {
        this.firstName = firstName;
        this.lastName = "";
    }

    public Person(@NotNull String firstName, @NotNull String lastName) {
        this(firstName);
        this.lastName = lastName;
    }

    @NotNull
    public final String getFirstName() {
        return this.firstName;
    }

    public final void setFirstName(@NotNull String <set-?>) {
        Intrinsics.checkParameterIsNotNull(<set-?>, "<set-?>");
        this.firstName = <set-?>;
    }

    @NotNull
    public final String getLastName() {
        return this.lastName;
    }

    public final void setLastName(@NotNull String <set-?>) {
        Intrinsics.checkParameterIsNotNull(<set-?>, "<set-?>");
        this.lastName = <set-?>;
    }
}

정리

클래스의 생성자를 정의하는 방법을 알아보았습니다. 코드를 자바로 변환하여 비교해보면서 코틀린 코드가 자바에서 어떻게 보이는지 알 수 있었습니다. 또한, 코틀린의 경우 프로퍼티가 필드와 get/set메소드를 알아서 만들어주기 때문에 자바보다 적은 코드로 동일한 구현을 만들 수 있다는 것을 알았습니다.

참고

Loading script...
codechachaCopyright ©2019 codechacha