Kotlin 인터뷰 치트 시트

ricky_0_k·2021년 10월 18일
0

원래는 github actions 포스트를 작성해야하지만 사정이 있어 이 내용을 먼저 정리하려 한다.

Kotlin weekly 였는지, medium 추천글이었는지 기억이 가물가물하지만
자신을 점검하기에 좋은 내용인 것 같아 정리 겸 포스트를 작성한다.

Q1. What is the difference between val and var?

val 과 var 의 차이이다. (TMI 로 현 회사의 면접 질문이기도 했다)

저자 Answer

var 는 일반 프로퍼티와 같으며 언제든지 할당이 가능하다.
val 은 한번 할당하면 더 이상 바꿀 수 없는 불변 프로퍼티이다 (Kotlin 상에서)

Q2. What is the difference between val and const val?

Q1 을 보면서 왜 이질문은 안나오나 했었다.
막상 이질문에 우리는 한번에 대답이 어려울수도 있다.

저자 Answer

둘 다 Java 상에서 불변 변수(final)로 인식된다
const val 은 컴파일 시간에서 값이 정해져야하지만, val 은 런타임 시간에 값이 정해질 수 있다

나는 지역 상수를 사용할 때 val 을 사용하고,
상위 영역 (ex. companion object 등) 에서는 const val 을 활용하는 편이다.

참고. val 을 통해 아래 같이 재미있는(?) 로직도 작성할 수 있다.

val abcd: String
val number = (Math.random() * 100).toInt() % 2 == 1
abcd = if (number) "홀수" else "짝수"

위 로직은 간단한 내용이지만 당장 abcd 를 할당하기 어려운 경우에는 위와 같이 처리할 수도 있다.
(abcd 선언을 아래로 옮겨도 되지 않느냐에 대해선 대답하지 않겠다 ㅇㅅㅇ..)

Q3. What is Difference between setValue() and PostValue() in MutableLiveData?

오호라 갑자기 LiveData?
"작성자가 Android 개발자인가?", "LiveData 가 순수 Kotlin 인가?" 를 생각해보며 이 질문을 보았다.

저자 Answer

setValue() 는 메인쓰레드에서 불린다
백그라운드 쓰레드에서 값을 설정해야 하는 경우에는 postValue() 를 사용해야 한다.

백그라운드 쓰레드에서 값을 설정한다는 게 바로 이해가 안될 수 있어 다른 포스트의 글도 빌렸다

백그라운드 쓰레드에서 동작하다가 메인 쓰레드에 값을 post 하는 방식으로 사용된다.
함수 내부적으로는 아래와 같은 코드가 실행된다.

new Handler(Looper.mainLooper()).post(() -> setValue())

위 코드가 실행되므로 가장 마지막에 입력된 값이 적용되고, postValue() 호출 이후 바로 값을 불러올 시 정확한 값을 가져온다는 보증을 할 수 없다. (이해가 안된다면 Handler 를 공부해보도록 하자)
즉시 값을 확인하고 싶다면 setValue() 를 해야하지만, 백그라운드에서 호출 시 crash 를 맛볼 수 있으므로 경우에 따라 사용하는 걸 개인적으론 추천한다.

Q4. How to check if lateinit property is initialized or not?

헷갈릴 수 있는 내용이라 생각한다.

저자 Answer

isInitialized() 을 통해 해당 lateinit 프로퍼티가 초기화되었는지 확인한다.
true : 초기화됨, false : 초기화 안됨

주의해야할 내용이 있다. 지역 변수에서는 사용할 수 없다.
믿지 못하겠다는 사람은 아래 코드를 playground 에서 컴파일해보자.

val number = (Math.random() * 100).toInt() % 2 == 1
abcd = if (number) "홀수" else "짝수"
abcd.isInitialized

저자가 명시한 프로퍼티에 대해서도 좀 더 자세히 분석해보자.

