IT_Programming/Kotlin

[펌] Kotlin 코딩 팁

JJun ™ 2016. 12. 31. 17:55



 출처: https://cchcc.github.io/blog/Kotlin-코딩-팁/



Kotlin 이 1.0이 나오고 실무에 도입해서 사용한지 11개월정도 됬습니다. Java 를 쓸때보다 생산성이 좋아짐을 체감하기도 했고 개인적으로도 코딩의 재미를 다시 한번 느끼고 있습니다. 아직 많이 부족하고  이란걸 적을정도로 충분히 사용한건 아닌것 같지만 Kotlin 을 시작하는 분들에게 조금이나마 도움이 되지않을까 하여 끄적여 봅니다.




Wakatime에서 Kotlin 사용 1위?!(아이고 의미없다) 일 평균 6시간 이상씩은 했네..


시작은 발코딩으로

Kotlin 의 타입 시스템에서는 변수(variable)와 상수(value)를 선언하기 위해 var/val를 사용합니다. 그리고 null 을 수용할지 말지에 따라 타입명 뒤에 ?를 붙여서 구분합니다. Kotlin 에서는 클래스 멤버의 초기값 설정을 객체 생성시점에 하는것이 원칙이지만 정 안될경우에는 lateinit를 var 앞에 붙여서 초기값 설정시점을 뒤로 미룰수 있습니다. 조금 복잡한가요?

클래스 멤버나 지역 변수를 선언할때 뭘로 할지 잘 모르겠으면 일단 val 로 시작합시다. 그렇게 코딩해보다가 정안되겠으면 lateinit var 로 또 안되겠으면 var 에 ? 를 붙여나가는 식으로 수정을 해나가면 요령이 생깁니다.

// 일단 val 로 시작
val hello: String = "hello"
// 객체 생성을 직접하는게 아니라 SDK나 framework에서 하면서 객체생명 주기관련 시점을
// 별도로 제공해주는 경우 그 시작 시점에 초기값을 설정
lateinit var hello: String
// 코딩문맥상 좀더 잘 맞는다거나 메모리를 아껴보겠다거나 등 어쩔수 없다 싶을때
var hello: String = "hello"
hello = "안녕"
// 기존 java 의 라이브러리에서 값을 받아와야 된다던가 할때
var hello: String? = null

이렇게 코딩함으로서의 장점은 val 을 사용하면 함수형 프로그래밍에서 말하는 불변상태의 이점을 그대로 가져올수 있고 코드를 읽는데 있어 뇌내스택에 기억해야할 정보가 줄어들면서 훨씬 빨리 읽힙니다. 가변 상태의 var와 null 을 최대한 배제하면서 클래스를 설계하고 로직을 짜다보면 구조도 깔끔해지고 가독성도 한결 올라갑니다.

표준 함수들의 적절한 활용

Kotlin 으로 코딩 하다보면 매우 자주 사용하게 되는 몇가지 표준 함수들이 있습니다. 어떻게 쓰냐에 따라 가독성을 높여주고 코딩을 편하게 하도록 도와주는데요, 그렇다고 남발하면 오히려 가독성을 해치기도 합니다. 저는 주로 어떤 상황에서 사용했는지 써보겠습니다.

let

// 리턴값을 변수로 안받고 메소드 체이닝을 통해 바로 뭔가를 수행할때
locationFinder.currentLocation(provider).let {
  map.moveTo(it.lat, it.lon)
}
// 이전 문맥에서 not null을 체크해서 뭔가를 수행하거나 elvis 연산자와 연계해서 값을 대입할때
val holder = view.tag?.let { it as Holder } ?: Holder(view)

apply

// 객체 생성직후 멤버값의 세팅이 필요하거나 멤버 메소드를 호출해야 하는경우
val book = Book().apply {
  name = "코틀린은 사랑입니다"
  price = if (isFree) 0 else 10000
  show()
}
// 메소드 체이닝중에 자기 자신을 리턴해야 되는경우
map.apply {
  if (size == 0)
    put("key", listOf(1, 2, 3))
}.filter { it.value.size > 2 }.take(3)

with

// 객체 생성이후 멤버값의 세팅이 필요하거나 멤버 메소드를 호출해야 하는경우
with(book) {
  price = 0
  println(this)
}

run

// 메소드 체이닝중에 자기 자신이 아닌 다른 객체를 리턴해야 되는경우
map.run {
  get("key") ?: listOf(1 ,2, 3)
}.take(3)
// 가독성을 높이기위헤 의미 단위로 영역을 나누어 볼 경우(그냥 미적인 용도)
run { // step1 : ...
  doSomething1()
}
run { // step2 : ...
  doSomething2()
}

위 경우 말고도 다양한 상황에서 다양한 방법으로 활용할수 있습니다. 표준 함수를 활용 해보고자 마음먹고 써볼려면 어디든 다 쓸수 있을거 같습니다. 그러다 보면 억지스러운 코드가 나오기도 합니다.

// 표준 함수로 떡칠되어 차라리 안쓰느니만 못한 상황.. 아래 코드가 쉽게 읽히나요?
with(product) {
  isReadyToDisplay = true
  getStoreOrNull()?.let {
    it.products.sortBy { it.price }.take(10)
  } ?: run {
    val topProducts = allProducts.take(10)
    if (topProducts.size < 10)
      topProducts + recommendedProducts(10 - topProducts.size)
    else
      topProducts
  }
}.run {
  forEach {
    it.display()
  }
  show(this)
}

