[Koltin] Kotlin식 문법: 클래스, 객체, 인터페이스 -1

박준규·2022년 2월 15일
0

코틀린

목록 보기
10/19

이번에 다룰 내용
1. class, interface
2. 뻔하지 않은 constructor와 property
3. data class
4. class delegation
5. object

개발자가 한 언어를 다루는 데 걸리는 시간이 대략적으로 2주 정도 걸린다. 어떤 근거를 통해서 이런 말을 하는건지는 잘 모르겠지만, '정확히 2주 정도 걸린다.'는 말은 난 반대다. 여러 언어를 공부하면서 내가 느꼈던 것은 어떤 언어를 제대로 아주 확실히 그리고 아주 깊게 알기 위해서는 최소 몇 년은 걸리는 거 같다. 단순히 문법을 읽고 간단한 코드를 작성하는 것이 아니라, 어떤 언어를 deep하게 작성하고 고민한 흔적을 보이기 위해서는 오랫 동안 한 언어에 대한 내공을 쌓아야 한다. 그리고 작년 9월 부터 제대로는 작년 12월 부터 코틀린에 대해 계속 공부하고 있지만, 아직도 kotlin에 대해 딥하게 알고 있다고 자부하지 못하고 있다. 여튼 지금 이렇게 서론이 긴 이유는 내가 Kotlin 공부를 제대로 시작하면서 가장 기대했던 순간이기 때문이다. class와 interface는 안드로이드 개발에 있어 service 단의 꽃이라 생각하고, 이 부분을 제대로 알고 있다면, Android개발을 이해하는 데 있어 큰 도움이 되리라 생각한다. 그럼 바로 시작하자!

이번에는 kotlin만의 class와 interface에 대해 자세히 알아보려 한다. 물론 kotlin스럽게 말이다. 그리고 무엇보다 재밌는 것은 kotlin의 interface는 자바와는 약간 다르다. 예로 kotlin의 interface에는 property를 선언할 수 없다. 그 이유는 java와 다르게 kotlin의 선언은 기본적으로 final이고 public이기 때문이다. 또한 중첩 클래스(class안 class)는 기본적으로 내부 클래스(inner class)가 아니다. 예를 들어서 아래의 코드를 보면 바로 알 수 있다.

fun main(args: Array<String>) {
    Outer.Nested().introdue() // Nested Class
    val nested = Nested() // Unresolved reference: Nested
}

class Outer {
    class Nested {
        fun introdue() {
            println("Nested Class")
        }
    }
}

중첩 클래스의 경우 곧바로 instance를 생성할 수 없다. 무조건 Outer라는 클래스를 인스턴스로 만든 뒤에 이를 통해 Nested를 이용할 수 있다. 접근도 이와 같다. 즉 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다.

Kotlin compiler는 번잡스러움을 피하기 위해 유용한 메소드를 자동으로 만들어준다. 클래스를 data로 선언하면 컴파일러가 일부 표준 메소드를 생성해준다. 그리고 kotlin 언어가 제공하는 delegation을 사용하면 이 역시 위임을 처리하기 위한 준비 메소드를 직접 작성할 필요가 없다. 이번에는 이러한 kotlin의 특성을 확인해보자.

1. 클래스의 계층 정의

1-1 kotlin interface

Kotlin에서 interface를 정의하고 구현하는 방법을 살펴보자. Kotlin interface는 java8 interface와 비슷하다. Kotlin interface안에는 추상 메소드 뿐만 아니라 구현이 있는 메소드도 정의할 수 있다. 하지만 그 어떤 필드도 들어갈 수 없다. 구현되어 있는 메소드는 자바와 비슷하지만, 필드가 들어갈 수 없는 것은 자바와 다르다. 그리고 interface를 구현하려면 interface 키워드를 사용한다.

interface Clickable {
	fun click()
}

위 코드는 click이라는 추상 메소드가 포함되어 있는 interface를 정의한다. 이 interface를 구현하는 구체화된 클래스는 click에 대한 구현을 제공해야 한다. interface를 class로 구체화시켜보자.

interface Clickable {
	fun click()
}

class Button: Clickable {
    override fun click() = println("The button was Cliecked")
}

fun main() {
    val button = Button() 
    button.click() // The button was Cliecked
}

