본문 바로가기

Kotlin

공식 문서로 배우는 코틀린 - 13. Classes

열세 번째, 클래스와 객체 - 클래스입니다.

 

Kotlin에서 클래스는 class 키워드를 사용하여 선언합니다.

class Person { /*...*/ }

 

클래스 선언은 클래스 이름과 클래스 헤더(타입 매개변수, 주 생성자, 그 외의 것들을 명시하는 부분) 그리고, 중괄호로 둘러 쌓이는 클래스 몸체로 구성됩니다. 헤더와 몸체는 선택 사항입니다. 클래스의 몸체가 없는 경우 중괄호는 생략할 수 있습니다.

class Empty

 

생성자

Kotlin에서 클래스는 주 생성자와 필요에 따라 하나 이상의 부 생성자를 갖습니다. 주 생성자는 클래스 헤더에 선언되는데, 클래스 이름과  (선택적인) 타입 매개변수 뒤에 위치합니다 (타입 매개변수는 추후 제네릭 부분에서 나옵니다).

class Person constructor(firstName: String) { /*...*/ }

 

주 생성자가 어떠한 어노테이션이나 가시성 수정자를 갖지 않는 경우 constructor 키워드는 다음과 같이 생략할 수 있습니다.

class Person(firstName: String) { /*...*/ }

 

주 생성자는 클래스 인스턴스와 클래스 헤더에 있는 프로퍼티들을 초기화합니다. 클래스 헤더에는 어떠한 실행 가능한 코드도 포함할 수 없습니다. 객체를 생성하는 동안 다소의 코드를 실행하고 싶은 경우 클래스 몸체 내에 초기화 블록(initializer block)을 사용합니다. 초기화 블록은 중괄호가 뒤따르는 init 키워드로 선언됩니다. 실행하고 싶은 코드를 중괄호 안에 작성하면 됩니다.

 

인스턴스를 초기화하는 동안 초기화 블록은 클래스 몸체에 위치한 순서대로 실행되며, 그 사이에 프로퍼티 초기화 부분이 있는 경우 그 부분이 실행된 후에 실행됩니다. 즉, 초기화 블록과 프로퍼티 초기화는 그 순서대로 차례로 실행됩니다.

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints $name")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

fun main() {
    InitOrderDemo("hello")
}

 

주 생성자의 매개변수는 초기화 블록에서 사용할 수 있습니다. 그리고, 프로퍼티 초기화에도 사용할 수 있습니다.

class Customer(name: String) {
    val customerKey = name.uppercase()
}

 

Kotlin은 주 생성자를 통해 프로퍼티를 선언하고 초기화하는 간결한 구문을 가지고 있습니다.

// 주 생성자에 val이나 var을 붙인 매개변수는 프로퍼티가 됩니다.
// Person("foo", "bar", 100) 같이 인스턴스화 하면,
// firstName, lastName, age 프로퍼티는 각각의 인수로 초기화됩니다.
class Person(val firstName: String, val lastName: String, var age: Int)

 

다음과 같이 프로퍼티의 기본 값을 포함하여 선언할 수도 있습니다.

class Person(val firstName: String, val lastName: String, var isEmployed: Boolean = true)

 

클래스 프로퍼티를 선언할 때, 후행 콤마(trailing comma)를 사용할 수 있습니다.

class Person(
    val firstName: String,
    val lastName: String,
    var age: Int, // trailing comma
) { /*...*/ }

 

정규 프로퍼티처럼, 주 생성자에 선언되는 프로퍼티도 가변적(var)이거나 읽지 전용(val)일 수 있습니다.

 

생성자가 어노테이션이나 가시성 수정자를 갖는 경우에는 constructor 키워드는 필수이며 수정자는 해당 키워드 앞에 위치합니다.

class Customer public @Inject constructor(name: String) { /*...*/ }

 

가시성 수정자에 대한 상세 내용은 여기에서 볼 수 있습니다.

 

부 생성자

클래스는 constructor를 앞에 붙인 부 생성자들을 가질 수 있습니다.

class Person(val pets: MutableList<Pet> = mutableListOf())

class Pet {
    constructor(owner: Person) {
        owner.pets.add(this) // 자신을 owner의 pets에 추가
    }
}

 

