Coding Log

Kotlin

본 카테고리는 2017년 Android 공식 언어로 채택된 Kotlin에 관하여 다룬다.

Kotlin을 이용해 개발하는 Android는 추후 따로 다루기로 하고 언어 자체에만 집중한다.

참고 kotlin 공식 사이트

Class - 클래스와 상속

클래스

Kotlin은 Java와 마찬가지로 class 키워드를 사용해서 클래스를 선언한다.

class Invoice {
}

클래스의 선언은 클래스명, parameter와 같은 header 와 중괄호로 묶은 body 로 구성된다.

이 중 header와 body는 선택사항이며, 만약 body가 존재하지 않는 클래스라면 중괄호도 생략할 수 있다.

class Empty

생성자(Constructor)

Kotlin의 클래스는 하나의 기본 생성자 와 함께 한 개 이상의 보조 생성자 를 가질 수 있다.

기본 생성자(Primary Constructor)

기본 생성자는 header의 일부로 클래스명과 header를 그대로 따른다.

아래의 코드를 통해 확인하자.

class Person constructor(firstName: String) {
}

기본 생성자가 특정 Annotation이나 가시적인 수정이 없는 경우 constructor 키워드도 생략할 수 있다.

참고 가시적인 수정을 말하는 Visibility modifiers 는 추후 포스팅에서 다룬다.

class Person(firstName: String) {
}

이러한 기본 생성자는 어떤 코드도 포함되서 작성될 수 없으며, 초기화 코드는 init 키워드를 접두어로 붙인 initializer 블록에 위치시켜야 한다.

객체를 초기화하는 도중 initializer 블록은 클래스명과 동일한 순서로 실행되며 해당 객체의 속성을 초기화한다.

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}")
    }
}

기본 생성자의 parameter는 initializer 블록과 클래스 body에 선언한 속성의 initializer에서 사용할 수 있다.

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

Kotlin은 기본 생성자에서 속성값을 선언하고 초기화할 수 있는 간단한 방법 또한 제공한다.

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

위의 코드를 보면 나와 있듯 일반적인 속성값들과 동일하게 기본 생성자에 선언된 속성값들도 변경가능한 var 타입과 상수인 val로 구분될 수 있다.

위에서 언급했듯 만약 생성자가 특정 Annotation이나 가시적인 수정이 있을 땐, constructor 키워드를 사용해야 하며 키워드 앞에 수정자가 붙는다.

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

참고 가시적인 수정을 말하는 Visibility modifiers 는 추후 포스팅에서 다룬다.

보조 생성자(Secondary Constructor)

상술했듯, 클래스는 보조 생성자를 선언할 수 있으며 이 또한 constructor 키워드를 통해 선언할 수 있다.

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

만약 클래스에 이미 기본 생성자가 존재하는 경우 각 보조 생성자는 직접 기본 생성자에게 위임하거나 다른 보조 생성자를 통해서라도 간접적으로 위임해야 한다.

동일한 클래스안에서 타 생성자에 대한 위임은 this 키워드를 사용한다.

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

initializer 블록의 코드는 기본 생성자의 일부가 되며, 기본 생성자에 대한 위임 또한 보조 생성자에서 제일 처음 동작하므로, 모든 initializer 블록의 코드는 보조 생성자의 body보다 먼저 실행된다.

따라서 클래스에 기본 생성자가 없더라도 암시적으로 위임이 발생하게 되어 initializer 블록의 정상 작동을 보장한다.

class Constructors {
    init {
        println("Init block")
    }constructor(i: Int) {
        println("Constructor")
    }
}

fun main(args: Array<String>) {
    Constructors(1)
}

non-abstract 클래스(비추상화 클래스)에서 기본 생성자나 보조 생성자를 선언하지 않으면 parameter가 없는 기본 생성자를 자동으로 생성해준다.

이 생성자는 기본적으로 public으로 작성되므로, public 기본 생성자를 없애려면 비어있는 기본 생성자를 선언하도록 한다.

