본문 바로가기

Kotlin

[Kotlin] Extension Function (확장 함수)

 

안녕하세요.

코틀린이 제공하는 유용한 기능으로 확장 함수가 있습니다.

 

오늘은 확장 함수 선언과 테스트 방법에 대해 알아보도록 하겠습니다.


확장 함수

Kotlin의 확장 함수 (Extension Function) 는 기존 클래스의 코드를 수정하지 않고도,

그 클래스에 새로운 함수를 추가하는 기능입니다.

 

fun String.isPalindrome(): Boolean {
    return this == this.reversed()
}

fun main() {
    val word1 = "madam"
    val word2 = "hello"

    println("${word1.isPalindrome()}")  // true
    println("${word2.isPalindrome()}")  // false
}

 

확장 함수는 원래 클래스에 새로운 메서드를 추가하는 것처럼 보일 뿐,

실제로 클래스 내부에 있는 메서드처럼 동작하지 않습니다.

 

그래서 unit test 하는 방법이 조금 다릅니다.


선언 방식

확장 함수를 선언하는 방식으로는 총 3가지가 있습니다.

이 3가지 방식을 다 살펴보고 각 방법의 장단점을 확인해보겠습니다.

 

1) 클래스 내부 선언

Extension 클래스를 만들어 그 안에 위치시키는 방법입니다.

 

캡슐화가 좋지만,

사용이 번거롭고 재사용성이 낮다는 단점이 있습니다.

data class Person(
    val name: String,
    val age: Int
)

class PersonExtension {
    fun Person.extensionInClass() = "$name, $age"
}
fun main() {
    val person = Person("Kang", 26)
    with (PersonExtension()) {
        println(person.extensionInClass())
    }
}

 

2) Top-Level 선언

확장 함수를 파일의 최상위 레벨에 정의하는 방법입니다.

 

언제 어디서나 간편하게 사용 가능합니다.

하지만 테스트할 때 좀 까다로울 수 있는데, 이는 아래 [테스트 방식] 문단에서 다뤘습니다.

data class Person(
    val name: String,
    val age: Int
)

fun Person.extensionTopLevel() = "$name, $age"
fun main() {
    val person = Person("Kang", 26)
    println(person.extensionTopLevel())
}

 

3) Object 내부 선언

확장 함수를 object 내부에 정의하는 방법입니다.

 

확장 함수를 싱글턴으로 관리할 수 있지만,

해당 객체가 비대해질 수 있다는 단점이 있습니다.

data class Person(
    val name: String,
    val age: Int
)

object PersonExtensionObject {
    fun Person.extensionInObject() = "$name, $age"
}
import com.example.unittest.extension.PersonExtensionObject.extensionInObject

fun main() {
    val person = Person("Kang", 26)
    println(person.extensionInObject())
}

테스트 방식

앞서 설명한대로 확장 함수는 테스트하는 방식이 조금 다르다고 했습니다.

각 방식 별로 어떻게 스터빙을 해 테스트하는지 알아보겠습니다.

 

1) 클래스 내부 선언

아래와 같이 테스트를 작성할 수 있습니다.

Person의 클래스라면 Person 클래스를 모킹해야 합니다.

 

하지만 PersonExtension을 모킹해야 합니다.

@Test
fun `test - extension in class`() {
    with(mockk<PersonExtension>()) {
        val person = Person("Alice", 20)
        every { person.extensionInClass() } returns "Kang, 11"
        
        val result = person.extensionInClass()
        assertEquals("Kang, 11", result)
    }
}

 

java bytecode를 보면 왜 이렇게 테스트 해야 하는지 이해하기 쉽습니다.

(bytecode 보는 법 : tools -> kotlin -> show kotlin bytecode 이 후 decompile 클릭)

 

PersonExtension 클래스 안에 존재하는 확장 함수라서

PersonExtension을 모킹해야 합니다.

public final class PersonExtension {
   @NotNull
   public final String extensionInClass(@NotNull Person $this$extensionInClass) {
      Intrinsics.checkNotNullParameter($this$extensionInClass, "$this$extensionInClass");
      return $this$extensionInClass.getName() + ", " + $this$extensionInClass.getAge();
   }
}

 

2) Top-Level 선언

좀 특이하게 파일 경로에 해당하는 클래스를 직접 모킹해줘야 합니다.

파일 경로가 바뀌면, 테스트 코드도 수작업으로 바꿔줘야 하는 단점이 있습니다.

 

+) 아래 주석에 적은 방법으로도 테스트가 가능합니다.

직접 파일 경로를 적는 것보다 훨씬 간단하게 테스트가 됩니다.

@Test
fun `test - top level extension`() {
    val person = Person("Alice", 20)
    
    mockkStatic("com.example.unittest.extension.PersonKt")
    // mockkStatic(Person::extensionTopLevel)
    every { person.extensionTopLevel() } returns "Kang, 11"
    
    val result = person.extensionTopLevel()
    assertEquals("Kang, 11", result)
}

 

얘도 마찬가지로 java bytecode를 봤습니다.

top level 확장 함수는 [속해있는 클래스명 + Kt] 파일 안에 위치하기에 테스트를 위와 같이 해줘야 합니다.

public final class PersonKt {
   @NotNull
   public static final String extensionTopLevel(@NotNull Person $this$extensionTopLevel) {
      Intrinsics.checkNotNullParameter($this$extensionTopLevel, "$this$extensionTopLevel");
      return $this$extensionTopLevel.getName() + ", " + $this$extensionTopLevel.getAge();
   }
}

 

3) Object 내부 선언

클래스 내부 선언 방식과 마찬가지로,

Person을 모킹하는 것이 아닌 PersonExtensionObject를 모킹해야 합니다.

@Test
fun `test - extension in object`() {
    val person = Person("Alice", 20)
    
    mockkObject(PersonExtensionObject)
    every { person.extensionInObject() } returns "Kang, 11"
    
    val result = person.extensionInObject()
    assertEquals("Kang, 11", result)
}

 

bytecode가 아래와 같이 나오기에 테스트를 위와 같이 해줘야 했습니다.

 

얘도 마찬가지로 Person 안에 있는 함수가 아닌,

PersonExtensionObject 안에 있는 확장 함수라서 모킹을 PersonExtensionObject를 해줘야 합니다.

public final class PersonExtensionObject {
   @NotNull
   public static final PersonExtensionObject INSTANCE;

   @NotNull
   public final String extensionInObject(@NotNull Person $this$extensionInObject) {
      Intrinsics.checkNotNullParameter($this$extensionInObject, "$this$extensionInObject");
      return $this$extensionInObject.getName() + ", " + $this$extensionInObject.getAge();
   }

   private PersonExtensionObject() {
   }

   static {
      PersonExtensionObject var0 = new PersonExtensionObject();
      INSTANCE = var0;
   }
}

결론

Object 내부에 선언하는 방식이

사용과 테스트 모두 큰 단점 없이, 편하게 작성이 가능합니다.

 

그래서 저희 팀에서는 해당 방법을 사용해 확장 함수를 관리하기로 했습니다.

 

각 방법이 장단점이 뚜렷하기에, 

각 상황에 맞춰 확장 함수를 관리하면 좋을 것 같습니다.

 

감사합니다.

'Kotlin' 카테고리의 다른 글

[Kotlin] Scope Function  (2) 2024.10.11
[Kotlin] Sealed Class  (3) 2024.09.22
[Kotlin] Null Safety  (0) 2024.07.29
[Kotlin] Coroutine (3) - 예외 처리  (1) 2023.12.01
[Kotlin] Coroutine (2) - Use in Kotlin  (0) 2023.11.30