1. Extension Function(확장 함수) 개념
- 개념적으로 확장 함수는 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 밖에 선언된 함수를 의미합니다.
- 코틀린은 클래스를 상속하거나 데코레이터와 같은 디자인 패턴을 사용하지 않고도 클래스나 인터페이스에 새로운 기능을 확장할 수 있는 강력한 기능을 제공합니다.
- 이 기능은 '확장(extensions)'이라고 불리는 특별한 선언을 통해 이루어집니다.
- 확장 함수를 사용하면 수정할 수 없는 서드파티 라이브러리의 클 래스나 인터페이스에도 새로운 함수를 추가할 수 있습니다.
- 이러한 함수들은 마치 원래 클래스의 메서드인 것처럼 일반적인 방식으로 호출됩니다.
- 확장 함수는 코틀린 표준 라이브러리의 핵심적인 부분으로,
String
,List
,Collection
등 많은 클래스에 풍부한 기능을 제공합니다.
노트
코틀린 확장 함수는 실제로 클래스를 수정하지 않습니다. 대신 해당 타입의 객체에 대해 호출할 수 있는 새로운 함수를 만듭니다. 이는 Open-Closed Principle(개방-폐쇄 원칙)을 자연스럽게 지원하는 방식입니다.
1.1 확장 함수의 장점
- 코드의 간결성: 기존 클래스에 유틸리티 메서드를 추가하여 코드를 더 읽기 쉽고 간결하게 만듭니다.
- 기존 코드 수정 없음: 원본 클래 스의 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
- API 확장: 라이브러리나 프레임워크 API를 확장하여 특정 도메인이나 프로젝트에 맞게 맞춤화할 수 있습니다.
- 명확한 네임스페이스: 함수는 특정 타입에 연결되어 있어 글로벌 유틸리티 함수보다 발견하기 쉽습니다.
- 명시적인 수신 객체: 코드의 가독성과 명확성을 높입니다.
1.2 확장 프로퍼티
- 확장 함수와 유사하게, 코틀린은 확장 프로퍼티(Extension Properties)도 지원합니다.
- 확장 프로퍼티는 기존 클래스에 새로운 프로퍼티를 추가할 수 있게 해줍니다.
- 실제로는 프로퍼티처럼 보이지만 내부적으로는 getter와 setter를 사용합니다.
val String.lastIndex: Int
get() = this.length - 1
val String.lastChar: Char
get() = this[lastIndex]
var StringBuilder.lastChar: Char
get() = this[this.length - 1]
set(value) {
this.setCharAt(this.length - 1, value)
}
2. 확장 함수 만들기
2.1 기본 문법과 구조
fun 수신타입.함수이름(매개변수): 반환타입 {
// 함수 본문
// 여기서 'this'는 수신 객체를 가리킵니다
}
- 일반 메서드 본문에서 this를 생략하는 것처럼, 확장 함수 본문에 서도 this를 생략할 수 있습니다.
예시
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 여기서 'this'는 리스트에 해당합니다
this[index1] = this[index2]
this[index2] = tmp
}
- 가장 기본적인 확장 함수 예제로,
MutableList<Int>
에 요소 교환 기능을 추가하는 코드를 살펴보겠습니다. - 확장 함수를 선언하려면, 함수 이름 앞에 '수신 타입(receiver type)'을 붙이는데, 이는 확장하려는 타입을 가리킵니다.
- 위 예시에서
MutableList<Int>
가 수신 타입입니다.
- 위 예시에서
- 확장 함수 내부의 this 키워드는 수신 객체(점 앞에 위치한 객체)에 해당합니다.
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 호출 후: [3, 2, 1]
- 위와 같이
MutableList<Int>
타입의 객체에 대해 swap 함수를 호출할 수 있습니다. - 확장 함수는 수신 객체의 멤버 함수처럼 호출됩니다.
2.2 제네릭을 사용한 확장 함수
- 특정 타입에 한정되지 않고 여러 타입에 적용할 수 있는 확장 함수를 만들려면 제네릭을 사용합니다.
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
- 이제 어떤 타입의
MutableList
든 swap 함수를 호출할 수 있습니다. - 수신 타입 표현식에서 제네릭 타입 파라미터를 사용하려면 함수 이름 앞에 제네릭 타입 파라미터를 선언해야 합니다.
val numbers = mutableListOf(1, 2, 3)
numbers.swap(0, 2) // [3, 2, 1]
val names = mutableListOf("Alice", "Bob", "Charlie")
names.swap(0, 2) // [Charlie, Bob, Alice]
2.3 확장 함수 제약 조건 설정
- 타입 제약을 사용하여 특정 조건을 만족하는 타입에만 확장 함수를 적용할 수도 있습니다.
// Comparable을 구현하는 타입에만 적용되는 확장 함수
fun <T : Comparable<T>> MutableList<T>.sortIfNotEmpty() {
if (this.isNotEmpty()) {
this.sort()
}
}
2.4 널러블(Nullable) 타입 확장
널러블 타입에 대한 확장 함수를 정의할 수도 있습니다:
fun String?.isNullOrBlank(): Boolean {
// this는 nullable이므로, null 체크가 필요합니다
return this == null || this.isBlank()
}
val name: String? = null
println(name.isNullOrBlank()) // true
3. 확장 함수의 내부 구현
- 확장 함수는 실제로 확장하는 클래스를 수정하지 않습니다.
- 내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메서드로 컴파일됩니다.
- 확장 함수는 정적으로 처리되므로 어떤 확장 함수가 호출될지는 수신 타입에 기반하여 컴파일 시간에 이미 결정됩니다.
- 이러한 구현 방식은 런타임 오버헤드가 없다는 장점이 있습니다.
3.1 코틀린 확장 함수의 자바 변환
- 코틀린 확장 함수가 내부적으로 어떻게 구현되는지 이해하기 위해, 다음 코틀린 코드가 자바로 어떻게 변환되는지 살펴보겠습니다.
코틀린 코드 (KotlinExtensions.kt):
package strings
// String이 수신 객체 타입이고 this가 수신 객체입니다
fun String.lastChar(): Char = this.get(this.length - 1)
// 일반 메서드와 마찬가지로 this를 생략할 수 있습니다
fun String.lastChar2(): Char = get(length - 1)
// "Kotlin"이 수신 객체입니다
fun main() {
println("Kotlin".lastChar())
}
자바로 변환된 코드:
// 자바로 변환된 정적 메서드
public class StringExtensionsKt {
public static char lastChar(String receiver) {
return receiver.charAt(receiver.length() - 1);
}
public static char lastChar2(String receiver) {
return receiver.charAt(receiver.length() - 1);
}
public static void main(String[] args) {
System.out.println(lastChar("Kotlin"));
}
}
3.2 바이트코드 레벨에서의 오버헤드
- 확장 함수는 정적 메서드로 컴파일되므로 가상 메서드 호출(virtual method call)이 발생하지 않습니다.
- 이는 일반 인스턴스 메서드에 비해 성능상의 이점이 있을 수 있습니다.
- 하지만 확장 함수가 오버라이드된 메서드를 호출할 경우, 여전히 동적 디스패치(dynamic dispatch)가 발생합니다.
4. 확장 함수와 가시성
4.1 캡슐화와 접근 제한
확장 함수는 캡슐화를 깨트리지 않습니다!
- 클래스 안에서 정의한 메서드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private, protected 멤버를 사용할 수 없습니다.
- 확장 함수는 클래스의 public API만 접근할 수 있습니다.
class SecretClass {
private val secret = "비밀 정보"
fun revealSecret(): String = secret
}
// 확장 함수는 private 멤버에 접근할 수 없습니다
fun SecretClass.tryToExposeSecret(): String {
// return this.secret // 컴파일 오류! private 멤버 접근 불가
return this.revealSecret() // 가능: public 메서드 호출
}
- 위 예시에서
SecretClass
의 private 멤버인secret
에 접근하려고 하면 컴파일 오류가 발생합니다.
4.2 확장 함수의 가시성 제어
- 확장 함수 자체의 가시성은 일반 함수와 동일한 방식으로 제어됩니다.
// 패키지 내부에서만 사용 가능한 확장 함수
internal fun String.internalFunction(): String = this.toUpperCase()
// 모듈 외부에서도 사용 가능한 확장 함수
public fun String.publicFunction(): String = this.toLowerCase()
// 선언된 파일 내에서만 사용 가능한 확장 함수
private fun String.privateFunction(): String = this.capitalize()
5. 확장 함수 임포트
- 확장 함수를 정의했다고 해도 자동으로 프로젝트 안의 모든 소스코드에서 해당 함수를 사용할 수 있는 것은 아닙니다.
- 다른 클래스나 함수와 마찬가지로 임포트가 필요합니다.
- 이는 네임스페이스 충돌을 방지하고 코드 가독성을 높이는 데 도움이 됩니다.