본문 바로가기

코틀린

코틀린(Kotlin) - 확장 함수 만들기

자바 컬렉션에는 디폴트 toString()이 구현되어 있습니다.

하지만, 그 디폴트 toString()의 출력 형식은 고정되어 있기 때문에, 우리에게 필요한 형식이 아닐 수도 있습니다.

 

val list = listOf(1, 2, 3)

>>> println(list) //디폴트 toString() 호출
[1, 2, 3]

 

위의 디폴트 구현과 달리, 우리 입맛대로 toString()을 커스텀 하고 싶다면 어떻게 해야 할까요?

 

코틀린에는 이런 요구 사항을 처리할 수 있는 함수가 표준 라이브러리에 이미 탑재 되어 있습니다!

확장함수(Extension Function) 라고 하며, 마치 기본 클래스에 정의된 함수인 것처럼 새로운 기능을 추가하는 기능입니다.

 

우선 먼저, 알고가야할 개념으로는 "코틀린에서는 함수를 클래스 안에 선언하지 않아도 된다." 는 것입니다.

 

우리가 흔히, 자바에서는 모든 코드를 클래스안에 작성하지만, 개발을 하다보면 어느 한 클래스에 포함시키기 애매한 코드들도 분명히 생기기 마련입니다.

중복되는 역할을 하는 메소드가 있다면 Utils 클래스에 따로 분리하여 정적인 메소드들로만 쫙~ 모아두기도 하죠.

 

코틀린에서는, 이런 클래스를 굳이 만들 필요가 없습니다.

대신 함수를 최상위 수준, 다른 클래스의 밖에 위치시키기만 하면 됩니다.

그런 함수들은 그 파일의 맨 앞에 정의된 패키지의 멤버 함수이므로, 다른 패키지에서 사용시에는 그 함수가 정의된 패키지를 임포트(import) 해서 사용합니다.

확장 함수

자, 그럼 이제 마치 기존 클래스의 멤버 함수인것처럼 사용할수 있는 확장 함수(Extension Function) 를 만들어 보도록 하겠습니다.

기본 구조는, 아래와 같이 원하는 함수명 앞에 확장할 클래스명을 붙여주기만 하면 됩니다!

 

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

 

저는 예제로 Int 클래스를 확장하여, 자동차 주행거리(int타입)값을 천 단위 콤마와 "km"를 붙여 String 타입의 주행거리 포맷으로 변환하는 메소드를 확장 해보도록 하겠습니다.

 

원하는 함수명(convertToMileage) 앞에, 확장할 클래스(Int)를 붙여주었고, 함수내부에서는 this로 수신 객체 멤버에 접근하였습니다.

 

수신 객체 타입확장이 정의될 클래스의 타입이며, 수신 객체는 그 클래스에 속한 인스턴스 객체 입니다. 

 

특정 클래스에 확장 함수를 추가하려면, 위의 예제처럼 

확장할 클래스(수신 객체 타입) 뒤에 .(Dot) 을 찍고, 원하는 메소드명을 정의한 뒤, 함수 내부에서는 this 키워드로 수신 객체 멤버를 사용하여 구현하게 됩니다.

 

그렇다면, 위에서 생성한 확장함수가 잘 동작하는지 테스트 해보도록 하겠습니다.

참고로, 저는 확장함수들만 따로 관리하기 위해 Extensions.kt 파일을 만들었고, 어디서든 사용할수있도록 클래스밖에 즉, class 파일이 아닌 코틀린(kt) 파일안에 확장함수를 구현해두었습니다.

 

>>> val mileage = 52000 //int형 변수
>>> println(mileage.convertToMileage()) //확장함수 호출
52,000km

 

자동차 주행거리값에 해당하는 int형 변수에 마치 Int 클래스의 메소드인것처럼 우리가 위에서 만들었던 확장 함수를 호출할수가 있게 되었습니다!

클래스에 멤버를 새롭게 추가하거나, 기존 함수를 수정하지 않고도 우리의 요구사항대로 함수를 쉽게 추가 할 수 있게 되었네요!

