본문 바로가기
프로그래밍/Kotlin

Kotlin - 클래스와 상속

by pentode 2019. 8. 15.

코틀린에서 클래스를 만드는 방법에 대해 알아봅니다. 코틀린도 객체지향언어 이군요. 뭐, 요즘은 자바스크립트도 PHP도 모두 객체를 사용할 수 있도록 변해 왔으니까 특이할 것도 없겠죠.


클래스(class)는 객체가 어떤 데이터를 가지고, 어떻게 동작할지를 나타내는 코드 정보라고 할 수 있겠습니다. 실제 프로그램이 실행되어 정의된 클래스에 따라 컴퓨터의 메모리상에 객체가 만들어지면 이것을 인스턴스(instance)라고 합니다.



1. 클래스(Classes)


코틀린에서 클래스는 class 키워드를 사용해서 정의합니다.


class Invoice { ... }


class Box<T>(t: T) { ... }


클래스의 선언은 클래스 이름, 클래스 헤더(클래스의 타입 파라미터의  정의, 기본 생성자 등), 중괄호로 둘러싸인 클래스 본체로 구성됩니다. 헤더와 바디는 선택 사항입니다. 바디가 없다면 중괄호를 생략할 수 있습니다.


class Empty


※ 참고

- 타입 파라미터(type parameter): Generic 에서 사용되는 사용되는 실제 사용시 교체되는 파라미터를 말합니다.

- 기본 생성자(primary constructor) : 코틀린에서 클래스 본체가 아니라 헤더에 정의 되는 생성자 입니다. Primary constructor를 어떻게 번역해야 할지 애매해서 기본 생성자라고 했습니다. 주 생성자로 해도 의미가 맞을것 같습니다. 자바에서 기본 생성자(default Constructor)는 생성자를 하나도 정의하지 않을때 컴파일러가 자동으로 넣어주는 인자없는 생성자를 말합니다. 이것과는 다릅니다.



2. 생성자(Constructors)


코틀린에서 클래스는 기본 생성자(primary constructor)와 하나 이상의 보조 생성자(secondary constructor)를 가질 수 있습니다. 주 생성자는 클래스 헤더의 일부입니다. 클래스 이름 뒤에 나오며 타입 파라미터는 선택사항 입니다.


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


만약 기본 생성자가 어떤 아노테이션(annotaiton)이나 가시성 수정자(visivility modifier)도 가지지 않으면 constructor 키워드는 생략할 수 있습니다.


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


기본 생성자는 어떤 코드도 가지지 않습니다. 초기화 코드는 초기화 블록 안에 위치할 수 있습니다. 초기화 블럭은 init 키워드로 시작합니다.


인스턴스를 초기화 하는 동안, 초기화 블럭은 클래스 본체에 나오는 순서와 같은 순서로 실행됩니다. 아래 예에서 사이사이에 나오는 속성 초기화 부분도 순서에 포함되어 실행됩니다.


class InitOrderDemo(name: String) {

    val firstProperty = "첫 번째 속성: $name".also(::println)


    init {

        println("첫 번째 초기화 블럭: ${name}")

    }


    val secondProperty = "두 번째 속성: ${name.length}".also(::println)


    init {

        println("두 번째 초기화 블럭: ${name.length}")

    }

}


결과)

첫 번째 속성: hello

첫 번째 초기화 블럭: hello

두 번째 속성: 5

두 번째 초기화 블럭: 5


유의할 점은 기본 생성자의 인자는 초기화 블럭에서 사용될 수 있다는 것입니다. 또한 기본 생성자의 인자는 클래스 바디내의 속성 초기화에도 사용될 수 있습니다.


class Customer(name: String) {

    val customerKey = name.toUpperCase()

}


사실상, 코틀린은 속성들을 선언하고 기본 생성자에서 속성들을 초기화 하는 간결한 구문을 가지고 있습니다. 생성자의 인자를 var 또는 val로 선언하면 됩니다.


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


일반 속성과 같은 방식으로 기본 생성자에서 선언된 속성은 변형 가능(var)하거나 읽기 전용(val)일 수 있습니다.


생성자가 아노테이션(annotation) 또는 가시성 수정자(visibility modifier)를 가진다면 constructor 키워드가 필요하고 아노테이션이나 수정자는 constructor 키워드 앞에 옵니다.


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


※ 참고

