Kotlin 클래스 (Class)

여기에서는 클래스, 인스턴스 생성, 메소드, 생성자 등에 대해서 배워보도록 하겠다.

클래스(class) 및 인스턴스 생성

Kotlin은 Java와 마찬가지로 클래스 기반 객체 지향 언어다. 즉, 먼저 클래스로 부터 인스턴스를 생성해서 이용하는 방식이다.

멤버를 가지지 않는 클래스의 정의는 아래와 같이 표현한다. Java와 마찬가지로 class 키워드로 클래스를 정의한다.

예제: 최소 클래스 정의

class MyClass

MyClass 클래스를 인스턴스화해 보자.

예제: 클래스의 인스턴스 생성

fun main() {
    val obj = MyClass()
    println(obj)
}

Output:

MyClass@5a07e868

MyClass()MyClass 클래스의 인스턴스를 생성한다. 자바에서는 인스턴스로 생성할 때는 new 연산자로 명시해줘야 했지만, 코틀린에서는 명시를 해주지 않아도 된다. 위 예제에서는 MyClass클래스의 인스턴스를 생성하고 그 참조를 변수 obj에 할당하였다. 그러고 println 메소드에 넣고 실행하면, 클래스 이름과 해시 코드가 출력된다.

메소드(method)

이번에는 메소드가 있는 클래스를 정의해 보자. 아래 예제의 Greeter 클래스에는 greet메소드가 있다. 이와 같이 클래스는 멤버를 중간괄호({}) 로 묶어야 한다. 메소드는 fun 키워드를 사용하여 함수처럼 작성한다.

예제: 메소드를 가진 클래스

class Greeter {
  fun greet() {
    println("Hello!")
  }
}

아래와 같이 Greeter클래스를 인스턴스를 생성하고 그 인스턴스의 greet 메소드를 호출한다.

예제: 메소드 호출

fun main() {
    val greeter = Greeter()
    greeter.greet()
}

Output:

Hello!

Kotlin에서는 메소드 선언시에 키워드 fun를 사용하는 것을 봐서는 메소드라기 보다는 함수라고 하는게 맞는거 같다. 기능이나 의미는 같아서 구지 둘을 구별하는 것은 크게 의미가 없다고 생각히기에 여기에서는 메소드와 함수를 혼용해서 사용하도록 하겠다.

속성(property)

클래스는 프로퍼티를 가질 수 있다. 프로퍼티란, 쉽게 말하면 Java에 있어서의 필드와 그 접근자(setter, getter)가 있는 것이라고 설명할 수 있다.

아래 예제의 User 클래스는 idname라는 속성을 가지고 있다. 인스턴스 생성하고 속성에 값을 설정하거나 가져온다.

예제: 속성을 가진 클래스

class User {
    var id: Long = 0
    var name: String = ""
}

아래 예제의 세번째 행에서는 name속성에 값을 설정한다. 그리고 네번째 행에서 name 속성과 id속성의 값을 가져온다. Java에서 필드에 직접 액세스하는 것처럼 보이지만 실제로는 (기본적으로 생성된) setter 또는 getter를 통해 액세스한다.

예제: 속성 사용법

fun main() {
   val user = User()
   user.name = "devkuma"
   println("${user.name}(${user.id})")
}

Output:

devkuma(0)

setter / getter

settergetter를 커스터마이징을 하고 싶은 경우는 아래 예제와 같이, 함수와 비슷한 기법을 이용해 자유롭게 처리를 넣을 수 있다.

예제: setter와 getter 사용자 정의

class User {
    var id: Long = 0

    var name: String = ""
        // 값을 설정할 때 로그를 출력한다.
        set(value) {
            println("set: $value")
            field = value
        }
        // 값을 받아올 때 로그를 출력한다.
        get() {
            println("get")
            return field
        }
}

Output:

set: devkuma
get
devkuma(0)

클래스가 가지는 속성은 set()에서 값 설정시 함수(setter)이고, get()에서 값을 받오는 함수(getter)이다. 함수내에서 field 속성 값을 보여준다.

생성자(constructor)

constructor는 클래스의 인스턴스가 생성될 때 자동으로 호출되는 생성자를 정의한다.

예제: 생성자 생성

class User1 constructor(id: Long, name: String) {
    var name = name
}

fun main() {
    var user1 = User1(1,"devkuma")
    println(user1.name)
}

output:

devkuma

생성자에 접근 제한자, 어노테이선 등을 지정할 필요가 없는 경우는 constructor를 생략할 수도 있다.
아래 예제 같이 클래스 이름 뒤에 생성자의 인수 목록을 선언할 수 있다. 여기서 받은 인수로 속성을 초기화할 수 있다.

예제: constructor 생략

class User2(name: String) {
    var name = name
}

생성자가 단순히 속성을 초기화하기만 한다면, 아래와 같이 생략할 수도 있다.
생성자 인수의 이름 앞에 val이나 var를 붙이는 것으로 속성의 정의할 수도 있어, 코드를 간략화 할 수 있다.

예제: 생성자 인수로 속성 정의

class User3(var name: String)

