Kotlin 더 잘 활용하기



개인 광고 영역

러닝 커브가 낮은 Kotlin 학습 방법 및 적용 방법은 다양하다. Java와 Kotlin 언제든 혼용해서 사용할 수 있는 매력적인 언어이다.

그렇다고 안드로이드 전용의 개발 언어는 아닌데 JVM 위에서 동작하는 대부분에 적용이 가능하며, 더 나아가 Kotlin native를 통해 LLVM 컴파일을 할 수 있도록 배포 중인데 우선 iOS에서 이를 활용할 수 있는 게 목표로 보인다.

이런 코틀린을 잘 활용하려면 어떡해야 할까? 수많은 라이브러리가 배포 중이지만 어떻게 구현되어있는지 알면 더 좋지 않을까?

그리하여 3가지 구성요소를 정리해보려고 하는데 아래와 같다.


발표 자료

이 포스팅은 GDG Korea AndroidGDG Suwon에서 발표한 내용을 토대로 정리하는 글이라서 아래 링크에서 발표 자료 확인이 가능하다.

GDG Korea Android 발표 자료

GDG Suwon 발표 자료


Java util class

위 발표 자료의 메인은 Kotlin Extensions 이야기이다.

Kotlin Extensions을 소개하기 전에 Java에서 Util 클래스에 대해서 먼저 이야기해보도록 하자.

Java에서 Util을 접근하려면 아래와 같은 ViewUtil class에 notNullText라는 function이 있다고 가정하자.

public class StringUtil {

  // other method

  public static void notNullText(@Nullable String param, @NonNull NonNullParam<String> callback) {
    if (param != null && !param.isEmpty()) {
      callback.nonNullParam(param);
    }
  }

  // other method
}

위와 같이 하나만 있으면 문제는 없지만 1개 만 있을 리 만무하다. 보통 N 개의 Util을 구성하는데 그래야 중복 코드 사용이 편할 수 있다.

그리고 StringUtil이라고 이름 지어두면 내가 사용해야 할 class가 명확하고, 접근하기 조금은 쉬워진다. 하지만 1 개의 Util.java라는 이름에 메서드를 다 넣어둔다면?

이 경우 Util.으로 시작하면 접근하기는 쉽지만 결국 메서드 관리하기 힘들어질 수 있다.

정리하면 Java에서는 내가 써야 할 method가 어떠한 classUtil의 소속인지를 명확하게 알아야 접근하기 쉽다.

하지만 그걸 모두 기억하는 사람은 없다. 협업하다 보면 분명 누군가가 새로 만들고 추가할 수도 있다.


Kotlin Extensions

다행히도 Kotlin에서는 extensions을 제공하는데, 필자의 경우 이를 많이 활용하고 있다.

아마 Kotlin을 처음 접하는 개발자라면 최소한 아래 유용한 함수들로 소개받았을 것이다.

let, run, with, apply, also

코틀린의 유용한 함수들 - let, apply, run, with - by 커니의 안드로이드에서 잘 소개하고 있으니 참고 가능하다.

이들은 이미 잘 활용하고 있는 extensions 들인데 Receiver type을 앞에 적고, 이 Receiver을 사용할 때는 this 키워드를 사용하여 접근할 수 있다.

this는 알다시피 생략이 가능하다.

extension-init

위 그림으로부터 정리를 해보면

  • 접근자(Receiver type)를 메서드 이름의 앞쪽에 둔다.
  • 접근자(Receiver type)의 변수 값은 this를 이용하여 사용한다.

Java였다면 어떤 Class에 Util method가 존재하는지 확인했어야 했다면 Kotlin에서는 그럴 필요 없이 아래와 같이 접근할 수 있다.

use-extensions

  • kotlin extensions은 원래 있던 클래스 정의를 확장할 수 있다.
  • 그렇다고 실제 클래스를 건드리지 않는다.
  • 단, class 내부에 존재하는 동일 함수의 이름을 override 해주지는 않는다.

위와 같은 조건이 있긴 하지만 kotlin extensions은 유용하게 사용할 수 있는데 전역 어딘가에 존재하는 메서드를 접근해서 이를 활용할 수 있다.

그렇다고 아무 곳에나 kotlin extensions을 정의해두는 건 좋지 않은데, A.kt, B.kt, C.kt 이렇게 3개의 파일에 중구난방 코드를 추가해도 동작에는 문제가 없으나, 나중에 유지 보수 차원에서는 아래와 같은 방법을 사용해서 Util을 정리해두는 게 좋다.

  • String과 관련한 extensions : StringUtil.kt
  • View와 관련한 extensions : ViewUtil.kt

2 가지 예만 들었지만 위와 같이 가급적 Util class를 모아두는 편이 추후 유지 보수하는 데 있어 좋지 않을까 생각한다.


Kotlin Extensions 직접 만들어보자

발표 자료에 소개한 내용을 풀어서 글로 정리해본다.

