반응형

프로그램을 개발하다 보면 기존에 만들었던 class 에 새로운 기능을 추가해야 하는 경우가 있습니다.

만약 기존 class 를 여러분이 직접 설계하여 .kt 파일을 소유한 상태라면 파일을 열어 직접 새로운 기능메소드를 추가하였을 겁니다.

 

근데. 만약 여러분이 제작한 class 가 아니어서 .kt 파일을 보유하지 않았거나, 다른 모듈에서 새로운 기능메소드 없이 기존의 클래스를 사용하고 있어서 원본 class 를 수정하지 못하는 상황이 있을 수도 있습니다. 또는 외부 라이브러리를 통해 제공된 class 인데 새로운 기능을 추가해서 사용해야 할 수 도 있습니다. 하지만 외부에서 제작된 class 이기에 역시 원본 class .kt 파일에 메소드를 추가로 작성하는 것이 불가능 합니다. 이런 경우에는 어떻게 새로운 기능메소드를 추가해야 하는 걸까요?

 

그럴때는 보통 상속을 통해 기존 class 를 가져온 후 새로운 기능 메소드를 추가하는 방법을 사용합니다. 

하지만 코틀린의 class 는 기본적으로 상속이 불가능한 final class 입니다. 상속을 허용하려면 open 키워드를 이용해야 하죠. 즉, 기존의 클래스가 상속이 불가능한 클래스 일 수 도 있습니다. 

 

이를 위해 코틀린은 별도의 상속없이 기존 클래스에 새로운 기능메소드를 추가할 수 있도록 확장(extension) 기능을 제공합니다.

 

기존 클래스의 .kt 원본 파일에 추가되는 것이 아니고 메모리 상에 정적으로 연결되는 기능이기에 실행 중에만 확장이 되어 사용되며 원본에는 영향을 주지 않아 부담없이 그 앱에서만 필요한 기능을 추가해 사용할 수 있는 특징이 있습니다. 

 

실제로 제가 AI 앱 개발을 할 때 촬영된 이미지를 분석하여 object detecting 을 하고 ImageView 가 보여주는 Bitmap 이미지에 오버레이된 도형과 글씨를 표시할 때 기존 ImageView 클래스에 확장함수로 drawBitmapWithBounce() 라는 확장 함수를 추가하여 기능을 구현한 적이 있습니다. 


 

실습을 위해 새로운 Test11ExtensionFunction.kt 코틀린 파일을 만들겠습니다.

프로그램의 시작 함수 main() 까지 작성하고 시작하겠습니다.

 

 

1. 확장 함수 Extenstion Function

기존 클래스에 새로운 기능을 추가하는 대표적인 방법은 상속 Inheritace 입니다. 하지만 상속이 불가능한 final class 는 상속이 불가능 하여 새로운 기능을 추가하지 못합니다. 

상속이 불가능한 클래스에 기능을 추가할 수 있는 아주 손쉬운 방법인 확장 함수를 알아보기 위해 간단한 로봇캐릭터를 이용한 게임을 제작한다고 가정해 보겠습니다.

게임의 메인 캐릭터인 로봇을 설계해야 겠지요. 실습을 위한 것인 만큼 아주 간결하게 멤버변수 1개와 멤버함수 1개로 설계하겠습니다.

 

클래스 명 : Robot

멤버변수(property) :  캐릭터 이름 [  name : String? ]

멤버함수 (method) :  이동 기능 [ move() ]

 

먼저, main() 함수 밖에 Robot class 를 설계하겠습니다.

//시작 함수
fun main(){


}//main 함수 종료..

// [이름, 이동기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------

 

클래스가 잘 설계 되었는지 확인하기 위해 main()함수에서 Robot 객체를 생성하고 [이동기능]을 사용해 보겠습니다.

//시작 함수
fun main(){

    // Robot 객체 생성
    val robot= Robot()
    // 원래 있는 메소드 호출
    robot.move()
    
}//main 함수 종료..

