본문 바로가기

안드로이드 Android

안드로이드 개발 (40) Kotlin In Action 정리 - 4

(이전편 다시보기)

 

안드로이드 개발 (38) Kotlin In Action 정리 - 2

(이전 편 다시보기) 안드로이드 개발 (37) Kotlin In Action 정리 - 1 현업에서 일을 하다보면, Back To The Basic 을 통하여 디버깅 추적 및 안전한 코드 작성 능력을 강화 할 수 있다는 것을 알게 됩니다. 이

gift123.tistory.com

kotlin in action 요약 4번째 편 4장 요약입니다. kotlin 의 간단한 문법 설명 보다, 잊기 쉬울 만한 내용을 위주로 골라 정리하였습니다.  

 

4장 - 클래스, 객체, 인터페이스

java와 kotlin은 비슷하면서도 사뭇 다릅니다. 예를들어, kotlin의 interface에 프로퍼티 선언이 들어갈수 있고, class는 기본적으로 final 이며 public 입니다. 그외에 kotlin의 익명 클래스 사용이나, koltin의 compainon object , kotlin Object (싱글톤 클래스) 등등 자바와 차별점 되는 점들이 있습니다.

 

1) 코틀린 인터페이스

- kotlin의 interface는 default 구현 기능을 제공하며, 구현 클래스에서 default 구현 기능을 사용할때 override를 강제하지 않는다.

class LonerImpl : Loner {
 // ide에서 구현을 강제하지 않는다.
}

interface Loner {
    fun blogPost() = print("블로그 작성")
}

 

- kotlin은 interface를 다중 구현 할때, 두개의 인터페이스의 메서드 명이 같다면 구현할 클래스에서 반드시 중복된 override를 해야하며 어느 interface의 메서드를 사용할지 super<T> 로 명시가 가능하다. 

class LonerImpl : Loner, Mom {
// default 구현이라도 해도 인터페이스가 중복 된다면, ide에서 override를 강제 한다.
    override fun sleep() {
        super<Loner>.sleep()
        super<Mom>.sleep()
    }
}

interface Loner {
    fun blogPost() = print("블로그 작성")
    fun sleep() = println("로너가 잠을 자다")
}

interface Mom {
    fun sleep() = println("엄마가 잠을 자다")
}

- kotlin 1.5v 부터 interface의 default 구현을 사용할때 kotlin complier가 java interface의 default 메서드를 생성해줍니다.

 

 kotlin -> java 디컴파일 코드

public interface Mom {
   void sleep();

   @Metadata(
      mv = {1, 1, 18},
      bv = {1, 0, 3},
      k = 3
   )
   public static final class DefaultImpls {
      public static void sleep(@NotNull Mom $this) {
         String var1 = "엄마가 잠을 자다";
         System.out.println(var1);
      }
   }
}

 

2) 기본적으로 final 

- 모든 클래스에 상속을 허용 하면 생기는 문제점으로 상위 클래스가 하위 클래스에 올바른 사용법을 제시하지 않거나, 상위클래스에서 의도한 안전한 사용법으로 하위 클래스에서 구현하지 않거나, 하위 클래스가 상위 클래스가 의도한대로 만들지 않아서 상위 클래스의 변경으로 하위 클래스 기능에 문제 생기거나 등등 문제로 인하여, 모든 클래스가 쉽게 상속에 열려있으면 좋지 않습니다.

또한, 정확한 설계나 지침으로 만들지 않은 class는 상속하지 않는 것이 하는 것이 낫다고 Effective Java 책에서 언급 합니다.그래서 상속을 생각하지 않는 class는 java에서 기본적으로 final로 하는것을 권장한다. kottlin은 위 같은 문제를 생각하여 class에 앞에 명시가 없다면 기본적으로 final 입니다.

* Loner 생각: 중요한 내용이라 생각되서 길게 기술했습니다. 그래서 상속보다 합성 방식이 더 낫기도 합니다. 사례로

 android View 기반 class들 (Button,TextVeiw..)의 구조와 android Jetpack Compose를 비교해볼 수 있겠습니다.* 

 

- class의 기본적인 상속 가능 상태를 final의 장점으로 다양한 경우에서 kotlin의 smart Cast가 가능하다는 점이 생깁니다. 

만약, final을 가진 타입의 property가 final 이 아니라면 그 property를 다른 클래스가 상속하면서 커스텀 접근자를 정의하면서 스마트 캐스트의 요구 사항을 꺨 수 있습니다.

 

3) 가시성 변경자 

- java와 달리 kotlin은 패키지를 name space를 관리하기 위한 용도로 사용합니다.

- kotlin은 module 내부 가시성을 제공 합니다. (internal)

- kotlin은 최상위 선언(클래스, 함수 프로퍼티) 에 대해 private 가시성을 허용합니다. 

//최상위 파일 안에서 1개의 클래스와 1개의 확장함수

internal class Test{
    private fun test01() = print("test01")
    protected fun test02() = print("test02")
}
//Test에 빨간줄이 그어짐, 위에 internal이기 때문
fun Test.test(){

  //test01에 빨간줄이 그어짐 private 이기 때문
    test01()
    //test02에 빨간줄이 그어짐 protected 이기 때문
    test02()
}

- java는 같은 패키지 안에서 protected 멤버에 접근할 수 있지만, kotlin은 그렇지 않다. protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보입니다.

 

 - kotlin의 public, protected, private 변경자는 컴파일 된 자바 바이트코드 안에서도 그대로 유지 됩니다. 예외적으로 private class는 java의 패키지 전용 클래스로 컴파일 합니다.

 

