코틀린 언어 정리 2-17

class 확장(Extensions)

정리

클래스 확장 ( Extensions )

기본적으로 클래스를 확장하는 방법에는 상속이 있습니다. 그런데 코틀린에는 이 방법 이외에 확장 이라는 기법을 제공합니다. 확장은 클래스의 멤버를 직접 추가하지 않고 "객체.멤버" 와 같은 문법(클래스의 멤버 처럼 보이기 하는 문법)을 지원해 줍니다. 기본 동작이 멤버와 유사하지만 다르게 동작하는 부분이 있으므로 어떻게 동작 하는지 잘 확인하고 사용해야 합니다.

선언 방법

선언 가능한 위치

  • 최상위(Top Level)
    ex. main 함수 외부

  • 클래스 내부

  • 함수 내부


문법

일반 멤버 선언과 동일하지만 함수나 프로퍼티 이름 앞에 [Extension Receiver class의 이름] + ["."] 가 추가됩니다.

  • 함수
    fun [extension receiver class 이름].[함수 이름]() { }

  • 프로퍼티
    var [extension receiver class 이름].[프로퍼티 이름]
    get() { }
    set() { }

    확장 프로퍼티는 backing field를 지원하지 않으므로 프로퍼티의 명시적 초기화(프로퍼티를 선언하면서 바로 값을 대입)는 불가능합니다. 따라서 초기화를 위해 getter / setter 를 명시적으로 정의해야 합니다.


용어

extension receiver type

  • 확장 함수나 프로퍼티 선언 시 함수나 프로퍼티 이름 앞에 "."으로 구분하여 추가하는 클래스

  • 확장 함수나 프로퍼티가 멤버 처럼 추가되는 클래스


extension receiver object

  • extension receiver type의 인스턴스

  • 확장 함수 구현 내에서 this로 참조하는 객체

dispatch receiver type

  • 확장이 클래스의 멤버처럼 클래스 내부에 선언되어 있는 경우, 확장 선언을 포함하는 클래스/인스턴스(객체)


dispatch receiver object

  • 확장 함수 구현 내에서 this, this@클래스이름 으로 참조되는 객체



확장

확장이라는 단어는 매우 일반적으로 사용되어 범위를 좁혀야 할 필요가 있습니다. 여기서는 확장이라는 용어를 문맥에 따라 두 가지 의미로 사용 했습니다.

  • 코틀린의 확장 기능에 대한 명칭

  • 확장 함수나 확장 프로퍼티를 한번에 지칭할 때 사용함. 설명할 때마다 매번 "확장 함수와 확장 프로퍼티를..." 와 같이 쓰면 길어지므로 통합하여 "확장" 이라고 하였음. 클래스의 "멤버"와 개념 상 혼동하기 쉬울 것 같아 "확장 멤버"라는 용어를 사용하지 않음


멤버

  • 클래스의 멤버 함수나 멤버 프로퍼티



확장에 대한 가시성(접근 권한)

확장에 접근할 수 있는 권한을 확장이 선언된 위치별로 확인해 보면 다음과 같이 3가지 정도로 나누어 볼 수 있습니다.


Top(전역)에 선언 시

  • 확장 함수가 선언된 package 내의 모든 곳에서 접근 가능합니다.

  • package 외부에서 접근 시, 사용하고자 하는 곳에서 확장 선언한 함수나 프로퍼티 각각을 명시적으로 import 해야 합니다.


class 내부(dispatch receiver type)에 선언 시

  • dispatch receiver class 내부에서 접근 가능

  • dispatch receiver class를 상속 받은 클래스에서 접근 가능

  • 클래스 외부에서 접근 불가(dispatch receiver type이 extension receiver type과 동일한 경우, 즉 extension receiver type 내부에 확장 함수가 선언될 때에도 외부에서 접근 불가)


함수 내부

  • 함수 내부에서 접근 가능

  • 함수 외부에서 접근 불가



확장 함수 내에서 멤버에 대한 가시성

확장 함수 내에서 extension 또는 dispatch receiver의 멤버 등에 접근할 수 있는 방법은 다음과 같습니다.


extension receiver에 접근

  • this를 이용하여 extension receiver type/class 및 상위 클래스의 public 멤버에 접근 가능합니다.

  • this는 생략 가능합니다.

  • super는 사용할 수 없습니다. 하지만 this를 상위 타입으로 캐스팅하여 명시적으로 접근하는 것은 가능합니다.


dispatch receiver에 접근

  • this@[dispatch receiver class name] 문법(qualified this syntax)를 이용하여 dispatch receiver 클래스의 모든 멤버 및 상위 클래스의 public, protected 멤버에 접근 가능합니다.

  • this@[...] 문법은 생략 가능합니다. extension receiver class, dispatch receiver class 간의 멤버 사이에 이름 충돌이 없다면 this를 생략하여 각 class의 멤버를 구분 없이 사용할 수 있습니다. 그러나 이름 충돌이 있을 경우 qualified this syntax를 사용해야 합니다.

  • dispatch receiver type 내부에 선언된 확장 함수 구현은 dispatch receiver type에 있는 다른 일반 멤버 함수 구현에서의 접근 규칙들이 그대로 적용됩니다.



