Kotlin typealias와 inline class를 알아보고 적절한 사용법을 알아보자.



개인 광고 영역

Kotlin inline class는 Kotlin 1.3 버전에 추가되었다. 사용법은 매우 간단하다.

하지만 이와 유사한 kotlin Type aliases도 있다.

이번 글에서는 이 2가지 사용법과 좀 더 좋은 사용법을 함께 알아본다.


이 글에서 알아볼 내용

  • kotlin Type aliases의 사용방법을 알아본다.
  • Kotlin inline class의 사용방법을 알아본다.


Type aliases

코틀린에는 Type aliases는 문서에 나온 대로 긴 제네릭 타입을 사용하는 변수들에 대한 새로운 별명을 지어주고, 짧게 사용할 수 있다.

예를 들면 아래와 같이 사용하는 게 가능하다.(공식 문서의 코드를 가져옴.)

typealias NodeSet = Set<Network.Node>

typealias FileTable<K> = MutableMap<K, MutableList<File>>

아니면 Higher-Order function을 사용할 수 있다.

typealias MyHandler = (Int, String, Any) -> Unit

typealias Predicate<T> = (T) -> Boolean

또는 클래스의 정보를 줄일 때도 사용할 수 있다.

class User {
  inner class Name {}
}

class User {
  inner class Info {}
}

typealias UserName = User.Name
typealias UserInfo = User.Info


typealias 정의 방법

typealias는 Top level로만 정의할 수 있다. 아래와 같이 클래스 내, 함수 내에 정의하는 건 불가능하다.

정의할 경우 즉시 오류가 발생한다.

Nested and local type aliases are not supported

typealias UseUse = () -> Unit // 정의 가능

class MainFragment : Fragment() {

    /* typealias UseUse = () -> Unit // 정의 불가  */

    fun test() {
        /* typealias UseUse = () -> Unit // 정의 불가  */
    }
}

top level 변수로만 생성할 수 있기에 누구나 접근 가능한 public으로 선언된다.

참고로 모듈 내에서만 사용하고 싶다면 internal 키워드를 추가하면 되겠다.


typealias 사용 시 주의할 점

주의할게 몇 가지 있다.

typealias는 누가 나 접근할 수 있는 Top level 변수이다.

typealias는 Top level로만 정의할 수 있다. 그렇기에 누구나 접근해 사용할 수 있다. 특정 클래스나 함수 내에서만 사용하는 형태가 아니다.

그렇기 때문에 누구나 접근해 사용해버리기 시작하면 매우 복잡해질 수 있다. 더군다나 별명을 지정해 줘 짧은 코드로 가독성을 올릴 수 있을 수 있기에 더 위험하다.

동일한 Type을 여러 개의 이름으로 사용할 수 있다.

동일한 Type을 여러 개의 별명을 지정하여 사용할 수 있다. 하지만 이게 허용되기 때문에 원하는 목적과 맞지 않을 수 있다.

이 부분 때문에 typealias는 사용할 때 매우 신중하고, 팀원들과 함께 고민해 정의할 필요가 있다. 그리고 명세를 해두는 게 가장 좋다.

아래와 같이 Use라는 변수를 하나 만든다. Higher-Order function을 쓰기 쉽게 Use로 만들었다.

typealias Use = () -> Unit

이제 어디서든 변수 Use는 () -> Unit 형태를 받는 변수가 되었다. 실제 사용은 아래와 같이 할 수 있다.

private lateinit var use: Use

// 사용은
use = {
    // ...
}

여기까지는 문제없다. 변수 정의는 Use는 무조건 람다 표현식을 받도록 만들었기 때문이다. 이제는 변수가 없는 람다 표현식을 어디서든 받을 수 있다.

이번엔 함수를 만들어보자.

아래와 같이 2개의 함수가 있다. 어디까지나 별명으로 지칭한 Use를 안 쓰고 아래 2번째와 같이 () -> Unit을 직접 넘겨받아도 문제가 없다.