//private은 아래와 같이 된다. 
final class PrivateTest {
   public PrivateTest() {
   }
}

- kotlin -> java 바이트코드 변경에서, internal 변경자는 java의 패키지 전용 internal과 다르고 모듈은 보통 여러 패키지로 이뤄지며, 서로 다른 모듈에 같은 패키지 속한 선언이 들어 있을수도 있어서, internal은 바이트 코드상 public으로 변합니다.

 

// 변경전 internal class InternalTest.. 

public final class InternalTest {

}

 

- kotlin -> java 바이트코드 변경상에서 internal 의 멤버 이름을 보기 나쁘게(?) 변경하는 이유는 두가지 입니다. 첫번째는 한 모듈에 속한 어떤 클래스를 모듈 밖에서 상속한 경우 그 하위 클래스 내부의 메서드 이름이 우연히 상위 클래스의 internal 메서드와 같아져서 내부 메서드를 override 하는 경우를 방지하기 위함이고, 두번째는 internal 클래스를 모듈 외부에서 사용하는 일을 막기 위함 입니다.

 

4) 내부 클래스와 중첩된 클래스 

- java와 정반대로 kotlin은 클래스 본문 안에 새로운 클래스를 선언해도 내부 클래스는 바깥 클래스를 기본적으로 참조할 수 없습니다. 내부 클래스가 바깥 클래스를 참조하면서 생기는 단점을 생각해서 기본적으로 바깥 클래스를 참조를 할 수 없게 되었습니다. 바깥 클래스를 참조하기 위해서 내부클래스에 inner를 사용 해야합니다.

 

class OuterClass{
    val test = 1
    class InnerClass{
        init {
        //참조 할 수 없다고 뜹니다.
            test
        }
    }
}
class OuterClass{
    val test = 1
    inner class InnerClass{
        init {
        //inner를 추가해서 바깥 클래스 참조 가능해졌습니다.
            test
        }
    }
}

 

5) sealed class 

sealed class는 본문 안쪽에서만 하위 클래스를 선언 할 수 있는 확장성이 막혀있는 class 입니다. 본문 외부에서 하위 클래스를 만들어 낼 수 없기 때문에 타입이 보장되는 장점이 있습니다. 대표적으로아래 코드 처럼, when 식이 모든 하위 클래스를 검사 하기 때문에 별도의 else가 필요 없어집니다.

fun test(sealedClassTest: SealedClassTest){
    when(sealedClassTest){
        is SealedClassTest.Test01 -> {}
        is SealedClassTest.Test02 -> {}
    }
}

sealed class SealedClassTest{
    object Test01:SealedClassTest()
    data class Test02(val test:Int):SealedClassTest()
}

6) 클래스 선언

- kotlin은 class 생성자는  primary 생성자와 secondary 생성자, init 블록 크게 3가지를 사용할 수 있다.

- secondary 생성자는 주로 자바와의 상호 운용성 및 자바 프레임워크 클래스를 확장해야할 때 사용하는 경우가 많다.

- secondary 생성자는 super() 를 통해 상위 클래스 생성자에게 객체를 위임 할 수 있다.

- secondary 생성자는 this를 통해서 자신의 다른 생성자를 호출 할 수 있습니다.

 

7) 접근자의 가시성 변경

- custom getter setter도 가시성 변경이 가능하다.

fun main() {
    //아래 set은 오류 난다.
    BlogExample.postCount++
    //아래 get은 문제 없이 사용 가능하다.
    println(BlogExample.postCount)
}

object BlogExample{
    var postCount = 0
    private set
}

8) 클래스위임 by 키워드 사용

- kotlin에서 위임 패턴을 돕는 by 키워드가 있다. by 키워드를 통해서 불필요한 override fun를 쓰지 않고 필요한 것만 override를 해서 언어 차원에서 불필요한 코드를 줄일 수 있다. 

 

fun main() {
    val testList = TestList<Int>()
    println(testList.add(0))
}

class TestList<T>(
    val innerTestList:MutableCollection<T> = mutableListOf()
): MutableCollection<T> by innerTestList{
    override fun add(element: T): Boolean {
        return false 
    }
}

* Loner 생각: by 키워드가 없었다면.. MutableCollection에 있는 미구현 함수들을 모두 override 해야했을 것이고, 상속을 하지 않고 외부 객체 위임을 통해서 기능을 확장하니, 상속에 대한 단점을 두려워 하지 않아도 되서 좋다.* 

 

9) companion object 

- companion object의 함수도 확장함수로 이용 가능하다.

- companion object의 이름을 붙일 수 있다.

    companion object Loner{
        ...
    }
}

- companion object를 통해서 class의 primary 생성자를 비공개로 만들고 팩토리 메서드를 활용해주면 매우 유용하다.  

class CompanionObjectTest private constructor(val test:Int){
    companion object{
        ...
    }
}

- companion object  에서 인터페이스 구현이 가능하다.

interface TestInterface{
    fun test()
}
class CompanionObjectTest {
    companion object :TestInterface{
        override fun test() {
            ...
        }
    }
}

4장의 경우 내용이 꽤나 방대하게 느껴졌습니다. 아직은 크게 어렵지 않는 내용이었습니다. 잊기 쉬운 내용들을 위주로 정리를 하였고 기본기를 다시 리마인드 하는 느낌으로 꾸준히 진행할 예정입니다.