
당신이 코틀린을 사용하고 있다면, '확장 함수'를 사용해봤거나 들어봤을 것이다.
(아니어도 상관없다. 지금부터 알아보자)
코틀린은 확장 함수라는 기능을 제공한다.
우선 확장 함수가 어떻게 생긴 녀석인지 예시를 한 번 보자!
예시
String 클래스는 문자열의 처음, 끝을 가져올 수 있는 first(), last() 함수를 제공한다.
그런데 정말 만약에 문자열의 두 번째 글자가 필요한 일이 생겼다고 가정해보자.
이때 String에 대한 확장함수로 second() 함수를 만들어 볼 수 있을 것이다.
(물론 [1]로 가져올 수 있겠지만, 좀 더 예쁘게 확장함수를 만들어보자!)
fun String.second(): Char {
if (this.isEmpty()) {
throw NoSuchElementException("String is empty.")
}
return this[1]
}
문자열의 두 번째 글자를 반환하는 second 함수를 만들어보았다!
(문자열이 비어있다면 두 번째 글자가 없으므로 예외를 발생시켰다.)
이제 우리는 아래처럼 깔끔하게 두 번째 글자를 가져올 수 있다.
val names = listOf<String>("bixx", "sangun", "metPig")
names.forEach { println(it.second()) }

정말 마법같지 않은가?
난 확장 함수를 처음 만났을 때 정말 신기했고 이해도 잘 안됐다.
우선 함수 선언을 타입.함수명 형태로 하는 것부터 생소했다.
코틀린 갓난아기였던 시절, 어떤 형태로 선언하는지 헷갈려 여러번 찾아봤었다.
그럼 이 글을 읽는 여러분은 나처럼 헷갈리지 않도록
확장 함수에 대해 확실하게 알아보자.
확장 함수
개념적으로 확장함수는 단순하다.
어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수다.
위 예시를 생각해보자.
나는 String 클래스를 직접 작성하지도, 클래스의 소스 코드를 소유하지도 않았지만
String 클래스에 원하는 메소드를 추가할 수 있게 되었다!

확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다.
클래스의 이름을 수신 객체 타입(receiver type)이라 부르며,
확장함수가 호출되는 대상이 되는 값(객체)을 수신 객체(receiver object)라고 부른다.
위 second() 예시에서는 String이 수신 객체 타입, "bixx", "sangun", "metPig"가 각각 수신 객체이다.
확장 함수 내에서 수신 객체는 this로 나타내며 this는 대부분의 경우 생략이 가능하다.
(위 예시에서 this.isEmpty()의 this는 생략이 가능하고, this[1]의 this는 생략이 불가능하다.)
확장 함수는 함수 내부에서 수신 객체의 메서드와 프로퍼티를 사용할 수 있다.
위에서 든 예시 또한 수신 객체의 메서드 isEmpty()를 사용했다!
하지만 여기서 확장 함수와 멤버 메서드의 차이점이 발생한다.
멤버 메서드는 정말 수신 객체의 멤버(메서드, 프로퍼티)에 접근 및 사용이 가능하지만
확장 함수는 멤버의 가시성 제한자가 private 혹은 protected인 경우 접근 자체가 불가능하다.
즉, 확장 함수는 캡슐화를 깨지 않는다는 것이다!
그러니까 혹시나 확장 함수가 캡슐화를 깨지 않을까? 라는 고민은 접어두도록 하자!
물론 무분별한 확장 함수 남발은 객체지향을 해칠 수 있지만 이는 뒤에서 얘기하도록 하자
자 이제 확장 함수가 뭔지 알았다.
아직 애매하거나 헷갈려도 괜찮다. 남은 글들을 통해 익숙해질 것이다.
코틀린 api에서 사용한 확장 함수
자 그럼 이런 확장함수를 언제 어디서 사용할 수 있을까?
이럴땐 역시 코틀린에서 제공하는 api를 살펴보는게 최고다.
다음 코드를 살펴보자
public fun CharSequence.first(): Char {
if (isEmpty())
throw NoSuchElementException("Char sequence is empty.")
return this[0]
}
public fun CharSequence.last(): Char {
if (isEmpty())
throw NoSuchElementException("Char sequence is empty.")
return this[lastIndex]
}
어디서 많이 본 것 같지 않은가?
바로 second() 함수를 만들기 전에 언급했던 first()와 last()이다.
그렇다. 위에서 예시로 만들었던 second()는 코틀린에서 제공하는 first(), last()와 똑같이 만든 것이다.
즉, 코틀린의 first(), last()도 확장 함수로 만들어진 것이다.
뿐만 아니라 trim, contains와 같은 코틀린의 String에서 제공하는 강력한 함수들 또한 확장함수로 만들어졌다.
+
예시로 만든 second()와 first(), last()간에 조금의 차이점이 있다.
second()의 수신객체 타입은 String이지만 first()와 last()의 수신 객체 타입은 CharSequence이다.
어떤 차이가 있을까?