프로퍼티 TMI

  1. 프로퍼티는 getter, setter 가 내장되어 있는 개념이다 (변수와는 다르다)

    var <propertyName>[: <PropertyType>] [= <property_initializer>]
        [<getter>]
        [<setter>]

    대괄호로 처리되어 있는 내용은 생략 가능하다는 내용이며, 타입의 경우 조건부 생략가능이다.

  2. 프로퍼티 내에는 필드가 내장되어 있지만 숨어있다. 예외적으로 접근자(getter, setter) 에서 내장되어 있는 필드를 참조할 수 있는데 이를 backing field 라고 한다.

    var count = 0
      set(value) {
          if(value >= 0) field = value
      }

    위와 같이 backing field 를 활용할 수 있으며, 반드시 field 를 사용해야 할 필요는 없다.

  3. 프로퍼티를 특정 프로퍼티에 내장시킬 수 있다. (backing properties)

    private val _items = MutableLiveData<List<String>>()
    val items: LiveData<List<String>> = _items

    저자가 LiveData 를 언급했기에, 실제 사용하는 예를 가져와 보았다.
    이런 식으로 _items 를 내장화시켜 외부에서 함부로 호출하지 못하게 할 수 있다.

  4. 인터페이스에 프로퍼티를 선언한 경우, 상속받는 자식 클래스는 강제 override (재정의) 을 해주어야 한다.

    interface Person {
      val name: String
    }
    
    class Minsu(override val name: String) : Person {
    
    }

    인터페이스의 경우에는 재정의 필수여부에 대해 optional 하다.

그 외에 여러 TMI 들이 있지만 이 포스트는 프로퍼티에 대한 포스트가 아니므로 여기까지만 하고 넘어간다.

Q5. When to use lateinit and lazy keywords in kotlin?

저자 Answer

lateinit 은 가변(var) 과 같이 사용해야하며, lazy 는 불변(val) 과 같이 사용한다.

아무래도 저자가 덧붙인 코드로 설명하는 게 나아 바로 코드로 들어간다.

private lateinit var display : DisplayAdapter
private val githubApiService : GithubApiService by lazy {
    RetrofitClient.getGithubApiService()
}

저자 예시에서 Retrofit 을 언급한 걸 보니 저자는 빼박 Android 개발자가 틀림없다
lazy 에 대해 부가 설명하면 게으르게 초기화 하는 것이다. 실제 돌아가는 순서는 아래와 같다.

  1. lazy 람다 초기화 구문을 initializer 에 둔다.
  2. githubApiService 프로퍼티 필드값 자체는 UNINITIALIZED_VALUE 로 둔다.
  3. 해당 프로퍼티를 호출할 시, initializer 가 필드값에 주입된다.
  4. 이후 initializer 를 통해 초기화된 값을 계속 사용한다.

그림을 보며 이해하고 싶다면 Kotlin 위임에 대해 분석한 좋은 글1 을 참고하는 걸 추천한다.

by 심층 분석

이제 lazy 는 이해했는데 by 가 궁금한 사람들도 있을 것 같아 준비했다.
먼저 위에서 활용한 by 는 특정 프로퍼티의 구현을 위임한다 로 이야기할 수 있다.

개발에서 위임이라는 말부터 이해가 안되는 사람들도 있을 것이다 (일단 나요 대신 개발해주는건가?)
위임 패턴 단어 자체를 위키피디아에서 보면 아래와 같이 정리할 수 있다.

상속과 동일한 코드 사용을 할 수 있도록 도와주는 패턴이며,
해당 객체(수신 객체)는 대리 객체에 구현된 내용을 그대로 가져온다.

그리고 코틀린 공식문서를 살펴보면 위임에 대해 아래 2가지를 이야기한다.

  1. 프로퍼티에 대한 접근자의 구현을 다른 개체에 위임 -> 방금 이야기한 거
  2. 인터페이스 구현을 다른 객체에 위임

1. 프로퍼티에 대한 접근자의 구현을 다른 개체에 위임

위에서 활용한 예를 곱씹어보면, githubApiService 는 결국 lazy 람다 내의 구현체로 정의가 된다.
간단하게만 이해하면 lazy 람다 에게 변수 초기화를 맡긴다 (= 위임한다) 로 이해할 수 있다.

2. 인터페이스 구현을 다른 객체에 위임

