관리 메뉴

나만을 위한 블로그

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

책/Effective Kotlin

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

참깨빵위에참깨빵 2023. 5. 7. 22:36
728x90
반응형

하나의 값을 갖는 객체도 inline으로 만들 수 있다. 코틀린 1.3부터 도입된 기능으로, 기본 생성자 프로퍼티가 하나인 클래스 앞에 inline을 붙이면 해당 객체를 사용하는 위치가 모두 해당 프로퍼티로 교체된다.

 

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

 

이런 인라인 클래스는 타입만 맞다면 값을 곧바로 집어넣는 것도 허용된다.

 

fun main() {
    val name = Name("김철수")
    // 위 코드는 컴파일 시 아래와 같이 바뀐다
    val name: String = "김철수"
}

 

인라인 클래스의 메서드는 모두 정적 메서드로 만들어진다.

 

fun main() {
    val name = Name("김철수")
    name.greet() // 안녕, 내 이름은 김철수야
}

inline class Name(private val value: String) {
    fun greet() = print("안녕, 내 이름은 ${value}야")
}

 

인라인 클래스는 다른 자료형을 래핑해서 새 자료형을 만들 때 많이 사용된다. 이 때 어떤 오버헤드도 발생하지 않는다. 인라인 클래스는 다음 상황에서 많이 쓰인다.

 

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

 

측정 단위를 표현할 때

 

타이머 클래스를 만드는 경우를 가정한다. 이 클래스는 특정 시간 후에 파라미터로 받은 함수를 호출한다.

 

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

 

여기서 time은 정확히 무슨 단위인가? 밀리초, 초, 분 중에서 어떤 단위인지 명확하지 않다. 따라서 심각한 실수로 여러 문제가 발생할 수 있는 지점이다. 이런 문제를 해결할 수 있는 가장 쉬운 방법은 파라미터 이름에 측정 단위를 붙이는 것이다.

하지만 함수를 사용할 때 프로퍼티 이름이 표시되지 않을 수 있으므로 여전히 실수할 수 있다. 아래 코드의 decideAboutTime은 시간을 어떤 단위로 리턴하는지 알려주지 않는다.

 

interface User {
    fun decideAboutTime(): Int
    fun wakeUp()
}

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

fun setUpUserWakeUpUser(user: User, timer: Timer) {
    val time: Int = user.decideAboutTime()
    timer.callAfter(time) {
        user.wakeUp()
    }
}

 

더 좋은 해결법은 타입에 제한을 거는 것이다. 제한을 걸면 제네릭 유형을 잘못 사용하는 문제를 줄일 수 있다.

 

inline class Minutes(private val minutes: Int) {
    fun toMillis(): Millis = Millis(minutes * 60 * 1000)
}

inline class Millis(val milliseconds: Int) {
    // ...
}

interface User {
    fun decideAboutTime(): Minutes
    fun wakeUp()
}

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

fun setUpUserWakeUpUser(user: User, timer: Timer) {
    val time: Minutes = user.decideAboutTime()
    timer.callAfter(time) { // Type mismatch 컴파일 에러
        user.wakeUp()
    }
}

 

이렇게 하면 올바른 타입 사용이 강제된다. 프론트엔드 개발에선 px, mm, dp 등의 여러 단위를 사용하는데 이런 단위를 제한할 때 활용하면 좋다.

 

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

 

SQL DB는 일반적으로 ID를 써서 요소를 식별한다. ID는 일반적으로 단순한 숫자다. 학생 성적 관리 시스템이 있다고 할 때 학생, 교사, 학교 등의 데이터들이 모두 ID를 갖고 있을 것이다.

 

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

 

이런 코드는 모든 ID가 Int 타입이라서 잘못된 값을 넣을 수 있다. 이런 문제가 발생해도 어떤 오류도 발생하지 않아서 문제 찾기가 힘들어진다. 이런 문제를 막으려면 Int 타입의 값을 인라인 클래스를 활용해 래핑한다.

 

@Entity(tableName = "grades")
class Grades(
    @ColumnInfo(name = "studentId")
    val studentId: StudentId,
    @ColumnInfo(name = "teacherId")
    val teacherId: TeacherId,
    @ColumnInfo(name = "schoolId")
    val schoolId: SchoolId
)

inline class StudentId(val studentId: Int)
inline class TeacherId(val teacherId: Int)
inline class SchoolId(val schoolId: Int)

 

이렇게 하면 ID 사용이 안전해지며 컴파일 시 타입이 Int로 대체되므로 코드를 바꿔도 별도의 문제가 발생하지 않는다. 이렇게 인라인 클래스를 쓰면 안전을 위해 새 타입을 도입해도 추가적인 오버헤드가 발생하지 않는다.

반응형
Comments