// [이름, 이동 기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------


//**출력**
아장 아장 이동.

 

잘 동작 되는 군요. 근데 로봇이 그냥 이동만 하면 게임이 재미 없을 것 같네요. 로봇끼리 서로 싸울 수 있도록 [공격기능]을 추가해 보겠습니다. 통상적으로 특정 클래스에 기능을 추가하려면 원본 클래스 파일을 수정하지만, 이 로봇클래스의 원본은 수정하는 않는 것을 가정하여 제작해 보겠습니다. 원본을 수정할 수 없다면 기능을 상속받아 새로운 기능을 추가하는 것이 보편적입니다. 시도해 볼까요?

// Robot 에게 attack(공격) 가능 추가 하기
// [상속을 통한 기능 추가 시도]
class MyRobot : Robot(){ //error -  final class Robot 은 상속 불가.

}

 

주석으로 설명했듯이 Robot 클래스는 상속이 불가능한 클래스입니다. 프로그램을 개발하다보면 이런 경우가 꽤 존재합니다. 특정 기능을 추가하고 싶은데 final class 여서 상속이 안되기에 별도의 기능함수에 파라미터로 전달하여 대신 처리하는 등 피동적인 기법을 사용할 수 밖에 없습니다. 

이번에 이 글의 주제인 확장 함수를 이용하여 Robot 클래스에 [공격]기능을 추가해 보겠습니다.

// [이름, 이동 기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------


// Robot 에게 attack(공격) 가능 추가 하기

// [확장 함수 만들기]  -- 클래스명.함수명  으로 새로운 함수 제작
fun Robot.attack(){
    println("주먹 발사!!")
}

 

원본 클래스인 Robot 클래스의 밖이면 어디든 상관없습니다. 함수를 만드는 문법은 그대로 사용하되 함수의 이름앞에 어떤 클래스의 확장함수 인지만 명시해 주시면 됩니다.  [클래스명.함수명]

 

자. 이제 Robot 클래스에 attack() 기능 함수가 추가되었습니다. 이 함수는 메모리에 정적 바인딩되어 있기에 앱이 종료될 때 까지, 즉, 앱이 실행 중에는 Robot 클래스의 멤버메소드 인것 처럼 사용이 가능합니다.

main() 함수에서 생성한 robot 객체에게 확장함수로 만든 [공격 attack 기능]을 사용해 보겠습니다.

//시작 함수
fun main(){

    // Robot 객체 생성
    val robot= Robot()
    // 원래 있는 메소드 호출
    robot.move()

    // 원래 class 작성할 때는 없던 확장 함수 호출. -- 호출의 문법적 차이는 없음.
    robot.attack()

}//main 함수 종료..

// [이름, 이동 기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------


// Robot 에게 attack(공격) 가능 추가 하기

// [확장 함수 만들기]  -- 클래스명.함수명  으로 새로운 함수 제작
fun Robot.attack(){
    println("주먹 발사!!")
}


//**출력**
아장 아장 이동.
주먹 발사!!

 

너무 쉽고 간편하게 기능을 추가하였습니다.

자바에는 없는 문법입니다.[참고로. kotlin 파일은 컴파일 되면 java 로 변환됩니다. 이 확장함수는 자바에서는 static 함수로 대체됩니다.]

 

이번에는 확장함수에 파라미터를 전달해볼까요? 더불어 클래스의 중괄호 영역 밖에서 만든 확정함수안에서 원본 클래스의 영역안에 선언된 멤버변수를 인식할 수 있을 까요? 뭐. 미리 정답을 말하면 당연히 가능합니다. 원래 있는 기능처럼 사용하는 것인 만큼 당연히 허용됩니다.

아! 조심하세요. 위에 참고사항으로 설명했듯이 자바로 변환될때 static 함수를 만들고 파라미터로 객체를 전달받아 해당 기능을 동작하도록 하기에 외부에서 접근이 불가능한 private 접근 제한자가 있는 멤버변수는 사용이 불가능합니다. 그러니 엄밀하게는 기존 메소드와 완전히 같지는 않습니다.

 