class DontCreateMe private constructor () {
}

참고 JVM 환경에서 기본 생성자의 모든 parameter가 기본값을 가지게 되는 경우, 컴파일러가 자동으로 parameter가 없는 생성자를 선언한다.

클래스의 인스턴스 생성

클래스로부터 인스턴스를 생성하기 위해선 일반적인 함수를 호출하듯 생성자를 호출하면 된다.

val invoice = Invoice()

val customer = Customer("Joe Smith")

참고 Kotlin은 Java처럼 new 라는 키워드가 존재하지 않는다.

중첩된 클래스의 인스턴스 생성은 추후 다루도록 하겠다.

클래스의 멤버

클래스는 아래와 같은 멤버들을 가질 수 있다.

  • 생성자 및 초기화 블록
    • 생성자 및 초기화 블록은 본 포스팅의 상위부분을 확인하자.
  • 함수(Functions)
    • 추후 포스팅에서 다루도록 하겠다.
  • 변수(Properties)
    • 추후 포스팅에서 다루도록 하겠다.
  • 중첩 클래스와 내부 클래스
    • 추후 포스팅에서 다루도록 하겠다.
  • Object 선언
    • 추후 포스팅에서 다루도록 하겠다.

상속(Inheritance)

Kotlin의 모든 클래스는 최상위 클래스를 Any를 상속한다.

만약 슈퍼클래스가 없더라도 기본적으로 Any를 상속받도록 되어있다.

class Example // Implicitly inherits from Any

위와 같은 설명을 보면 Java의 java.lang.Object라고 하는 최상위 클래스가 생각날 것이다.

하지만 Any와 java.lang.Object는 같은 클래스가 아니며, Any 클래스가 가진 멤버는 equals()hasCode()toString() 뿐이다.

참고 더 자세한 내용은 추후 자바의 상호 이용(Java interoperability)에서 다루겠다.

슈퍼 클래스의 타입을 직접 지정하려면 아래와 같이 콜론(:)을 이용하여 클래스 헤더에 선언한다.

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

만약 클래스에 기본 생성자가 있으며 해당 생성자와 parameter를 통해 바로 초기화할 수 있으며, 만약 기본 생성자가 없는 경우 각 보조 생성자의 super 키워드를 사용하여 초기화하거나 다른 생성자에 위임하여야 한다.

class MyView : View {
    constructor(ctx: Context) : super(ctx)

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

위의 코드와 같은 경우엔 다른 보조 생성자가 기본 유형의 생성자를 호출하도록 한다.

메소드 오버라이딩(Overriding Methods)

open 키워드는 다른 클래스가 open 키워드가 선언된 클래스를 상속할 수 있게 허용한다는 의미이다.

참고 Kotlin의 open 은 Java의 final 과 반대로 작용한다.

Kotlin은 명시적으로 메소드 오버라이딩을 수행해야 하며, 오버라이딩이 가능한 멤버에 위에 서술한 open 키워드를 설정한다.

아래의 예제 코드를 확인하자.

open class Base {
    open fun v() {}
    fun nv() {}
}
class Derived() : Base() {
    override fun v() {}
}

위의 코드에서 open 키워드는 Base 클래스에 붙은 것을 확인할 수 있다.

Derived.v()에 override 키워드가 붙어있으므로 재정의한다는 것을 알 수 있으며, 만약 override를 붙이지 않으면 컴파일러는 오류를 출력하게 된다.

Derived 클래스가 Base 클래스를 상속 받는 이상 open 키워드가 없는 Base.nv()는 Derived 클래스에서 같은 이름으로 메소드를 선언할 수 없다.

만약 override를 붙이는 경우라도 Base.nv()에 open이 없기때문에 선언할 수 없다.

추가적으로 상술했듯 final은 open의 반대기 때문에 final로 선언된 클래스는 open키워드를 붙일 수 없다.

open class AnotherDerived() : Base() {
    final override fun v() {}
}

위의 코드에서 보듯 override로 선언된 멤버는 그 자체로 open키워드가 적용된 것으로 간주되어 서브클래스에서 상속하여 재정의될 수 있다.

물론 override를 막으려면 final을 사용하면 된다.

변수 오버라이딩 (Overriding Properties)

변수의 오버라이딩도 메소드와 비슷한 방식으로 진행된다.

슈퍼클래스에 선언된 변수를 서브클래스에서 재정의하는 것도 override 키워드를 통해 진행되며, 상속받은 변수와 호환되는 타입으로 선언하면 된다.

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

class Bar1 : Foo() {
    override val x: Int = ...
}

클래스에서 선언된 변수들은 initializer를 가진 값이나 getter 메소드를 가진 변수로 오버라이딩 할 수 있다.

아래 코드를 보자.

interface Foo {
    val count: Int
}

class Bar1(override val count: Int) : Foo

class Bar2 : Foo {
    override var count: Int = 0
}

위의 코드의 count를 보면 알 수 있듯. val로 선언된 변수는 var로 오버라이딩할 수 있다.

val는 기본적으로 getter메소드를 선언하고, 이를 상속받은 var에서 추가적으로 setter메소드를 선언할 수 있기 때문이며, 동일한 이유로 var는 val로 오버라이딩할 수 없다.

추가적으로 기본 생성자에 선언된 값들도 override 키워드를 사용하여 오버라이딩할 수 있다.

상위 클래스 구현체 호출

서브클래스에서는 Java와 마찬가지로 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
}