공식문서 코틀린 위임에서는 아래와 같이 이야기 하고 있다.

상속을 표현하는 슈퍼타입 리스트 내의 by 절은 b(에 대한 참조)가 상속 오브젝트의 내부에 저장되고 컴파일러가 b가 가지는 Base 인터페이스의 모든 메소드를 생성함을 나타냅니다(?)

바로 이해가 안되어서 Kotlin 위임에 대해 분석한 좋은 글2의 힘을 빌리면 아래와 같이 정리할 수 있다.

하나의 클래스(B)를 다른 클래스(C)에 위임(by)하도록 선언하여, 위임된 클래스(C)가 가지는 인터페이스 메소드(A의 메소드) 를 참조 없이 호출할 수 있도록 생성해주는 기능이라고 한다.

interface A { ... }
class B : A { }

val b = B()

// C를 생성하고, A에서 정의하는 B의 모든 메서드를 C에 위임합니다.
class C : A by b

위 내용은 C는 B가 가지는 모든 A의 메소드를 가지고 있다. 로 정리할 수 있다.

왜 사용할까?

결론부터 이야기하면,상속 대신 활용할 수 있는 방법으로 정리될 수 있다.

interface A {
    val abcd: String
}
class B : A {
    override val abcd: String = "cf 찍자"
}

val b = B()

// C를 생성하고, A에서 정의하는 B의 모든 메서드를 C에 위임합니다.
class C : A by b

위임을 통해 아래의 메리트를 얻을 수 있다.

  1. 먼저 별도의 코드 추가 없이 상속의 기능을 제공한다.
    (A 의 인터페이스 내용을 구현한 B 를 그대로 받으므로, 우리는 B를 상속받았다고 볼 수 있다.)
  2. 인터페이스에 의해 정의된 메소드만 호출할 수 있다.
    (B 에서 추가로 구현한 내용은 C 에 반영되지 않는다.)
  3. private 필드에 위임된 인스턴스를 저장하여 직접적인 접근 차단
    (이건 잘 모르겠다..)

다른 포스트도 참고해보니 결론적으로 상속의 대체제, 말 그대로 구현의 늪을 특정 변수에 위임시킴 이렇게 표현될 수 있을 것 같다.

Q6. What is difference between companion object and object?Companion Object is initialized when class is loaded. But Object is initialized lazily by default — when accessed for the first time.

companion object 와 일반 object 의 차이점을 이야기하는 질문이다.

저자 Answer

object 는 처음 액세스될 때 초기화된다.
companion object 는 클래스라 로드될 때 초기화된다.

부가적으로 설명하면

  1. object 는 실제로 사용될 때 초기화 된다.

    object ABCD {
        val abcd = "String"
        fun abcd(){
          println("abcd")
        }
    }
    
    fun main() {
        ABCD.abcd()  // 이 시점에서 초기화
        println(ABCD.abcd)
    }

    실제로 내부 함수에 접근해야 object 내의 init 블럭이 호출되는 구조이다.

  2. companion object 는 이를 가지고 있는 특정 클래스가 로드될 때 초기화 된다.

    class ABCD {
        companion object {
            val abcd: String = "abcd"
            fun abcd() {
                println(abcd)
            }
        }
    }
    
    fun main() {
        ABCD.abcd()  // 이 시점에서 초기화
    }

    ABCD 가 언급될 때 companion object 내의 init 블럭이 호출되는 구조이다.

object

Java 의 public final class 와 똑같다. (실제 디컴파일하면 똑같이 나온다)
내부에 해당 class의 인스턴스를 static 블록에서 초기화한다.

const 가 없거나 일반 함수 형태인 경우는 static 이 아니므로, INSTANCE 프로퍼티를 통해 접근이 가능하다. (const 를 언급하거나, @JvmStatic 를 함수에 언급하면 프로퍼티 언급없이 쓸 수 있다.)

object 는 추후 코루틴에서 활용하는 내용이기도 하다. 경량화 쓰레드라 불리는 코루틴은 object switching 을 활용하기 때문에 Thread 의 Context Switching 에 비해 소요가 적고, 비동기처리 또한 Thread 와 비슷한 처리가 가능하다.