일단, 코틀린에서는 대부분 private 으로 프로퍼티를 제작하지 않으니 이는 잠시 뒤로 하고 멤버변수를 사용하는 확정함수를 만들어 보겠습니다.

// [확장 함수 만들기2 - 멤버 변수 사용해 보기 (이름을 매개변수로 전달받아 멤버변수에 대입하기)]
fun Robot.fly(name: String){
    this.name= name
    println("${this.name} 난다~~ 난다~~~")
}

 

잘 동작하는 지 main()함수에서 로봇객체의 이름을 파라미터에 전달하면서 확장함수 fly 기능을 사용해 보겠습니다.

//시작 함수
fun main(){

    // Robot 객체 생성
    val robot= Robot()
    // 원래 있는 메소드 호출
    robot.move()

    // 원래 class 작성할 때는 없던 확장 함수 호출. -- 호출의 문법적 차이는 없음.
    robot.attack()

    // 파라미터 값 전달 및 멤버변수를 사용하는 확장함수 호출
    robot.fly("옵티머스 프라임")

}//main 함수 종료..

// [이름, 이동 기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------


// Robot 에게 attack(공격) 가능 추가 하기

// [확장 함수 만들기]  -- 클래스명.함수명  으로 새로운 함수 제작
fun Robot.attack(){
    println("주먹 발사!!")
}

// [확장 함수 만들기2 - 멤버 변수 사용해 보기 (이름을 매개변수로 전달받아 멤버변수에 대입하기)]
fun Robot.fly(name: String){
    this.name= name
    println("${this.name} 난다~~ 난다~~~")
}

//**출력**
아장 아장 이동.
주먹 발사!!
옵티머스 프라임 난다~~ 난다~~~

 

 

확장함수는 내가 만든 클래스에만 적용할 수 있는 것이 아닙니다.

이번에는 Kotlin APIs 클래스 중에서 상속이 불가능한 String 클래스에 새로운 기능 추가해 보겠습니다.

//시작 함수
fun main(){

    // Kotlin APIs 로 제공되는 final class String 에 새로운 확장 함수 사용하기!
    "안녕하세요.".twicePrint()

}//main 함수 종료..

// String 클래스에 새로운 기능 추가!!
fun String.twicePrint(){
    println("$this $this")
}

 

 

제네릭이 있는 컬렉션 List 에도 확장함수 적용이 가능합니다. 

//시작 함수
fun main(){

    // 제네릭 사용 클래스에 확장 함수 사용하기!
    val scores= listOf(90,88,75,68,95,100)
    val total= scores.total()
    println("성적 총합 : $total")    

}//main 함수 종료..

// 제네릭 타입에 확장함수 추가하기 - 정수값들을 가진 리스트의 총합을 구해서 리턴해주는 기능 추가!!
fun List<Int>.total():Int{
    var total:Int=0
    this.forEach { total += it }
    return total
}


//**출력***
성적 총합 : 516

 

단, 같은 리스트여도 제네릭 타입이 다르면 확장함수를 사용할 수 없습니다.

// 같은 리스트여도 제네릭 타입이 다르면 확장함수 사용 불가
val scores2= listOf(60.5, 43.5, 66.7)
val total2= scores2.total() //error - 확장함수 안됨..

 

 

확장 함수 기타 특성

- 확장 프로퍼티(멤버변수)도 가능함.

- 오버로딩 Overloading 가능함.

- 오버라이드 Override 는 불가능함.

- 기존에 클래스에 있는 메소드명과 같은 이름의 확장함수를 만들어도 에러는 아니지만 기존의 메소드가 우선시 됨.

- package 가 다른 곳에서 만든 확장 함수를 사용하려면 import 해야 함.

 

 

반응형

+ Recent posts