코틀린 공식문서를 보면 CharSequence는 인터페이스다.
엥? 나는 분명 문자열에서 first()함수를 사용했는데?
흠.. 그럼 String 클래스를 보자!

그렇다. String이 CharSequence의 구현체이다.
String은 Comparable<String>과 CharSequence를 구현하고있다.
여기서 알 수 있는 점은 확장 함수는 클래스뿐만 아니라 인터페이스 또한 확장이 가능하다는 것이다.
인터페이스를 확장하면 해당 인터페이스를 구현하는 모든 구현체에서 확장 함수를 쓸 수 있는 것이다.
이런 놀라운 일은 이 외에도 우리 주변에 많이 벌어지고 있다.
코틀린의 list, set 과 같은 컬렉션은 멋진 함수들을 제공한다.
이를테면 map, filter, associate 등과 같은 함수들 말이다.
이런 함수들을 사용하다보면 "역시 코틀린 컬렉션은 짱이야!" 라는 생각이 든다.
그런데 말이다, 아래 코드를 잠시 보도록 하자
val names: ArrayList<String> = ArrayList()
names.add("빅스")
names.add("산군")
names.add("멧돼지")
names.forEach { print("$it, ") }
위 코드는 java.util의 ArrayList를 사용한 코드다.
ArrayList는 자바의 컬렉션이다.
여기에 두 줄의 코드를 추가해보겠다.
val names: ArrayList<String> = ArrayList()
names.add("빅스")
names.add("산군")
names.add("멧돼지")
names.forEach { print("$it, ") }
print("\n여기서 필자의 이름은? ")
println(names.filter { it == "빅스" })

names에 filter함수를 적용했다.
어라..? 저 filter 함수, 코틀린 컬렉션 함수가 아니었던가?
그래서 filter 함수에 들어가봤다.

filter 함수는 Iterable의 확장함수였다. 그리고 Iterable은 인터페이스이다.
위에서 봤던대로 인터페이스도 확장이 가능한 것을 볼 수 있다.
인터페이스를 확장했기 때문에 Iterable을 상속, 구현하는 객체들은 모두 filter를 사용할 수 있게 된다.
어 그럼 혹시..? 맞다, 자바의 ArrayList는 Iterable을 구현하는 객체이다.
(코틀린과 자바의 컬렉션에 대해 더 얘기하고 싶지만 확장 함수라는 주제에 맞지 않는 것 같아 지금 헷갈릴만한 내용은 다음 컬렉션 글에서 하도록 하겠슴당)
위 내용으로 유추해보면 코틀린은 사실 자체 컬렉션을 제공하지 않는다. (응 갑자기? 라는 생각이 든다면 다음편인 컬렉션 글을 보자.)
그저 확장 함수를 추가해서 자바 컬렉션을 확장할 뿐이다. 위의 filter 함수처럼 말이다.
(그 이유는 무엇일까? 자체 컬렉션이 아닌 표준 자바 컬렉션을 활용하면 자바 코드 상호작용하기 훨씬 쉽기 때문이다. 자바에 코틀린 함수 호출하거 코틀린에서 자바 함수를 호출할 때 자바와 코틀린 컬렉션을 서로 변환할 필요가 없다.)
난 처음에 이 사실을 알고 너무 놀랐다.
내가 사랑하며 애용하던 코틀린의 컬렉션 함수들이 사실 전부 확장 함수였다니!!
코틀린의 컬렉션이 자바의 컬렉션보다 훨씬 많은 기능을 제공해서 더 좋다고 생각했는데,
사실 자바의 컬렉션에 확장 함수를 추가했을 뿐이라는 것이다. 정말 놀랍지 않은가?
음.. 나만 놀랐나? 하하
여튼 이렇게, 코틀린이 제공하는 api에서도 확장 함수는 많은 곳에서 쓰인다.
그래서 이게 어떻게 가능한건데!!
지금까지 본 확장함수는 정말 마법같다. 이런 일이 어떻게 가능한걸까?
확장함수는 어떻게 구현되어있는지 알아보자!
위의 코틀린 코드를 자바로 디컴파일 해보았다.
좀 알아보기 어려운 코드들이 많다. 하지만 중요한 부분은 충분히 보인다.
public static final void showOff(@NotNull View $this$showOff) {
Intrinsics.checkNotNullParameter($this$showOff, "<this>");
System.out.println("I'm a view!");
}
public static final void showOff(@NotNull Button $this$showOff) {
Intrinsics.checkNotNullParameter($this$showOff, "<this>");
System.out.println("I'm a button!");
}
public static final void main() {
View view = (View)(new Button());
view.click();
showOff(view);
}
showOff()가 확장했던 수신객체 View와 Button을 인자로 받고있다.
즉 확장함수가 확장했던 수신 객체 타입은 그저 해당 함수가 받으려는 매개변수의 타입이었을 뿐이다.
좀 더 이해하기 쉬운 코드로 보자
// 확장 함수
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
// 사실 이거임 ㅋㅋ
fun showOff(view: View) = println("I'm a view!")
fun showOff(button: Button) = println("I'm a button!")
이제 좀 이해가 되는가?
사실 위 두 코드는 같은 코드라는 것이다!
즉, 확장 함수는 코틀린의 가독성을 높이기 위한 수단이며 일반 메서드와 다를 바 없이 작동한다.
마법같던 일의 실마리가 드디어 풀렸다!!
이제 우리도 의문 없이 마법을 부릴 수 있게 됐다!
윙 가르디움 레비오우사~!
확장 함수는 오버라이드가 안된다구요?
마법에도 안되는 건 있다. 이제 뭐가 안되는지 알아보자.
확장 함수는 오버라이드 할 수 없다.
이제부터 예시를 보자.
View와 View를 상속하는 Button이 있다고 생각해보자.
그리고 Button이 View의 click 메서드를 오버라이드 한다고 하자.
open class View() {
open fun click() {
println("뷰가 클릭되었습니다")
}
}
class Button : View() {
override fun click() {
println("버튼이 클릭되었습니다")
}
}
fun main() {
val view: View = Button()
view.click()
}
Button은 View의 하위 타입이기 때문에, View 타입으로 선언해도 Button으로 초기화가 가능하다.

또 click 함수를 호출하면 오버라이드한 Button의 click 함수가 호출된다.
우리가 일반적으로 생각하는 상속과 오버라이드의 결과이다.
하지만 확장함수는 그렇지 않다.
다음과 같이 View와 Button에 showOff()라는 확장함수를 선언했다.
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
그럼 결과를 살펴보자
open class View {
open fun click() = println("View Clicked")
}
class Button : View() {
override fun click() = println("Button Clicked")
}
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
fun main() {
val view: View = Button()
view.click()
view.showOff()
}

멤버 함수인 click()과 다르게 showOff()는 View를 출력한다.
왜 이럴까?
우선 확장함수는 클래스의 일부가 아니다. 확장함수는 클래스 밖에 선언된다.
멤버 함수는 클래스의 일부로, 변수에 저장된 객체의 타입에 따라 동적으로 결정된다.
하지만 확장함수는 클래스의 타입을 확장하므로 정적 타입에 의해 결정된다.
아래 보다시피 Button의 showOff에는 불이 들어오지 않는다.
코틀린은 호출될 확장 함수를 정적으로 결정하기 때문이다.

(사실 이렇게 보지 않더라도 Button의 showOff엔 override 키워드 조차 없다.)
여기서 주의할 점이 하나 있다.
확장 함수는 수신 객체 타입을 확장하므로 위의 예시에서 View를 확장했다.
하지만 그렇다고 항상 View로 인지한다는 것은 아니다.
open class View {
open val name = "View"
open fun click() = println("View Clicked")
}
class Button : View() {
override val name = "Button"
override fun click() = println("Button Clicked")
}
fun View.showOff() = println("I'm a $name!")
fun Button.showOff() = println("I'm a $name!")
fun main() {
val view: View = Button()
view.click()
view.showOff()
}
View에 name을 추가하고 Button은 이를 오버라이드했다.
이때 showOff()의 결과는 어떻게 나올까?

여러분이 뭘 예상했을지는 모르지만 Button을 출력한다.
showOff의 수신 객체 타입은 View이지만 수신 객체는 view 변수에 할당된 Button인 것이다!
그러니 헷갈리지 말자~!
+ 추가로
A 클래스를 확장한 확장 함수를 만들었는데, 해당 함수의 시그니처가 A의 멤버 함수와 같다면
확장 함수는 멤버함수에 가려져 호출되지 않는다.
주의하자!!
확장 프로퍼티
이제 진짜 마지막이다.
확장 함수가 있다면, 확장 프로퍼티도 있다.
그러나 확장 프로퍼티는 어떠한 객체의 프로퍼티가 아니므로 상태를 저장할 방법이 없다.
즉, 확장 프로퍼티는 스토어드 프로퍼티가 될 수 없고 계산된 프로퍼티만 가능하다.
(용어가 어렵다면 이 글을 보러가자)
사용법은 멤버 프로퍼티와 같고 확장 방법도 확장 함수와 같으므로 예시 코드만 보여주고 끝내겠다!
class Person() {
val name = "Kwon Bixx"
}
val Person.firstName get() = name.split(" ").first()
val Person.lastName get() = name.split(" ").last()
fun main() {
val person = Person()
println(person.firstName)
println(person.lastName)
}

중간에 잠시 멈추는 바람에 적는데 너무 오랜 시간이 걸렸다...
다음 글은 후딱 써보자... ㅠㅠ
'Kotlin' 카테고리의 다른 글
[Kotlin] 프로퍼티 용어 정리 (0) | 2023.04.05 |
---|---|
[Kotlin] 인터페이스와 추상클래스의 차이 (2) | 2023.03.22 |
[Kotlin] 코틀린 인터페이스(Interface) (1) | 2023.02.21 |
[Kotlin] 코틀린의 헷갈리는 생성자와 프로퍼티 알아보기 (Java와 비교하여) (0) | 2023.02.20 |

당신이 코틀린을 사용하고 있다면, '확장 함수'를 사용해봤거나 들어봤을 것이다.
(아니어도 상관없다. 지금부터 알아보자)
코틀린은 확장 함수라는 기능을 제공한다.
우선 확장 함수가 어떻게 생긴 녀석인지 예시를 한 번 보자!
예시
String 클래스는 문자열의 처음, 끝을 가져올 수 있는 first(), last() 함수를 제공한다.
그런데 정말 만약에 문자열의 두 번째 글자가 필요한 일이 생겼다고 가정해보자.
이때 String에 대한 확장함수로 second() 함수를 만들어 볼 수 있을 것이다.
(물론 [1]로 가져올 수 있겠지만, 좀 더 예쁘게 확장함수를 만들어보자!)
fun String.second(): Char {
if (this.isEmpty()) {
throw NoSuchElementException("String is empty.")
}
return this[1]
}
문자열의 두 번째 글자를 반환하는 second 함수를 만들어보았다!
(문자열이 비어있다면 두 번째 글자가 없으므로 예외를 발생시켰다.)
이제 우리는 아래처럼 깔끔하게 두 번째 글자를 가져올 수 있다.
val names = listOf<String>("bixx", "sangun", "metPig")
names.forEach { println(it.second()) }

정말 마법같지 않은가?
난 확장 함수를 처음 만났을 때 정말 신기했고 이해도 잘 안됐다.
우선 함수 선언을 타입.함수명 형태로 하는 것부터 생소했다.
코틀린 갓난아기였던 시절, 어떤 형태로 선언하는지 헷갈려 여러번 찾아봤었다.
그럼 이 글을 읽는 여러분은 나처럼 헷갈리지 않도록
확장 함수에 대해 확실하게 알아보자.
확장 함수
개념적으로 확장함수는 단순하다.
어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수다.
위 예시를 생각해보자.
나는 String 클래스를 직접 작성하지도, 클래스의 소스 코드를 소유하지도 않았지만
String 클래스에 원하는 메소드를 추가할 수 있게 되었다!

확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다.
클래스의 이름을 수신 객체 타입(receiver type)이라 부르며,
확장함수가 호출되는 대상이 되는 값(객체)을 수신 객체(receiver object)라고 부른다.
위 second() 예시에서는 String이 수신 객체 타입, "bixx", "sangun", "metPig"가 각각 수신 객체이다.
확장 함수 내에서 수신 객체는 this로 나타내며 this는 대부분의 경우 생략이 가능하다.
(위 예시에서 this.isEmpty()의 this는 생략이 가능하고, this[1]의 this는 생략이 불가능하다.)
확장 함수는 함수 내부에서 수신 객체의 메서드와 프로퍼티를 사용할 수 있다.
위에서 든 예시 또한 수신 객체의 메서드 isEmpty()를 사용했다!
하지만 여기서 확장 함수와 멤버 메서드의 차이점이 발생한다.
멤버 메서드는 정말 수신 객체의 멤버(메서드, 프로퍼티)에 접근 및 사용이 가능하지만
확장 함수는 멤버의 가시성 제한자가 private 혹은 protected인 경우 접근 자체가 불가능하다.
즉, 확장 함수는 캡슐화를 깨지 않는다는 것이다!
그러니까 혹시나 확장 함수가 캡슐화를 깨지 않을까? 라는 고민은 접어두도록 하자!
물론 무분별한 확장 함수 남발은 객체지향을 해칠 수 있지만 이는 뒤에서 얘기하도록 하자
자 이제 확장 함수가 뭔지 알았다.
아직 애매하거나 헷갈려도 괜찮다. 남은 글들을 통해 익숙해질 것이다.
코틀린 api에서 사용한 확장 함수
자 그럼 이런 확장함수를 언제 어디서 사용할 수 있을까?
이럴땐 역시 코틀린에서 제공하는 api를 살펴보는게 최고다.
다음 코드를 살펴보자
public fun CharSequence.first(): Char {
if (isEmpty())
throw NoSuchElementException("Char sequence is empty.")
return this[0]
}
public fun CharSequence.last(): Char {
if (isEmpty())
throw NoSuchElementException("Char sequence is empty.")
return this[lastIndex]
}
어디서 많이 본 것 같지 않은가?
바로 second() 함수를 만들기 전에 언급했던 first()와 last()이다.
그렇다. 위에서 예시로 만들었던 second()는 코틀린에서 제공하는 first(), last()와 똑같이 만든 것이다.
즉, 코틀린의 first(), last()도 확장 함수로 만들어진 것이다.
뿐만 아니라 trim, contains와 같은 코틀린의 String에서 제공하는 강력한 함수들 또한 확장함수로 만들어졌다.
+
예시로 만든 second()와 first(), last()간에 조금의 차이점이 있다.
second()의 수신객체 타입은 String이지만 first()와 last()의 수신 객체 타입은 CharSequence이다.
어떤 차이가 있을까?

코틀린 공식문서를 보면 CharSequence는 인터페이스다.
엥? 나는 분명 문자열에서 first()함수를 사용했는데?
흠.. 그럼 String 클래스를 보자!

그렇다. String이 CharSequence의 구현체이다.
String은 Comparable<String>과 CharSequence를 구현하고있다.
여기서 알 수 있는 점은 확장 함수는 클래스뿐만 아니라 인터페이스 또한 확장이 가능하다는 것이다.
인터페이스를 확장하면 해당 인터페이스를 구현하는 모든 구현체에서 확장 함수를 쓸 수 있는 것이다.
이런 놀라운 일은 이 외에도 우리 주변에 많이 벌어지고 있다.
코틀린의 list, set 과 같은 컬렉션은 멋진 함수들을 제공한다.
이를테면 map, filter, associate 등과 같은 함수들 말이다.
이런 함수들을 사용하다보면 "역시 코틀린 컬렉션은 짱이야!" 라는 생각이 든다.
그런데 말이다, 아래 코드를 잠시 보도록 하자
val names: ArrayList<String> = ArrayList()
names.add("빅스")
names.add("산군")
names.add("멧돼지")
names.forEach { print("$it, ") }
위 코드는 java.util의 ArrayList를 사용한 코드다.
ArrayList는 자바의 컬렉션이다.
여기에 두 줄의 코드를 추가해보겠다.
val names: ArrayList<String> = ArrayList()
names.add("빅스")
names.add("산군")
names.add("멧돼지")
names.forEach { print("$it, ") }
print("\n여기서 필자의 이름은? ")
println(names.filter { it == "빅스" })

