선생님, 개발을 잘하고 싶어요.

[kotlin] 안드로이드 개발에 필요한 최소의 코틀린 강좌 (part3) 본문

강좌/kotlin 강좌

[kotlin] 안드로이드 개발에 필요한 최소의 코틀린 강좌 (part3)

알고싶은 승민 2020. 12. 25. 16:42

도입

part2에서 클래스에 대해서 다루었습니다. part3에서는 이 클래스를 이용해서 상속을 구현하는 방법을 알아보도록 하겠습니다.

Java의 상속만 다뤄보신 분들은 코틀린이 제공하는 상속 문법과 제약이 처음에는 생소하실 수 있는데요. 정리된 걸 보시고 작업하시다 보면 코틀린의 섬세함에 놀라게 될 거예요. 👏

TL;DR

- class 정의, 메서드 정의, 속성정의는 기본 final이다.
- 상속 가능한 클래스를 만들기 위해서 open 키워드를 사용해야 한다.
- override 가능한 메서드, 속성을 만들기 위해서 open 키워드를 사용해야 한다.
- interface는 모든 함수가 기본 open 함수이다.

Kotlin Way

코틀린은 상속 하기 위해서 extend와 같은 키워드가 필요하지 않아요. 그냥 : SuperClass면 충분합니다.

 

// Java Way
class Super {}
class Child extends Super {}

// Kotlin Way...?
class Super
class Child : Super()

 

그런데 위의 코드는 동작하지 않습니다. 문제는 Super클래스 선언 방식에 있어요. 

코틀린에서는 그냥 class 선언이 의미하는 것은 Java의 final class와 똑같아요. "이 클래스는 상속이 불가능해요."를 뜻합니다.

 

그래서 코틀린은 상속에 열려있음을 의미하는 open 키워드를 사용해줘야 합니다.

//Kotlin Way
open class Super
class Child : Super()

 

마찬가지로 메서드 override에도 코틀린만의 문법이 적용되는 데요. 

open class Shape {
    open fun draw() = Unit
}

class Rectangle : Shape() {
    override fun draw() {
        //DO
    }    
}

 

부모 클래스에 draw 메소드는 open 키워드가 없으면 하위 클래스인 Rectangle에서 draw를 재정의 하는 게 불가능해요.

코드를 보시면 알겠지만 또 다른 키워드가 들어가 있죠. override 키워드가 바로 그것인데요.

부모 클래스의 함수를 override 하는 함수의 경우 무조건 override 키워드를 붙여줘야 해요. 안 그러면 컴파일 에러가 발생합니다.

 

그러니까 지금 부모 클래스에 메서드를 재정의 하기 위해서는 1) 부모 클래스의 메서드에 open 키워드 달기 2) 자식 클래스 메서드에 override 키워드달기 를 해주어야 합니다. 매우 번거로운데 코틀린은 왜 이런 선택을 했을까요?

 

사실, Java에서는 그런 거 다 필요 없습니다.

//Shape.java
public class Shape {

    void draw() { }
}

//Rectangle.java
public class Rectangle extends Shape {

    void draw() {
        //DO Something
    }
}

open, override 키워드 없이도 잘 동작하는 모습을 볼 수 있습니다. 편리하다고 생각하시나요? 코틀린이 바보 같은 선택을 한 것 같으신가요?

 

하지만 이런 경우를 생각해 볼 수 있습니다. draw를 타이핑 하다가 drew이라고 잘못 타이핑했다고 합시다.

//Shape.java
public class Shape {

    void draw() { }
}

//Rectangle.java
public class Rectangle extends Shape {

	// drew!! 잘못 타이핑 함. 하지만 프로그래머의 의도는 부모 메서드를 재정의 하는 일이였음.
    void drew() {
        //DO Something
    }
}

그러면 실제 Java 코드에서 해당 객체를 사용할 때 어떻게 사용될까요? 보통 부모가 상속에 열려있는 경우, 코드에는 부모 참조만 가지고 코드를 전개하고, 하위 구현체에 따라 동작을 변경하는 다형성을 이용하는 코딩을 하는 경우가 많은데요. 즉 이런 형태의 코드가 될 거라는 소리죠.

Shape shape = Rectangle()

...

shape.draw()

어라? 이 경우 프로그래머가 의도한 drew안의 코드는 실행되지 않습니다. 단지 부모 클래스의 draw 구현이 호출될 뿐이죠. 오탈자와 같은 흔한 실수가 빈번히 일어나게 됩니다. 따라서 이를 방지하기 위해서 코틀린은 프로그래머가 명시적으로 부모 클래스로 부터 재정의 함(override)자식 클래스에서 재정의해서 동작할 것임(open) 키워드를 분리시켜 이런 오류를 원천 방지하도록 한 것이지요.