확장 함수의 다형성

  • extension receiver object를 통해 접근(호출)하는 확장 함수 호출은 정적으로 처리됩니다. 즉 확장 함수는 extension receiver type에 정적으로 연결됩니다.


  • dispatch receiver type 내부에 선언된 확장 함수는 dispatch receiver의 멤버 함수이면서 extension receiver의 확장 함수입니다. 따라서 dispatch receiver class의 상속 계층에서 overriding 가능하며 객체의 인스턴스 type에 따라 확장 함수가 선택(동적/다형적으로 처리)됩니다. 하지만 extension receiver에 대해서는 원래대로 정적으로 처리 됩니다.


부록: 멤버 함수에서 인라인으로 전달 받는 확장 함수

당연한 이야기지만 리시버 클래스가 접근할 수 없는 곳에 선언된 확장함수를 리시버 클래스의 멤버함수가 인자로 전달받아 실행할 수도 있습니다.그런데 멤버함수가 인라인 고차함수일 경우에도 문제 없이 동작하는지는 궁금할 수도 있습니다.

인라인 고차함수의 경우 람다 표현식(함수)을 호출하는 것이 아니라, 람다 표현식을 호출한 코드를, 입력 받은 람다 표현식(구현 코드)으로 대체하기 때문에, 인라인을 단순히 구현 코드의 copy/paste로 잘못 생각할 경우 혼동의 여지가 있기 때문입니다. 그래서 예제로 동작 결과를 확인해 보니 일반 함수와 마찬가지로 정상동작합니다. 

(람다나 인라인 고차함수에 대한 더 자세한 내용은 나중에 설명할 함수형 프로그래밍 부분에서 확인할 수 있습니다.)


예제

class ER {

   fun access_test() {

       println("ER::access_test")

       //func_inFunc() // 컴파일 에러: 외부 함수에 선언된 확장 함수 접근 불가

   }


   // 확장 함수를 전달 받는 인라인 고차함수

   inline fun test_lambdaPassing(extFunc: ER.() -> Unit) {

       println("ER::test_lambdaPassing <===== start")

       extFunc()

       println("ER::test_lambdaPassing =====> end")

   }

}


fun ER.func_outFunc() {

   //func_inFunc() // 컴파일 에러: 외부 함수의 내부에 선언된 확장 함수는 함수의 외부에서 접근할 수 없습니다.

}


fun test () {

   // 함수 내부에 확장 함수 구현

   fun ER.func_inFunc() {

       println("ER.inFunc() <==== start")

       println("this: $this")

       access_test() // 클래스의 멤버 접근 가능

       println("ER.inFunc()  ====> end")

       return

   }


   // ER 클래스의 확장 함수 형식으로 람다 함수 구현

   var vFunc_inFunc: ER.()->Unit = {

       println("ER.vFunc_inFunc() <==== start")

       println("this: $this")

       access_test() // 클래스의 멤버 접근 가능

       println("ER.vFunc_inFunc()  ====> end")

   }


   ER().func_inFunc()

   //인라인 호출 테스트

   ER().test_lambdaPassing { func_inFunc() } // 지역(함수 내부)에서 선언한 확장 함수를 호출하는 람다를 전달. 람다가 확장함수 인수로 입력되는 점에 주의

   ER().test_lambdaPassing { vFunc_inFunc() } // 지역에서 선언한 확장 함수 람다 구현을 저장한 변수로 함수를 호추하는 람다를 전달. 람다가 확장함수 인수로 입력되는 점에 주의


   //함수 전달 테스트

   ER().test_lambdaPassing(ER::func_inFunc) // 지역(함수 내부)에서 선언한 확장 함수를 전달

   ER().test_lambdaPassing(vFunc_inFunc) // 지역에서 선언한 확장 함수 람다 구현을 변수로 저장하여 전달


   // 확장 함수 람다 구현을 인라인으로 멤버 함수에 전달

   ER().test_lambdaPassing {

       println("lambda block inFunc() <==== start")

       println("this: $this")

       func_inFunc()

       access_test() // 클래스의 멤버 접근 가능

       println("lambda block inFunc()  ====> end")

       return // 인라인 고차함수의 인자로 사용되는 람다 함수에는 return 사용 가능함

   }

}

func_inFunc는 ER 클래스 내부에서 직접 접근할 수 없어, 이를 호출하는 람다를 인라인으로 전달하여 ER 클래스 내부에서 호출하는 것은 불가능하다고 생각할 수 있지만 실제로는 호출이 가능합니다. 접근 권한은 코딩한 위치에서 판단하고, this(extension receiver object)는 코드가 동작하는 위치에서 판단합니다.


+ Recent posts