클래스에 주 생성자가 있으면, 부 생성자는 직접적으로든 또는 다른 부생성자를 통해 간접적으로는 주생성자에게 (초기화를) 위임(delegation)하는 것이 필요합니다. 같은 클래스에 다른 생성자에게 위임하는 것은 this 키워드 통해서 수행됩니다. 

class Person(val name: String) {
    val children: MutableList<Person> = mutableListOf()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

 

초기화 블록에 있는 코드는 실제로 주 생성자의 부분이 됩니다. 주 생성자로의 위임은 부 생성자의 첫번째 문(statement)에 접근했을 때 일어납니다. 그래서, 모든 초기화 블록과 프로퍼티 초기화는 부 생성자의 몸체 전에 실행됩니다.

 

심지어, 클래스가 주 생성자를 가지고 있지 않더라도 위임은 묵시적으로 일어나고 초기화 블록은 실행됩니다.

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor $i")
    }
}

fun main() {
    Constructors(1)
}
/* 결과
Init block
Constructor 1
*/

 

추상 클래스가 아닌 클래스가 어떠한 주/부 생성자를 선언하지 않은 경우에는 (자동으로) 생성된 매개변수가 없는 주 생성자를 갖게 됩니다. 생성자의 가시성은 public 입니다.

 

public 생성자를 원하지 않는 경우에는 기본 가시성이 아닌 가시성을 갖는 빈 내용의 주 생성자를 선언합니다.

class DontCreateMe private constructor() { /*...*/ }
JVM에서는, 주 생성자의 모든 매개변수가 기본값을 가지는 경우, 컴파일러는 추가적으로 기본값을 사용하는 매개변수가 없는 생성자를 만들게 됩니다. 이는 매개변수가 없는 생성자를 통해 클래스 인스턴스를 만드는 Jackson이나 JPA 같은 라이브러리와 함께 Kotlin을 쉽게 사용할 수 있도록 해 줍니다.
class Customer(val customerName: String = "")

 

클래스 인스턴스 생성

클래스의 인스턴스를 생성할 때는 생성자를 일반 함수인 것처럼 호출합니다.

val invoice = Invoice()

val customer = Customer("Joe Smith")
Kotlin에는 new 키워가 없습니다.

 

중첩, 내부, 익명 내부 클래스의 인스턴스 생성 절차는 중첩된 클래스에 설명돼 있습니다.

 

클래스 멤버

클래스는 다음의 것들을 포함할 수 있습니다.

  • 생성자와 초기화 블록
  • 함수
  • 프로퍼티
  • 중첩, 내부 클래스
  • 객체 선언

상속

클래스는 서로 파생되어 상속 계층을 형성할 수 있습니다. 자세한 내용은 여기에서 배울 수 있습니다.

 

추상 클래스

클래스는 멤버의 일부나 전체와 함께 abstract에 의해 추상 클래스로 선언될 수 있습니다. 추상 멤버는 추상 클래스 내에서 구현 부분을 갖지 않습니다. 추상 클래스나 함수에는 open을 지정할 필요가 없습니다.

abstract class Polygon {
    abstract fun draw()
}

class Rectangle : Polygon() {
    override fun draw() {
        // draw the rectangle
    }
}

 

추상적이 아닌 open 멤버를 추상적으로 오버라이드 할 수 있습니다.

※ override를 개정하다라고도 표현할 수 있으나, 객체 지향 프로그래밍 쪽에서는 워낙에 오버라이드로 많이 사용되고 있어서 오버라이드라고 표현합니다.

open class Polygon {
    open fun draw() {
        // some default polygon drawing method
    }
}

abstract class WildShape : Polygon() {
    // abstract로 오버라이드 했기 때문에
    // WildShape를 상속하는 클래스는 Polygon에 있는 draw를 쓰는 대신에
    // 자신의 draw 메소드를 구현해야 합니다.
    abstract override fun draw()
}

 

동반 객체(Companion objects)

클래스를 인스턴스 없이 팩토리 메소드 같은 클래스 내부의 멤버를 접근하는 함수를 호출해야 하는 경우에는 클래스 내부에 객체 선언의 멤버로서 해당 함수를 작성할 수 있습니다.

 

좀 더 구체적으로, 클래스 내에 동반 객체로 선언하게 되면, 해당 객체의 멤버를 단지 클래스 이름을 붙여 사용할 수 있습니다. 보다 자세한 내용은 추후에 이어지는 객체 선언 부분에서 설명합니다.

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.create()