- 가시성 수정자(visibility modifier): private, public 등 클래스에 접근 가능한 범위를 나타내는 수식어를 뜻합니다.



3. 보조 생성자(Secondary Constructors)


클래스에는 constructor 접두사를 사용하여 보조 생성자를 선언할 수 있습니다.


class Person {

    var children: MutableList<Person> = mutableListOf<Person>();

    constructor(parent: Person) {

        parent.children.add(this)

    }

}


클래스가 기본 생성자를 가진다면 각 보조 생성자는 기본 생성자에게 위임(delegation)해야만 합니다(기본 생성자를 호출 해야만 합니다). 이 위임은 기본 생성자에 직접 하거나 다른 보조 생성자를 통해서 간접적으로 할 수 있습니다(기본 생성자를 직접 호출하거나, 기본 생성자를 호출하는 다른 보조 생성자를 호출 합니다). 같은 클래스의 다른 생성자에 대한 위임은 this 키워드를 사용합니다.


class Person(val name: String) {

    var children: MutableList<Person> = mutableListOf<Person>();

    constructor(name: String, parent: Person) : this(name) {

        parent.children.add(this)

    }

}


초기화 블럭 안의 코드는 효과적으로 기본 생성자의 일부가 됩니다. 기본 생성자로의 위임은 보조 생성자의 첫번째 문장으로 발생합니다. 그래서 모든 초기화 블럭내의 코드는 보조 생성자 몸체 전에 실행됩니다. 클래스가 기본 생성자를 가지지 않을때도 위임은 여전히 암시적으로 발생하고, 초기화 블럭은 여전히 실행됩니다.


class Constructors {

    init {

        println("Init block")

    }


    constructor(i: Int) {

        println("Constructor")

    }

}


결과)

Init block

Constructor


추상클래스가 아닌 클래스에 어떤 생성자(기본 또는 보조)도 선언하지 않으면 인자가 없는 기본 생성자가 생성됩니다. 그 생성자의 가시성은 public 입니다. 클래스가 public 생성자를 가지기를 원하지 않는다면, 기본 가시성을 가지지 않는 빈 기본 생성자를 정의하면 됩니다.


class DontCreateMe private constructor () { ... }


참고: JVM에서 기본 생성자의 모든 파라미터가 기본값을 갖는 경우, 컴파일러는 기본 값을 사용하는 파라미터 없는 생성자를 추가합니다. 이것은 파라미터 없는 생성자를 통해 클래스 인스턴스를 만드는 Jackson 또는 JPA와 같은 라이브러리와 함께 코틀린을 사용하기 쉽게 합니다.


class Customer(val customerName: String = "")



4. 클래스 인스턴스 생성하기


클래스 인스턴스를 생성하려면, 생성자를 일반 함수인 것처럼 호출합니다.


val invoice = Invoice()


val customer = Customer("Joe Smith")


코틀린은 객체를 생성하는데 new 키워드를 사용하지 않습니다.



5. 클래스 멤버


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


- 생성자와 초기화 블럭Constructors and initializer blocks

- 함수(Functions)

- 속성(Properties)

- 중첩 클래스와 내부 클래스(Nested and Inner Classes)

- 객체 선언(Object Declarations)



6. 상속(Inheritance)


상속은 클래스의 기능을 확장하고자 할때 현재의 클래스의 기능을 모두 가지고 자신만의 기능이 추가된 새로운 클래스를 정의 하는 방법입니다. 따라서 상속을 하게 되면 클래스가 상, 하의 계층구조를 가지게 됩니다. 상속을 여러 단계로 일어날 수 있습니다.


코틀린에서 모든 클래스는 공통의 상위클래스(superclass)로 Any 클래스를 가집니다. 이것이 클래스에 상위 클래스를 선언하지 않을때 가지는 기본 상위 클래스 입니다.


class Example // 암시적으로 Any로 부터 상속 받습니다.


참고: Any는 자바에서의 java.lang.Object가 아닙니다. 특히 Any는 equals(), hashCode(), toString()이외의 멤버는 가지지 않습니다.


명시적으로 상위 클래스를 지정하기 위해서 클래스 헤더에 콜론 뒤에 상위 타입(supertype)을 적습니다.


open class Base(p: Int)


class Derived(p: Int) : Base(p)


파생 클래스가 기본 생성자를 가진다면, 베이스 클래스(base class)는 기본 생성자의 인자를 사용해서 바로 초기화 될 수 있고, 그렇게 되어야 합니다.


