Please enable JavaScript to view the comments powered by Disqus.Kotlin extensions 사용하기 코틀린을 확장하여 Util class를 제거해보자.
Search

Kotlin extensions 사용하기 코틀린을 확장하여 Util class를 제거해보자.

태그
Kotlin
extensions
utils
공개여부
작성일자
2022/12/21
최근 BigQuery 를 연동하는 프로젝트에서 Kotlin 을 사용하게 되었다. JPA와 같이 table, entity 간의 차이를 해결해주는 기능이 없는 탓에 BigQuery 의 다양한 API를 직접 호출해서 사용해야 한다. 여기에 맨 처음엔 Util 클래스를 정의하여 사용했지만, 후에 리팩토링을 거치면서 kotlin extension 을 사용하게 되었다.
그래서 이번 기회에 Kotlin extension 에 대한 정리를 하며 한계는 어디인지, 어떻게 사용해야 효율적인지 사용법을 정리해두고자 한다.

Kotlin extensions 의 정의

Kotlin 에서는 class, interface 를 상속하지 않고, decorator pattern 을 사용하지 않고도 새로운 기능을 추가하는 방법을 제공하며, 이를 kotlin extensions 이라 한다.
예를 들어, 수정할 수 없는 third party library, class, interface 에 새로운 함수를 추가할 수 있다. 이러한 함수는 일반적인 방법으로 호출할 수 있고, 원래 클래스에 있는 함수처럼 동작한다. 기존 클래스에 새로운 속성을 정의할 수 있는 확장 속성도 있다.

간단한 사용법

UTC로 저장되어 있는 시간을 UTC+9(KST)로 반환할 때 다음과 같은 함수를 정의할 수 있다.
val KST: ZoneId = ZoneId.of("Asia/Seoul") val UTC: ZoneId = ZoneId.of("UTC") fun LocalDateTime.convertUtcToKstZonedDateTime(): ZonedDateTime { return atZone(UTC).withZoneSameInstant(KST) } fun LocalDateTime.convertUtcToKst(): LocalDateTime { return convertUtcToKstZonedDateTime().toLocalDateTime() }
Kotlin
복사
이 함수를 다음과 같이 호출한다.
@Test fun `한국시간으로 변환한다`() { Article( title = "오늘은 Kotlin extensions 을 알아본다 ", content = "Kotlin extension 으로 가독성을 높히자", writtenAt = LocalDateTime.of(2022, 12, 18, 14, 0, 0), updatedAt = LocalDateTime.of(2022, 12, 19, 14, 0, 0), ).let { it.writtenAt.convertUtcToKst() }.also { assertThat(it).isEqualTo(LocalDateTime.of(2022, 12, 18, 23, 0, 0)) } }
Kotlin
복사
LocalDateTime 에는 convertUtcToKst 라는 함수가 존재하지 않는다.
Kotlin 파일을 하나 만들어서 대상 클래스.extension함수이름 으로 정의하면 그대로 확장 함수가 만들어진다.
그래서 Kotlin extensions 는 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만, 그 class의 밖에서 정의된 함수이다.
수신자. receiver Kotlin extensions에 관련된 글을 읽다보면 “수신자”, “receiver” 라는 단어가 자주 나온다. 위 예제에서 LocalDateTime 은 수신 객체 타입이 되고, extensions 내부가 수신 객체가 된다. 즉 extensions 을 받는 곳이 수신자 이다.

Extensions 는 정적으로 결정된다.

