[Kotlin] Kotlin 확장 함수에 대해 알아보자! + Android에서 사용해보기

Kotlin에는 확장 함수(Extension Function)가 존재합니다. 확장 함수를 사용하면 여러 장단점이 존재하며 Android 개발을 할 때도 종종 사용되곤 합니다! 오늘은 확장 함수에 대해 정리해보도록 하겠습니다.


먼저 확장 함수가 무엇인지에 대해 알아보도록 하겠습니다.

 

확장 함수(Extension Function)이란? 

코틀린은 클래스에 상속하거나 디자인 패턴을 사용하지 않고 새로운 기능으로 클래스를 확장할 수 있는 기능을 제공하는데 이것이 확장(extension)이라는 선언을 통해 이루어지게 됩니다.

이때 추가적인 메소드를 구현하면 이를 확장 함수 라고 하고 추가적인 프로퍼티를 구현하면 확장 프로퍼티라고 합니다.

 마치 기본 클래스에 정의된 함수인 것처럼 새로운 기능을 추가하는 기능!

언제 사용할까?

- 수정할 수 없는 Third-party-libarary의 클래스에 새로운 기능을 확장하고 싶을 때

Retrofit이나 Glide 같은 외부 라이브러리의 Class는 변형할 수 없지만, 함수나 프로퍼티를 추가하여 확장할 수 있습니다.

- 기존 이미 있는 기본 클래스에 함수 추가

 예를 들면 아래와 같이 IntString와 같은 기본 타입(클래스)에도 함수를 추가할 있습니다.

 

 

그럼, 이제 확장 함수 형태에 대해 알아보도록 하겠습니다.

 

확장 함수 선언하기

확장 함수를 선언하려면 확장되는 유형을 참조하는 수신 타입(receiver type, 확장 대상이 되는 클래스)을 이름 앞에 접두사로 붙여야 합니다. 또한 확장 함수를 만들기 위한 용어를 간단하게 정리해보도록 하겠습니다.

- receiver type : 확장 대상이 될 클래스

- receiver object : 확장 함수의 내부 구현 시 this 키워드를 사용하여 receiver type이 가지고 있는 public 인스턴스에 접근하는 객체

fun 확장할 클래스.함수명: 리턴타입 {
   return 리턴값
}

위 정의처럼 확장할 클래스에 수신의 타입을 접두사로 붙이고, 괄호 안에 확장할 기능을 구현하여 작성해주면 됩니다.  

아주 간단한  String 예제부터 봐보도록 하겠습니다.

fun String.hello() = println("Hello $this")

fun main() {
    "dada".hello()
}

String이라는 수신 타입을 지정하고 해당 함수는 수신 타입에 "Hello" 문자를 붙여 출력하도록 정의하였습니다. 해당 코드를 실행하면 "Hello dada"가 실행되는 것을 확인할 수 있겠죠?!

 

Kotlin 공식 홈페이지에 나와있는 예제도 참고해보도록 하겠습니다.

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

해당 코드는 MutableList<Int>를 수신 타입으로 받고, 인덱스 1번과 2번 스왑 하는 함수를 구현한 것입니다. this라는 키워드를 볼 수 있는데요, 해당 키워드는 확장 함수 내부의 this 키워드는 수신 객체(함수명 점 앞에 전달되는 객체)에 해당합니다.

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'swap()' 내부에 'this'는 'list' 값을 가지고 있다.

다음은 앞서 만든 확장 함수를 사용한 예제입니다. list를 수신 객체로 보냈을 때 this 키워드는 list 값을 가지고 있는 걸 확인할 수 있습니다.

 

또한 Generic class에 대해서도 확장 함수를 추가할 수 있습니다. 위 코드에 Int뿐 아니라 다른 객체에서도 활용될 수 있게, 제네릭을 사용하여 MutableList<T>으로 일반화할 수 있습니다. 그럼 이제 Android 프로젝트에서 어떻게 응용되는지 확인해보도록 하겠습니다.

 

Android에서 응용해보기

보통 Util 클래스를 만들어 공통적으로 많이 사용하는 기능들을 정의합니다. 몇 가지 예를 들어보도록 하겠습니다.

 

1. 토스트 메세지 띄우기

fun Context.showToast(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }

이렇게 정의를 해둔다면, 

showToast("이것이 확장함수다!")

아주 간단하게 한줄로 표현할 수 있습니다. 해당 예제는 Activity에서 import 하여 사용한 경우이고, Fragment에서 사용하고 싶다면

requireContext(). showToast("이것이 확장 함수다!")와 같은 형태로 작성하면 됩니다.

 

2. dp를 px로 변환하기

fun Int.dp(): Int { 
    val metrics = resources.displayMetrics
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), metrics).toInt()
}

 

개발하다보면 해상도별 레이아웃을 맞추기 위해 dp를 pixel로 변환하는 경우가 종종 있습니다. 해당 함수를 사용하기 위해서는 

textView.setPadding(0, 10.dp(), 0, 10.dp())

다음과 같이 10.dp()를 호출한다면 변환된 pixel값으로 출력됩니다.

 

3. 리스트 매핑하기

data class Item(val value: Int, var isShowing: Boolean)

val ArrayList<Item>.filterInvisible
    get() = this.filter { it.isShowing }

만약, RecyclerView에서 isShowing이 true인 아이템만 출력하고 싶다면 filter 키워드를 사용하여 items.filter { it.isShowing }. forEach { }와 같은 방식으로 작성하는 방식이 있습니다. 하지만 위 코드처럼 확장 프로퍼티를 사용한다면 items.filterInvisible.forEach { }와 같은 방식으로 작성하여 재사용성을 높일 수 있습니다.

해당 예제를 통해 확장 함수뿐만 아니라 확장 프로퍼티 또한 가능하다는 것을 알 수 있죠?!

 

 

확장 함수 주의할 점

1. 확장 함수 이름이 기본 함수 이름과 같을 때

 -  만약 Class의 기존 함수와 똑같은 이름, 인자를 추가한다면 에러는 발생하지 않지만 기존 함수가 먼저 실행됩니다.

2. 확장 함수는 오버라이드 될 수 없다.

- 확장 함수는 static 함수이며, 클래스 밖에 실행되기 때문에 오버라이드 될 수 없습니다.

- 확장 함수는 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되기 때문에, 그 변수에 저장된 객체의 동적인 타입에 의해 결정되지 않습니다.

 

 

마무리

간단하지만 알아두면 유용한 확장 함수에 대해 알아보았습니다. 확장 함수를  잘 활용한다면 재사용성을 높이고 수정이 필요할 때도 간편하게 사용할 수 있는 장점이 있습니다. 하지만, 단순히 코드를 간결화하게 위한 사용은 주의해야 한다고 합니다. 코드의 양을 줄이는 목적으로 확장을 만드는 것은 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만들 수 있기 때문입니다! 그럼 다들 확장 함수를 잘 활용해봅시다🙌🏻

 

참고

https://kotlinlang.org/docs/extensions.html

https://0391kjy.tistory.com/18