본문 바로가기
오늘은 뭘 배울까?/Android

Kotlin 확장 함수(Extension Function)를 아시나요?

by Kim Juhwan 2022. 7. 16.

1. 요약
2. Extension
3. Extension Function
   3-1. 개념
   3-2. 예제
4. Extension Property
   4-1. 개념
   4-2. 예제
5. 퀴즈
   5-1. 확장 함수는 오버라이딩이 가능할까?
   5-2. 확장 함수는 오버로드가 가능할까?
   5-3. 멤버 메서드 이름과 확장 함수 이름이 같으면? 확장 함수가 실행될까?
   5-4. 확장 함수는 프로젝트 안의 모든 곳에서 사용할 수 있을까?
   5-5. 확장 함수는 정적 바인딩 된다?

 

 

 


 

 

1. 요약

🧑‍💻: Kotlin Extension을 아시나요?

 

👨🏻‍🦱: 네. 상속이나 디자인 패턴 없이 클래스를 간단하게 확장할 수 있는 방법입니다.
실제로 클래스 내부에 메서드나 프로퍼티가 생성되는 것은 아니며,
정적 바인딩 된다는 특징이 있습니다.

 

2. Extension

내가 만든 클래스에 새로운 기능을 추가하고 싶을 때 어떻게 해야 할까??

클래스 안에 새로운 메서드를 구현하거나 아니면 상속을 받아서 사용하는 등 여러 방법이 있을 것이다.

 

그렇다면 남이 만든 클래스에 새로운 기능을 추가하고 싶으면 어떻게 해야 할까??

예를 들어 외부 라이브러리의 경우 메서드를 추가하는 것은 매우 까다롭고 어렵다.

또, Java와 달리 Kotlin은 클래스가 기본적으로 final이라서 open 키워드를 달아주지 않는 이상 상속을 받을 수 없다. 

 

그래서 Kotlin에서는 Extension. 즉, 확장을 지원한다.

지금부터 확장에 대해서 알아보자

 

3. Extension Function

3-1. 개념

  • 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에서 선언된 함수
  • 따로 상속받지 않고 하나의 클래스에 추가적인 메서드를 구현하고 싶을 때 사용하는 함수
  • 새로운 클래스를 만드는 번거로움을 줄일 수 있음
  • 확장 함수는 각 클래스에 정적 메서드로 생성되기 때문에 virtual method invocation이 발생하지 않음

 

정리를 하자면 위와 같은데 예제를 보는 게 이해가 더 빠르다.

아래 예제와 함께 알아보자.

 

홍진호씨를 클래스로 만들어보자

 

class Kong {
    val name = "홍진호"

    fun playGame() {
        println("폭풍 저그! 홍진호가 간다!")
    }
}

홍진호씨를 클래스로 만들어 보았다.

여기에 만약 '말하다'라는 기능을 추가하고 싶다면

 

class Kong {
    val name = "홍진호"

    fun playGame() {
        println("폭풍 저그! 홍진호가 간다!")
    }
    
    fun speak(sentence: String) {
    	println(sentence)
    	println(sentence)
    }
}

이렇게 Kong 클래스 안에 새로 메서드를 만들거나 상속을 받거나 등 방법들이 있을 것이다.

지금은 Kong 클래스를 우리가 만들어서 마음대로 메서드를 추가할 수 있지만

만약 라이브러리에 들어있는 클래스라면...?

메서드를 추가하기도 어렵고 final 클래스라 상속도 못 받는다.

 

그러니 확장 함수를 사용해보자.

 

// 일반 함수
fun speak(sentence: String) {
    println(sentence)
    println(sentence)
}
 
// 확장 함수
fun Kong.speak(sentence: String) {
    println(sentence)
    println(sentence)
}

확장 함수를 만드는 방법은 아주 간단하다.

함수 앞에 클래스 이름과 .(온점)을 찍어주면 된다.

 

class Kong {
    val name = "홍진호"

    fun playGame() {
        println("폭풍 저그! 홍진호가 간다!")
    }
}

fun Kong.speak(sentence: String) {
    println(sentence)
    println(sentence)
}

fun main() {
    val kong = Kong()
    kong.speak("어 왜 말이 두 번씩 쳐지지")
}

바로 이렇게 말이다!

확장 함수가 클래스 안에 들어가지 않는다는 점을 유의하자

확장 함수는 마치 클래스 안에 메서드를 구현해서 사용하는 것처럼 만들어 주는 효과가 있는 거지

실제로 클래스 안에 메서드가 만들어지진 않는다.

 

fun String.speak() {
    println(this)
    println(this)
}