내부 클래스(inner class) 안에서는 외부 클래스의 이름을 추가적으로 사용하여 슈퍼클래스에 접근할 수 있다.

아래 코드에서는 super@Bar.*의 형태로 쓰였다.

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
        }
    }
}

오버라이딩 규칙(Overriding Rules)

Kotlin에서의 오버라이딩은 아래와 같은 제한사항이 있다.

특정 클래스가 슈퍼클래스의 같은 멤버의 구현체를 여러 개 상속 받으면 반드시 상속받은 멤버들을 오버라이딩하고 이 오버라이딩을 통해 자체적인 구현체를 제공해야 한다.

슈퍼클래스의 구현체를 사용할지 안할지 선택하려면 꺽회 기호와 super 키워드를 사용한다.

아래의 코드를 보면서 이해하자.

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

interface B {
    fun f() { print("B") } // interface members are 'open' by default
    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()
    }
}

클래스 C가 A와 B를 동시에 상속받는 경우 멤버 a()와 b()는 중복되지 않으므로 상관없지만, f()는 중복되기 때문에 명확한 구현체를 오버라이딩하여 제공해야 한다.

추상 클래스(Abstract Classes)

클래스나 해당 클래스의 멤버를 abstract 키워드를 이용해 추상적으로 선언할 수 있다.

이 키워드가 적용된 멤버는 해당 클래스에 구현부가 존재하지 않게된다.

이 경우엔 굳이 open 키워드로 명시할 필요는 없다.

아래 코드를 통해 확인하자.

open class Base {
    open fun f() {}
}

abstract class Derived : Base() {
    override abstract fun f()
}

짝 객체(Companion Objects)

Scala + Java 구조에서 쓰이는 이름인 짝 객체는 Kotlin에도 존재한다.

Kotlin은 Java나 C#와 다르게 정적 메소드가 존재하지 않기 때문에, 대부분 패키지 수준의 함수를 대신 사용하도록 권장되고 있다.

Java에서 static을 사용하게 되면 클래스의 객체를 생성하지 않더라도 호출할 수 있는 장점이 있지만, 클래스의 내부에 접근하는 메소드를 작성해야 하는 경우엔 얘기가 달라진다.

참고 위의 장점에 대한 예시는 Static Factory Method 포스팅를 참고하자.

따라서 Kotlin에서는 접근하고자 하는 클래스에 속한 객체 선언으로 해당 클래스에 구현할 수 있다.


DISQUS 로드 중…
댓글 로드 중…

트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다