본문으로 건너뛰기

🔧 확장 함수

📖 확장 함수란?

**확장 함수(Extension Function)**는 기존 클래스를 수정하지 않고 새로운 함수를 추가하는 기능입니다. 마치 원래 있던 메서드처럼 사용할 수 있습니다!

💡 기본 사용법

첫 확장 함수

// String에 새 함수 추가!
fun String.isEmail(): Boolean {
return this.contains("@") && this.contains(".")
}

fun main() {
val email = "hong@example.com"

println(email.isEmail()) // true
println("invalid".isEmail()) // false
}

확장 프로퍼티

val String.firstChar: Char
get() = if (this.isNotEmpty()) this[0] else ' '

fun main() {
println("Hello".firstChar) // H
println("Kotlin".firstChar) // K
}

🎯 실전 예제

문자열 유틸

// 전화번호 형식
fun String.toPhoneFormat(): String {
return if (this.length == 11) {
"${substring(0, 3)}-${substring(3, 7)}-${substring(7)}"
} else {
this
}
}

// 문자열 자르기
fun String.truncate(length: Int): String {
return if (this.length > length) {
"${substring(0, length)}..."
} else {
this
}
}

// 숫자만 추출
fun String.numbersOnly(): String {
return this.filter { it.isDigit() }
}

fun main() {
println("01012345678".toPhoneFormat()) // 010-1234-5678

val long = "This is a very long text"
println(long.truncate(10)) // This is a ...

println("가격: 1,500원".numbersOnly()) // 1500
}

숫자 유틸

// 짝수 판별
fun Int.isEven(): Boolean = this % 2 == 0

// 범위 체크
fun Int.isBetween(min: Int, max: Int): Boolean {
return this in min..max
}

// 통화 형식
fun Int.toCurrency(): String {
return "%,d원".format(this)
}

fun main() {
println(4.isEven()) // true
println(7.isEven()) // false

println(50.isBetween(0, 100)) // true

println(1500000.toCurrency()) // 1,500,000원
}

컬렉션 유틸

// 안전한 두 번째 요소
fun <T> List<T>.secondOrNull(): T? {
return if (this.size >= 2) this[1] else null
}

// 조건에 맞는 첫 인덱스
fun <T> List<T>.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? {
val index = this.indexOfFirst(predicate)
return if (index >= 0) index else null
}

// 리스트를 n개씩 묶기
fun <T> List<T>.chunked(size: Int): List<List<T>> {
return this.chunked(size)
}

fun main() {
val numbers = listOf(1, 2, 3, 4, 5)

println(numbers.secondOrNull()) // 2

val index = numbers.indexOfFirstOrNull { it > 3 }
println(index) // 3 (인덱스)

println(numbers.chunked(2)) // [[1, 2], [3, 4], [5]]
}

날짜/시간 (간단 버전)

data class SimpleDate(val year: Int, val month: Int, val day: Int)

fun SimpleDate.format(): String {
return "%04d-%02d-%02d".format(year, month, day)
}

fun SimpleDate.isWeekend(): Boolean {
// 간단히 요일 계산 (Zeller's congruence)
val y = if (month < 3) year - 1 else year
val m = if (month < 3) month + 12 else month
val dayOfWeek = (day + (13 * (m + 1) / 5) + (y % 100) +
(y % 100) / 4 + (y / 100) / 4 - 2 * (y / 100)) % 7
return dayOfWeek == 0 || dayOfWeek == 6 // 토/일
}

fun main() {
val date = SimpleDate(2024, 12, 25)
println(date.format()) // 2024-12-25
}

🔍 nullable 확장

null 안전한 확장

// null이거나 빈 문자열
fun String?.isNullOrEmpty(): Boolean {
return this == null || this.isEmpty()
}

// 기본값 반환
fun String?.orDefault(default: String): String {
return this ?: default
}

fun main() {
val text: String? = null

println(text.isNullOrEmpty()) // true
println(text.orDefault("기본값")) // 기본값
}

🎨 고급 활용

제네릭 확장

// 컬렉션 섞기
fun <T> List<T>.shuffled(): List<T> {
return this.shuffled()
}

// 조건부 변환
fun <T, R> T.letIf(condition: Boolean, block: (T) -> R): R? {
return if (condition) block(this) else null
}

fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.shuffled())

val result = "hello".letIf(true) { it.uppercase() }
println(result) // HELLO
}

중위 함수

// 중위 표기법
infix fun Int.multipliedBy(other: Int): Int {
return this * other
}

infix fun String.concat(other: String): String {
return "$this $other"
}

fun main() {
println(5 multipliedBy 3) // 15

val greeting = "Hello" concat "World"
println(greeting) // Hello World
}

🛠️ 실용적인 확장 모음

검증 확장

// 이메일 검증
fun String.isValidEmail(): Boolean {
val pattern = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+"
return this.matches(pattern.toRegex())
}

// 비밀번호 강도
fun String.isStrongPassword(): Boolean {
return this.length >= 8 &&
this.any { it.isDigit() } &&
this.any { it.isUpperCase() } &&
this.any { it.isLowerCase() }
}

// URL 검증
fun String.isValidUrl(): Boolean {
return this.startsWith("http://") || this.startsWith("https://")
}

fun main() {
println("hong@example.com".isValidEmail()) // true
println("Password123".isStrongPassword()) // true
println("https://kotlin.org".isValidUrl()) // true
}

변환 확장

// 캐멀케이스 → 스네이크케이스
fun String.toSnakeCase(): String {
return this.replace(Regex("([a-z])([A-Z])")) {
"${it.groupValues[1]}_${it.groupValues[2]}"
}.lowercase()
}

// 스네이크케이스 → 캐멀케이스
fun String.toCamelCase(): String {
return this.split("_").mapIndexed { index, word ->
if (index == 0) word else word.capitalize()
}.joinToString("")
}

fun main() {
println("userName".toSnakeCase()) // user_name
println("user_name".toCamelCase()) // userName
}

🤔 자주 묻는 질문

Q1. 확장 함수는 어디에 정의하나요?

A: 보통 별도 파일에!

// StringExtensions.kt
fun String.isEmail(): Boolean {
return this.contains("@")
}

// NumberExtensions.kt
fun Int.isEven(): Boolean {
return this % 2 == 0
}

// 사용할 때는 import
import com.example.extensions.*

Q2. 기존 메서드와 같은 이름이면?

A: 기존 메서드가 우선!

class MyClass {
fun test() {
println("원본 메서드")
}
}

// 확장 함수 (호출 안 됨!)
fun MyClass.test() {
println("확장 함수")
}

fun main() {
MyClass().test() // 원본 메서드
}

Q3. private 멤버에 접근할 수 있나요?

A: 안 됩니다!

class Person(private val age: Int)

// ❌ private 접근 불가
fun Person.getAge(): Int {
// return this.age // 오류!
return 0
}

🎬 마치며

확장 함수로 코드를 더 편리하게!

핵심 정리:
✅ 기존 클래스 수정 없이 기능 추가
✅ fun 타입.함수명() 형태
✅ nullable 타입도 확장 가능
✅ private 멤버는 접근 불가
✅ 유틸리티 함수로 활용

다음 단계: 예외 처리에서 에러를 안전하게 다뤄보세요!