자바에서는 extends와 implements를 통해 class나 interface를 상속받지만, 코틀린에서는 :를 사용하여 class를 확장하거나 interface를 구체화 시킨다. 자바와 같이 원하는 개수 만큼 interface를 구체화할 수 있으나, 클래스는 역시 단일 상속만 가능하다.

자바와 같이 kotlin의 interface 역시 구체화된 메소드를 구현할 수 있으나, 이때 java처럼 default 키워드를 사용하여 구현하지 않아도 된다. 다음의 예제를 봐보자.

interface Clickable {
	fun click()
    fun showOff() = println("I am a Clickable")
}

class Button: Clickable {
    override fun click() = println("The button was Cliecked")
}

fun main() {
    val button = Button()
    button.showOff() // I am a Clickable
}

이 역시 이미 구체화되어 있기 때문에 따로 override 하지 않아도 해당 메소드를 바로 사용할 수 있다. 물론 새롭게 정의하고 싶다면 override를 사용하여 진행하면 된다.

그렇다면 다른 interface에 같은 이름의 메소드가 있다면 어떻게 될까?
결론부터 말하면, 어느 쪽도 선택되지 않는다.

그렇다면 어떻게 해결할 수 있을까?
구현할 오버라이팅 메소드를 직접 제공하면 된다. 즉 특정 인터페이스의 메소드구현을 강제하면 된다.

interface Clickable {
    fun showOff() = println("I am a Clickable")
}

interface Focusable {
    fun showOff() = println("I am a Focusable")
}

class Button: Clickable, Focusable {
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

fun main() {
    val button = Button()
    button.showOff()
    // "I am a Clickable"
    // "I am a Focusable"
}

위와 같이 다 함께 출력되는 것을 확인할 수 있다. 물론 필요한 메소드만 있는 경우 하나만 작성하면 되며, java의 경우 interfaceName.super.methodName으로 사용했던 것과는 다르게 super 이후 <>안에 interfaceName과 .이후 methodName을 붙여준다. super<interfaceName>.methodName

1-2. open, final, abstract 변경자: 기본값 final

java에는 명시적으로 final을 사용하여 상속을 금지하는 keyword를 사용하지 않으면 모든 class는 다른 클래스에 상속할 수 있다. 이것이 편리한 경우도 있지만, 오히려 문제가 된느 경우도 많다.

예를 들어서 fragile base class라는 문제에 직면하게 되는데, 이 문제는 하위 클래스가 상위 클래스의 속성을 갖고 구현된 상태에서 상위 클래스가 변경된다면, 하위 클래스에서 특정 규칙하에 구현했던 가정이 깨져버리는 경우를 일컷는다. 이것이 왜 문제가 될까?

혼자 코드를 작성하는 경우에는 아마 문제가 없을 것이다. 본인이 가장 해당 코드를 자세히 알고 있으며 어떤 부분을 수정해야 할지 바로 감이 오는 경우가 대부분이지만, 이부분은 code convention 부분에서 중요한 역할을 한다.

즉 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지 않는다면 해당 클래스를 이용하는 개발자는 상위 클래스를 작성한 사람의 의도와는 다른 방식으로 메소드를 override할 위험이 있다. 이럴 경우 코드 design 자체가 꼬이게 된다.

이때 모든 하위 클래스를 변경하는 것은 사실상 불가능하기 때문에 특히나 자바에서는 final keyword를 작성하지 않은 상위 클래스는 모든 클래스에 상속이 가능하기 때문에 더욱 혼란스럽다.

결국에는 상위 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수도 있다는 면에서 상위 클래스는 취약하다는 의미이다.

더욱이 Effective Java에서는 상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라라는 문구를 찾을 수 있다. 이말은 상속을 위해 작성한 class가 아니라면 final로 상속을 제한하라는 말이 된다.

코틀린은 이 가치관을 따르고 있다. 따라서 어떤 클래스를 상속하기 위해서는 open이라는 키워드를 사용해야 하며, 해당 클래스의 함수나 프로퍼티에 open 키워드를 붙여야 override할 수 있다.

아래의 코드를 봐보자.

interface Clickable {
    fun click() = println("I am a click!")
    fun showOff() = println("I am a Clickable")
}


open class RichButton: Clickable { // RichButton은 상속이 가능하다.
    fun disable() {} // 아무런 keyword가 없기 때문에 default는 final이며, 따라서 override는 불가능하다.
    open fun animate() {} //  이 함수는 override가 가능하다.
    override fun click() {} // interface의 method는 기본적으로 override가 가능하다.
}

fun main() {
    val richButton = RichButton()
    richButton.showOff()
}

앞서 설명한 대로 위 interface와 class는 작동할 것이다. 하지만, 우리가 여기 하나 더 생각을 해야 하는 것은 interface의 method를 상위 클래스에서 상속 받아 아무런 구현체도 남기지 않은 상태에서 별 다른 keyword를 남기지 않았다. 이때 그 메소드는 하위 클래스에서 구현체를 작성할 수 있을까?

결론은 그렇다이다. 그렇기 때문에 interface를 상속받은 상위 클래스에서 특정 메소드를 override 했고 하위 클래스에서 해당 메소드 구현을 금지하려면 final 키워드를 작성해야 한다.

아래와 같이 말이다.

interface Clickable {
    fun click() = println("I am a click!")
    fun showOff() = println("I am a Clickable")
}


open class RichButton: Clickable {
    final override fun click() {}
}

fun main() {
    val richButton = RichButton()
    richButton.showOff()
}

RichButton 클래스의 click에 붙은 final은 하위 클래스에서 상속을 금지시키는 역할을 한다.

open class와 smart cast
그렇다면 class의 default final로 지정했을 때 얻게 되는 가장 큰 이득은 무엇일까?
그것은 바로 다양한 상황에서 smart cast가 가능하다는 점이다. smart case는 타입 검사 뒤에 변경할 수 없는 변수에만 적용이 가능하다. 즉 class의 property의 경우 val이면서 custom 접근자가 없는 경우에만 smart cast를 쓸 수 있다. 이 요구 사항은 프로퍼티가 final이어야만 한다는 뜻이다.
위 이야기를 자세히 생각하면 즉 property가 final이 아니라면 그 property를 다른 class가 상속하면서 custom 접근자를 정의함으로써 smart cast의 요구사항을 깰 수 있다.
property는 기본적으로 final이기 때문에 따로 고민할 필요 없이 대부분의 프로퍼티를 smart cast에 활용할 수 있다.
결론적으로 코드를 더 이해하기 쉽게 만든다.

다음은 추상 클래스를 정의해보자.

abstract class Animated { // 1
    abstract fun animate() // 2
    open fun stopAnimating() {} // 3
    fun animateTwice() {} // 4
}
  1. 위 class는 abstract class이기 때문에 instance를 만들 수 없다.
  2. 추상함수이므로 반드시 override하여 함수의 구현체를 만들어야 한다.
  3. open으로 override를 허용할 수 있다.
  4. open이 없으므로 override를 할 수 없다.

다음은 코틀린의 상속 제어 변경자이다.
1. interface의 경우 final, open, abstract를 사용하지 않는다.
2. interface 멤버는 항상 열러 있으며 final로 변경할 수 없다.
3. interface 멤버에게 본문이 없으면 자동으로 추상 멤버가 되지만 따로 멤버 선언 앞에 abstract 키워드를 덧붙일 필요는 없다.

변경자역할설명
finaloverride 불가클래스 멤버의 기본 변경자이다
openoverride 가능반드시 open 키워드를 사용해야 override할 수 있다.
abstract반드시 override 해야한다.추상클래스의 멤버에만 이 변경자를 붙일 수 있으며, 해당 멤버는 구현되어 있으면 안 된다.
overridesuper class나 super instance의 멤버를 override 하는 중override하는 멤버는 기본적으로 열러있으나 sub class의 override를 금지하려면 final을 명시해야 한다.

본 포스팅은 Kotlin in Action을 기반으로 작성되었다. 여기서 용어를 재조명하기 위해서 다음을 작성한다.
public, private, protected, interval 변경자를 접근 변경자 또는 가시성 변경자라고 부르지만, 상속 관련 final, open, override, abstract 등을 한번에 부르는 영어 용어는 없다. 이 책에서는 상속 관련 키워드도 접근 변경자라는 말로 통일했다.

profile
'개발'은 '예술'이고 '서비스'는 '작품'이다

0개의 댓글