[kotlin in Action] Extension Function (확장 함수)

가영·2022년 2월 1일
0

정의

코틀린 확장함수는 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스 밖에 선언된 함수다.

package strings

fun String.lastChar() : Char = this.get(this.length - 1)

확장할 클래스를 Receiver type, 호출되는 객체를 Receiver object라고 부른다. 그럼 위의 코드에서는 String이 Receiver type, this가 Receiver object가 될 것이다.

특징

내가 작성한 클래스가 아니어도 확장함수를 만들 수 있다

위의 예제에서 봤듯, 내가 작성하지 않은 유틸 클래스들에도 원하는 메서드를 만들어줄 수 있다.

추가로, 확장하려는 Receiver Type 클래스가 어떤 언어(JVM 언어)로 작성되든 JAVA 클래스로 컴파일한 클래스 파일이 있다면 전부 확장함수를 추가할 수 있다.

클래스 본문처럼,

확장 함수 본문에도 this를 사용할 수 있다.

위의 예제를 다시 보면

fun String.lastChar() : Char = this.get(this.length - 1)

클래스 안에 선언하는 것이 아닌데도 this를 사용할 수 있었다.

확장 함수 본문에도 this를 생략할 수 있다.

위와 같은 역할을 하는 코드다.

package strings

fun String.lastChar() : Char = get(length - 1)

오오...👏🏻

수신 객체의 메서드나 프로퍼티를 바로 사용할 수 있다.

위의 예제들에서 봤듯, String 클래스의 get()이나 length 같은 수신객체 멤버들을 본문에서 사용할 수 있었다.

하지만, 확장 함수가 캡슐화를 깨지는 않는다.

클래스 안에서 정의한 메서드와는 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 또는 protected 변수들은 사용할 수 없다.


확장함수 사용법

확장함수를 정의해도 자동으로 프로젝트 안의 모든 소스코드에서 그 함수를 사용할 수는 없다. 확장함수를 사용하기 위해서는 임포트해야만 한다.

코틀린에서는 개별함수를 임포트할 수 있다.

import strings.lastChar

val c = "kotlin".lastChar()

그런데, 확장 함수를 정의하자마자 어디서든 그 함수를 쓸 수 있다면 한 클래스에 같은 이름의 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 자주 생길 수 있다. 그럴땐 as 키워드를 사용하면 다른 이름으로 부를 수 있다.

import strings.lastChar as last

val c = "kotlin".last()

코틀린으로 작성한 확장함수를 자바에서 사용하기

내부적으로, 코틀린의 확장함수는 Receiver object를 첫번째 인자로 받는 정적메서드이다.

만약 확장함수를 StringUtil.kt라는 코틀린 파일에 정의 했다면 다음과 같이 호출할 수 있다.

char c = StringUtilKt.lastChar("Java");

정적 메서드이기 때문에, 다른 어댑터 객체나 실행 시점의 부가 비용이 들지 않는다!

주의 사항

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

코틀린의 메서드 오버라이드는 일반적인 오버라이드와 같다.
하지만 확장함수는 오버라이드할 수 없다고한다. 먼저 예제를 보자.

예제

  1. RectangleShape을 상속한다
open class Shape
class Rectangle: Shape()
  1. 두 클래스에 같은 시그니처로 확장함수를 추가한다
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
  1. Shape 타입을 파라미터로 갖는 메서드를 만들고, 인자로 Rectangle 객체를 넣어본다.
fun printClassName(s: Shape) {
    println(s.getName())
}

printClassName(Rectangle())

과연 getName()을 호출했을 때 "Rectangle"이 나올까?

아니었다. Shape 클래스의 확장함수가 호출되었다.

왜 그럴까?

Extension functions are dispatched statically, which means they are not virtual by receiver type. An extension function being called is determined by the type of the expression on which the function is invoked, not by the type of the result from evaluating that expression at runtime
- Extensions are resolved statically, 코틀린 공식문서

위에 자바에서 확장함수를 사용하는 방법에서 언급했듯, 확장함수는 정적메서드로 만들어지기 때문에, static dispatch가 사용된다. (dispatch는 어떤 메소드를 호출할지 결정하는 것)

따라서 조상-자손 클래스의 확장함수는 다음과같이 메서드 오버로딩의 형태로 구현된다.

@NotNull
public static final String getName(@NotNull Shape $this$getName) {
    Intrinsics.checkNotNullParameter($this$getName, "$this$getName");
    return "Shape";
}

@NotNull
public static final String getName(@NotNull Rectangle $this$getName) {
    Intrinsics.checkNotNullParameter($this$getName, "$this$getName");
    return "Rectangle";
}

정적 디스패치, 즉 컴파일 타임에 어떤 확장함수가 사용될지 정해지기 때문에 객체의 런타임 타입은 전혀 중요치 않다. 선택은 컴파일 타임에, 오직 매개변수의 컴파일타임 타입에 의해 이뤄진다.

printClassName(Rectangle()) 에서 어떤 getName() 이 호출될지는 👇🏻 이걸 보면 그냥 당연한 거처럼 느껴진다.

1개의 댓글

comment-user-thumbnail
2022년 2월 9일

안녕하세요~ 서로 이웃해요~

답글 달기