그런데 자식 클래스에서 재정의해 동작함을 의도하고 만드는 클래스가 있죠? 바로 abstract classinterface가 그것입니다. 그래서 이 두 친구는 open 키워드 이외의 방법으로도 open의 동작성을 의도할 수 있습니다.

 

abstract 먼저 살펴봅시다. abstract class는 여러 가지 함수를 선언할 수 있습니다.

 

abstract class Shape {

    // 재정의할 수 없는 함수
    fun attachOther(other: Shape) {
        //DO
    }

    // 재정의할 수 있는 함수
    open fun print() {
        //DO
    }

    // 반드시 재정의 해야하는 함수
    abstract fun draw()
}

class Rectangle: Shape() {
    
    // 재정의 불가능 컴파일 에러
    override fun attachOther(other: Shape) {
        
    }
    
    override fun draw() {
        //TODO 반드시 구현
    }
}

총 세가지 케이스를 다 다뤄볼까요?

 

재정의할 수 없는 함수

앞서 말했듯 코틀린은 기본적으로 final가 생략되어 있다고 보시면 됩니다.

부모 클래스의 메서드를 그냥 fun 키워드만 사용해서 선언하면 자식 클래스에서 재정의가 불가능합니다. 컴파일 단계에서 에러 메시지를 보여주니 실수할 일이 없습니다.

 

재정의할 수 있는 함수

부모 클래스에 기본 구현체가 있고, 선택적으로 자식 클래스에서 구현체를 변경할 수 있는 함수의 경우 open 키워드를 사용합니다.

 

반드시 재정의 해야하는 함수

abstract class의 메인 컨셉은 역시나 추상 메서드죠. 추상 메서드는 기본 구현체를 포함하지 않습니다. 따라서 반드시 자식 클래스에서 재정의가 필요합니다. 이러한 abstract fun은 open 키워드가 없어도 하위 클래스에서 재정의가 가능합니다.

 

 

다음은 interface를 살펴봅시다. 

interface Duplicatable {
    fun duplicate(): Duplicatable
}

 

interface는 엄청 간단합니다. 외부로 노출되는 함수만 정의할 수 있으며 실제 구현체가 없다는 특징 때문에 그냥 fun 키워드만 사용해도 하위 클래스에서 재정의가 가능합니다. open 없이요.

 


그러면 어러 개의 부모가 있는 클래스는 어떻게 정의할 수 있을까요?

우선 Java와 마찬가지로 코틀린은 하나의 부모에 대한 extend만 가능합니다. 또한 여러 개의 interface를 implement 하는 것도 가능하죠. 근데 extends와 implements 키워드가 없이 어떻게 가능할까요?

 

위에 abstract class를 상속받는 코드와 interface를 상속받는 코드를 자세히 보신 분은 차이를 알 수도 있는데, 바로 ()의 차이입니다.

 

interface Disposable
interface Duplicatable
abstract class Shape { ... }

// extends는 ()를 사용해서 생성자를 호출해 주는 형태로,
// implements는 그냥 인터페이스 명만 사용해서 적어주는 형태로.
class Rectangle : Shape(), Duplicatable, Disposable { ... }

 

쉽죠? 그냥 쉼표로 상속받을 부모 클래스를 열거하고 구체 클래스에 대해서 생성자 호출하는 의미의 ()가 있으면 됩니다.


()는 생성자 호출입니다. 자식 클래스는 생성자가 있는 부모 클래스의 생성자를 호출해 주어야 할 의무가 있는데요. 그를 위해선 부모 클래스의 () 안에 인자를 넣어주면 됩니다.

 

abstract class Shape(val name: String)
class Rectangle: Shape("rectangle")

 

 

안드로이드 기본 코드에선 어떻게 쓰이고 있을까?

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

}

// AppCompatActivity.java

public class AppCompatActivity {

    ...

    @Override
    public void setContentView(View view) {
        getDelegate().setContentView(view);
    }
    
    ...
}

 

Activity를 만들면 처음 보이는 코드입니다. 이제 첫 번째 라인의 의미가 이해되실 겁니다.

 

확장 불가능한 MainActivity 클래스를 만들었다.

AppCompatActivity라는 구체 클래스를 부모로 한다.

부모 클래스에 정의되어 있는 onCreate를 오버라이드 한다.

부모 클래스의 setContentView를 호출한다.

 

주저리

사실, 클래스의 상속 개념은 안드로이드 개발과 같이 프레임워크를 사용하는 우리 입장에서 필수적으로 알아야 하는 개념입니다. onCreate, onResume과 같은 lifecycle 함수를 시스템이 호출하려면 다형성의 개념을 이용해야 했거든요.

 

참조 링크

https://kotlinlang.org/docs/reference/classes.html#inheritance


[part1] 선언, 자료형, 리스트

[part2] 흐름 제어, 함수 정의, 코틀린만의 클래스

[part3] 클래스 심화, 확장 (we're here)

[part4] 확장 함수

Comments