fun main() {
    "어 왜 말이 두 번씩 쳐지지".speak()
}

이번엔 한 번 진짜로 이미 존재하는 클래스를 확장해보자.

String 클래스에는 speak라는 함수가 없는데

우리가 String 클래스를 '확장'해서 함수를 만들어내면 위와 같이 사용이 가능하다. 와우!

 

3-2. 예제

// list에서 num보다 큰 값들을 새로운 list로 리턴해주는 함수
fun List<Int>.getHigherThan(num: Int): List<Int> {
    val result = arrayListOf<Int>()
    for (item in this) { // 여기서 this는 리스트를 의미한다.
        if (item > num) {
            result.add(item)
        }
    }
 
    return result
}
 
fun main() {
    val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
    val filtered = numbers.getHigherThan(3).toString()
    println(filtered)
}

이해를 돕기 위해 너무 쉬운 예제를 들었으니

이번엔 좀 더 다양한 예제를 알아보자.

위 예제는 List를 확장해 getHigherThan이라는 확장 함수를 만든 예제이다.

이런 식으로도 활용 가능하고...

 

fun <E> List<E>.getHigherThan(num: E): List<E> {
    // 구현....
    return arrayListOf<E>()
}

제네릭을 이용하여 확장 함수를 정의하고 싶다면 위와 같은 구조로 작성하면 된다.

대신 구현하는 부분에서는 모든 타입을 고려하여 처리해줘야 한다.

 

4. Extension Property

4-1. 개념

[목차 3]에서는 함수를 확장하는 방법에 대해서 알아보았다.

클래스의 구성 요소에는 함수만 있는 것이 아니다.

프로퍼티. 즉, 멤버 변수도 존재하는데 이 값도 확장이 가능하다.

그리고 이를 확장 프로퍼티라고 부른다.

 

4-2. 예제

// 참고로 좌항에 있는 get은 getter/setter할 때 get이고
// 우항에 있는 get은 String의 해당 인덱스의 값을 가져오는 get이다.

val String.lastChar: Char
    get() = get(length - 1) // 초기화

fun main() {
    println("Gold".lastChar) // d 출력
}

String 클래스를 확장시켜 문자열의 마지막 문자를 저장하는 프로퍼티를 만들었다.

lastChar에는 'd'가 들어가게 된다.

 

이전 목차에서 설명했지만 확장을 하게 되면

실제로 그 클래스에 메서드가 추가된다거나 프로퍼티가 추가되는 것이 아니기 때문에

객체가 값을 저장하고 있을 방법이 없다.

즉, Gold의 d를 저장한 lastChar이라는 프로터피는 저 순간에만 쓰이고 사라진다는 것이다.

(일회성 같은 느낌...?)

 

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }
    
fun main() {
    val sb = StringBuilder("Gold")
    sb.lastChar = 'f'
    println(sb) // golf
}

프로퍼티에 setter도 설정할 수 있다.

코드가 어렵진 않아서 딱히 설명할 건 없고...

확장 프로퍼티를 사용할 땐 getter를 필수로 정의해야 한다는 점을 알아두면 좋을 듯하다.

 

5. 퀴즈

자라나라 머리머리

당신은 탈모빔에 맞았습니다.

다음 주어지는 문제를 3문제 이상 맞추지 못하면

짤처럼 머리가 빠집니다.

 

건승을 빕니다... 👨🏻‍🦲✨

 

 

5-1. 확장 함수는 오버라이딩이 가능할까?

open class Idol {
    open fun sing() {
        println("노래 노래~")
    }
}

fun Idol.dance() {
    println("댄스 댄스~")
}

class BTS: Idol() {
    // 오버라이딩 가능
    override fun sing() {
        
    }
    
    // 오버라이딩 불가능
    override fun dance() {
        
    }
}

이전 목차에서 계속 언급했지만 확장 함수는 클래스의 일부가 아니다.

그렇기 때문에 확장 함수는 오버라이딩 할 수 없다!

정답은 X !

 

5-2. 확장 함수는 오버로드가 가능할까?

class Me {
    fun getHeight(height: Int) {
        println("제 키는 ${height}cm 입니다.")
    }
}

fun Me.getHeigth(height: Float) {
    println("제 키는 ${height}cm 입니다.")
}

fun main() {
    val me = Me()
    me.getHeight(180) // 제 키는 180cm 입니다.
    me.getHeigth(180.3F) // 제 키는 180.3cm 입니다.
}

클래스가 가지고 있는 메서드와 확장 함수의 이름이 같더라도

매개 변수 타입이 다르면 오버로드가 된다.

즉, 확장 함수는 오버로드가 가능하다.