companion object

class 와 한쌍이며, class 내에 종속되어 있는 object 이다.
Java 의 클래스 내 static inner class 로 보면 이해가 될 것이다.

확장 가능한 class나 interface를 상속받는 경우도 있다.

Q7. Difference between safe calls(?.) and Non-null Assertion(!!)?

저자 Answer

?. 왼쪽의 프로퍼티가 null 인 경우에는 실행하지 않습니다. (크래시를 방지합니다.)
!! 를 활용했는데 왼쪽의 프로퍼티가 null 인 경우 KotlinNullPointerException 을 발생시킵니다.

null safety 에 대한 질문이었다.
위 질문과 더불어 elvis (?:) 처리 방식도 알면 도움이 될듯하다.

Q8. What are data classes in kotlin?

저자 Answer

toString(), equals(), hashCode() 를 자체 구현하고, 편리한 함수 (ex. copy()) 들을 제공합니다.

data class 에 대한 내용이다.
Kotlin 에서 == 을 사용해도 되는 이유, kotlin 에서 특별한 경우가 아닌 이상 hashCode 직접 구현 할 필요가 없는 이유 등이 이것이다.
(hashCode 는 HashTable 과 연계되며 알고리즘의 해시 개념하고도 연관된다.)

추가적으로 data class 끼리 상속을 못하는 이유도 알면 좋지 않을까 생각한다.
간단한 이유는 상 하위 data class 중 어떤 equals() 를 실행해야할지 모르기 때문이다.

Q9. Why kotlin classes are final by default ?

저자 Answer

Effective Java 에 근거하여 무분별한 상속을 막는다 (ex. 부모의 가변성이 큰 경우)
코틀린에서는 class 를 final 화시켰고 이로 인해 컴파일 에러로 안내를 받는다.

위의 by 와 연결되기도 하는 듯 하다. 코틀린 개발자는 정말 상속이 싫었나보다.
만약 위의 경고에도 불구하고 상속을 받게 하려면 open 을 써야한다.

Q10. Difference between == operator and === operator?

저자 Answer

== 는 단순 값만 비교하지만, === 는 참조값 자체를 비교한다.

val number = Integer(1)
val anotherNumber = Integer(1)
number == anotherNumber // true (structural equality)
number === anotherNumber // false (referential equality)

Java 와의 차이점 설명이 있으면 좋았을 것 같았다

Java

== 는 Java 에서 Primitive 냐 Wrapper 냐에 따라 다르다.
Primitive 는 단순 값만 비교하지만, Wrapper 는 주소값을 비교한다.

=== 는 지원하지 않는다.

Kotlin

== 는 Kotlin 도 Primitive 냐 Wrapper 냐에 따라 다르다.
Primitive 는 단순 값만 비교하지만, Wrapper 는 equals() 로 비교한다.

=== 는 주소값 비교 시 활용한다.

Q11. Access/Visibility Modifiers in Kotlin

접근제어자에 대한 이야기이다.

  1. protected : 특정 클래스 혹은 파일, 그리고 하위 클래스에서 볼 수 있음
  2. private : 파일 내에서만 보임
  3. internal : 특정 모듈차원에서만 보임
  4. public : 어디서든지 보임

기본은 public

Java 와 다르게 default 가 public 이다.
protected 를 통해 LiveData 와 MutableLiveData 의 차이를 두기도 했으므로
활용방식도 기억해두면 좋을듯하다.

Q12. What are extension functions in Kotlin?

확장함수에 대한 이야기이다.

클래스를 상속하지 않고 메서드를 추가하는 방식
해당 클래스 내에서 일반함수로 활용된다

개인적으로 Kotlin 의 꽃이라고 생각하는 내용이 나왔다.
(TMI : 필자는 왠만한 유틸과 데이터바인딩 코드는 전부 확장함수 개념을 이용해 구현하였다.)
javascript 의 prototype 하고 비슷한 것 같기도 하다.
비슷한 개념으로 확장 프로퍼티도 있으며, 이 경우 필드는 활용할 수 없으므로 주의가 필요하다.