파생 클래스가 기본 생성자를 가지지 않는다면, 각 보조 생성자는 super 키워드를 사용해서 베이스 타입(base type)을 초기화 하거나, 초기화 수행하는 다른 생성자에게 위임해야 합니다. 이 경우 서로 다른 보조 생성자들이 베이스 타입의 다른 생성자를 호출할 수 있음에 유의해야 합니다.


class MyView : View {

    constructor(ctx: Context) : super(ctx)


    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)

}



7. 메소드 오버라이드


자바와는 다르게 코틀린은 오버라이드를 위해 명시적인 수정자가 필요합니다. 멤버가 오버라이드 가능하다는 것을 나타내기 위해 open 키워드를 사용하고, 오버라이드 하고 있다는 것을 나타내기 위해 override 키워드를 사용합니다.


open class Base {

    open fun v() { ... }

    fun nv() { ... }

}


class Derived() : Base() {

    override fun v() { ... }

}


위 예제에서 Derived 클래스의 v() 메소드를 위해 override 수정자가 필요합니다. 이것이 빠지면 컴파일이 되지 않습니다. Base 클래스의 nv() 메소드 처럼 메소드에 open 수정자가 없으면, 하위 클래스의 동일한 메소드는 override 수정자가 있던, 없던간에 잘못된 것입니다. open 수정자를 final 클래스의 멤버에 추가할 때에는 아무런 영양도 미치지 않습니다(즉, open 수정자가 없는 클래스에 붙일 때).


override 표시된 멤버는 자체가 open 입니다. 즉, 그 메소드는 하위 클래스(subclass)에서 오버라이드 될 수 있습니다. 재 오버라이드를 못하게 하려면 final 수정자를 붙이면 됩니다.


open class AnotherDerived() : Base() {

    final override fun v() { ... }

}



8. 속성 오버라이드


속성을 오버라이드 하는 것은 메소드를 오버라이드 하는것과 유사하게 동작합니다. 상위 클래스(superclass)에 정의된 속성을 재정의하는 파생 클래스의 속성은 override 수정자를 붙여야 하며, 호환되는 타입이어야 합니다. 각 정의된 속성은 초기화를 가지는 속성이나, getter메소드를 가지는 속성으로 오버라이드 될 수 있습니다.


open class Foo {

    open val x: Int get() { ... }

}


class Bar1 : Foo() {

    override val x: Int = ...

}


또한 val로 정의된 속성을 var로 정의된 속성으로 오버라이드할 수 있습니다. 그러나 그 반대는 안됩니다. 이것은 val 속성이 본질적으로 getter 메소드를 선언하고, 그것을 var로 오버라이드 하는것이 파생 클래스에 seter 메소드를 추가하는 것이기 때문에 허용됩니다.


override 키워드를 기본 생성자의 속성 선언의 일부로 사용할 수 있습니다.


interface Foo {

    val count: Int

}


class Bar1(override val count: Int) : Foo


class Bar2 : Foo {

    override var count: Int = 0

}



9. 파생 클래스의 초기화 순서


파생 클래스의 새로운 인스턴스를 생성하는 동안 베이스 클래스의 초기화는 첫 번째 단계(베이스 클래스 생성자에 대한 인자의 평가만 수행됨)로 완료되어지며, 따라서 파생 클래스의 초기화 로직이 실행되기 전에 발생합니다.


open class Base(val name: String) {


    init { println("Initializing Base") }


    open val size: Int =

        name.length.also { println("Initializing size in Base: $it") }

}


class Derived(

    name: String,

    val lastName: String

) : Base(name.capitalize().also { println("Argument for Base: $it") }) {


    init { println("Initializing Derived") }


    override val size: Int =

        (super.size + lastName.length).also { println("Initializing size in Derived: $it") }

}


결과)

Constructing Derived("hello", "world")

Argument for Base: Hello

Initializing Base

Initializing size in Base: 5

Initializing Derived

Initializing size in Derived: 10


