Kotlin In Action 4장

클래스, 객체, 인터페이스

코틀린의 클래스와 인터페이스는 자바와 약간 다르다.

예를 들어, 인터페이스에 프로퍼티 선언이 들어갈 수 있다.

또한 자바와 달리 코틀린 선언은 기본적으로 final이며 public이다.

중첩 클래스는 기본적으로 내부 클래스가 아니다.

클래스를 data로 선언하면 컴파일러가 일부 표준 메소드를 자동으로 생성해준다. (굉장히 강력한 점)

클래스 계층 정의

코틀린에서 인터페이스의 선언은 자바의 그것과 동일하다.

이를 구현하기 위해서는 클래스 뒤에 : 콜론을 붙이기만하면 끝이다.

자바처럼 extendsimplements 키워드를 사용하지 않아도 된다.

또한 자바에서는 해당 메소드가 오버라이딩된 것을 알려주는 @Override 애노테이션을 선택적으로 붙여도 되는 것과 달리,

코틀린에서는 override 변경자를 반드시 사용해야 한다.

또한 자바8에서 추가된 디폴트 메소드 또한 그대로 사용가능하다.

interface A {
    fun go() = println("I love Golang")
}
interface B {
    fun go() = println("I love Kotlin")
}

만약 두 인터페이스에서 시그니쳐가 같은 두 디폴트 메소드를 선언하면 어떻게 될까?

정답은 어느 쪽도 선택되지 않는다.이다.

그래서 코틀린 컴파일러는 두 메소드를 아우르는 구현을 하위 클래스에 직접 구현하게 강제한다.

class C: A,B {
    override fun go() {
        super<A>.go()
        super<B>.go()
    }
}

하나만 사용하는 경우라면 아래와 같이 사용할 수 있다.

ovverride fun go() = super<B>.go()

open, final, abstract 변경자: 기본적으로 final

조슈야 블로크의 Effective Java에서는 상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라는 조언을 한다.

이는 Java의 창시자인 제임스 고슬링이 한 인터뷰에서 내가 자바를 만들면서 가장 후회하는 일은 상속을 만든 점이다라는 말과 일맥상통한다.

특별히 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들라는 뜻이다.

코틀린 또한 이러한 철학을 그대로 따라서 코틀린의 클래스와 메소드는 기본적으로 final이다.

그래서 어떤 클래스의 상속을 허용하고 싶은 경우 open 변경자를 붙여주어야 한다. 메소드도 마찬가지로.

만약 어떤 오버라이딩된 메소드를 하위 클래스에서 오버라이드 못하게 하려고 하는 경우 final 키워드를 붙여서 이를 막을 수 있다. (약간 내로남불 같긴 하네..)

final override fun go() {}

그리고 코틀린에서도 추상 클래스를 위한 abstract 키워드를 제공한다.

그래서 이를 간략화 해보면,

변경자이 변경자가 붙은 멤버는..설명
final오버라이드할 수 없음클래스 멤버의 기본 변경자다.
open오버라이드할 수 있음반드시 open을 명시해야 오버라이드할 수 있다.
abstract반드시 오버라이드해야 함추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버는 구현이 있으면 안된다.
override상위 클래스나 상위 인스턴스의 멤버를 오버라이딩 하는 중오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이딩을 금지하려면 final을 명시해야한다.

가시성 변경자 : 기본적으로는 공개

코틀린은 기본적으로 public이다.

코틀린에서는 패키지를 네임스페이스를 관리하기 위한 용도로만 사용한다. 그래서 패키지를 가시성 제어에 사용하지 않는다.

그래서 코틀린에서는 internal이라는 새로운 가시성 변경자를 도입했다.

이는 모듈 내부에서만 볼 수 있음이라는 뜻이다.

모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일들을 의미한다.

모듈 내부 가시성은 여러분의 모듈의 구현에 대해 진정한 캡슐화를 제공한다는 장점이 있다.

자바에서는 패키지가 같은 클래스를 선언하기만 하면 어떤 프로젝트의 외부에 있는 코드라도 패키지 내부에 있는 패키지 전용 선언에 쉽게 접근이 가능했었다.

그래서 모듈의 캡슐화가 쉽게 깨진다.

다른 차이는 코틀린에서는 최상위 선언에 대해 private 가시성을 허용한다는 점이다. (자바에서는 최상위 클래스에 private 사용 불가)

최상위 선언에는 클래스,함수,프로퍼티 등이 포함된다.

변경자클래스 멤버최상위 선언
public(기본 가시성)모든 곳에서 볼 수 있다.모든 곳에서 볼 수 있다.
internal같은 모듈 안에서만 볼 수 있다.같은 모듈 안에서만 볼 수 있다.
protected하위 클래스 안에서만 볼 수 있다.(최상위 선언에 적용할 수 없음)
private같은 클래스 안에서만 볼 수 있다.같은 파일 안에서만 볼 수 있다.

일반적으로 가시성은 클래스나 메소드 자신의 가시성과 같거나 더 높아야 사용할 수 있다.