이것이 바로, 확장 함수(Extension Function) 입니다!

 

단, 확장함수는 수신객체의 private이나 protected 멤버를 사용할수 없기 때문에, 해당 수신객체의 public 멤버에만 접근이 가능합니다.

(확장함수는 외부에서 해당 object에 접근하는 형태이므로, public만 가능한 이유!)

확장 함수 Import

확장 함수를 정의했다고 해도 자동으로 프로젝트 안의 모든 소스코드에서 그 함수를 사용할 수 있지는 않습니다.

기존과 마찬가지로, 임포트 해서 사용해야만 하는데요, 이런 과정에서 같은 이름의 확장 함수가 둘 이상 있을 경우에는 충돌이 생길수가 있습니다.

이런 경우를 방지하기 위해, 코틀린에서는 개별 함수별로 임포트를 할 수가 있습니다.

한 파일 안에서 다른 여러 패키지에 속해있는 같은 이름의 함수를 사용해야 하는 경우, 이름을 바꿔서 임포트하는 것으로 충돌을 막을 수 있습니다.

 

예로, common 패키지와 utils 패키지 안에 동일한 이름으로 convertToMileage 함수를 각각 추가한 경우, 아래와 같이 동일한 이름의 두가지 확장함수를 사용할수 있게 되는데, 이러한 경우 충돌이 발생할 수가 있습니다.

 

 

그렇기 때문에 아래와 같이 as 키워드를 사용하여, 임포트 이름을 바꿔주어 충돌을 해결 할 수 가 있습니다.

 

import ~.common.convertToMileage
import ~.utils.convertToMileage as toMileage

val mileage = 52000
mileage.convertToMileage() //common 패키지의 확장함수
mileage.toMileage() //utils 패키지의 확장함수

자바에서 확장 함수 호출

내부적으로 확장 함수는 수신 객체를 첫번째 파라미터로 받는 정적 메소드이기 때문에, 자바에서 확장 함수를 사용하기도 굉장히 편합니다.

단순히, 정적메소드를 호출하면서 첫번째 파라미터로 수신 객체를 넘기기만 하면 됩니다.

또한, 확장 함수가 들어있는 파일 이름에 따라 자바 클래스 이름이 결정되는데,

예로, Extensions.kt 파일에 확장함수를 정의했다고 하면 다음과 같이 자바에서 호출 할 수 있습니다.

 

/* 자바에서 확장함수 호출 */
String mileage = ExtensionsKt.convertToMileage(52000);

확장 함수는 오버라이드를 할 수 없다

확장함수는 오버라이딩을 할 수 가 없는데, 이유는 확장함수는 static 함수이면서, 클래스 밖에 선언되기 때문입니다.

 

//print()라는 확장 함수 추가
fun View.print() = println("View")
fun Button.print() = println("Button")

>>> val view: View = Button()
>>> view.print()
View

 

테스트를 위해 위와 같이, print() 라는 확장함수를 View와 Button에 추가하였고

출력의 결과를 보면, "View" 가 출력되는 것을 알 수가 있습니다.

보기에는 마치 View에 Button 객체가 할당되어 오버라이딩 되는 것 같지만, 확장함수는 View에 정의된 함수의 호출이 이루어 지고 있는 것이죠.

 

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

즉, view가 가리키는 객체의 실제 타입은 Button 이지만, view의 타입이 View이기 때문에 무조건 View의 확장 함수가 호출 되는 것입니다.

 

응용

간단히 확장함수를 응용하기 위해, 저는 AppCompatActivity에서 토스트 기능을 확장함수로 구현해보았습니다.

 

//토스트 확장함수
fun AppCompatActivity.showToast(text: String) {
    Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
}

//실제 사용
class KotlinActivity: AppCompatActivity() {
    ...
    fun onClick(view: View) {
        showToast("토스트 확장함수 응용하기")
    }
    ...
}

 

이와 같이 자주 사용되는 기능들에 대해, 확장함수로 구현하여 사용해보시는 것도 추천드립니다 ^^