위의 생성자를 기본(Prime) 생성자라고 한다. 그리고, 아래와 같이 보조(Secondary) 생성자를 정의할 수 있다. 보조 생성자는 인수의 수와 데이터 타입에 따라 여러개를 작성할 수 있다.

예제: 기존 생성자와 보조 생성자

class User4 {
    var name: String
    var age: Int = -1

    constructor(name: String) {
        this.name = name
    }

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

클래스가 기본 생성자가 있는 경우에는 this를 이용하여 기본 생성자를 호출해야만 한다.

class User5(var name: String, var age: Int) {
    constructor(name: String) : this(name, -1)
}

초기화 블록

클래스는 init {...}으로 초기화 블록을 작성할 수 있다. 초기화 블록은 생성자가 호출되는 이전에 호출된다.

class Foo {
    init {
        println("Foo is created.")
    }
}

클래스의 상속

Java 및 다른 객체지향 언어에서도 있듯이 클래스는 다른 클래스를 상속 받을 수 있다. 상속하면 부모 클래스(슈퍼 클래스)의 타입의 서브 타입이 되어, 또 슈퍼 클래스의 멤버를 서브 클래스와 같이 취급할 수 있게 된다.

상속 클래스 생성 (open)

먼저 슈퍼 클래스가 될 Animal 클래스를 정의해 보자.

예제: 슈퍼 클래스 정의

open class Animal {
    fun running() {
        println("running")
    }
}

Animal 클래스는, 생성자 인수, 프로퍼티를 가지지 않고, runing 메소드를 가지는 클래스이다. 여기서 더 주목해야 할 포인트는 open 키워드가 붙어 있는 것이다. Kotlin은 클래스가 상속 가능하다는 것을 open한정자와 함께 명시해야 한다. 반대로 말하면 open가 붙어 있지 않은 일반 클래스는 상속할 수 없다.

또는, 아래와 같이 final 키워드를 붙여 상속 받지 못하는 클래스라고 명시할 수도 있지만, 기본적으로는 이를 생략을 한다.

final class Animal() { ... }

이는 엄격한 제약처럼 보일 수도 있지만 “Effective Java 제2판"의 항목 17에 준거한 설계 사상이다.

이제는 Animal 상속하는 Cat 클래스를 정의해 보자. 아래와 같이하여 슈퍼 클래스(Animal)를 상속하는 서브 클래스(Cat)을 정의 할 수 있다.

예제: 자식 클래스 정의

class Cat : Animal() {
    fun sound() {
        println("sound")
    }
}

클래스 이름 다음에 콜론(:)⁠, 부모 클래스의 생성자 호출을 작성한다. 여기에서는 Animal 클래스에는 생성자 인수가 없기 때문에 단순히 Animal()으로 작성되어 있다.

이제 Cat 인스턴스에서 슈퍼 클래스에서 정의한 메소드를 호출할 수 있는지 확인해 보자.

예제: 슈퍼 클래스의 메소드 호출

fun main() {
    val cat = Cat()
    cat.running()
    cat.sound()
}

Output:

running
sound

그리고 CatAnimal 클래스의 서브 타입이므로 val animal: Animal = cat 유효한 코드이다.

val animal: Animal = cat
animal.sound() // Error

대신 이렇게 하면 animal에서 서브 클래스를 호출할 수 없다.

클래스의 함수를 재정의 (override)

override는 부모 클래스의 함수를 재정의 할 수 있다.

open class Foo {
    open fun print() {
        println("Foo")
    }
}

class Bar() : Foo() {
    override fun print() {
        println("Bar")
    }
}

fun main() {
    var bar = Bar()
    bar.print()
}

Output:

Bar

부모 클래스의 생성자를 호출 (super)

부모 클래스의 생성자를 호출하려면 super()를 사용한다.

open class Animal(var type: String)

class Cat: Animal {
    var name: String
    constructor(name: String): super("Cat") {
        this.name = name
    }
}

또는 다음과 같이 작성할 수도 있다.

open class Animal(var type: String)
class Cat(var name: String): Animal("Cat") {}

클래스 중첩(nested and inner class)

클래스는 중첩할 수 있다.

class A {
    class B { ... }
}

var obj = A.B()

inner는 중첩된 클래스가 내부 클래스임을 나타낸다.

class A {
    inner class B { ... }
}

var obj = A().B()

.toString()

클래스는 모든 문자열를 위한 .toString() 함수가 있다. println()로 출력할 때 등에 암시적으로 불린다. 아래와 같이 덮어 쓸 수 있다.

class Person(val name: String, var age: Int) {
    override fun toString() = "$name($age)"
}

fun main() {
    println(Person("devkuma", 26))  // devkuma(26)
}

.equals ()

모든 클래스는 비교를 위한 .equals() 메소드가 있다. ==로 비교 할 때 등에 암시적으로 불린다. 아래와 같이 덮어 쓸 수 있다.

class Foo(val name: String) {
    override fun equals(other: Any?) = this.name == (other as Foo).name
}

fun main() {
    val a1 = Foo("devkuma")
    val a2 = Foo("devkuma")
    println(a1 == a2)        // true
}

참조