정답은 O !

 

5-3. 멤버 메서드 이름과 확장 함수 이름이 같으면? 확장 함수가 실행될까?

class Me {
    fun getHeight(height: Int) {
        println("제 키는 ${height}cm 입니다.") // 이게 실행됨 (우선시 됨)
    }
}

fun Me.getHeigth(height: Int) {
    println("${height}cm 이 되고 싶어요...")
}

fun main() {
    val me = Me()
    me.getHeight(180) // 제 키는 180cm 입니다.
}

함수명과 메서드명까지 같아버린다면

에러는 나지 않지만 클래스가 가지고 있는 메서드가 우선시 된다.

정답은 X !

 

5-4. 확장 함수는 프로젝트 안의 모든 곳에서 사용할 수 있을까?

import 확장함수가 위치한 경로
// ex) import com.example.presentation.views.getHeigth


fun main() {
    // 확장함수 사용
}

확장 함수를 정의했어도 프로젝트의 모든 곳에서 사용할 수 있는 것은 아니다.

import를 하면 사용 가능하다. (확장 프로퍼티도 마찬가지!)

정답은 X !

 

5-5. 확장 함수는 정적 바인딩 된다?

open class Idol {
    open fun sing() = println("아이돌이 노래를 불러요")
}

class BTS: Idol() {
    override fun sing() = println("BTS가 노래를 불러요")
}

Idol을 상속받는 BTS 클래스를 만들어봤다.

sing 메서드를 오버라이딩 하고 있다.

 

open class Idol {
    open fun sing() = println("아이돌이 노래를 불러요")
}

class BTS: Idol() {
    override fun sing() = println("BTS가 노래를 불러요")
}

fun Idol.dance() = println("아이돌이 춤을 춰요")
fun BTS.dance() = println("BTS가 춤을 춰요")

그리고 Idol의 확장 함수 dance와

BTS의 확장함수 dance를 만들었다.

 

open class Idol {
    open fun sing() = println("아이돌이 노래를 불러요")
}

class BTS: Idol() {
    override fun sing() = println("BTS가 노래를 불러요")
}

fun Idol.dance() = println("아이돌이 춤을 춰요")
fun BTS.dance() = println("BTS가 춤을 춰요")
    
fun main() {
    // 부모 타입으로 자식 객체 생성
    val bts: Idol = BTS()
    bts.sing() // --------- 1번
    bts.dance() // -------- 2번
}

부모 타입으로 자식 객체를 생성한 다음

sing(일반 함수)과 dance(확장 함수)를 호출했을 때 각각 어떤 결과가 나올까?

1번과 2번 실행문의 결과를 예측해보자.

 

open class Idol {
    open fun sing() = println("아이돌이 노래를 불러요")
}

class BTS: Idol() {
    override fun sing() = println("BTS가 노래를 불러요")
}

fun Idol.dance() = println("아이돌이 춤을 춰요")
fun BTS.dance() = println("BTS가 춤을 춰요")
    
fun main() {
    // 부모 타입으로 자식 객체 생성
    val bts: Idol = BTS()
    bts.sing() // BTS가 노래를 불러요
    bts.dance() // 아이돌이 춤을 춰요
}

결과는 위와 같이 나온다.

그 이유는 확장 함수가 정적 바인딩되기 때문이다.

 

일반 함수를 호출할 때는 객체를 따라가지만

(위 예제에서는 BTS 객체니까 BTS의 sing을 호출함)

 

확장 함수는 타입을 따라간다.

(위 예제에서는 Idol 타입이므로 Idol의 dance를 호출함)

 

일반 함수는 동적 바인딩 되어서 -> 런타임 시간에 함수의 메모리 주소가 결정되는 거고

확장 함수정적 바인딩 되어서 -> 컴파일 시간에 Idol의 메모리 주소로 결정되어 버린다.

 

동적 바인딩과 정적 바인딩의 더 자세한 설명은

포스팅 주제를 벗어나므로 생략!

 

아무튼 확장 함수는 정적 바인딩이 된다.

정답은 O !

 

 

 

이 게시글에 좋아요를 누르면 탈모빔 효과가 해제됩니다. 😘

 

 


💡 느낀 점

  • 면접 때 kotlin extension이 뭔지도 몰라서 입 뻥긋 못했었는데... 이젠 자신 있게 대답할 수 있을 것 같다.
  • Kotlin은 참 편하고 좋은 방법을 많이 제공하는구나 싶다.
  • 더불어 내가 아직도 코틀린을 제대로 활용하고 있지 못하는구나 싶다.

📘참고한 자료


 

 

 

반응형

댓글