fun useData(use: Use) {

}

// or

fun useData(use: () -> Unit) {

}

문제는 여기에 있다.

Use로 정의한 () -> Unit을 useData에서도 받을 수 있고, 2번째 useData에서도 받을 수 있다.

위에서 정의한 lateinit 변수 use를 아래와 같이 넘겨도 위 2개 함수 모두가 동작하는데 문제가 없다.

useData(use)
useData {
    // ...
}


좀 더 알아보자

useData에는 Use라는 별칭을 지정한 use(Higher-order function)을 쓰던, 그냥 () -> Unit을 직접 지정하든 상관이 없다.

typealias는 어떠한 보증도 해주지 않는다.

동일한 primitive을 사용하는 data class를 아래와 같이 생성했다. 시, 분 초 모두 Int로 정의했다.

data class TimeData(
    val hour: Int,
    val minute: Int,
    val seconds: Int
)

이 데이터 클래스를 접근하는 데에는 hour, minute, seconds를 통해 접근한다. 그런데 실수할 수 있는 부분이 있다. 모두 Int이니 값을 매핑하다가 실수로, 아래와 같이 매핑 시켰다.

fun test() {
    val hour = 3
    val minute = 35
    val seconds = 55

    TimeData(minute, seconds, hour)
}

이렇게 간단한 코드에서는 실수할 일은 많지 않겠으나… 실수를 가정해본다. 실수했으니 정상적인 값이 들어가지 않는다.

그래서 typealias를 이용해 수정해보았다. 정의만 하고, 사용하던 곳은 그대로 두었다.

typealias Hour = Int
typealias Minute = Int
typealias Seconds = Int

data class TimeData(
    val hour: Hour,
    val minute: Minute,
    val seconds: Seconds
)

fun test() {
    val hour = 3
    val minute = 35
    val seconds = 55

    TimeData(minute, seconds, hour)
}

위 코드에서는 사실 typealias로 해결을 시도해보았으나, 여전히 문제는 있다. typealias는 위에서 보았듯 별칭만 달아줄 뿐 값에 대한 보증을 해주지 않는다.

이 코드 역시 hour에 minute을 추가하더라도 전혀 문제가 없다. 단순 Int이기 때문이다. 그래서 변수를 지정해 주는 형태를 사용하면 실수를 조금이나마 줄일 수 있다.

TimeData(hour = hour, minute = minute, seconds = seconds)


정리하면

  • 코드 가독성을 해치지 않고, 실수를 줄이기 위해서는 유의미한 이름을 사용해야 한다.
  • 꼭 필요한 게 아니라면 typealias 정의는 없으니만 못하다.
  • typealias는 Top level 변수로 정의하기 때문에 내부에서만 사용하는 건 힘들다. 단, 모듈에서만 사용하려면 internal 키워드를 활용할 수 있다.
  • Higher-Order function에서 사용할 때는 주의해서 사용할 것


inline class

typealias를 먼저 알아본 이유는 단순 별칭의 역할만 함을 알아보았다. 그렇다면 Hour, Minute, Seconds의 값을 직접 보장을 위한 방법은 없을까?

inline class를 이용하면 보장받을 수 있다.

inline class는 kotlin 1.4.10에서도 실험적 기능으로 빠져있다. 추후 크게 변하지 않다면 현 상태로 이어나갈 듯하다.

gradle에 아래 코드를 포함하도록 한다.

compileKotlin {
    kotlinOptions {
        freeCompilerArgs = ["-Xinline-classes"]
    }
}

inline은 감춘다는 것이다. Java로도 이를 할 순 있지만, inline class의 장점은 inline 시키는 데 있다.

inline의 정의는 매우 간단하다.

inline class Name(val value: String)

val name = Name("Name")

이렇게 정의함으로써 어떠한 값이나 들어올 수 있던 String 대신 Name으로 명확화 시킨 것이다.