이러한 extensions 는 statically 하게 결정된다. 실제 class를 수정하는 것이 아니며, 실제로 새로운 member를 삽입하는 것이 아니라 . 을 통해서 호출 가능한 새로운 함수를 만드는 것이다.
그래서 runtime 에서 동적으로 변경되지 않는다.
open class Person class Man: Person() fun Person.name(): String = "Person" fun Man.name(): String = "Man" fun printIt(person: Person) { println(person.name()) } fun main() { printIt(Person()) printIt(Man()) }
Kotlin
복사
Person Person Process finished with exit code 0
Plain Text
복사
console 의 실행 결과
이 예제에서 보면 printIt 내부에선 Person type의 name() 함수를 호출한다.
즉, 매개변수의 type 이 Man 이라 하더라도 매개변수 type에 의존하여 실행되는 함수가 결정되는 것이다.
그렇다면 signature 가 member function과 동일한 extensions 이 있다면 어떻게 될까?
open class Person { fun name() { println("class name is person") } } fun Person.name() { println("Extension, name is in extensions") } fun main() { Person().name() }
Kotlin
복사
class name is person Process finished with exit code 0
Plain Text
복사
결과는 member로 정의된 function 이 호출된다. 하지만, 다행인 것은 overloading 은 가능하다.

Nullable receiver

Kotlin extensions 는 nullable receiver type도 정의가 가능하다.
이러한 extensions 는 body 에서 this == null 과 같이 수신자의 값이 null 인지 확인할 수 있다.
fun Any?.toString(): String { if (this == null) return "null" // null check 이후 부터 this 는 non-null type 으로 auto casting이 된다. return toString() }
Kotlin
복사

Extension properties

확장함수의 훌륭한 기능은 properties 를 확장하는 것이다.
val <T> List<T>.lastIndex: Int get() = size - 1
Kotlin
복사
이건 알고리즘 풀 때 사용하면 좋을듯 물론 Kotlin 으로 알고리즘을 풀진 않지만
중요한 점은 Kotlin extensions 은 초기화에 해당하지 않는다. 실제 class 에 member variable 을 추가하는 것이 아니다. 이 동작은 철저하게 getter 와 setter를 통한 명시적인 함수의 실행으로 정의된다.
Extension properties를 사용하려고 리팩토링을 시도했으나 결국 properties getter, setter를 호출하는것 보다 매개변수로 넘기는 것을 택했다.

Companion object extensions

Kotlin class가 만약 companion object 로 정의된 함수, property 가 존재한다면 함수와 property 모두 companion 으로 extensions을 정의할 수 있다.
그렇다면 클래스이름.함수() 와 같이 호출이 가능해진다.
class MyClass { // REQUIRED companion object { } } fun MyClass.Companion.printCompanion() { println("companion") } fun main() { MyClass.printCompanion() }
Kotlin
복사
어떠한 클래스에 Companion extension을 정의할 수 없다면 companion object 가 없기 때문이다.
그런데 여기서 kotlin extensions 의 한계가 있다.
예를 들어 LocalDateTimeof(), now() 와 같은 함수는 static 함수이고, kotlin 에서 companion object 는 java 의 static 과 같은 역할로 간주된다.
하지만 static이 companion object 인 것은 아니다. 따라서 java에서 가져온 libarary 들은 companion object extensions을 만들 수 없게 된다.

사용 사례

Kotlin extensions을 사용하게 된 계기는 BigQuery를 이용한 지표였다.
JPA 수준으로 entity mapping이 되지 않기 때문에 schema에 없는 column의 값을 찾으면 에러를 반환한다.
하지만, 항상 특정 컬럼이 존재하지 않는 경우도 있어 이 문제를 해결하는데 사용했다.
fun TableResult.exportColumns(): Set<String> { return this.schema.fields.map { it.name }.toSet() } fun FieldValueList.getString(key: String, tableResult: TableResult): String? { val columns = tableResult.exportColumns() return if (columns.contains(key)) getString(key) else null } fun FieldValueList.getString(key: String): String? { val value = get(key) return if (value?.value == null) { null } else { value.stringValue } }
Kotlin
복사
이러한 확장 함수를 만들어 특정 컬럼이 스키마에 존재하면 값을, 없으면 null을 반환하도록 만들었다.
덕분에 개발 속도는 빨라질 수 있었다. Kotlin 에선 Util 클래스를 만드는 대신 이러한 확장함수를 사용할 예정이다.