클래스를 수정하지 않고 마치 그 클래스의 메서드인 것처럼 함수를 더한다.
fun String.isValidEmail(): Boolean =
matches(Regex("^[\\w.]+@[\\w.]+\\.\\w+$"))
"a@b.com".isValidEmail() // 원래 있던 메서드처럼 쓴다
실제로는 정적 함수다
// 컴파일되면 이렇게 된다 (개념적으로)
public static boolean isValidEmail(String $this) { ... }
여기서 세 가지 성질이 따라 나온다.
- ① private 멤버에 접근할 수 없다 — 밖에서 부르는 함수일 뿐이다
- ② 오버라이드되지 않는다 — 정적 디스패치다
- ③ 멤버 함수가 우선한다 — 이름이 겹치면 원래 메서드가 이긴다
open class Base
class Derived : Base()
fun Base.name() = "Base"
fun Derived.name() = "Derived"
val x: Base = Derived()
x.name() // "Base" — 선언된 타입으로 결정된다 (런타임 타입이 아니다)
②가 함정이다 — 다형성을 기대하면 틀린다.
널 가능 타입에도 붙일 수 있다
fun String?.orEmpty(): String = this ?: ""
val s: String? = null
s.orEmpty() // 안전 호출 없이 부를 수 있다
// 표준 라이브러리가 이 방식을 쓴다
fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank()
확장 프로퍼티
val String.lastChar: Char get() = this[length - 1]
// 백킹 필드가 없다 — 상태를 저장할 수 없고 게터/세터만 정의한다
안드로이드에서의 관용구
fun View.show() { visibility = View.VISIBLE }
fun View.hide() { visibility = View.GONE }
fun Context.toast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
fun Int.dpToPx(context: Context): Int =
(this * context.resources.displayMetrics.density).toInt()
프레임워크 클래스에 우리 팀의 관용구를 얹는 것이 가장 흔한 용도다.
남용의 경계
- 좋은 쓰임 — 범용 유틸 · 프레임워크 클래스 보완 · 가독성 개선
- 나쁜 쓰임 — 내가 소유한 클래스에 붙이기 (그냥 멤버로 넣으면 된다)
- 도메인 로직을 밖으로 흩뿌리기 (응집도가 깨진다)
면접 함정
- ❌ "확장 함수는 클래스를 수정한다" → 바이트코드상 정적 함수일 뿐이다.
- ❌ "확장 함수를 오버라이드할 수 있다" → 정적 디스패치라 선언 타입으로 결정된다.
어디에 두나
파일 상단(최상위)에 두는 것이 기본이다
- 임포트만 하면 어디서든 쓸 수 있다
클래스 안에 두면 그 클래스 안에서만 보인다 (멤버 확장 함수)
- 수신자가 둘이 되어 헷갈리기 쉽다. 잘 쓰지 않는다
// 프로젝트 관례: ui/ext/ViewExt.kt · ContextExt.kt 처럼 대상별로 모은다
제네릭 확장
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
// 특정 타입에만 붙일 수도 있다
fun List<Int>.sumEvens() = filter { it % 2 == 0 }.sum()
표준 라이브러리가 전부 이 방식이다
// String · List · Map 의 유용한 함수 대부분이 확장 함수다
"abc".reversed()
listOf(1,2,3).sumOf { it }
mapOf(1 to "a").getOrDefault(2, "b")
그래서 "코틀린 표준 라이브러리가 풍부하다"는 인상의 상당 부분이 확장 함수 덕분이다 — 자바 클래스를 건드리지 않고도 API를 얹었다.