표준 함수를 써서 코딩을 했다면 코딩후에 한발짝 물러서서 이게 쉽게 읽히는지 한번 살펴 봅시다.

use

c#의 using 과 비슷합니다. JDBC에서 Connection → Statement → ResultSet 으로 이어지는 지저분한 try/catch 문을 그나마 깔끔하게 만들어주는 고마운 녀석입니다. use 의 용도보다는 이를 사용함으로써 얻어지는 장점을 다른 상황에서도 이용해 보고자 합니다.

코딩을 하다보면 open/close 나 begin/commit 과 같은 메카니즘을 종종 마주칩니다. 이런 상황에서 use 와 같은 방식으로 하나 만들어서 가독성을 높여 보는건 어떨까요?

// Android 에서 SharedPreferences 를 이용해 사용자 값을 저장하는 경우
inline fun SharedPreferences.save(block: SharedPreferences.Editor.() -> Unit)
   = apply {
     val editor = edit()
     editor.block()
     editor.apply()
}
preferences.save { putString("key", "value") }

유틸성 함수들

재사용을 위해 함수를 잘게 쪼개다 보면 어디에 소속시키기 애매한 유틸성 함수들이 있습니다. 우리는 이런것들을 CommmonxxUtil 같은 이름의 클래스에 static 으로 몰아 넣고 사용해 왔습니다. Kotlin 에서는 이런것들을 extension 으로 구현하면 찝찝한 클래스도 안생기고 사용하기도 편해집니다. extension 으로 포함시킬 클래스를 못찾겠다면 그냥 일반 함수 로 만드는것도 좋은 방법입니다.

fun String.extractInt(): Int = try { asSequence()
                                    .filter { '0' <= it && it <= '9' }
                                    .joinToString("")
                                    .toInt()
                                 } catch(e: NumberFormatException) { 0 }
"$ 1,000,000".extractInt()
fun jsonOf(vararg pair: Pair<String, Any?>): String {
  // 구현했다고 가정...
}
val json = jsonOf("key" to "value")

빌더 패턴, 오버로딩은 이제 그만

java에서는 객체를 생성할때 초기값을 세팅해줘야 할 멤버가 많다거나 일부는 디폴트값을 이용하고 싶다던가 하면 이를 빌더 패턴으로 적절하게 구현이 가능합니다. Kotlin 에서는 이런 상황에서 함수 디폴트 인자(argument)를 이용해 훨씬더 간결하고 사용하기 쉽게 만들수 있습니다.

// OkHttpClient 가  Kotlin 으로 작성되었다면 빌더 패턴을 안쓰고 이렇게 생성했을듯
val okHttpClient = OkHttpClient(
        sslSocketFactory = mySSLSocketFactory
      , hostnameVerifier = { hostName: String, session: Session -> true }
      , connectTimeout = 30 * 1000
      , connectionPool = ConnectionPool(10, 10, TimeUnit.MINUTES))

함수 파라매터를 조절하기 위한 오버로딩역시 디폴트 인자를 통해 구현이 가능합니다.

fun Context.toast(any: Any?, duration: Int = Toast.LENGTH_SHORT) {
    val s = when(any) {
        is Int -> try { resources.getText(any) } catch (e: Exception) { e.message }
        is CharSequence -> any
        else -> any?.toString() ?: "null"
    }
    Toast.makeText(this, s, duration).show()
}
// 사용은 이런식으로
toast(R.string.hello)
toast("hello", Toast.LENGTH_LONG)

만약 java와 연계해서 사용한다면 @JvmOverloads 을 이용하여 디폴트 인자로 처리된 부분을 오버로딩 함수로 자동으로 생성되게끔 해줄수도 있습니다.

// Android 에서 커스텀 뷰를 만든다고 한다면 아래 3가지 생성자를 구현해야 하지만,
class CustomView : View {
    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}
// @JvmOverloads 를 이용해서 코드를 더 간결하게 만들 수 있다.
class CustomView @JvmOverloads constructor(context: Context
                            , attrs: AttributeSet? = null
                            , defStyleAttr: Int = 0
                            ) : View(context, attrs, defStyleAttr) {
}

by lazy, 단일 표현 함수를 쓸때 타입을 명시하기

프로퍼티에 by lazy 를 사용하거나 간단한 함수를 만들때 단일 표현식으로 만드는데요, 이때 Kotlin 의 타입 추론 을 이용해 타입을 생략할수 있습니다. 이게 간결해 보이더라도 코더의 실수를 유발할수도 있고 가독성을 해치기도 합니다.

private val session by lazy { // 타입추론에 의해 제일 마지막 줄 리턴값의 타입으로 결정
  val properties = Properties("setting.properties")
  properties.remove("key")
  val factory = getSessionFactory()
  factory.addProperties(properties)
  factory.createAuthSession()  // 만약 여기를 어떤 이유로 주석처리하거나 살짝 수정을 한다면?
}
// --> 타입명시
private val session: AuthSession by lazy {
  // ...
}
// 이 함수의 리턴값이 뭔지 궁금해서 잠깐 따라 들어왔는데 한번에 알수가 없음
fun sendMessage(msg: Message) = with(msg) {
    if (senderIsEmpty)
      sender = "..."
    send(msg)
}
// --> 타입명시
fun sendMessage(msg: Message): Boolean = with(msg) {
  // ...
}

별거 아니지만 저상황에서 타입만 명시해 주더라도 가독성을 확 올라가는걸 느낄수 있습니다.