약 5 단계를 통해 Kotlin Extensions을 만들어보는 방법을 정리해보려고 한다.

  1. Java Util 클래스처럼 Util을 생성한다.
  2. Kotlin extensions으로 변환한다.
  3. Higher-Order function을 적용해본다.
  4. Decompile을 통해 Higher-Order function을 살펴본다
  5. inline을 추가해본다

1. Java Util class처럼 Util을 만들어본다.

첫 번째로 String이 Null이 아닌 경우 Callback을 받는 메서드를 하나 생성한다.

interface Callback {
    fun notEmptyString(param: String)
}

fun notEmptyString(param: String?, callback: Callback) {
    if (!param.isNullOrEmpty()) {
        callback.notEmptyString(param!!)
    }
}

위 코드는 Util.notEmptyString(“message”, callback) 형태로 접근하여 사용한다.

2. Kotlin extensions으로 변환한다.

위 코드를 extensions으로 변환하는 과정은 간단한데 아래와 같다.

change-extension

notEmptyString의 앞에 String?.을 붙이면 kotlin extensions 적용은 끝이 난다.

이 경우 아래와 같이 접근할 수 있는데, name.으로 시작하면 notEmptyString이 노출되며 이를 활용할 수 있다.

val name: String
name.

3. Higher-Order function을 적용해본다.

Higher-Order function은 아래와 같다.

  • ()을 통해 함수의 파라메터를 정의한다.
    • ()안에 변수 타입을 지정하면 파라메터를 받을 수 있다.
      • (position: Int, ...)
  • return은 ->으로 표시한다.
  • return type은 -> 이후에 type을 지정한다.
    • -> Type

종합하면 아래와 같은데 해석하면

String param을 받고, return이 Boolean인 함수를 생성한다

lateinit var higherOrder: (param: String) -> Boolean

위의 함수를 사용하려면 아래와 같이 초기화해야 한다.

higherOrder = { param ->
  println(param.length)
  this.isNotEmpty() // return
}

Higher-Order function에도 extensions을 적용할 수 있는데, 아래와 같이 표현할 수 있다.

이때 String.은 Receiver Type이며, 사용은 this로 처리할 수 있다.

property-higher-order-function

Kotlin의 Lambdas 표현식에서 return을 명시하지 않더라도, 마지막 줄을 return으로 처리해준다. 그래서 return true를 정의하지 않더라도, 마지막 라인이 true로 끝나면 Boolean을 리턴하는 람다 표현식을 구현할 수 있다.

최종적으로 아래와 같은 Higher-Order function method 정의를 할 수 있다.

fun String?.notNullMessage(callback: String.() -> Unit) {
    if (!this.isNullOrEmpty()) {
        this!!.callback()
    }
}

4. Decompile을 통해 Higher-Order function을 살펴본다

최종 코드를 Decompile 해본다.

디컴파일의 결과물은 아래와 같다. 잘 보면 1 단계에서 작성한 코드와 유사하게 Callback의 Function1이라는 class를 불러다가 사용하고 있다. 아래 코드에서 확인할 수 있지만 결국 null이 아니면 Listener를 불러다가 사용하고 있다.

public static final void notNullMessage(@Nullable String $receiver, @NotNull Function1 callback) {
   Intrinsics.checkParameterIsNotNull(callback, "callback");
   CharSequence var2 = (CharSequence)$receiver;
   if (var2 != null && var2.length() != 0) {
      if ($receiver == null) {
         Intrinsics.throwNpe();
      }

      callback.invoke($receiver);
   }

}

결국 사용하는 곳에서 아래와 같이 function을 호출한다.

public static final void test() {
   notNullMessage("", (Function1)null.INSTANCE);
}

추가로 Function1의 클래스는 아래와 같이 invoke 메서드를 정의하는 interface이다.

/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

간단하게 if 체크만을 하는데 좀 더 유용하게 접근할 수 있는 방법은 없을까? 저런 단순한 코드를 위해서 function 콜을 해야 하는 걸까?를 고민해볼 수 있다.

단 5 줄의 코드를 하기 위해서 function을 호출하는 건 비용의 증가일 수 있다.

5. inline을 추가해본다

다행히도 kotlin에서 inline을 제공한다. inline 적용 시 함수 호출이 아닌 호출 시점 부분에 함수 내부의 내용을 inline 해서 풀어써준다.

inline fun String?.notNullMessage(callback: String.() -> Unit) {
    if (!this.isNullOrEmpty()) {
        this!!.callback()
    }
}

// 사용하는 부분
fun test() {
    "".notNullMessage {
        // ...
    }
}

위 코드처럼 inline 추가만 해주고 Decompile 해보면 아래의 결과를 빠르게 얻을 수 있다. 함수를 호출하던 부분 대신 notNullMessage의 내부 코드를 아래와 같이 함수 호출한 것 대신 inline 해준다.

public static final void test() {
   String $receiver$iv = "";
   CharSequence var1 = (CharSequence)$receiver$iv;
   if (var1.length() != 0) {
      ;
   }
}

이러한 inline이 C++의 경우에는 무조건 적용하지 않는다고 하는데(덩치가 큰 경우 inline을 하지 않는다고 한다), 코틀린에서는 덩치가 큰 경우에도 inline 처리해준다. 그래서 고민을 해야 할 필요는 있다.