protected는 자바에서는 같은 패키지 안에서 접근할 수 있었지만,

코틀린에서는 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 접근할 수 있다.

역시나 이전 시간에 배웠던 확장 함수는 그 클래스의 private이나 protected 멤버에 접근할 수 없다는 사실을 기억하자.

코틀린의 가시성은 바이트코드로 바뀐 후 어떻게 자바가 읽을 수 있게 될까?

코틀린의 public,protected,private 변경자는 컴파일된 자바 바이트코드 안에서도 그대로 유지된다.

유일한 예외는 private 클래스인데 코틀린은 내부적으로 private 클래스를 패키지-전용 클래스로 컴파일한다고 한다.

internal의 경우 바이트 코드상에서는 public이 된다.

대신 코틀린 컴파일러가 internal 멤버의 이름을 아주 가독성이 떨어지게 바꾸게 되는데,

그 이유는 두가지가 있다.

첫 번째는 한 한 모듈에 속한 어떤 클래스를 모듈 밖에서 상속한 경우 하위 클래스 내부의 메소드 이름이 우연히 상위 클래스의 internal 메소드와 같아져서 내부 메소드 오버라이드하는 경우를 방지하기 위함이고,

두 번째는 실수로 internal 클래스를 모듈 외부에서 사용하는 일을 막기 위함이다.

내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스

자바와 코틀린의 중첩 클래스의 차이는, 코틀린의 경우 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다는 점이다.

interface State: Serializable
interface View {
    fun getCurrentState():State
    fun restoreState(state: State)()
}

자바에서 이를 어떻게 표현하는지 살펴보자.

public class Button implements View {
    @Override
    public State getCurrentState() {
        return new ButtonState();
    }

    @Override
    public void restoreState(State state) { ... }
    public class ButtonState implements State { ... }
}

위 코드는 java.io.NotSerializableException: Button이라는 오류를 발생시킨다.

왜 그럴까?

이는 ButtonState 클래스가 자동으로 inner class가 되기 때문인데,

ButtonState 클래스 바깥쪽 Button 클래스에 대한 참조를 묵시적으로 포함한다.

이로 인해 ButtonState를 직렬화 할 수 없게 된다. 왜냐하면 Button을 직렬화 할 수 없기 때문에 버튼에 대한 참조가 ButtonState를 방해하는 것이다.

이를 해결하려면 ButtonState를 static 클래스로 선언하면 된다.

이번엔 코틀린의 예제를 보자.

class Button:View{
    override fun getCurrentState(): State = ButtonState()
    override fun restoreState(state:State) { ... }
    class ButtonState: State { ... }
}

이는 코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같아진다.

이를 내부 클래스로 변경하고 싶다면 inner 키워드를 붙여주어야 한다.

클래스 B 안에 정의된 클래스 A자바의 경우코틀린의 경우
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음)static class Aclass A
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함)class Ainner class A

또한 코틀린에서는 바깥쪽 클래스의 인스턴스를 가리키는 참조를 표기하는 방법도 자바와 다르다.

내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer라고 써야 한다.

class Outer {
    inner class Inner {
        fun getOuterReference() : Outer = this@Outer
    }
}

봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한

코틀린에서는 상위 클래스에 sealed 변경자를 붙임으로써 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다.

sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 한다.


sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
    when(e) {
        is Expr.Num -> e.value
        is..Expr.Sum -> eval(e.right) + eval(e.left)
    }

when은 기본적으로 특정 타입의 클래스를 검사할 때 반드시 디폴트 분기인 else를 명시하는 것을 강제한다.

sealed는 내부 클래스를 봉인하기 때문에 when을 사용하더라도 별도의 else 분기처리를 필요로하지 않는다.

내부적으로 Expr 클래스는 private 생성자를 가진다.

그 생성자는 클래스 내부에서만 호출이 가능하다.

그리고 sealed는 인터페이스로는 정의가 안된다.

왜냐하면 봉인된 인터페이스를 만들 수 있다면 그 인터페이스를 자바 쪽에서 구현하지 못하게 막을 수 있는 수단이 코틀린 컴파일러에게는 없기 때문이다.

클래스의 초기화 : 주 생성자와 초기화 블록

1개의 매개변수를 갖는 생성자를 가진 클래스를 생성해보자.

class User(val nickname: String)

이 클래스에서 괄호로 둘러싸인 코드를 주 생성자라고 부른다.

주 생성자가 어떻게 동작하는지 좀 더 자세하게 알아보자.

class User constructor(_nickname: String){
    val nickname: String

    init {
        nickname = _nickname
    }
}

여기서 constructorinit이라는 새로운 키워드를 만났다.

constructor는 주 생성자나 부 생성자 정의를 시작할 때 사용한다.

init은 초기화 블록을 시작한다. 즉 객체가 생성되는 시점에 실행될 초기화 코드가 들어가게 된다.

생성자 역시 디폴트 값을 정의할 수 있다.

부 생성자 : 상위 클래스를 다른 방식으로 초기화

부 생성자는 constructor 키워드로 선언할 수 있다.

