[Kotlin] Kotlin의 Default가 final인 이유에 대해서

이승우·2023년 5월 25일
2
post-custom-banner

🤔 Kotlin final이 default인 이유

Kotlin에서 Default가 final로 선언되어 있는데, 왜 그렇게 되어 있을까에 대해서는 한번도 고민해본 적이 없다가 최근에 면접을 통해서 질문을 받으면서 찾아보게 되었다.

먼저, 첫번째로 코틀린은 함수형 프로그래밍에서 아이디어를 얻어왔고 가변을 사용했을 때, 발생하는 문제점들을 줄이기 위해서 불변을 사용한다. 그래서 모든 클래스들이 기본값으로 final로 선언되어 있는 이유이기도 하다.

두번째는 상속에 대한 문제이다.

코틀린의 공식 문서에 상속을 설명하는 부분을 보면 아래와 같이 설명이 되어 있다.

By default, Kotlin classes are final – they can't be inherited. To make a class inheritable, mark it with the open keyword:

코틀린 클래스는 기본적으로 final이며, 이는 상속이 불가능하게 한다. 만약, 상속이 가능하게 하려면 open 키워드를 써야 한다.

찾아보니 예전에는 이펙티브 자바 17에 관련된 내용도 있었던 것 같아 찾아보았다.

  • 오버라이딩이 가능한 메소드는 반드시 문서화를 해야한다. 상황에 따라 다르게 구현할 수도 있음을 포함한 자세한 내용을 알려줘야 한다.
  • 오버라이딩이 가능한 메소드는 하위 클래스를 만들어서 반드시 테스트해봐야 한다.
  • 생성자에서는 오버라이딩이 가능한 메소드를 호출하면 안된다. 초기화가 아직 안된 필드에 접근해서 동작을 수행하면 원치 않는 결과가 나올 수 있다.
  • Cloneable이나 Serializable을 구현하는 클래스는 생성자때와 같은 이유로 clone과 readObject에서 오버라이딩이 가능한 메소드를 호출하면 안된다.
  • 상속을 이용할 때는 위의 사항들을 잘 지켜서 설계와 문서화를 잘하거나 아니면 허용하지 말아라.

이처럼 상속을 올바르게 사용하지 못하기도 하고 개발자들의 실수가 존재하여 객체지향의 의도나 목적과는 다르게 설계할 가능성이 있다.

Classes final by default라는 곳에서 보면 open vs default가 비슷한 비율로 의견이 나뉘고 있다.

📝 Conclusion

final 클래스와 멤버 함수는

  • 개발자들이 상속을 올바르게 사용하지 못하거나 실수를 해서 부작용을 일으킬 가능성이 있다. 이로 인해 객체지향의 의도나 목적과는 다르게 설계할 수 있다.

    • 이는 코드의 복잡성과 가독성 문제가 발생할 수도 있다.
  • kotlin에서는 이러한 문제를 방지하기 위해 final을 default로 가지게 함으로써 상속과 오버라이딩을 허용하지 않는다.

    • 이를 통해 코드의 예측 가능성을 높이고 안정성을 챙길 수 있다. (상속을 통한 동작의 변경이나 오버라이딩에 따른 부작용을 방지하여 코드의 의도를 명확하게 보여줌)
  • 컴파일 시점에 결정되는 정적 바인딩(static binding)을 사용하므로 런타임에 발생할 수 있는 동적 바인딩(dynamic binding)에 따른 오류 가능성을 줄인다. 이는 런타임에서의 예기치 않은 동작을 방지하여 안정성을 향상시킨다.

  • 더 효율적인 컴파일러 최적화를 가능하게 한다. 컴파일러는 final 클래스의 인스턴스 생성과 호출된 final 멤버 함수의 동작을 미리 알고 있기 때문에 불필요한 가상 호출 메커니즘을 건너뛰고 직접 호출할 수 있다. 이로 인해 성능 향상을 기대할 수 있다.

🙏 More

3번과 4번에 대해서 조금 더 알아보도록 하자.

kotlin의 final 클래스와 멤버 함수는 컴파일 시점에 정적 바인딩(static binding)을 사용하여 다형성을 처리한다. 즉, 런타임에 다형성을 체크하지 않는다.

final 클래스는 상속이 금지되어 있으므로 컴파일러는 해당 클래스의 인스턴스를 생성할 때 정적으로 결정된 생성자를 호출한다. 또한, final 멤버 함수는 오버라이딩이 금지되어 있으므로 컴파일러는 해당 멤버 함수의 호출시에 정적으로 바인딩하여 해당 클래스의 구현을 직접 호출한다.

이러한 정적 바인딩은 런타임(실행시점)에 동적으로 다양한 구현을 결정하는 동적 바인딩과 달리 컴파일 시점에 이미 결정되어 있기 때문에 일종의 최적화이다. 정적 바인딩은 가상 호출의 오버헤드를 피하고 직접 호출을 통해 실행 속도를 향상시킬 수 있다.

Kotlin은 필요시에 open 키워드를 사용하여 상속을 가능하게 한다. 이 경우에는 다형성을 위해 동적 바인딩이 발생한다. 동적 바인딩은 런타임(실행시점)에 실제 객체의 타입에 따라 호출될 메소드를 결정하므로 런타임에 다형성 체크가 이뤄진다.

요약 : Kotlin의 final 클래스와 final 멤버 함수는 컴파일 시점에 정적 바인딩을 통해 다형성을 처리하여 런타임에 다형성 체크를 하지 않는다. 이는 성능 향상과 코드의 예측 가능성을 제공한다. 나아가 open 키워드를 명시적으로 사용하여 클래스와 멤버 함수를 확장 가능하게 만들 수 있으며, 이 경우에는 동적 바인딩을 통해 런타임에 다형성 체크가 이뤄진다.

Ref

profile
Android Developer
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 11월 8일

kotlin 의 default 가 final 인 점에 대해 궁금했는데 조금은 이해하게 되네요 감사합니다.

답글 달기