names에 filter함수를 적용했다.
어라..? 저 filter 함수, 코틀린 컬렉션 함수가 아니었던가?
그래서 filter 함수에 들어가봤다.

filter 함수는 Iterable의 확장함수였다. 그리고 Iterable은 인터페이스이다.
위에서 봤던대로 인터페이스도 확장이 가능한 것을 볼 수 있다.
인터페이스를 확장했기 때문에 Iterable을 상속, 구현하는 객체들은 모두 filter를 사용할 수 있게 된다.
어 그럼 혹시..? 맞다, 자바의 ArrayList는 Iterable을 구현하는 객체이다.
(코틀린과 자바의 컬렉션에 대해 더 얘기하고 싶지만 확장 함수라는 주제에 맞지 않는 것 같아 지금 헷갈릴만한 내용은 다음 컬렉션 글에서 하도록 하겠슴당)
위 내용으로 유추해보면 코틀린은 사실 자체 컬렉션을 제공하지 않는다. (응 갑자기? 라는 생각이 든다면 다음편인 컬렉션 글을 보자.)
그저 확장 함수를 추가해서 자바 컬렉션을 확장할 뿐이다. 위의 filter 함수처럼 말이다.
(그 이유는 무엇일까? 자체 컬렉션이 아닌 표준 자바 컬렉션을 활용하면 자바 코드 상호작용하기 훨씬 쉽기 때문이다. 자바에 코틀린 함수 호출하거 코틀린에서 자바 함수를 호출할 때 자바와 코틀린 컬렉션을 서로 변환할 필요가 없다.)
난 처음에 이 사실을 알고 너무 놀랐다.
내가 사랑하며 애용하던 코틀린의 컬렉션 함수들이 사실 전부 확장 함수였다니!!
코틀린의 컬렉션이 자바의 컬렉션보다 훨씬 많은 기능을 제공해서 더 좋다고 생각했는데,
사실 자바의 컬렉션에 확장 함수를 추가했을 뿐이라는 것이다. 정말 놀랍지 않은가?
음.. 나만 놀랐나? 하하
여튼 이렇게, 코틀린이 제공하는 api에서도 확장 함수는 많은 곳에서 쓰인다.
그래서 이게 어떻게 가능한건데!!
지금까지 본 확장함수는 정말 마법같다. 이런 일이 어떻게 가능한걸까?
확장함수는 어떻게 구현되어있는지 알아보자!
위의 코틀린 코드를 자바로 디컴파일 해보았다.
좀 알아보기 어려운 코드들이 많다. 하지만 중요한 부분은 충분히 보인다.
public static final void showOff(@NotNull View $this$showOff) {
Intrinsics.checkNotNullParameter($this$showOff, "<this>");
System.out.println("I'm a view!");
}
public static final void showOff(@NotNull Button $this$showOff) {
Intrinsics.checkNotNullParameter($this$showOff, "<this>");
System.out.println("I'm a button!");
}
public static final void main() {
View view = (View)(new Button());
view.click();
showOff(view);
}
showOff()가 확장했던 수신객체 View와 Button을 인자로 받고있다.
즉 확장함수가 확장했던 수신 객체 타입은 그저 해당 함수가 받으려는 매개변수의 타입이었을 뿐이다.
좀 더 이해하기 쉬운 코드로 보자
// 확장 함수
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
// 사실 이거임 ㅋㅋ
fun showOff(view: View) = println("I'm a view!")
fun showOff(button: Button) = println("I'm a button!")
이제 좀 이해가 되는가?
사실 위 두 코드는 같은 코드라는 것이다!
즉, 확장 함수는 코틀린의 가독성을 높이기 위한 수단이며 일반 메서드와 다를 바 없이 작동한다.
마법같던 일의 실마리가 드디어 풀렸다!!
이제 우리도 의문 없이 마법을 부릴 수 있게 됐다!
윙 가르디움 레비오우사~!
확장 함수는 오버라이드가 안된다구요?
마법에도 안되는 건 있다. 이제 뭐가 안되는지 알아보자.
확장 함수는 오버라이드 할 수 없다.
이제부터 예시를 보자.
View와 View를 상속하는 Button이 있다고 생각해보자.
그리고 Button이 View의 click 메서드를 오버라이드 한다고 하자.
open class View() {
open fun click() {
println("뷰가 클릭되었습니다")
}
}
class Button : View() {
override fun click() {
println("버튼이 클릭되었습니다")
}
}
fun main() {
val view: View = Button()
view.click()
}
Button은 View의 하위 타입이기 때문에, View 타입으로 선언해도 Button으로 초기화가 가능하다.

