kyucumber
전체 글 보기

이펙티브 코틀린 아이템 47. 인라인 클래스의 사용을 고려하라

하나의 값을 보유하는 객체도 inline으로 만들 수 있다. (Kotlin 1.3 부터)

기본 생성자 프로퍼티가 하나인 클래스 앞에 inline을 붙이면, 해당 객체를 사용하는 위치가 모두 해당 프로퍼티로 교체된다.

inline class Name(private val value: String) { // .. }

이러한 inline 클래스는 다음과 같이 값을 곧바로 집어 넣는 것도 허용된다.

val name: Name = Name("Marcin")

위 코드는 컴파일 시 아래와 같은 형태로 변경된다.

val name: String = "Marcin"

inline 클래스의 메소드는 모두 정적 메소드로 만들어진다.

inline class Name(private val value: String) { fun greet() { print("Hello, I am $value") } } val name: Name = Name("Marcin") name.greet()

컴파일 시 아래 형태로 바뀐다.

val name: String = "Marcin" Name.'greet-impl'(name)

inline 클래스는 다른 자료형을 래핑해서 새로운 자료형을 만들 때 많이 사용된다. 이 때 어떠한 오버헤드도 발생하지 않는다.

inline 클래스는 아래 상황에 많이 사용된다.

  • 측정 단위를 표현할 때
  • 타입 오용으로 발생하는 문제를 막을 때

측정 단위를 표현할 때

특정 시간 이후 파라미터로 받은 callback을 실행하는 타이머 클래스를 만드는 경우

interface Timer { fun callAfter(time: Int, callback: () -> Unit) }

time은 ms, s, min 중 어떤 단위인지 명확하지 않으며 이로 인해 여러 문제가 발생할 수 있다.

가장 쉬운 방법은 아래처럼 측정 단위를 붙여주는 것이다.

interface Timer { fun callAfter(timeMillis: Int, callback: () -> Unit) }

하지만 함수를 사용할 때 프로퍼티 이름이 표시되지 않을 수 있어 여전히 실수의 여지는 남아있다. 그리고 리턴 값의 경우 이름을 붙일 수 없으며 이는 똑같이 큰 문제가 될 수 있는 부분이다.

더 좋은 해결 방법은 타입 자체에 제한을 거는 것이며 이 때 코드를 효율적으로 만들기 위해서는 다음과 같이 인라인 클래스를 활용할 수 있다.

inline class Minutes(val minutes: Int) { fun toMillis(): Millis = Millis(minutes * 60 * 1000) // .. } inline class Millis(val milliseconds: Int) { // .. } interface Timer { fun callAfter(timeMillis: Millis, callback: () -> Unit) }

이렇게 하면 올바른 타입을 사용하는 것이 강제된다.

이러한 단위를 제한할 때 활용하면 좋다. 또한 객체 생성을 위해 DSL-like 확장 프로퍼티를 만들어 두어도 좋다.

val Int.min get() = Minutes(this) val Int.ms get() = Millis(this) val timeMin: Minutes = 10.min

타입 오용으로 발생하는 문제를 막을 때

SQL 데이터베이스는 일반적으로 ID를 사용해 요소를 식별한다.

@Entity(tableName = "grades") class Grades( @ColumnInfo(name = "studentId") val studentId: Int, // .. )

이런 코드는 ID가 Int 자료형이라 실수로 잘못된 값을 넣을 수 있다.

inline class StudentId(val studentId: Int) @Entity(tableName = "grades") class Grades( @ColumnInfo(name = "studentId") val studentId: StudentId, // .. )

이런 경우 ID 사용이 안전해지며 컴파일 시 타입이 Int로 대체되어 코드를 바꾸어도 별도의 문제가 발생하지 않는다.

인라인 클래스와 인터페이스

인라인 클래스도 다른 클래스와 마찬가지로 인터페이스를 구현할 수 있다.

interface TimeUnit { val millis: Long } inline class Minutes(val minutes: Int): TimeUnit { override val millis: Long get() = minutes * 60 * 1000 // .. } inline class Millis(val milliseconds: Int): TimeUnit { // .. } fun setUpTimer(time: TimeUnit) { val millis = time.millis // .. } setUpTimer(Minuites(123))

하지만 실제 클래스는 inline으로 동작하지 않으며 장점이 하나도 없다. 이는 인터페이스를 통해서 타입을 나타내려면 객체를 래핑해서 사용해야 하기 때문이다.

따라서 인터페이스를 구현하는 인라인 클래스는 아무런 의미가 없다.

typealias

typealias를 사용하면 타입에 새로운 이름을 붙여줄 수 있다.

typealias NewName = Int val n: NewName = 10

이는 길고 반복적으로 사용해야 할 때 유용하다.

typealias ClickListener = (view: View, event: Event) -> Unit class View { fun addClickListener(listener: ClickListener) {} // .. }

하지만 typealias는 안전하지 않다. 아래에서 Seconds와 Millis 모두 단순하게 Int를 나타내며 실수로 혼용해 잘못 입력하더라도 어떠한 오류도 발생하지 않는다.

typealias Second = Int typealias Millis = Int fun getTime(): Millis = 10 fun setUpTimer(time: Seconds) {} fun main() { val seconds: Seconds = 10 val millis: Millis = seconds // 오류가 발생하지 않음 }

이는 오히려 문제를 찾는것은 어렵게 만든다. 따라서 이런 형태로 typealias를 사용하면 안된다.

단위 등을 표현하려면 파라미터 이름 또는 클래스를 사용해야 한다.

인라인 클래스를 사용하면 비용과 안전 측면 모두에서 장점이 있다.

정리

인라인 클래스를 사용하면 성능적인 오버헤드 없이 타입을 래핑할 수 있다.

Reference

  • 이펙티브 코틀린 - 프로그래밍 인사이트, 마르친 모스칼라 지음, 윤인성 옮김

개인적인 기록을 위해 작성된 글이라 잘못된 내용이 있을 수 있습니다.

오류가 있다면 댓글을 남겨주세요.