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 관련 글은 태그로 모아 링크를 추가한다.
Comments