KOTLIN · 심층 가이드
Kotlin 함수 완전 정리
확장 함수로 남의 클래스를 넓히고, 스코프 함수와 inline·reified로 람다 비용까지 통제하는 Kotlin 함수 설계를 20개 주제로 정리했습니다.
Kotlin에서 함수는 클래스의 부속품이 아닙니다. 파일 최상위에 그냥 놓을 수 있고, 수신자 타입을 앞에 붙이면 이미 컴파일된 남의 클래스에도 함수를 덧붙일 수 있습니다. 자바에서 넘어온 사람이 가장 먼저 감을 잃는 지점이 여기입니다. 확장 함수는 실제로는 첫 인자로 수신자를 받는 정적 메서드로 컴파일되고, 기본값 파라미터는 오버로딩 여러 개를 대체하지만 자바 쪽에서 보면 시그니처가 하나뿐입니다. 문법이 짧아진 만큼 컴파일된 모양을 한 번은 확인해 두는 편이 낫습니다.
순서는 함수 선언 & 기본값 & Named Argument에서 시작해 확장 함수로 넘어가는 흐름이 자연스럽습니다. 이 둘을 손에 익힌 뒤 스코프 함수 — let, run, apply, also, with를 보면 다섯 개가 결국 수신자를 this로 줄지 it으로 줄지, 그리고 수신자를 돌려줄지 람다 결과를 돌려줄지의 조합이라는 게 보입니다. 그 다음 인라인 함수 & reified와 DSL 만들기는 앞 문법을 재료로 쓰는 응용편이라 뒤로 미뤄도 됩니다.
inline은 공짜가 아닙니다. 람다 객체 할당과 가상 호출을 없애는 대신 호출 지점마다 본문이 복사되므로, 람다를 받지 않는 큰 함수에 붙이면 코드 크기만 불립니다. 인라인된 람다 안에서는 바깥 함수를 끝내는 non-local return이 가능한데, 그 람다를 다른 객체에 저장하거나 다른 스레드로 넘겨야 한다면 crossinline이나 noinline이 필요합니다. reified가 인라인 함수에서만 되는 이유도 같습니다. 타입 인자를 호출 지점에 박아 넣어야 하니까요.
01함수 선언 & 기본값 & Named Argument
기본값 파라미터로 오버로딩을 줄이고, Named Argument로 호출 가독성을 높입니다.
Kotlin code
// 기본 함수
fun add(a: Int, b: Int): Int {
return a + b
}
// 단일 표현식 함수 (타입 추론)
fun square(n: Int) = n * n
// 기본값 파라미터
fun greet(
name: String,
prefix: String = "안녕하세요",
suffix: String = "님!"
) = println("$prefix, $name$suffix")
fun main() {
greet("Kotlin") // 안녕하세요, Kotlin님!
greet("Java", "Hello") // Hello, Java님!
// Named argument — 순서 무관, 명확한 호출
greet(
name = "Go",
suffix = "!",
prefix = "Hi"
)
// vararg
fun sum(vararg nums: Int) = nums.sum()
println(sum(1, 2, 3, 4, 5)) // 15
// spread operator (배열 → vararg)
val arr = intArrayOf(1, 2, 3)
println(sum(*arr))
}Java에서 Kotlin 기본값 함수를 호출할 때는 모든 인자를 넣어야 합니다. @JvmOverloads 어노테이션을 추가하면 Java용 오버로딩 메서드가 자동 생성됩니다.
기본값 파라미터가 중간에 있으면 Named argument 없이는 뒤 파라미터를 전달하기 어렵습니다. 기본값 파라미터는 뒤쪽에 배치하세요.
02확장 함수 (Extension Function)
기존 클래스를 수정하지 않고 새 함수를 추가합니다. Kotlin 표준 라이브러리의 핵심 메커니즘입니다.
Kotlin code
// String 확장 함수
fun String.isValidEmail(): Boolean =
contains("@") && contains(".") && length > 5
// Int 확장 함수
fun Int.isEven() = this % 2 == 0
fun Int.factorial(): Long {
if (this < 0) throw IllegalArgumentException("음수 불가")
return (1..this).fold(1L) { acc, n -> acc * n }
}
// Nullable 수신자
fun String?.orEmpty(): String = this ?: ""
// Generic 확장 함수
fun <T> List<T>.secondOrNull(): T? =
if (size >= 2) this[1] else null
fun main() {
println("dev@kotlin.io".isValidEmail()) // true
println(4.isEven()) // true
println(5.factorial()) // 120
val nullStr: String? = null
println(nullStr.orEmpty()) // ""
println(listOf(1, 2, 3).secondOrNull()) // 2
println(listOf(1).secondOrNull()) // null
}확장 함수는 정적 디스패치됩니다. 오버라이드가 불가하며, 멤버 함수와 이름이 충돌하면 항상 멤버 함수가 우선합니다.
확장 함수에서는 private/protected 멤버에 접근할 수 없습니다. 내부 구현에 접근해야 한다면 클래스 내부 멤버 함수로 작성해야 합니다.
03스코프 함수 — let, run, apply, also, with
객체를 컨텍스트로 하는 코드 블록 실행. 각각 수신자(this/it)와 반환값이 다릅니다.
Kotlin code
data class Person(var name: String, var age: Int, var email: String = "")
// let — it 참조, 변환 결과 반환, null 체크 관용구
val name: String? = " Kotlin "
val result = name?.let { it.trim().uppercase() }
// result = "KOTLIN"
// apply — this 참조, 수신자 객체 반환 (빌더 패턴)
val person = Person("", 0).apply {
name = "Alice"
age = 30
email = "alice@example.com"
}
// also — it 참조, 수신자 객체 반환 (부가 작업)
val numbers = mutableListOf(3, 1, 4, 1, 5)
.also { println("원본: $it") }
.sorted()
.also { println("정렬: $it") }
// run — this 참조, 람다 결과 반환
val greeting = person.run {
"안녕, $name! 당신은 ${age}살입니다."
}
// with — 수신자를 인자로, 람다 결과 반환
val info = with(person) {
"$name ($age세) — $email"
}선택 기준: 객체 설정(빌더) → apply, null 체크/변환 → let, 부가 동작 추가 → also, 결과 계산 → run/with
apply 내부에서 this가 다른 클래스의 수신자와 충돌할 수 있습니다. 중첩 스코프 함수에서는 this@ClassName으로 명시하세요.
04인라인 함수 & reified
inline으로 람다 오버헤드를 제거하고, reified로 제네릭 타입 정보를 런타임에 사용합니다.
Kotlin code
import kotlin.reflect.KClass
// inline — 고차함수 성능 최적화
inline fun measure(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
// reified — 런타임 타입 정보 유지
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
filter { it is T }.map { it as T }
// reified로 JSON 파싱 패턴
inline fun <reified T> parseJson(json: String): T {
// 실제로는 Gson/Moshi 사용
// T::class.java 접근 가능!
println("파싱 타입: ${T::class.simpleName}")
TODO("실제 파싱 구현")
}
// noinline — 일부 람다만 인라인 제외
inline fun doWork(
inlined: () -> Unit,
noinline notInlined: () -> Unit
) {
inlined()
val saved = notInlined // 객체로 저장 가능
saved()
}
fun main() {
val time = measure { Thread.sleep(10) }
println("${time}ms 소요")
val mixed = listOf(1, "hello", 2, "world", 3)
val strings: List<String> = mixed.filterIsInstance()
println(strings) // [hello, world]
}inline은 컴파일러가 함수 본문을 호출 지점에 복사합니다. 람다를 많이 호출하는 루프에서 효과적입니다.
inline 함수는 public API에서 internal/private 멤버에 접근하면 컴파일 경고가 납니다. @PublishedApi internal로 해결합니다.
05Delegated Properties 심화
lazy, observable, vetoable 외 커스텀 위임 프로퍼티 구현
Kotlin code
<span class="cm">// Delegated Properties 심화 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("Delegated Properties 심화") }KOTLIN 공식 문서를 함께 참고하세요.
자주 발생하는 실수에 주의하세요.
06꼬리 재귀 (Tail Recursion)
tailrec 키워드는 꼬리 재귀 함수를 반복문으로 최적화합니다. 스택 오버플로 없이 재귀를 사용할 수 있습니다.
Kotlin code
tailrec fun factorial(n: Long, acc: Long = 1L): Long =
if (n <= 1) acc
else factorial(n - 1, n * acc)
tailrec fun gcd(a: Int, b: Int): Int =
if (b == 0) a else gcd(b, a % b)
tailrec fun findIndex(
list: List<Int>, target: Int, index: Int = 0
): Int = when {
index >= list.size -> -1
list[index] == target -> index
else -> findIndex(list, target, index + 1)
}
fun main() {
println(factorial(20)) // 2432902008176640000
println(gcd(48, 18)) // 6
println(findIndex(listOf(3, 7, 1, 9), 1)) // 2
}tailrec이 적용되려면 재귀 호출이 함수의 마지막 연산이어야 합니다. 컴파일러가 최적화 불가 시 경고를 표시합니다.
factorial(n - 1) * n처럼 재귀 호출 뒤에 연산이 있으면 꼬리 재귀가 아닙니다. 누산기(accumulator) 패턴으로 변환하세요.
07중위 함수 (Infix Function)
infix 키워드로 함수를 연산자처럼 중위 표기법으로 호출합니다. 가독성 높은 DSL 구성에 활용됩니다.
Kotlin code
infix fun Int.power(exp: Int): Long {
var result = 1L
repeat(exp) { result *= this }
return result
}
infix fun <T> T.shouldBe(expected: T) {
if (this != expected)
throw AssertionError("Expected $expected but got $this")
println("✓ $this == $expected")
}
class Route(val path: String) {
infix fun to(handler: (String) -> String) =
println("$path → ${handler(path)}")
}
fun main() {
println(2 power 10) // 1024
"Hello" shouldBe "Hello" // ✓
Route("/api/users") to { "GET $it" }
}중위 함수는 테스트 프레임워크의 assertion DSL에 많이 사용됩니다. value shouldBe expected 형태로 자연스러운 테스트를 작성할 수 있습니다.
중위 함수는 반드시 멤버 함수이거나 확장 함수여야 하며, 파라미터가 정확히 하나여야 합니다. 기본값이 있어도 안 됩니다.
08연산자 함수 (Operator Functions)
operator 키워드로 커스텀 타입에 연산자를 정의합니다. 확장 함수로도 기존 클래스에 연산자를 추가할 수 있습니다.
Kotlin code
data class Money(val amount: Int, val currency: String) {
operator fun plus(other: Money): Money {
require(currency == other.currency) { "통화 불일치" }
return Money(amount + other.amount, currency)
}
operator fun compareTo(other: Money): Int {
require(currency == other.currency) { "통화 불일치" }
return amount.compareTo(other.amount)
}
}
// 확장 연산자 함수
operator fun Money.times(factor: Int) =
Money(amount * factor, currency)
fun main() {
val a = Money(1000, "KRW")
val b = Money(2000, "KRW")
println(a + b) // Money(3000, KRW)
println(a * 3) // Money(3000, KRW)
println(a > b) // false (compareTo 사용)
}compareTo를 오버로딩하면 >, , >=, 가 모두 자동으로 동작합니다.
서로 다른 통화 간 연산은 비즈니스 로직 오류입니다. require로 사전 조건을 반드시 검증하세요.
09지역 함수 (Local Functions)
함수 내부에 함수를 정의하여 로직을 캡슐화합니다. 외부 함수의 변수에 접근할 수 있어 파라미터 전달이 줄어듭니다.
Kotlin code
fun validateUser(name: String, email: String, age: Int) {
// 지역 함수: 외부 파라미터에 직접 접근
fun validateField(value: String, field: String) {
if (value.isBlank())
throw IllegalArgumentException("$field 은(는) 필수입니다")
}
fun validateAge() {
if (age !in 1..150)
throw IllegalArgumentException("나이($age)가 유효하지 않습니다")
}
validateField(name, "이름")
validateField(email, "이메일")
validateAge()
println("검증 통과: $name, $email, $age")
}
fun main() {
validateUser("김철수", "kim@test.com", 25)
// validateUser("", "test@test.com", 25) // 예외!
}지역 함수는 유틸리티 함수로 추출하기엔 특정 컨텍스트에 밀접하고, 반복되는 검증 로직을 정리할 때 유용합니다.
지역 함수를 과도하게 중첩하면 가독성이 떨어집니다. 2단계 이상 중첩이 필요하면 별도 함수로 추출하는 것이 낫습니다.
10익명 함수 (Anonymous Functions)
람다와 달리 반환 타입을 명시하고 return으로 해당 함수에서만 반환합니다. 비지역 반환이 필요 없을 때 유용합니다.
Kotlin code
fun main() {
// 익명 함수: return이 자기 자신에서 반환
val square = fun(x: Int): Int {
return x * x
}
// 람다: return은 외부 함수에서 반환 (비지역 반환)
val items = listOf(1, 2, 3, 4, 5)
// 익명 함수 사용 - 필터링에서 안전
val evens = items.filter(fun(n: Int): Boolean {
return n % 2 == 0
})
// 여러 반환 지점이 필요한 경우
val classify = fun(score: Int): String {
if (score >= 90) return "A"
if (score >= 80) return "B"
if (score >= 70) return "C"
return "F"
}
println(square(5)) // 25
println(evens) // [2, 4]
println(classify(85)) // B
}익명 함수의 return은 항상 해당 익명 함수에서만 반환합니다. 인라인 함수 내에서 비지역 반환을 원치 않을 때 유용합니다.
람다 내의 return은 바깥 함수에서 반환됩니다(비지역 반환). 이를 방지하려면 익명 함수를 사용하거나 return@label을 사용하세요.
11함수 참조 (Function References)
:: 연산자로 함수, 생성자, 프로퍼티를 참조합니다. 고차 함수에 기존 함수를 간결하게 전달합니다.
Kotlin code
fun isEven(n: Int) = n % 2 == 0
data class User(val name: String, val age: Int) {
fun greet() = "안녕하세요, ${name}입니다"
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6)
// 함수 참조
println(numbers.filter(::isEven)) // [2, 4, 6]
// 생성자 참조
val names = listOf("김철수", "이영희")
val users = names.map { User(it, 25) }
// 멤버 함수 참조
val greetings = users.map(User::greet)
println(greetings)
// 바운드 참조 (특정 인스턴스에 바인딩)
val user = User("박지성", 30)
val greetFn: () -> String = user::greet
println(greetFn())
// 프로퍼티 참조
val ages = users.map(User::age)
println(ages)
}User::name처럼 프로퍼티 참조를 사용하면 { it.name }보다 간결하고 의도가 명확합니다.
오버로딩된 함수를 참조할 때 타입을 명시해야 합니다. val ref: (Int) -> Boolean = ::isEven처럼 타입을 지정하세요.
12SAM 변환 (Single Abstract Method)
Java의 단일 추상 메서드 인터페이스를 람다로 간결하게 구현합니다. Kotlin의 fun interface에서도 동일하게 동작합니다.
Kotlin code
// Kotlin fun interface (SAM)
fun interface Transformer<T> {
fun transform(input: T): T
}
fun interface Validator<T> {
fun validate(value: T): Boolean
}
fun <T> applyAll(value: T, vararg transforms: Transformer<T>): T =
transforms.fold(value) { acc, t -> t.transform(acc) }
fun main() {
// SAM 변환: 람다를 인터페이스로
val upper: Transformer<String> = Transformer { it.uppercase() }
val trim: Transformer<String> = Transformer { it.trim() }
println(applyAll(" hello world ", trim, upper))
// "HELLO WORLD"
// Java Runnable도 SAM 변환
val thread = Thread { println("별도 스레드 실행") }
thread.start()
val isPositive: Validator<Int> = Validator { it > 0 }
println(isPositive.validate(5)) // true
}Kotlin 인터페이스에서 SAM 변환을 사용하려면 반드시 fun interface로 선언해야 합니다. 일반 interface에는 적용되지 않습니다.
fun interface는 정확히 하나의 추상 메서드만 가져야 합니다. 메서드가 2개 이상이면 컴파일 오류가 발생합니다.
13타입 안전 빌더 (Type-Safe Builders)
수신 객체가 있는 람다를 활용하여 타입 안전한 빌더 패턴을 구현합니다. HTML, SQL, UI 등 구조적 DSL에 활용됩니다.
Kotlin code
class Html {
private val children = mutableListOf<String>()
fun head(init: Head.() -> Unit) {
children += Head().apply(init).render()
}
fun body(init: Body.() -> Unit) {
children += Body().apply(init).render()
}
fun render() = "<html>
${children.joinToString("
")}
</html>"
}
class Head {
var title = ""
fun render() = " <head><title>$title</title></head>"
}
class Body {
private val elements = mutableListOf<String>()
fun h1(text: String) { elements += " <h1>$text</h1>" }
fun p(text: String) { elements += " <p>$text</p>" }
fun render() = " <body>
${elements.joinToString("
")}
</body>"
}
fun html(init: Html.() -> Unit) = Html().apply(init).render()
fun main() {
val page = html {
head { title = "Kotlin DSL" }
body {
h1("환영합니다")
p("타입 안전 빌더 예제입니다")
}
}
println(page)
}@DslMarker 어노테이션을 사용하면 중첩된 빌더에서 바깥 스코프의 함수 호출을 제한할 수 있습니다.
빌더 내에서 바깥 스코프의 메서드가 의도치 않게 호출될 수 있습니다. @DslMarker를 적용하여 스코프를 제한하세요.
14DSL 만들기 (@DslMarker)
@DslMarker로 DSL 스코프를 제한하여 안전한 도메인 특화 언어를 구축합니다.
Kotlin code
@DslMarker
annotation class ConfigDsl
@ConfigDsl
class ServerConfig {
var host = "localhost"
var port = 8080
private var dbConfig: DbConfig? = null
fun database(init: DbConfig.() -> Unit) {
dbConfig = DbConfig().apply(init)
}
override fun toString() = "Server($host:$port, db=$dbConfig)"
}
@ConfigDsl
class DbConfig {
var url = ""
var maxPool = 10
override fun toString() = "DB($url, pool=$maxPool)"
}
fun server(init: ServerConfig.() -> Unit): ServerConfig =
ServerConfig().apply(init)
fun main() {
val config = server {
host = "0.0.0.0"
port = 9090
database {
url = "jdbc:postgresql://localhost/mydb"
maxPool = 20
// host = "x" // 컴파일 오류! @DslMarker 덕분
}
}
println(config)
}@DslMarker가 적용되면 중첩 빌더에서 바깥 빌더의 멤버에 직접 접근할 수 없어 실수를 방지합니다.
@DslMarker 없이 DSL을 만들면 database { host = "x" }가 바깥 ServerConfig의 host를 변경하는 실수가 발생합니다.
15커링 (Currying)
다중 인수 함수를 단일 인수 함수의 체인으로 변환하는 기법입니다. Kotlin에서 수동으로 커링을 구현합니다.
Kotlin code
// 커링 유틸리티
fun <A, B, C> ((A, B) -> C).curried(): (A) -> (B) -> C =
{ a -> { b -> this(a, b) } }
fun <A, B, C, D> ((A, B, C) -> D).curried(): (A) -> (B) -> (C) -> D =
{ a -> { b -> { c -> this(a, b, c) } } }
fun add(a: Int, b: Int) = a + b
fun formatLog(level: String, tag: String, msg: String) =
"[$level][$tag] $msg"
fun main() {
// 2인자 커링
val curriedAdd = ::add.curried()
val add5 = curriedAdd(5)
println(add5(3)) // 8
println(add5(10)) // 15
// 3인자 커링
val curriedLog = ::formatLog.curried()
val errorLog = curriedLog("ERROR")
val dbErrorLog = errorLog("DB")
println(dbErrorLog("연결 실패"))
println(dbErrorLog("타임아웃"))
}커링은 로깅, 설정, 검증 등에서 공통 파라미터를 미리 바인딩할 때 유용합니다.
커링된 함수를 과도하게 사용하면 타입 시그니처가 복잡해집니다. 간단한 경우 기본값 파라미터가 더 읽기 쉽습니다.
16부분 적용 (Partial Application)
함수의 일부 인수를 고정하여 새로운 함수를 만듭니다. 커링과 달리 여러 인수를 한번에 고정할 수 있습니다.
Kotlin code
// 부분 적용 유틸리티
fun <A, B, R> partial(f: (A, B) -> R, a: A): (B) -> R =
{ b -> f(a, b) }
fun <A, B, C, R> partial2(f: (A, B, C) -> R, a: A, b: B): (C) -> R =
{ c -> f(a, b, c) }
fun multiply(a: Int, b: Int) = a * b
fun sendEmail(from: String, to: String, body: String) {
println("From: $from | To: $to | Body: $body")
}
fun main() {
// 곱셈기 생성
val double = partial(::multiply, 2)
val triple = partial(::multiply, 3)
println(double(5)) // 10
println(triple(5)) // 15
// 발신자 고정
val sendFromAdmin = partial2(::sendEmail, "admin@co.kr", "user@co.kr")
sendFromAdmin("가입을 환영합니다!")
sendFromAdmin("비밀번호가 변경되었습니다")
// 리스트에 적용
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.map(double)) // [2, 4, 6, 8, 10]
}부분 적용은 의존성을 미리 주입하거나, 반복적인 호출에서 공통 인수를 제거할 때 깔끔합니다.
부분 적용과 커링을 혼동하지 마세요. 부분 적용은 N개 인수를 고정하고, 커링은 인수를 하나씩 받는 함수 체인으로 변환합니다.
17메모이제이션 (Memoization)
함수 호출 결과를 캐싱하여 동일 입력에 대한 재계산을 방지합니다. 순수 함수에 적합한 최적화 기법입니다.
Kotlin code
fun <T, R> memoize(fn: (T) -> R): (T) -> R {
val cache = mutableMapOf<T, R>()
return { input ->
cache.getOrPut(input) { fn(input) }
}
}
// 비용이 큰 연산 시뮬레이션
fun expensiveCompute(n: Int): Long {
println(" 계산 중: $n")
Thread.sleep(100) // 시뮬레이션
return (1..n.toLong()).fold(1L) { acc, i -> acc * i }
}
fun main() {
val memoizedFactorial = memoize(::expensiveCompute)
println("첫 호출:")
println(memoizedFactorial(10)) // 계산 실행
println("두번째 호출:")
println(memoizedFactorial(10)) // 캐시에서 반환
// 피보나치에도 적용 가능
lateinit var fib: (Int) -> Long
fib = memoize { n: Int ->
if (n <= 1) n.toLong() else fib(n - 1) + fib(n - 2)
}
println(fib(40)) // 빠르게 계산
}메모이제이션은 순수 함수(동일 입력에 항상 같은 출력)에만 사용하세요. 부수 효과가 있는 함수에는 적합하지 않습니다.
메모이제이션 캐시는 메모리를 계속 소비합니다. 입력 범위가 넓거나 무한한 경우 LRU 캐시를 사용하세요.
18함수 합성 (Function Composition)
두 함수를 조합하여 새 함수를 만듭니다. compose와 andThen 패턴으로 데이터 변환 파이프라인을 구성합니다.
Kotlin code
// 함수 합성 연산자
infix fun <A, B, C> ((B) -> C).compose(g: (A) -> B): (A) -> C =
{ a -> this(g(a)) }
infix fun <A, B, C> ((A) -> B).andThen(g: (B) -> C): (A) -> C =
{ a -> g(this(a)) }
val trim: (String) -> String = String::trim
val lowercase: (String) -> String = String::lowercase
val addGreeting: (String) -> String = { "안녕하세요, $it!" }
fun main() {
// compose: 오른쪽부터 실행
val greetClean = addGreeting compose lowercase compose trim
println(greetClean(" KOTLIN "))
// "안녕하세요, kotlin!"
// andThen: 왼쪽부터 실행
val pipeline = trim andThen lowercase andThen addGreeting
println(pipeline(" WORLD "))
// "안녕하세요, world!"
// 리스트 변환 파이프라인
val process = { s: String -> s.split(",") } andThen
{ list: List<String> -> list.map { it.trim().toInt() } } andThen
{ nums: List<Int> -> nums.filter { it > 0 }.sum() }
println(process("1, -2, 3, 4, -5")) // 8
}andThen은 실행 순서가 읽는 순서와 같아 파이프라인 구성 시 더 직관적입니다.
합성 함수의 타입이 일치하지 않으면 컴파일 오류가 발생합니다. f: (B) -> C와 g: (A) -> B에서 B 타입이 일치해야 합니다.
19제네릭 함수 (Generic Functions)
타입 파라미터를 사용하여 다양한 타입에 동작하는 범용 함수를 작성합니다. reified로 런타임 타입 정보도 유지합니다.
Kotlin code
// 기본 제네릭 함수
fun <T : Comparable<T>> findMax(list: List<T>): T? =
list.maxOrNull()
// 여러 타입 파라미터
fun <K, V> mapOfPairs(vararg pairs: Pair<K, V>): Map<K, V> =
pairs.toMap()
// reified: 런타임에 타입 정보 유지
inline fun <reified T> List<*>.filterByType(): List<T> =
filterIsInstance<T>()
inline fun <reified T> Any.isType(): Boolean = this is T
fun main() {
println(findMax(listOf(3, 1, 4, 1, 5))) // 5
println(findMax(listOf("b", "a", "c"))) // c
val mixed: List<Any> = listOf(1, "hello", 2.0, "world", 3)
val strings = mixed.filterByType<String>()
println(strings) // [hello, world]
println("Hello".isType<String>()) // true
println(42.isType<String>()) // false
}reified 타입 파라미터는 inline 함수에서만 사용 가능하며, 런타임 타입 검사나 리플렉션에 유용합니다.
reified를 사용하려면 함수가 반드시 inline이어야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다.
20확장 프로퍼티 (Extension Properties)
기존 클래스에 프로퍼티를 추가합니다. backing field가 없으므로 getter/setter를 통해 계산된 값을 반환합니다.
Kotlin code
// 문자열 확장 프로퍼티
val String.isEmail: Boolean
get() = matches(Regex("^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}$"))
val String.wordCount: Int
get() = trim().split("\s+".toRegex()).size
// 리스트 확장 프로퍼티
val <T> List<T>.secondOrNull: T?
get() = if (size >= 2) this[1] else null
val List<Int>.median: Double
get() {
val sorted = this.sorted()
val mid = size / 2
return if (size % 2 == 0)
(sorted[mid - 1] + sorted[mid]) / 2.0
else sorted[mid].toDouble()
}
fun main() {
println("user@example.com".isEmail) // true
println("not-email".isEmail) // false
println("Hello Kotlin World".wordCount) // 3
println(listOf(10, 20, 30).secondOrNull) // 20
println(listOf(1, 3, 5, 7).median) // 4.0
}확장 프로퍼티는 자주 사용하는 계산 로직을 자연스러운 프로퍼티 접근으로 만들어 코드 가독성을 높입니다.
확장 프로퍼티에는 backing field(field)를 사용할 수 없습니다. 상태 저장이 필요하면 확장 함수와 외부 맵을 조합하세요.
정리하며
- 확장 함수는 정적으로 결정되므로 오버라이드되지 않고, 멤버 함수와 이름이 겹치면 멤버가 이깁니다
- 기본값 파라미터 + Named Argument 조합이면 오버로딩을 대부분 없앨 수 있습니다
- 스코프 함수는 수신자 표기(this/it)와 반환값(수신자/람다 결과) 두 축으로 골라 씁니다
- inline은 람다를 받는 함수에만, reified가 필요할 때는 필수라는 기준으로 붙입니다
더 깊이 들어가고 싶다면 Kotlin 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.