기타 특징
1. 확장함수는 정적으로 처리된다. (정적인 별도의 코드가 생성된다)
이에 특정 타입을 강제로 형변환하면 형변환된 타입 기반의 확장함수가 사용된다.
2. 확장함수 < 멤버함수
이름이 겹칠경우 멤버함수가 우위이다.
3. 확장함수를 사용해도 실행 시점에 부가비용이 들지 않는다.

Q13. What are inline functions ?

저자 Answer

인라인 함수는, 코드에서 사용된 모든 위치에 자신(함수)의 내용 전체 본문을 삽입하도록 컴파일러에 지시합니다.

한없이 길어질 수 있는 토픽이자 두번째 꽃이 나왔다.
이해하는 데 난이도를 요하므로 이번에도 전문가의 inline 분석글을 참고해서 작성하였다.

inline (function)

우리는 inline 을 알게 모르게 사용하고 있다. 아래는 사용의 예이다.

listOf("a", "b", "c").filter { it == "a" }

"로그".apply {
    Log.i("TAG",this)
}

inline 의 동작원리도 같이 확인해보면 (ex. apply)

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
"로그".apply {
    Log.i("TAG",this)
}

위 코드는 컴파일 단계에서
1. apply 함수 자체를 코드내에 그대로 넣고
2. block() 부분을 Log.i("TAG", this) 로 바꿔치기한다.

무지성으로 위 개념대로 적용한다면 아래처럼 나오지 않을까 싶다.

...
contract {
    callsInPlace(Log.i("TAG", "로그"), InvocationKind.EXACTLY_ONCE)  // ??
}
Log.i("TAG", "로그") // 바꿔치기 되었다 ㅇㅅㅇ

이런식이 되지 않을까 싶다. 이렇게 함수 호출을 줄일 수 있기 때문에 성능상 좋을 것이라 생각된다.
하지만 Kotlin 은 inline 을 많이 쓰고 있어, inline 을 커스텀하여 큰 성능향상을 얻긴 힘들다고 한다.

그럼 언제 이득?

함수 유형의 매개변수를 활용하는 경우(Higher-Order functions)엔 이득이 될 수 있다.
익명 클래스 또는 무명 클래스를 생성하고 사용하는 비용을 inline 을 통해 획기적으로 줄일 수 있다.
(만약 그 함수를 collection.map { ... } 형태에서 사용한다면 성능은 더욱 좋을 것이다)

기타

inline 관련해서는 아래 키워드도 존재한다.

noinline

그 외 inline 함수 내 함수 변수 중, inline 을 원하지 않는 함수가 있다면
그 함수는 noinline 으로 설정하여 inline 을 막을 수 있다.

crossinline

그리고 현재 inline 함수에서 풀어내지 않고, 다음 함수로 넘겨야할 때
그 함수는 crossinline 으로 설정하여 다음 함수에서 inline 되도록 할 수 있다.
만약 넘겨야 할 다음 함수에서 block 이 명시되어 있는 경우 반드시 crossinline 를 언급해야 한다.

꽃이기는 하지만 inline 개념을 적극적으로 사용하진 않았어서
지금 봐도 완벽하게 이해는 안되지만.. 예전보단 아예 뭔 소린지 몰랐다면, 지금은 어렴풋이는 이해된다.

inline (class)

그나마 이건 자주 활용해봤었다.
하나의 클래스에서 동일한 타입으로 사용하거나 각 id 마다 고유한 타입을 부여하고 싶을 경우 inline class 를 활용해 명확화하고 직렬화, 역직렬화 상에서도 무리 없이 타입 변환도 가능하다.

나는 이런 식으로 많이 사용했다.

inline class ShippingId(val asInt: Int)

최근엔 inline class 로 활용하는 방식이 deprecated 된듯하다?

'inline' modifier is deprecated. Use 'value' instead

이런 경우 아래와 같이 작성이 가능하다.

@JvmInline
value class ShippingId(val asInt: Int)

Q14. What are scope functions in Kotlin ?

위에 apply 예시를 이야기했었는데 바로 나오네.. 소름이다.

저자 Answer

범위가 지정된 함수는 인스턴스 내에서 코드 블록을 실행하는 함수입니다.
kotlin에는 let, run, with, also, apply의 5가지 범위 기능이 있습니다.

apply 와 also 는 해당 인스턴스를 그대로 반환하며, 인스턴스 호출 후 범위 지정 함수에서 함수를 호출하여 릴레이 형태로도 사용할 수 있다.

let, run, with 은 람다의 결과 (맨 마지막 라인의 값)을 반환한다.

난 범위 지정 함수를 이럴 때 많이 활용했었다.
1. null 이 아닐 경우에만 실행하는 로직을 따로 둘 때,
2. 긴 변수명으로 인해 과한 줄바꿈을 방지할때,
3. 상기 언급한 릴레이 형태로 사용하여 해당 인스턴스를 보강할 때

범위 지정 함수는 만능이 아니다.

예전에는 과용하기도 했었다. 특히 if else 까지도 이걸로 대체해서 사용하기도 했었다.
하지만 사용하면서 단점을 마주하여 과용은 하지 말아야겠다고 생각했다.

1. 가독성, 성능을 모두 잃을수도 있음

let 사용을 지양하기에 대한 글 에서 let 이 디컴파일 되면서 추가 변수가 생기는 경우를 확인한 후, 가독성과 성능 모두를 잃을수도 있겠다고 느꼈다.

fun process(str: String?) {
    str?.let { /*Do something*/   }
}

-> 

public final void process(@Nullable String str) {
   if (str != null) {
      boolean var4 = false;
      /*Do something*/
   }
}

위 예제의 연장선으로 위 범위 지정 함수(let) 활용 이외에 다른 해결책도 있다. (run 사용하기)

2. 오히려 가독성 저해

과용할 경우 (몇 중으로 사용할 경우) it 또는 this 구분이 어려워진다.
도리어 다시 변수를 선언하거나, 들여쓰기만 늘어나는 경우가 생겨 조심해서 사용할 필요가 있다.

결론

그래도 타입 스마트 캐스팅이 안될 때나, 범위 지정이 필요할 때는 (ex. View 설정)
이거 만한게 없다고 생각이 들어 적절한 활용을 생각하고 있다.

Q15. What are sealed classes in kotlin?

열거형과 비슷하지만, 그 보다 더 나아가 여러 인스턴스를 구분할 수 있다.
열거형과 똑같이 when 문에서 else 가 필요없다.
object 도 sealed classes 에 속할 수 있다.

sealed interface Error

sealed class IOError(): Error

class FileReadError(val f: File): IOError()
class DatabaseError(val source: DataSource): IOError()

object RuntimeError : Error

예제에는 없지만 다양한 특징이 있다.

  1. 값이 제한된 집합의 유형 중 하나만을 가진다. (오버로딩 안됨)
    class FileReadError(val f: File): IOError()
    class FileReadError(val f: Int) : IOError()  // error
  2. 생성자는 기본적으로 private 이다.
    val error: Error = IOError()  // error

적극적으로 사용을 안했다보니 다시 깨닫는 부분이 많았다.
만약 활용한다면 Exception 클래스를 sealed classes 로 활용해보지 않을까 막연하게 생각해본다.

Q16. What is significance of annotations : @JvmStatic, @JvmOverloads, and @JvmField in Kotlin?

과거엔 참 많이 사용했었는데...

저자 Answer

@JvmStatic : 정적 메서드화시켜, Java 에서 INSTANCE 를 언급하지 않도록 한다.
@JvmOverloads : Java 코드에서 Kotlin 코드의 인수로 전달된 기본값을 사용한다.
@JvmField : 접근자를 사용하지 않고 Java 코드에서 Kotlin 클래스의 필드에 액세스합니다.

보면 알겠지만 죄다 Java 에서 요긴하게 호출하기 위해 사용된 어노테이션이다.

Q17. What are infix functions?

저자 Answer

대괄호나 괄호를 사용하지 않고 함수를 호출하는 데 사용된다.
infix 함수를 사용하려면 infix 키워드를 사용해야 한다.

처음 이걸 봤을 때 kotlin 으로 문장을 만들 수도 있겠다는 발칙한(?) 생각을 했던 적도 있었다.
우리는 알게 모르게 많이 사용하고 있으며 kotlin 에서 쓸 수 있는 or 가 그 예이다.
앞에 infix 키워드만 선언하면 바로 만들 수 있다.

public infix fun or(other: Boolean): Boolean

...
// 예시로만 사용한 것이므로, 실제 아래 코드는 사용하지 말자..
val abcd = true or false

그 외에 Pair, Map 에서 to 를 사용하는 것도 infix 함수의 예이다.

연산자 오버로딩과 비슷해보이는데?
operator 함수는 사칙연산, contains 등 기호 연산자를 활용할 수 있는 함수를 의미한다.
가운데에 사용할 수 있으므로 operator 함수와 헷갈릴 수 있겠지만 이 둘은 다른 개념이라고 생각한다.

왜냐하면 중위함수가 operator 함수와 동등한 개념이라면 우선순위가 언급되지 않았을 것 같기 때문이다.
(토막지식 : operator 함수infix 함수보다 우선순위가 높다)

Q18. How to create a singleton in Kotlin?

저자 Answer

object

위에서 나온 답이라고 생각이 든다. object!

Q19. What are advantages to when over switch in kotlin?

알긴 아는데 설명하기는 어려울 것 같은 내용이다.

저자 Answer

when 에서는 표현식이나 명령문을 사용할 수 있다는 것이 switch 보다 큰 메리트이다.

switch 와 다르게 정수로 제한되지 않고,
다양한 표현식 (in 4..7) 및 함수를 활용할 수 있다는 자체가 큰 메리트 아닐까 생각한다.

when  {
    "".isEmpty() -> println("empty")
    3 != null -> println(4)
    TextUtils.isDigitsOnly("3455") -> print("3455")
}

충격과 공포이다.

Q20. What are primary and secondary constructors in Kotlin?

저자 Answer

기본 생성자
클래스 헤더에서 초기화되고 생성자 키워드를 사용하여 클래스 이름 뒤에 온다.
매개변수는 기본 생성자에서 선택 사항이다.
기본 생성자는 코드를 포함할 수 없으며 초기화 코드는 init 키워드가 접두사로 붙는 별도의 초기화 블록에 배치할 수 있다.

보조 생성자
Kotlin에는 하나 이상의 보조 생성자가 있을 수 있다.
보조 생성자는 변수 초기화를 허용하고 클래스에 일부 논리를 제공할 수도 있다.
생성자 키워드가 접두사로 붙는다.

처음엔 해석이 안됬었는데 기본 생성자는 init {...} 이고, 보조 생성자는 constructor(...) {} 를 의미한다.

기본 생성자, 보조 생성자 등의 우선순위 비교하는 kotlin 문제가 있었던 것으로 기억한다.

open class Parent {
    private val a = println("Parent.a - #4")

    constructor(arg: Unit = println("Parent primary constructor default argument - #3")) {
        println("Parent primary constructor")
    }

    init {
        println("Parent.init - #5")
    }

    private val b = println("Parent.b - #6")
}

class Child : Parent {
    val a = println("Child.a - #7")

    init {
        println("Child.init 1 - #8")
    }

    constructor(arg: Unit = println("Child primary constructor default argument - #2")) : super() {
        println("Child primary constructor")
    }

    val b = println("Child.b - #9")

    constructor(arg: Int, arg2: Unit = println("Child secondary constructor default argument - #1")) : this() {
        println("Child secondary constructor - #11")
    }

    init {
        println("Child.init 2 - #10")
    }
}

숫자가 호출 순서여서 다행이었다... 정리하면 아래와 같다

main() 에서 Child() 인스턴스 호출 시

  1. #2 - 매개변수가 1개인 constructor 가 먼저 호출됨
  2. #3 - 부모 클래스의 constructor 가 호출됨
  3. #4 #5 #6 - 부모 클래스의 프로퍼티와 기본 생성자가 순차적으로 실행됨
  4. none - 2번 과정의 constructor 내부 로직 실행됨
  5. #7 #8 #9 #10 - 자식 클래스의 프로퍼티와 기본 생성자가 순차적으로 실행됨
  6. none - 1번 과정의 constructor 내부 로직 실행됨

main() 에서 Child(1) 인스턴스 호출 시

  1. #1 - 매개변수가 2개인 constructor 가 먼저 호출됨
  2. #2 - 매개변수가 1개인 constructor 가 먼저 호출됨
  3. #3 - 부모 클래스의 constructor 가 호출됨
  4. #4 #5 #6 - 부모 클래스의 프로퍼티와 기본 생성자가 순차적으로 실행됨
  5. none - 3번 과정의 constructor 내부 로직 실행됨
  6. #7 #8 #9 #10 - 자식 클래스의 프로퍼티와 기본 생성자가 순차적으로 실행됨
  7. #11 - 1번 과정의 constructor 내부 로직 실행됨

아래 3개 내용만 기억하면 된다.
1. init 은 property 와 동일시한다.
2. constructor 가 우선시 되지만 constructor 로직은 늦게 실행된다.
3. 상속 관계에 따라 상위 클래스 초기화가 먼저 처리될 수 있다.

Q21. What are Higher Order Functions?

저자 Answer

함수를 매개변수로 취하거나 함수를 반환하는 함수이다.

근본을 따지면 람다대수와 일급 객체(First-Class Citizens)라는 개념에 그 근간을 두고 있고, 함수형 프로그래밍을 극한으로 활용한 개념이다.
그리고 정의를 넘어 어떻게 활용하고 있는지를 직접 체감할 필요가 있다.

HOF의 특징

  1. HOF는 디컴파일하면 FunctionN 타입을 넘겨주고 받는다.
    그말인 즉슨 Java 에서 HOF 를 사용하려면 FunctionN 타입 객체를 만들어주어야 한다.
  2. interface 를 활용하여 특정 변수를 정의하지 않고 간단하게 로직을 처리할 수 있다.
    클릭 이벤트의 경우 단순히 람다를 만들어 넘겨주고 실행하는 형태로 처리할 수 있다.
    (ex. () -> Unit 등)

함수를 매개변수로 취한다.

  1. 함수를 다루는 함수로 이야기될 수 있으며,
    인자로 넘기는 함수에 따라 사용자의 입맛대로 비즈니스 로직을 제어할 수 있다.

    ex. 단순히 2번 실행하는 함수 -> 입맛에 따라 print 하기, 연산하기를 넣어 비즈니스 로직을 제어한다.

  2. 제어 패턴 추상화(Abstracting Patterns of Control)
    high-order-function 을 깊게 분석한 글을 보면, 기존에 단순 1부터 99까지 출력하던 함수가 어디까지 확장되었는지 확인할 수 있다.
    더불어 로직을 캡슐화하여 최종적으로 추상화 단계까지 나아갈 수 있게 된다.

함수를 반환한다.

함수를 클로저 형태로 계속 사용할 수 있도록 만들 수 있다. (어떻게 보면 당연한 얘기인데 어렵게 들린다.)
Filter, Map, Reduce 가 그 예가 된다.

HOF 를 활용해 함수를 합성할 수 있다.

실제 우리는 also 와 같은 범위 지정 함수에서
체이닝 형태로 계속 연결하여 호출할 수 있는 걸 확인했었다.

정리

막상 15분이면 다 볼 수 있다고 했는데 차근차근 정리하면서 보다보니 몇 시간이 걸렸다;;
이런식으로 QnA 만 정리하면서도 공부가 될 것 같다는 생각이고, 개발은 계속 파고 파고 들어가다보면 결국 끝이 없다는 걸 다시 한 번 느낀다.
까먹었던 내용들도 다시 돌아보고, 잘못 알거나 많이 다루지 않아 미숙했던 내용도 다시 볼 수 있었어서 만족하는 마음을 가져본다. (sealed 클래스와 inline 은 많이 활용해봐야겠다는 생각도 든다)

참고자료

profile
valuable 을 추구하려 노력하는 개발자

0개의 댓글