안녕하세요.
코틀린이 제공하는 유용한 기능으로 확장 함수가 있습니다.
오늘은 확장 함수 선언과 테스트 방법에 대해 알아보도록 하겠습니다.
확장 함수
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 |