또 click 함수를 호출하면 오버라이드한 Button의 click 함수가 호출된다.
우리가 일반적으로 생각하는 상속과 오버라이드의 결과이다.
하지만 확장함수는 그렇지 않다.
다음과 같이 View와 Button에 showOff()라는 확장함수를 선언했다.
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
그럼 결과를 살펴보자
open class View {
open fun click() = println("View Clicked")
}
class Button : View() {
override fun click() = println("Button Clicked")
}
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
fun main() {
val view: View = Button()
view.click()
view.showOff()
}

멤버 함수인 click()과 다르게 showOff()는 View를 출력한다.
왜 이럴까?
우선 확장함수는 클래스의 일부가 아니다. 확장함수는 클래스 밖에 선언된다.
멤버 함수는 클래스의 일부로, 변수에 저장된 객체의 타입에 따라 동적으로 결정된다.
하지만 확장함수는 클래스의 타입을 확장하므로 정적 타입에 의해 결정된다.
아래 보다시피 Button의 showOff에는 불이 들어오지 않는다.
코틀린은 호출될 확장 함수를 정적으로 결정하기 때문이다.

(사실 이렇게 보지 않더라도 Button의 showOff엔 override 키워드 조차 없다.)
여기서 주의할 점이 하나 있다.
확장 함수는 수신 객체 타입을 확장하므로 위의 예시에서 View를 확장했다.
하지만 그렇다고 항상 View로 인지한다는 것은 아니다.
open class View {
open val name = "View"
open fun click() = println("View Clicked")
}
class Button : View() {
override val name = "Button"
override fun click() = println("Button Clicked")
}
fun View.showOff() = println("I'm a $name!")
fun Button.showOff() = println("I'm a $name!")
fun main() {
val view: View = Button()
view.click()
view.showOff()
}
View에 name을 추가하고 Button은 이를 오버라이드했다.
이때 showOff()의 결과는 어떻게 나올까?

여러분이 뭘 예상했을지는 모르지만 Button을 출력한다.
showOff의 수신 객체 타입은 View이지만 수신 객체는 view 변수에 할당된 Button인 것이다!
그러니 헷갈리지 말자~!
+ 추가로
A 클래스를 확장한 확장 함수를 만들었는데, 해당 함수의 시그니처가 A의 멤버 함수와 같다면
확장 함수는 멤버함수에 가려져 호출되지 않는다.
주의하자!!
확장 프로퍼티
이제 진짜 마지막이다.
확장 함수가 있다면, 확장 프로퍼티도 있다.
그러나 확장 프로퍼티는 어떠한 객체의 프로퍼티가 아니므로 상태를 저장할 방법이 없다.
즉, 확장 프로퍼티는 스토어드 프로퍼티가 될 수 없고 계산된 프로퍼티만 가능하다.
(용어가 어렵다면 이 글을 보러가자)
사용법은 멤버 프로퍼티와 같고 확장 방법도 확장 함수와 같으므로 예시 코드만 보여주고 끝내겠다!
class Person() {
val name = "Kwon Bixx"
}
val Person.firstName get() = name.split(" ").first()
val Person.lastName get() = name.split(" ").last()
fun main() {
val person = Person()
println(person.firstName)
println(person.lastName)
}

중간에 잠시 멈추는 바람에 적는데 너무 오랜 시간이 걸렸다...
다음 글은 후딱 써보자... ㅠㅠ
'Kotlin' 카테고리의 다른 글
[Kotlin] 프로퍼티 용어 정리 (0) | 2023.04.05 |
---|---|
[Kotlin] 인터페이스와 추상클래스의 차이 (2) | 2023.03.22 |
[Kotlin] 코틀린 인터페이스(Interface) (1) | 2023.02.21 |
[Kotlin] 코틀린의 헷갈리는 생성자와 프로퍼티 알아보기 (Java와 비교하여) (0) | 2023.02.20 |