클래스 내부에 여러개의 부 생성자를 만들 수 있다.

접근자의 가시성 변경

class LengthCounter {
    var counter: Int = 0
        private set
}

get이나 set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.

컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임

코틀린에서는 자바처럼 클래스가 equals, hashCode, toString 등의 메소드를 구현할 필요가 없다.

코틀린에서는 equals==로 대체해서 사용할 수 있다.

자바에서는 ==는 객체간의 비교에서 참조 동일성을 검사하지만 코틀린에서는 ==를 사용해도 객체 동등성 검사를 한다. (와우)

Data Class : 모든 클래스가 정의해야하는 메소드 자동 생성

data class는 toString,equals,hashCode를 오버라이딩 하지 않고도 자동으로 만들어 준다.

다음과 같이

data class Client(val name:String, val postalCode: Int)

이 클래스는 자바에서 요구하는 모든 메소드를 포함한다.

  • 인스턴스간 비교를 위한 eqauls

  • HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode

  • 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어주는 toString

또한 copy도 자바에 비해서 매우 손쉽게 구현할 수 있다.

class Client(a:Int, bar:Bar, list:MutableList<int>){
    fun copy(a: Int,bar: Bar = this.bar, list: MutableList<Int> = this.lst) =
        Client(a,bar,list)
}

그러나 이는 엄연히 Shallow Copy이다.

Deep Copy를 위해선 다음과 같이 구현하면 된다.

(확실히 자바보다 훨씬 간단하고 직관적이다.)

class Client(a:Int, bar:Bar, list:MutableList<int>){
    fun shallowCopy(a: Int,bar: Bar = this.bar, list: MutableList<Int> = this.list) =
        Client(a,bar,list)

    fun deepCopy(a: Int,bar: Bar = this.bar.copy(), list: MutableList<Int> = this.list.toList()) =
        Client(a,bar,list)

}

Default Parameter를 통해 Deep Copy를 구현할 수 있다.

클래스 위임 : by

상속을 허용하지 않는 클래스에 새로운 동작을 추가해야할 때 사용하는 데코레이터 패턴은,

상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 만들되 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스 데코레이터 내부에 필드로 유지하는 것이다.

이 때 새로 정의해야하는 기능은 데코레이터의 메소드에 새로 정의하고 기존 기능이 그대로 필요한 부분은 데코레이터의 메소드가 기존 클래스의 메소드에게 요청을 전달해야 한다.

이는 코틀린의 함수가 1급 시민이기때문에 비교적 간단하게 구현할 수 있지만, 아무래도 일일이 하는건 귀찮다.

그래서 이러한 경우 by 키워드를 통해 위임을 할 수 있다.

다음과 같이,


class A<T> (
    innerList: Collection<T> = ArrayList<T>()
): Collection<T> by innerList { }

object 키워드

object 키워드는 코틀린에서 싱글톤 객체를 만드는 기능을 제공하는 키워드이다.

자바에서는 싱글턴을 만들려면 상당히 귀찮은 방법을 사용해야 했지만 코틀린에서는 object 하나로 간단하게 구현이 가능하다.

companion object

1_Gr557EcchFrxks4n9NuEWg

코틀린 클래스 내부에는 정적인 멤버가 없다.

코틀린은 자바의 static을 지원하지 않기 때문인데,

그 대신 코틀린은 패키지 수준의 최상위 함수나 객체 선언을 활용한다.

대부분의 경우 최상위 함수를 사용하는 것을 권장한다.

그러나 클래스와 밀접한 관계를 갖게 되어서 클래스 외부에 선언하기 애매한 경우가 있다.

그럴 때 사용하는 것이 companion object이다.

사용 방법은 아래와 같다.

class A{
    companion object {
        fun bar() {
            println("Companion Object Call")
        }
    }
}

이때 bar는 자바의 static 키워드를 붙인 메소드와 동일한 기능을 수행할 수 있다.

동반 객체에는 이름을 붙이는 것 또한 가능하다.

class A{
    companion object B{
        fun bar() {
            println("Companion Object Call")
        }
    }
}
A.B.bar()

동반 객체는 인터페이스를 구현하거나 확장함수를 만드는 것 또한 가능하다.

기본적으로 이름이 없으면 Companion으로 호출할 수 있다.

무명 객체

object 키워드는 싱글턴과 같은 객체를 정의할때만 사용할 수 있는 것은 아니다.

무명 객체(Anonymous Object)를 정의할 때도 object 키워드를 사용한다.

일반적으로 특정 인터페이스를 구현하여 사용하거나 할때 사용한다.

일반적으로 자바에서는 하나의 인터페이스만 구현하거나 하나의 클래스만 확장할 수 있는 무명 클래스와 달리

코틀린 무명 클래스는 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스 구현이 가능하다.

명심해야할 점은 객체 선언과 달리 무명 객체는 싱글턴이 아니다.

Reference

Kotlin In Action (드미트리 제메로프, 스베트라나 이사코바)



© 2022. by minkuk

Powered by minkuk