이 name의 값을 사용할 때는 한번 감싸두었기 때문에 아래처럼 사용해야 하는 단점은 있다.

name.value

inline은 컴파일에서 inline 해줄 태지만 이렇게 사용함으로써 typealias 보다 더 명확한 값을 가질 수 있다.


inline class 정의 규칙

inline class 정의는 매우 간단하다. 이 역시 typealias처럼 Top level로 정의해야 하기에 아래와 같이 class 내부 또는 함수 내부에서는 정의하지 못한다.

inline class Hour(val value: Int)

class MainFragment : Fragment() {

    /* inline class Test(val value: Int) // 정의 불가 */

    fun test() {
        /* inline class Test(val value: Int) // 정의 불가 */
    }
}


typealias 대신 inline class를 활용

typealias로 작성했던 코드를 inline class로 변경해보았다.

참고로 아래 조건은 허용치 않는다.

  • data class는 inline을 허용치 않는다.
  • inline class 정의 시 primitive만 허용한다. 제네릭이나 Map 형태를 적용할 수 없다.
inline class Hour(val value: Int)
inline class Minute(val value: Int)
inline class Seconds(val value: Int)

TimeData는 이전과 동일해 수정할 필요 없다.

data class TimeData(
    val hour: Hour,
    val minute: Minute,
    val seconds: Seconds
)

test에 적용되었던 코드에서 오류가 발생한다. 타입이 다르다는 오류가 난다.

fun test() {
    val hour = 3
    val minute = 35
    val seconds = 55

    TimeData(hour, minute, seconds)
}

typealias에서는 볼 수 없었던 오류이다. inline class에서는 아래와 같이 즉시 Type mismatch가 발생한다.

Type mismatch.
Required: Hour
Found: Int

아래처럼 수정을 해줘야 한다.

fun test() {
    val hour = 3
    val minute = 35
    val seconds = 55

    TimeData(Hour(hour), Minute(minute), Seconds(seconds))
}

// or

fun test() {
    val hour = Hour(3)
    val minute = Minute(35)
    val seconds = Seconds(55)

    TimeData(hour, minute, seconds)
}

만약 아래처럼 순서가 틀려도, 이는 즉시 오류가 발생한다.

TimeData(minute, seconds, hour)

typealias 보다 명확하게 값을 사용할 수 있고, 실수를 최소한으로 줄일 수 있다.


inline class에는 몇 가지 멤버를 가질 수 있다.

inline class는 몇 가지 멤버를 가질 수 있지만 못 가지는 것도 있다.

  • 추가적인 변수 선언은 불가능하다.
  • init block은 적용할 수 없다.

이를 제외하곤 아래와 같이 간단한 멤버를 포함할 수 있다.

inline class Hour(val value: Int) {

    val data: Int
        get() = value

    fun print() = println("value $value")
}


마무리

이 두 기능 모두 Top level로 정의해야 한다. 그렇기에 이름에 대한 명확화는 둘 다에 해당한다.

필요에 따른 값의 선택 조건을 정리하면 아래와 같을 수 있다.

  • 이름을 줄이고 명확화하는 게 필요하다 - typealias
  • 중복적인 데이터가 매우 많고, typealias를 사용하더라도 실수의 여지가 있다. - inline class

이와 같이 조건을 정리해 볼 수 있다. 두 값 모두 이름을 명확하게 사용하는 건 필수로 보인다. 명확하지 않을 때 생길 수 있는 문제는 다양하게 발생 가능하다.

여하튼 모든 건 잘 설계하는 것이 좋으니 잘 고민하여 적절한 것을 사용하길 바란다.


이 글만 보기 아쉽죠. Effective Kotlin 더 보기

지금까지 작성한 EffectiveKotlin 관련 글은 태그로 모아 링크를 추가한다.

더 보기 - Effective Kotlin 관련 글



About Taehwan

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

Comments