이것은 베이스 클래스의 생성자가 실행될 때 파생 클래스에 선언된 속성이나 오버라이드된 속성이 아직 초기화 되지 않았다는 것을 의미합니다. 이러한 속성들중 하나라도 베이스 클래스의 초기화 로직에서 사용된다면(직접적으로 사용되거나 다른 오버라이드된 open 멤버 구현을 통해 간접적으로 사용되거나), 그것은 잘못된 동작이나 런타일 실패로 이어질 수 있습니다. 베이스 클래스를 설계할 때 생성자, 속성 초기화, 초기화 블럭에서 open 멤버를 사용하지 않아야 합니다.


open class Base(baseName : String = "Name1") {

    open var name = baseName

    init {

        println("INIT name : $name")

        name = name.toUpperCase()

        println(name)

    }

}


class Derived(derivedName : String) : Base() {

    override var name : String = "Mr. $derivedName"

}


fun main(args: Array<String>) {

    var obj = Derived("John")

    println(obj.name)

}


결과)

INIT name : null

Exception in thread "main" kotlin.TypeCastException: null cannot be cast to non-null type java.lang.String

at Base.<init>(HelloKotlin.kt:5)

at Base.<init>(HelloKotlin.kt:1)

at Derived.<init>(HelloKotlin.kt:10)

at HelloKotlinKt.main(HelloKotlin.kt:15)


오버라이드된 속성 name은 파생 클래스 생성시 베이스 클래스에서는 초기화 되지 않으므로 null이어서 오류가 발생합니다.



10. 상위 클래스의 구현을 호출하기


파생 클래스의 코드는 super 키워드를 사용하여 상위 클래스의 메소드와 속성 접근자 구현을 호출할 수 있습니다.


open class Foo {

    open fun f() { println("Foo.f()") }

    open val x: Int get() = 1

}


class Bar : Foo() {

    override fun f() {

        super.f()

        println("Bar.f()")

    }


    override val x: Int get() = super.x + 1

}


내부 클래스 안에서 외부 클래스의 상위 클래스에 접근하는 것은 "super 키워드  +  @ + 외부 클래스" 이름으로 합니다.


class Bar : Foo() {

    override fun f() { /* ... */ }

    override val x: Int get() = 0


    inner class Baz {

        fun g() {

            super@Bar.f() // Calls Foo's implementation of f()

            println(super@Bar.x) // Uses Foo's implementation of x's getter

        }

    }

}



11. 오버라이딩 규칙


코틀린에서 상속의 구현은 다음의 규칙으로 규정됩니다. 클래스가 바로 위의 수퍼클래스로부터 같은 멤버의 많은 구현을 상속하면, 그 클래스는 이 멤버를 오버라이드 하고, 자신만의 구현을 제공해야 합니다(아마도 상속된것중의 하나를 사용). 상속된 구현이 어느 수퍼타입인지 표시하기 위해서 super키워드 뒤에 각괄호를 사용하서 수퍼타입 이름을 씁니다. 예) super<Base>


open class A {

    open fun f() { print("A") }

    fun a() { print("a") }

}


interface B {

    fun f() { print("B") } // 인터페이스의 멤버는 'open'이 기본값입니다.

    fun b() { print("b") }

}


class C() : A(), B {

    // The compiler requires f() to be overridden:

    override fun f() {

        super<A>.f() // call to A.f()

        super<B>.f() // call to B.f()

    }

}


A, B 두 클래스로부터 상속 받을 수 있습니다. 메소드 a()와 b()는 클래스 C가 단 하나의 구현만을 상속하므로 문제가 없습니다. 그러나 메소드 f() 는 두 개의 구현을 상속받게 되므로 f를 오버라이드해서 C 클래스에서 자신만의 구현을 제공해서 모호성을 제거 해야 합니다.



12. 추상 클래스


클래스와 그 클래스의 몇몇 멤버는  abstract로 선언될 수 있습니다. abstract로 선언된 멤버는 그 클래스내에 구현이 없습니다. abstract 클래스와 함수에는 open을 붙일 필요가 없습니다.


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


open class Base {

    open fun f() {}

}


abstract class Derived : Base() {

    override abstract fun f()

}



13. 동반자 객체(Companion Objects)


코틀린은 Java나 C#과 달리 클래스에 정적인 메소드가 없습니다. 대부분의 경우, package-level 함수를 대신 사용합니다.


또 다른 방법으로 클래스 인스턴스 없이 호출되어 클래스 내부에 접근할 수 있는 방법이 있는데,  object와 companion object 입니다. 이것은 뒤에 알아보겠습니다.


출처 : https://kotlinlang.org/docs/reference/classes.html

반응형