줄이 수십 줄이 넘어가는 경우 무조건 inline을 하는 건 오히려 file 사이즈를 증가시킬 수 있어, 적당한 선(10줄 내 외면 좋을 것 같다.)에서만 inline을 붙이는 게 좋을 수 있다.


결국 DSLs (Domain-specific language)

우리는 Lambdas, Kotlin Extensions, Higher-Order function을 학습하였다. 결국 이러한 걸 종합해서 DSL을 표현할 수 있다.

일반적인 DSL 표현식은 SQL과 HTML 태그 등에서 흔하게 볼 수 있다고 한다. SELECT로 시작하고 중간에 데이터베이스의 이름을 지정하고, WHERE 등의 조건문을 활용한다. 결국 사람은 읽기 쉬운 형태로 SQL 문을 작성하고, 공유할 수 있게 된다.

SELECT * FROM Customers WHERE Country='Mexico';

HTML 역시 이에 해당하고, Android에서는 Gradle과 xml 파일들이 이에 해당한다.

plus

간단하게 String 2개를 합치는 kotlin extension 함수를 추가하고, 이를 사용할 때 String.plus(String)으로 접근할 수 있다.

inline fun String.plus(param: String) = this + param

// 사용할 때
"Name".plus("Message")

이번엔 좀 더 읽기 쉽게 만들어보려고 kotlin에서 제공하는 infix를 추가해본다.

그러면 좀 더 읽기 쉽게 String plus String의 형태로 사용할 수 있다.

inline infix fun String.plus(param: String) = this + param

// 사용할 때
"Name" plus "Message"

코드 양도 적어지고, 좀 더 읽기 편해졌다.

use

이번엔 Higher-Order function을 추가하여 use라는 inline kotlin extensions을 추가해보았다.

inline infix fun <T: Any> T.use(block: T.() -> Unit) {
    block()
}

아래와 같이 Name을 사용하겠다는 표현식을 만들 수 있다.

"Name" use {
    println("size $length")
}

map put

kotlin에서 기본으로 제공하는 infix를 추가한 변수 중에 map의 key/value를 좀 더 살펴보려고 한다. 기본적으로는 put을 활용해서 key/value를 적용할 수 있다.

val map = mutableMapOf<String, Int>()
map.put("A", 0)

put에 매핑 해둔 to를 이용하여 key/value를 추가할 수 있다.

val map = mutableMapOf<String, Int>().run {
    "A" to 0
    "B" to 1
    "C" to 2
}

결국 어느 게 보기 좋은지를 선택할 필요가 있으며, 읽기 어렵다면 DSL의 형태를 적용한다고 해도 불편할 수 있지만, 직접 확장 라이브러리를 개발해서 사용이 가능해진다.

toast

간단하게 toast 메서드를 구현하면 아래와 같다.

inline fun Context.toast(message: () -> String) {
    Toast.makeText(this, message(), Toast.LENGTH_SHORT).show()
}

이를 AppCompatActivity 상속받고 있는 클래스에서 toast를 접근할 수 있게 만들어 주고, 이를 아래와 같이 Lambdas 표현으로 접근할 수 있다.

class SampleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        toast { "Message" }
    }
}


마무리

그래도 잘 만들어진 라이브러리를 활용하는 게 좋으니 읽을 수만 있어도 많은 도움이 될것 같아 정리해보았다.

Android와 Kotlin에서 가장 흔하게 볼 수 있는 DSL의 형태는 kotlin anko 일 것 같다.

anko에 포함되어있는 코드 중 일부를 가져와보았다.

아래에서 볼 수 있듯 inline extensions에 Higher-Order function을 종합해서 만들어두었고, 사용하는 곳에서는 Lambdas 표현으로 이를 구현할 수 있다.

inline fun <T: View> T.lparams(
        width: Int = android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
        height: Int = android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
        init: LinearLayout.LayoutParams.() -> Unit
): T {
    val layoutParams = LinearLayout.LayoutParams(width, height)
    layoutParams.init()
    this@lparams.layoutParams = layoutParams
    return this

필자의 경우에도 이전 글에서도 정리하였지만 AAC-ViewModel을 좀 더 편하게 사용하기 위한 extensions과 Higher-Order function을 활용하여 만든 라이브러리가 있다.

AAC-ViewModel Inject library

class MainActivity : AppCompatActivity() {

    private val viewModel: MyViewModel
            by lazyInject(/* @Option customKey = "custom key" */) {
        // create Your ViewModel
        MyViewModel(..., ..., ...)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        // Maybe init view model
        viewModel ...
    }
}

위와 같이 사용하지만 실제 뒤에 있는 코드는 약 30줄에 해당한다. lazy 패턴을 활용하여 lazyInject을 만들었고, ViewModel.Factory를 별도로 만들지 않아도 사용할 수 있도록 개발한 부분이 포함되어있다.

이 글을 통해 좀 더 유용하게 Kotlin을 활용하고 접근할 수 있었으면 한다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments